@sema-agent/core 7.29.0 → 7.30.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 (149) hide show
  1. package/CHANGELOG.md +361 -0
  2. package/dist/agents/child-model-seat.d.ts +59 -7
  3. package/dist/agents/child-model-seat.js +33 -7
  4. package/dist/agents/cross-session-judge.d.ts +27 -5
  5. package/dist/agents/cross-session-judge.js +23 -12
  6. package/dist/agents/list-agents-tool.d.ts +31 -6
  7. package/dist/agents/list-agents-tool.js +66 -14
  8. package/dist/agents/peer-admission.d.ts +20 -8
  9. package/dist/agents/peer-admission.js +21 -12
  10. package/dist/agents/peer-directory.d.ts +29 -0
  11. package/dist/agents/peer-directory.js +20 -0
  12. package/dist/agents/peer-held-queue.d.ts +31 -4
  13. package/dist/agents/peer-held-queue.js +32 -1
  14. package/dist/agents/peer-session-drain.d.ts +91 -13
  15. package/dist/agents/peer-session-drain.js +409 -102
  16. package/dist/agents/read-inbox-tool.d.ts +22 -0
  17. package/dist/agents/read-inbox-tool.js +39 -0
  18. package/dist/agents/roster-store.d.ts +2 -4
  19. package/dist/agents/roster-store.js +1 -1
  20. package/dist/agents/send-message-tool.d.ts +4 -4
  21. package/dist/agents/send-message-tool.js +22 -2
  22. package/dist/agents/subagent.js +62 -44
  23. package/dist/agents/team-role.d.ts +34 -0
  24. package/dist/agents/team-role.js +17 -0
  25. package/dist/agents/verify.js +2 -2
  26. package/dist/core/auto-compaction.d.ts +42 -0
  27. package/dist/core/auto-compaction.js +35 -2
  28. package/dist/core/child-clamp-seats.d.ts +11 -0
  29. package/dist/core/child-clamp-seats.js +3 -0
  30. package/dist/core/context-edit.d.ts +3 -2
  31. package/dist/core/context-edit.js +6 -5
  32. package/dist/core/context-guard.d.ts +2 -1
  33. package/dist/core/context-guard.js +3 -2
  34. package/dist/core/context-settings.d.ts +103 -0
  35. package/dist/core/context-settings.js +60 -0
  36. package/dist/core/engine-notice.d.ts +11 -1
  37. package/dist/core/frame-settlement.d.ts +97 -0
  38. package/dist/core/frame-settlement.js +36 -0
  39. package/dist/core/governance-codes.d.ts +1 -1
  40. package/dist/core/governance-codes.js +2 -0
  41. package/dist/core/hooks.d.ts +10 -10
  42. package/dist/core/mailbox-store.d.ts +41 -7
  43. package/dist/core/mailbox-store.js +9 -3
  44. package/dist/core/parent-spec-seat.d.ts +14 -1
  45. package/dist/core/parent-spec-seat.js +6 -0
  46. package/dist/core/permission-rule-model.js +1 -1
  47. package/dist/core/roles.d.ts +20 -0
  48. package/dist/core/roles.js +35 -16
  49. package/dist/core/runner/assemble-result.d.ts +2 -0
  50. package/dist/core/runner/assemble-result.js +1 -1
  51. package/dist/core/runner/compaction-call-options.d.ts +26 -2
  52. package/dist/core/runner/compaction-call-options.js +9 -0
  53. package/dist/core/runner/compaction-seams.d.ts +4 -3
  54. package/dist/core/runner/compaction-seams.js +13 -6
  55. package/dist/core/runner/contracts.d.ts +56 -7
  56. package/dist/core/runner/decide-continuation.d.ts +2 -1
  57. package/dist/core/runner/decide-continuation.js +11 -10
  58. package/dist/core/runner/initial-run-state.js +1 -1
  59. package/dist/core/runner/prepare-caps-and-workflow.js +9 -1
  60. package/dist/core/runner/prepare-config-doors.js +4 -1
  61. package/dist/core/runner/prepare-context-lane.d.ts +9 -2
  62. package/dist/core/runner/prepare-context-lane.js +15 -10
  63. package/dist/core/runner/prepare-delegation-surface.d.ts +3 -2
  64. package/dist/core/runner/prepare-delegation-surface.js +9 -3
  65. package/dist/core/runner/prepare-file-history.js +10 -8
  66. package/dist/core/runner/prepare-gate-stations.d.ts +4 -0
  67. package/dist/core/runner/prepare-gate-stations.js +3 -1
  68. package/dist/core/runner/prepare-inherited-gate.js +1 -0
  69. package/dist/core/runner/prepare-prompt-inputs.d.ts +6 -0
  70. package/dist/core/runner/prepare-prompt-inputs.js +2 -1
  71. package/dist/core/runner/prepare-task.js +22 -9
  72. package/dist/core/runner/prepare-wiring-manifest.d.ts +6 -1
  73. package/dist/core/runner/prepare-wiring-manifest.js +6 -1
  74. package/dist/core/runner/retired-runner-deps.d.ts +3 -1
  75. package/dist/core/runner/retired-runner-deps.js +1 -0
  76. package/dist/core/runner/run-compaction-machinery.d.ts +4 -2
  77. package/dist/core/runner/run-compaction-machinery.js +5 -3
  78. package/dist/core/runner/run-harness-handlers.js +1 -1
  79. package/dist/core/runner/run-identity-wiring.js +19 -1
  80. package/dist/core/runner/run-leg.js +1 -1
  81. package/dist/core/runner/run-notification-lane.js +8 -2
  82. package/dist/core/runner/run-recovery-lanes.js +9 -5
  83. package/dist/core/runner/run-settle-and-teardown.d.ts +4 -22
  84. package/dist/core/runner/run-settle-and-teardown.js +6 -10
  85. package/dist/core/runner/run-stop-and-final-verify.js +1 -0
  86. package/dist/core/runner/run-terminal-adoption.js +5 -5
  87. package/dist/core/runner/run-turn-boundary.js +32 -5
  88. package/dist/core/runner/runtask.d.ts +8 -1
  89. package/dist/core/runner/runtask.js +32 -1
  90. package/dist/core/runner-deps.d.ts +44 -13
  91. package/dist/core/swappable-deps.d.ts +20 -4
  92. package/dist/core/swappable-deps.js +6 -1
  93. package/dist/core/task-event.d.ts +5 -0
  94. package/dist/core/task-notification.d.ts +144 -2
  95. package/dist/core/task-notification.js +154 -5
  96. package/dist/core/task-result.d.ts +14 -0
  97. package/dist/core/task-spec.d.ts +7 -0
  98. package/dist/core/tool-catalog-entries.js +2 -1
  99. package/dist/core/tool-conformance.js +1 -0
  100. package/dist/core/tool-policy.d.ts +9 -4
  101. package/dist/core/tool-spec.d.ts +14 -0
  102. package/dist/core/wiring-manifest.d.ts +12 -0
  103. package/dist/core/wiring-manifest.js +5 -1
  104. package/dist/engine/harness/agent-harness.d.ts +14 -1
  105. package/dist/engine/harness/agent-harness.js +28 -3
  106. package/dist/engine/harness/types.d.ts +3 -1
  107. package/dist/engine/llm/types.d.ts +8 -7
  108. package/dist/engine/llm/types.js +1 -1
  109. package/dist/engine/session/import-validate.js +4 -0
  110. package/dist/index.d.ts +10 -7
  111. package/dist/index.js +7 -5
  112. package/dist/orchestration/run-workflow-tool.d.ts +4 -4
  113. package/dist/orchestration/workflow.d.ts +1 -1
  114. package/dist/orchestration/workflow.js +8 -5
  115. package/dist/shell/lex.d.ts +30 -51
  116. package/dist/shell/lex.js +237 -284
  117. package/dist/stores/cc/mailbox-store.js +3 -2
  118. package/dist/stores/file/corrupt-read-sink.d.ts +28 -2
  119. package/dist/stores/file/file-history-store.d.ts +2 -4
  120. package/dist/stores/file/file-history-store.js +1 -1
  121. package/dist/stores/file/fs-atomic.d.ts +16 -11
  122. package/dist/stores/file/fs-atomic.js +17 -6
  123. package/dist/stores/file/index.d.ts +5 -6
  124. package/dist/stores/file/index.js +1 -1
  125. package/dist/stores/file/mailbox-store.d.ts +4 -9
  126. package/dist/stores/file/mailbox-store.js +29 -30
  127. package/dist/stores/file/session-policy-store.d.ts +2 -0
  128. package/dist/stores/file/session-policy-store.js +1 -1
  129. package/dist/stores/file/session-store.d.ts +11 -7
  130. package/dist/stores/file/session-store.js +23 -16
  131. package/dist/stores/file/shared-ledger.js +3 -14
  132. package/dist/stores/file/tool-result-store.d.ts +2 -0
  133. package/dist/stores/file/tool-result-store.js +5 -2
  134. package/dist/stores/file/workflow-journal-store.js +14 -9
  135. package/dist/tools/fs/bash-program-position.d.ts +15 -24
  136. package/dist/tools/fs/bash-program-position.js +11 -29
  137. package/dist/tools/fs/bash-readonly-classifier.d.ts +125 -117
  138. package/dist/tools/fs/bash-readonly-classifier.js +400 -272
  139. package/dist/tools/fs/shell-read-check.js +3 -3
  140. package/dist/tools/fs/shell-read-establish.js +1 -1
  141. package/dist/tools/fs/shell-read-plan.d.ts +11 -6
  142. package/dist/tools/fs/shell-read-plan.js +167 -183
  143. package/dist/tools/fs/shell-read-word.d.ts +50 -0
  144. package/dist/tools/fs/shell-read-word.js +59 -0
  145. package/dist/tools/fs/shell-reentry-text.d.ts +10 -0
  146. package/dist/tools/fs/shell-reentry-text.js +73 -0
  147. package/package.json +1 -1
  148. package/test/export-surface.snapshot.json +58 -8
  149. package/wire-changes/core@7.30.0.json +1821 -0
@@ -8,27 +8,22 @@
8
8
  * readable, minted into one sentence by {@link programPositionRefusalSentence}. Reading less than this reader
9
9
  * does (an option skipped, a chain member missed) is how one face used to vouch for a program another refused.
10
10
  */
11
- import { SHELL_CONTROL_PREFIXES, type SegmentTokens } from "../../shell/lex.js";
11
+ import { SHELL_CONTROL_PREFIXES, type LexedWord } from "../../shell/lex.js";
12
12
  /** The verdict every unresolvable / out-of-root / unbounded finding ends on. The findings differ in what
13
13
  * they found; the consequence is one consequence, and a copy that drifts reads as a second rule. */
14
14
  export declare const NOT_AUTO_ALLOWED = "\u2014 not auto-allowed";
15
15
  /** argv[0] as the program bash will resolve it: `/usr/bin/cat` and `cat` are one name. */
16
16
  export declare function programBasename(word: string): string;
17
17
  /**
18
- * The `NAME=` / `NAME+=` prefix bash reads as an ASSIGNMENT — the ONE spelling of that grammar, matched where
19
- * it is WRITTEN. Both operators, because bash reads both; the match IS the prefix, so a caller holding raw
20
- * text can ask whether its token carries one bare. Every walk that strips leading assignments reads this:
21
- * a second copy of the grammar is how one walk came to strip `A=x` and skip `A+=x`.
18
+ * The `NAME=` / `NAME+=` prefix as WRITTEN in a whitespace-split token. This is NOT the position reader's grammar
19
+ * — the reader reads the lexer's {@link LexedWord.assignment} fact — but the two readers that still walk the
20
+ * command TEXT rather than its words (the unverifiable-delete policy and the activity summary) read it, and
21
+ * they read it here so that they read one spelling. It retires with them.
22
22
  */
23
23
  export declare const ASSIGNMENT_PREFIX: RegExp;
24
- /**
25
- * The prefix this raw token BEGINS with, or `undefined`.
26
- *
27
- * `name` is the VARIABLE the word assigns to, which is the part before any subscript: `A[0]=x` writes one
28
- * element of `A`, so a reader asking "is `$A` settled" must not be told that `A[0]` is. `settlesWholeName` is
29
- * false for exactly the two spellings that do not settle it — an append (the earlier content is not in the
30
- * text) and a subscript (the other elements are not) — so a consumer of that question asks it once.
31
- */
24
+ /** The prefix a whitespace-split token BEGINS with, or `undefined` — the text readers' view of
25
+ * {@link ASSIGNMENT_PREFIX}; `name` is the variable before any subscript, `settlesWholeName` false for an
26
+ * append and for a subscript. */
32
27
  export declare function writtenAssignmentPrefix(token: string): {
33
28
  readonly prefix: string;
34
29
  readonly name: string;
@@ -40,18 +35,14 @@ export { SHELL_CONTROL_PREFIXES };
40
35
  export declare const SHELL_BLOCK_WORDS: ReadonlySet<string>;
41
36
  /** A control word is a keyword only when bash sees it BARE: `'if'` / `"do"` / `\\!` are command names (a program
42
37
  * looked up on PATH, whose failure changes nothing), so a quoted or escaped spelling never counts as one. */
43
- export declare function isBareControlWord(toks: SegmentTokens, i: number, words: ReadonlySet<string>): boolean;
38
+ export declare function isBareControlWord(words: readonly LexedWord[], i: number, set: ReadonlySet<string>): boolean;
44
39
  /**
45
- * A word bash strips as an ASSIGNMENT before it chooses the program.
46
- *
47
- * WHAT HAS TO BE BARE IS THE PREFIX, NOT THE WORD, and it is ONE question for BOTH operators. Bash decides this
48
- * before it removes any quoting, and only the NAME and the OPERATOR take part in it: quoting any of `NAME=` /
49
- * `NAME+=` makes the whole word a program name looked up on PATH (`'A=x'`, `'A+=x'`, `A\+=x`), while quoting the
50
- * VALUE does not (`A+=".env"` assigns `.env` exactly as `A+=.env` does — verified against the shell). A
51
- * whole-word test conflates the two and reads the quoted VALUE as a program name; a FOLDED-word test conflates
52
- * them the other way and reads the quoted PROGRAM NAME as an assignment, stepping over the real program.
40
+ * A word bash strips as an ASSIGNMENT before it chooses the program: the lexer's assignment fact, written BARE
41
+ * (the name, the brackets and the operator unquoted — a quoted VALUE still assigns, a quoted operator is a
42
+ * program name), read where bash accepts one (the lexer's own command-position state, which this reader's walk
43
+ * over control words and earlier assignments re-traces). One fact, two readers that must agree.
53
44
  */
54
- export declare function isLeadingAssignmentWord(toks: SegmentTokens, i: number): boolean;
45
+ export declare function isLeadingAssignmentWord(words: readonly LexedWord[], i: number): boolean;
55
46
  /**
56
47
  * Programs that turn their STANDARD INPUT into the OPERANDS of another program, and EXEC that program directly.
57
48
  *
@@ -189,7 +180,7 @@ export interface ProgramPosition {
189
180
  * WRITTEN word (judged on its basename, as the program it names) refuses the same way: it is where the program
190
181
  * would be, and the word in front took an option no row explains.
191
182
  */
192
- export declare function readProgramPosition(toks: SegmentTokens): ProgramPosition;
183
+ export declare function readProgramPosition(words: readonly LexedWord[]): ProgramPosition;
193
184
  /** Does this program position hand a program operands the walk cannot see? True when the chain re-enters through
194
185
  * stdin AND the program it runs is not one the caller accepts as a stream target. ONE reading, shared by every
195
186
  * consumer; WHICH targets are accepted is the caller's declaration, not this reader's. */
@@ -15,31 +15,13 @@ export function writtenAssignmentPrefix(token) {
15
15
  }
16
16
  export { SHELL_CONTROL_PREFIXES };
17
17
  export const SHELL_BLOCK_WORDS = new Set(["for", "select", "case", "esac", "fi", "done", "function", "coproc", "[[", "]]", "in"]);
18
- export function isBareControlWord(toks, i, words) {
19
- const word = toks.folded[i];
20
- return word !== undefined && toks.raw[i] === word && words.has(word);
18
+ export function isBareControlWord(words, i, set) {
19
+ const w = words[i];
20
+ return w !== undefined && w.raw === w.folded && set.has(w.folded);
21
21
  }
22
- export function isLeadingAssignmentWord(toks, i) {
23
- const word = toks.folded[i];
24
- const raw = toks.raw[i];
25
- if (word === undefined || raw === undefined)
26
- return false;
27
- const written = writtenAssignmentPrefix(word);
28
- return written !== undefined && prefixWrittenBare(raw, written);
29
- }
30
- function prefixWrittenBare(raw, written) {
31
- if (!raw.startsWith(written.name))
32
- return false;
33
- let at = written.name.length;
34
- if (written.prefix[at] === "[") {
35
- if (raw[at] !== "[")
36
- return false;
37
- const close = raw.indexOf("]", at);
38
- if (close < 0)
39
- return false;
40
- at = close + 1;
41
- }
42
- return raw.startsWith(written.appends ? "+=" : "=", at);
22
+ export function isLeadingAssignmentWord(words, i) {
23
+ const w = words[i];
24
+ return w !== undefined && w.assignment !== undefined && w.assignment.bare && w.atCommandPosition;
43
25
  }
44
26
  export const ARGV_REENTRY_PROGRAM_RE = /^xargs$/;
45
27
  export const ARGV_REENTRY_DEFAULT_PROGRAM = "echo";
@@ -63,7 +45,7 @@ export const LAUNCHER_TABLE = new Map([
63
45
  ], { requiredOperands: 1 })],
64
46
  ["time", {
65
47
  optionArity: new Map([
66
- ["-p", 0], ["-a", 0], ["-v", 0], ["-q", 0], ["--portability", 0], ["--append", 0], ["--verbose", 0], ["--quiet", 0], ["--help", 0], ["--version", 0],
48
+ ["-p", 0], ["-a", 0], ["-v", 0], ["-q", 0], ["--portability", 0], ["--append", 0], ["--verbose", 0], ["--quiet", 0], ["--help", 0], ["--version", 0], ["--", 0],
67
49
  ["-f", 1], ["-o", 1], ["--format", 1], ["--output", 1],
68
50
  ]),
69
51
  }],
@@ -108,11 +90,11 @@ function launcherOptionStep(row, tok) {
108
90
  return undefined;
109
91
  }
110
92
  export const PROGRAM_POSITION_REFUSAL_KINDS = ["option_at_program_position", "control_syntax_at_program_position"];
111
- export function readProgramPosition(toks) {
112
- const { folded } = toks;
93
+ export function readProgramPosition(words) {
94
+ const folded = words.map((w) => w.folded);
113
95
  const controlWords = [];
114
96
  let i = 0;
115
- while (i < folded.length && isBareControlWord(toks, i, SHELL_CONTROL_PREFIXES)) {
97
+ while (i < folded.length && isBareControlWord(words, i, SHELL_CONTROL_PREFIXES)) {
116
98
  const controlWord = folded[i];
117
99
  controlWords.push(i);
118
100
  i = pastLauncherOptions(controlWord, folded, i + 1);
@@ -122,7 +104,7 @@ export function readProgramPosition(toks) {
122
104
  return none(undefined, []);
123
105
  const written = i;
124
106
  const assignments = [];
125
- while (i < folded.length && isLeadingAssignmentWord(toks, i)) {
107
+ while (i < folded.length && isLeadingAssignmentWord(words, i)) {
126
108
  assignments.push(i);
127
109
  i++;
128
110
  }
@@ -1,4 +1,4 @@
1
- import { type SegmentTokens, type ShellSegmentConnector } from "../../shell/lex.js";
1
+ import { type LexedSegment, type LexedWord, type ShellLex, type ShellSegmentConnector, type WordPiece } from "../../shell/lex.js";
2
2
  /**
3
3
  * Bare command names `bash_readonly` permits out of the box, ALSO the default reversible set for the
4
4
  * The {@link import("./fs-bash.js").bashReversibilityProbe} classifier. **Coarse first filter, NOT a security
@@ -75,16 +75,15 @@ export interface LeadingCommandNameOptions {
75
75
  * Read a shell operator that sits inside a QUOTED region as ordinary argument TEXT rather than as an
76
76
  * operator, so `grep -E "a|b" f.log` parses as the single simple command it is.
77
77
  *
78
- * OPT-IN, and the two callers that set it share one premise: the whole command has ALREADY been refused
79
- * for backslashes / an expanding `$` / backticks / subshells / line breaks, so they are inside
80
- * {@link quoteMask}'s exactness envelope — the compound classify face
78
+ * OPT-IN, read off the one lexer's result: an operator character bash reads as text (inside quotes, or
79
+ * behind an escape) is text here. Two callers set it — the compound classify face
81
80
  * ({@link classifyCompoundReadonlyDetailed}) and the persisted permission-RULE lane (`RULE_LANE_FLOOR`),
82
- * whose segments come out of that same gate ({@link splitShellCompoundSegments}). The `bash_readonly`
83
- * DECLARATION face does NOT set it: its contract is "a single simple command with no operator characters
84
- * at all", and it runs on raw tool input with no whole-string gate in front of it.
81
+ * whose segments come out of the same residual predicate ({@link splitShellCompoundSegments}). The
82
+ * `bash_readonly` DECLARATION face does NOT set it: its contract is "a single simple command with no
83
+ * operator characters at all", and it runs on raw tool input with no whole-string gate in front of it.
85
84
  *
86
- * Fail-closed on unbalanced quoting: with a region left open, the mask is not trusted and every
87
- * operator character counts, exactly as without this option.
85
+ * Fail-closed on a command the lexer cannot delimit: every operator character then counts, exactly as
86
+ * without this option.
88
87
  */
89
88
  quotedOperatorsAreText?: boolean;
90
89
  /**
@@ -146,10 +145,36 @@ export declare function coarseReadonlyCheck(command: string, allow: ReadonlySet<
146
145
  * Exported because the tolerating caller owes a PER-SEGMENT judgement in its place, and that judgement
147
146
  * must read the same character class this module tolerated — two hand-kept lists would drift, and a
148
147
  * drift here is a segment nobody judged. Says nothing about what a caller should DO about a hit: the
149
- * rule lane's offer face contributes no member for such a segment; the read-only classify face removes
150
- * its exempt spellings before splitting at all.
148
+ * rule lane's offer face contributes no member for such a segment; the read-only classify face passes
149
+ * only the two data-free spellings ({@link isDataFreeRedirection}).
151
150
  */
152
151
  export declare function carriesShellRedirection(text: string): boolean;
152
+ /** What a caller asks the residual predicate to pass: see {@link strictResidualRefusal}. */
153
+ export interface StrictResidualOptions {
154
+ /** `<`/`>` are not refused and a redirection is the segment's own business (the rule lane's offer reading). */
155
+ readonly tolerateRedirection?: boolean;
156
+ /** Only the two data-free spellings pass ({@link isDataFreeRedirection}); every other redirection refuses. */
157
+ readonly dataFreeRedirections?: boolean;
158
+ /** The declared home: a word that spells it ({@link homeExpansionOf}) is not an expansion to refuse — unless the
159
+ * command may rebind `HOME` ({@link homeMayBeRebound}), which makes the declared home a guess. */
160
+ readonly homeDir?: string;
161
+ }
162
+ /**
163
+ * THE classify faces' whole-string gate, as a PREDICATE over the one lexer's result: the residual space those
164
+ * faces live in is what this function does not refuse. Four passes, in the order the gate's sentences were
165
+ * always minted, so a refused command is refused for the same stated reason:
166
+ * 1. the quote-blind class ({@link SHELL_SEGMENT_QUOTE_BLIND_REJECT}) over every written spelling — a word, a
167
+ * redirection target under `tolerateRedirection` — plus every group, here-document, line-break connector
168
+ * and (unless tolerated or data-free) redirection token;
169
+ * 2. the expansion character where bash expands it ({@link expandsDollar}), the home-directory word excepted
170
+ * under `homeDeclared`;
171
+ * 3. a backgrounding `&` and a `|&`;
172
+ * 4. a word whose value the text does not settle ({@link UNRESOLVED_SENTENCE}) — a compound array, an
173
+ * off-table ANSI-C escape, a subscript or parameter expansion this lexer does not read.
174
+ * A command the lexer cannot delimit is refused for the class if its text carries one (the reading the gate
175
+ * gave an unbalanced string), else for the parse failure. Returns the refusal REASON, or `undefined` to proceed.
176
+ */
177
+ export declare function strictResidualRefusal(lexed: ShellLex, source: string, opts?: StrictResidualOptions): string | undefined;
153
178
  /** What {@link splitShellCompoundSegments} produces: the connector-delimited segments, plus which of
154
179
  * them a PIPE fed (the read-only classifier's stdin arm needs that distinction; a rule lane does not). */
155
180
  export interface ShellCompoundSegments {
@@ -163,39 +188,28 @@ export interface ShellCompoundSegments {
163
188
  * one this walk cannot verify. A consumer that does not read it gets the unverified reading, which is
164
189
  * the fail-closed one. */
165
190
  connectors: ShellSegmentConnector[];
191
+ /** Parallel to {@link segments}: the segments as the lexer minted them — the words (with their facts) and
192
+ * the redirections of each. A consumer that tokenizes reads these; the text is for the sentences. */
193
+ lexed: readonly LexedSegment[];
166
194
  }
167
195
  /**
168
196
  * Split a compound command into the segments bash would run, or refuse the whole string.
169
197
  *
170
- * THE one segmentation in this repository. Extracted from {@link classifyCompoundReadonlyDetailed}
171
- * (whose behaviour it reproduces exactly) when a SECOND face needed segments — the persisted
172
- * permission-rule lane, which must judge every segment of a compound against the deny/ask rules rather
173
- * than reading the whole string as one unmatched blob. The same argument that keeps ONE
174
- * {@link parseLeadingCommandName} applies with more force here: a second splitter would drift, and
175
- * drift in a segmentation is drift in what a deny rule is even looking at.
176
- *
177
- * Three steps, in this order and load-bearing:
178
- * 1. WHOLE-STRING hard reject ({@link hardRejectReason}) — redirection, substitution,
179
- * subshells, escapes, line breaks. This is what makes step 3's quote mask EXACT: with those
180
- * characters gone, `'`/`"` pairing is the entirety of quoting, so the mask's regions are exactly
181
- * bash's quoted regions and the unquoted connectors are exactly bash's command boundaries. Callers
182
- * that tolerate a narrow redirection subset (the read-only classify face and its two data-free
183
- * spellings) remove those words BEFORE calling; nothing here restores them. `redirection:
184
- * "tolerate"` narrows this step to {@link rejectedSansRedirection} — the mask argument
185
- * is untouched (`<`/`>` neither quote nor bound) and the refusal moves to the caller's per-segment
186
- * vetting via {@link carriesShellRedirection}. The default refuses the whole string as it always did.
187
- * 2. one trailing `;` is stripped — a no-op terminator, not an empty command. OPTIONAL, because it is
188
- * the one step whose answer depends on what the caller is asking. A face that asks "which programs
189
- * does this run" wants it stripped (`ls;` runs `ls`, nothing else). A face that asks "is this ONE
190
- * command" must NOT strip it: `"keep"` makes `ls;` two segments, the second empty, so the caller's
191
- * own empty-command rule puts the whole string outside its lane — which is what keeps a statement
192
- * like "this rule form never matches a command containing a connector" literally true instead of
193
- * true-except-for-one-spelling.
194
- * 3. the quote-aware connector scan over `;` `&&` `||` `|` — the strict mode of the ONE lexer
195
- * ({@link lexShellText}). A lone `&` is REFUSED rather than split on: it backgrounds a process that
196
- * outlives the command, which is not a combinator any face here is willing to reason about.
197
- * Unbalanced quoting falls back to the quote-blind scan — strictly MORE segments, each of which the
198
- * caller must still vet, i.e. the fail-closed direction.
198
+ * THE one segmentation in this repository, and the one place the classify faces' residual predicate is
199
+ * applied: the persisted permission-rule lane and the read-only classify face both read their segments
200
+ * here, so a drift in what a deny rule is even looking at cannot open between them.
201
+ *
202
+ * Two steps: the trailing-terminator strip, then ONE lex ({@link lexShell}) judged by
203
+ * {@link strictResidualRefusal}. The strip is OPTIONAL because it is the one step whose answer depends on
204
+ * what the caller is asking. A face that asks "which programs does this run" wants it stripped (`ls;` runs
205
+ * `ls`, nothing else). A face that asks "is this ONE command" must NOT strip it: `"keep"` makes `ls;` two
206
+ * segments, the second empty, so the caller's own empty-command rule puts the whole string outside its lane
207
+ * — which is what keeps a statement like "this rule form never matches a command containing a connector"
208
+ * literally true instead of true-except-for-one-spelling. `redirection` picks which redirections the
209
+ * predicate passes: none (the default, the incumbent whole-string refusal), every one (`"tolerate"` — the
210
+ * refusal moves to the caller's per-segment vetting via {@link carriesShellRedirection}), or the two
211
+ * data-free spellings (`"data-free"`, the read-only classify face). `homeDir` lets the word spelling the
212
+ * declared home through the expansion refusal.
199
213
  *
200
214
  * Empty segments are NOT refused here (`;;`, a leading `;`, a trailing `&&`) — they come back as empty
201
215
  * strings, and every caller's per-segment vetting rejects them through its own empty-command rule. The
@@ -203,7 +217,8 @@ export interface ShellCompoundSegments {
203
217
  */
204
218
  export declare function splitShellCompoundSegments(source: string, options?: {
205
219
  trailingTerminator?: "strip" | "keep";
206
- redirection?: "reject" | "tolerate";
220
+ redirection?: "reject" | "tolerate" | "data-free";
221
+ homeDir?: string;
207
222
  }): ShellCompoundSegments | {
208
223
  reject: string;
209
224
  };
@@ -213,7 +228,7 @@ export declare function splitShellCompoundSegments(source: string, options?: {
213
228
  * {@link coarseReadonlyCheck} (the single {@link parseLeadingCommandName} parser + bare-name allowlist).
214
229
  * All segments green ⇒ read-only. Returns a reason when rejected, else undefined.
215
230
  *
216
- * G2 — the split CARRIES QUOTE STATE ({@link quoteMask}): a connector character inside a quoted region is
231
+ * G2 — the split CARRIES QUOTE STATE (the one lexer): a connector character inside a quoted region is
217
232
  * argument text, not a boundary, so `grep -E "error:|BUILD FAILED" x.log | tail -10` is two segments
218
233
  * (`grep …` and `tail -10`) rather than four, and the phantom head `BUILD` no longer decides the verdict.
219
234
  * The whole compound face is what this fixes: a report of an every-command approval prompt on a read-only
@@ -221,8 +236,8 @@ export declare function splitShellCompoundSegments(source: string, options?: {
221
236
  * ordinary spelling of that idiom.
222
237
  *
223
238
  * Soundness — why the quote-aware split is NOT a relaxation. Backslash/`$`/backtick/subshells/redirection/
224
- * line breaks are whole-string rejected first (the two neutral forms in {@link EXEMPT_REDIRECTION} are
225
- * removed beforehand, and they move no data into any file), so in the residual space `'`/`"` pairing is
239
+ * line breaks are refused first (the two data-free forms of {@link isDataFreeRedirection} pass, and they
240
+ * move no data into any file), so in the residual space `'`/`"` pairing is
226
241
  * the ENTIRETY of quoting and the mask's regions are exactly bash's quoted regions. The unquoted connectors it splits on are therefore exactly bash's command boundaries:
227
242
  * every program bash runs still starts at the head of some segment this classifier vetted, and its argv[0]
228
243
  * is the head token (a token carrying a quote/brace/glob/tilde character is rejected by the parser's
@@ -231,14 +246,13 @@ export declare function splitShellCompoundSegments(source: string, options?: {
231
246
  * program was ever going to run — are gone. Those could only ever ADD a rejection, so removing them
232
247
  * removes false demotions and admits no additional program.
233
248
  *
234
- * UNBALANCED QUOTING falls back to the quote-blind scan (fail-closed). A string with an unterminated quote
235
- * is one bash cannot parse into commands at all, so there is no "true" segmentation to be accurate about;
236
- * the blind scan yields strictly MORE segments, each of which must still pass the allowlist.
249
+ * UNBALANCED QUOTING is a parse failure, refused whole (fail-closed). A string with an unterminated quote
250
+ * is one bash cannot parse into commands at all, so there is no "true" segmentation to be accurate about.
237
251
  *
238
252
  * A LONE `&` is hard-rejected (not a connector): it backgrounds a process that survives the shell — the node
239
253
  * execution env spools stdio and deliberately leaves descendants alive on completion — escaping TaskStop and
240
254
  * background-lifecycle governance entirely. Backgrounding is not a read-only combinator, even for a listed
241
- * reader (`tail -f x &` outlives the call). `|&` falls out of the same scan (its `&` reads as lone → reject).
255
+ * reader (`tail -f x &` outlives the call). `|&` is refused by name.
242
256
  * One TRAILING `;` is stripped (a no-op terminator); any other empty segment (`;;`, leading `;`, trailing
243
257
  * `&&`/`|`) rejects via the parser's empty-command rule.
244
258
  *
@@ -269,7 +283,7 @@ export interface BashReadonlyRootBoundary {
269
283
  * should pass the LIVE working directory, or a `..` operand will be judged against the wrong base. */
270
284
  cwd?: string;
271
285
  /** Expansion for a leading `~` — and the value the read faces substitute for the `$HOME` / `${HOME}` word
272
- * spellings before segmenting ({@link foldHomeVariableSpelling}). Without it a `~`-rooted operand is unresolvable
286
+ * spellings ({@link homeExpansionOf}). Without it a `~`-rooted operand is unresolvable
273
287
  * and demotes (with no out-of-root signal — the classifier does not know where it points, which is a
274
288
  * different verdict from knowing it points outside). */
275
289
  homeDir?: string;
@@ -400,39 +414,29 @@ export declare function resolveOperandSpellings(base: string | undefined, operan
400
414
  * for the reason it is not a launcher: it hands a BUILT command line to a shell. */
401
415
  export declare const SHELL_REENTRY_PROGRAM_RE: RegExp;
402
416
  /**
403
- * Does this RAW token carry a metacharacter the shell EXPANDS, unquoted?
404
- *
405
- * The boundary scan below judges the token text as if it were the argv word the program receives. Quote
406
- * removal (folded above) is the only rewriting step it models — but brace expansion runs BEFORE tilde
407
- * expansion and before the word ever reaches the program, so `cat {<abs>,}` hands the program an absolute
408
- * path this scan never saw, and `ls {~,}` hands it a home directory the `~` arm of
409
- * {@link resolveOperandLexically} never got the chance to refuse. Both were real out-of-root reads.
410
- *
411
- * Rather than emulate expansion (the classifier resolves paths lexically and has no filesystem, so it
412
- * cannot know what a brace list or a glob expands TO), an operand carrying one of these unquoted is
413
- * declared unresolvable — the same verdict `~user` already gets, and the same direction the whole file
414
- * errs in. Quoting is honoured exactly as bash does it: nothing expands inside single quotes, while `$`
415
- * and a backtick still expand inside double quotes (braces do not).
416
- *
417
- * Deliberately NOT in the set: the GLOB characters `* ? [ ]`. A glob expands only to names that already
418
- * exist in the directory the pattern itself names, and a path component can never match a `/` — so a glob
419
- * cannot manufacture a traversal that is not already textually present (`cat ../*` still carries `../`
420
- * and is judged on it), while listing them would demote the everyday `ls *.txt`. `$`, backtick and the
421
- * parentheses are redundant against a compliant caller (both faces reject them upstream, whole-string;
422
- * a home-directory variable word never arrives as `$` — {@link foldHomeVariableSpelling} substituted the
423
- * declared value before segmentation) and are kept here only so this scan is self-standing.
417
+ * Does the shell REWRITE this word before the program sees it — a brace list, a parameter, command, process or
418
+ * arithmetic expansion, or a `$` / backtick / brace character where bash reads it (unquoted; `$` and a backtick
419
+ * also inside double quotes)? Read off the lexer's pieces. Brace expansion runs BEFORE tilde expansion and
420
+ * before the word ever reaches the program, so `cat {<abs>,}` hands the program an absolute path no spelling
421
+ * shows, and `ls {~,}` a home directory the `~` arm of {@link resolveOperandLexically} never got to refuse —
422
+ * both were real out-of-root reads. Rather than emulate expansion (the classifier resolves paths lexically and
423
+ * has no filesystem), such a word is declared unresolvable — the direction the whole file errs in.
424
+ *
425
+ * Deliberately NOT here: the GLOB characters ({@link globInWord}) — a glob only ever names entries of a
426
+ * directory that is already on the command line — and a tilde, which the resolver reads.
424
427
  */
425
- export declare function hasUnquotedExpansionMetachar(rawToken: string): boolean;
428
+ export declare function expansionInWord(w: {
429
+ readonly pieces: readonly WordPiece[];
430
+ }): boolean;
426
431
  /**
427
- * The same quote walk for PATHNAME expansion (`*`, `?`, `[`). Split from
428
- * {@link hasUnquotedExpansionMetachar} because the two have different consequences, not different
429
- * detection: a brace/variable/command expansion can produce ANY word (including a fresh absolute path or
430
- * an extra operand), so it is unresolvable; a glob only ever names entries of a directory that is already
431
- * on the command line, so a caller holding a filesystem can decide it — see
432
- * {@link CompoundReadonlyVerdict.undecidedPaths}. Both quoting forms suppress globbing in bash, so unlike
433
- * the expansion walk there is no `"`-open arm here.
432
+ * Does this word carry PATHNAME expansion (`*`, `?`, `[`), unquoted? Split from {@link expansionInWord} because
433
+ * the two have different consequences, not different detection: a brace/variable/command expansion can produce
434
+ * ANY word, so it is unresolvable; a glob only ever names entries of a directory that is already on the command
435
+ * line, so a caller holding a filesystem can decide it. Both quoting forms suppress globbing in bash.
434
436
  */
435
- export declare function hasUnquotedGlobMetachar(rawToken: string): boolean;
437
+ export declare function globInWord(w: {
438
+ readonly pieces: readonly WordPiece[];
439
+ }): boolean;
436
440
  /**
437
441
  * Does this token supply the verb's FIRST-OPERAND slot, so that the first operand is a FILE rather than the
438
442
  * slot ({@link VerbOptionSlots.firstOperand})? One reader, every verb with such a row.
@@ -811,15 +815,6 @@ export declare function inlineProgramClause(model: InlineProgramModel, args: rea
811
815
  * command name. Names-only traversals (`find`, `ls -R`, `du`, `tree`, an archive write) are in the table
812
816
  * and answer `false` here — see {@link RecursiveReadFormModel.discloses}. */
813
817
  export declare function segmentSelectsRecursiveRead(name: string, args: readonly string[]): boolean;
814
- /** `raw` is the word as WRITTEN; it defaults to the folded spelling, which is the reading that expands most. */
815
- export declare function assignmentWordPayload(word: string, raw?: string): {
816
- kind: "value";
817
- value: string;
818
- } | {
819
- kind: "unresolved";
820
- reason: string;
821
- value?: string;
822
- } | undefined;
823
818
  /** WHICH programs are DECLARED not to open the value of a `NAME=value` ARGUMENT — an allow-list, because
824
819
  * the other polarity was measured false. `make PREFIX=/usr/local`, `cmake -D…`, `docker run -e HOME=/root`
825
820
  * name a build prefix and an environment value their program never opens, and judging those was the one
@@ -839,31 +834,48 @@ export declare const ASSIGNMENT_VALUE_NOT_PATHS: ReadonlySet<string>;
839
834
  * disagree. The LEADING assignments (both spellings) are the program-position reader's own step, so this
840
835
  * seat no longer strips them a second time: a second strip read the quoted `'A+=x'` as an assignment the
841
836
  * one reader calls a program name, which is two readers with two answers. */
842
- export declare function argvProgramOf(tokens: SegmentTokens): {
837
+ export declare function argvProgramOf(words: readonly LexedWord[]): {
843
838
  program: string;
844
839
  from: number;
845
840
  } | undefined;
846
841
  export declare function attachedOptionPayloads(tok: string): string[];
847
842
  /**
848
- * The home directory's VARIABLE spellings, SUBSTITUTED with the declared value before a read face segments the
849
- * command: `$HOME` / `${HOME}` at the START of a word (an empty quote pair before it included), unquoted or
850
- * double-quoted, followed by `/` or by the word's end. Those are exactly the words whose expansion is the home
851
- * directory and nothing else, so the word is rewritten to what bash hands the program — the value itself, in
852
- * place, with the word's quote structure untouched (`"$HOME/x"` → `"/home/u/x"`, `$HOME"/x"` → `/home/u"/x"`)
853
- * — and every arm reads an ordinary absolute path; no tilde is minted, so the tilde-prefix rule of
854
- * {@link tokenizeSegment} judges only tildes the command actually wrote. Substituted only against a DECLARED
855
- * home (`homeDir`): with none declared the child's `$HOME` may differ from what `~` falls back to, so the
856
- * spelling is left as written and meets the substitution hard-reject. The value must survive this file's own
857
- * reading of the rewritten word: the UNQUOTED spelling only for a value with no `$IFS` whitespace, glob or
858
- * shell metacharacter (word splitting / pathname expansion / a connector this file would then split on — none
859
- * of which a real expansion is subject to here), the double-quoted one for any value carrying no `"`, `$`,
860
- * backtick, backslash or line break (the characters that would change the quote structure or meet the
861
- * hard-reject). Everything else is left as written: a single-quoted spelling does not expand, and a mid-word
862
- * or suffixed one (`x$HOME`, `$HOME.bak`, `$HOMES`) still meets the substitution hard-reject. Bails whole on
863
- * an escape or a backtick in the command (the hard-reject refuses those strings regardless, and the quote
864
- * walk below is exact only without them) and on unbalanced quoting.
843
+ * The home directory's VARIABLE spelling as a word FACT: `$HOME` / `${HOME}` as the word's FIRST piece,
844
+ * unquoted or double-quoted, followed by `/`, by a double-quoted piece, or by nothing (unquoted), or — inside
845
+ * the quotes — by `/` or the closing quote; every later piece literal. Those are exactly the words whose expansion
846
+ * is the DECLARED home and nothing else, so `spelled` is what bash hands the program: `<home><rest>`. Everything
847
+ * else is left to be the expansion it is: a single-quoted spelling does not expand, a mid-word or suffixed one
848
+ * (`x$HOME`, `$HOME.bak`, `$HOMES`) is a substitution the classify faces refuse and the read station cannot
849
+ * place, and an unquoted spelling of a home bash would split or glob ({@link HOME_EXPANDS_TO_ONE_WORD}) is not
850
+ * one word. The caller reads it only against a declared home (with none declared the child's `$HOME` may differ
851
+ * from what `~` falls back to) and only when the command cannot rebind `HOME` ({@link homeMayBeRebound}).
865
852
  */
866
- export declare function foldHomeVariableSpelling(command: string, homeDir: string | undefined): string;
853
+ export declare function homeExpansionOf(w: LexedWord, home: string): {
854
+ readonly spelled: string;
855
+ } | undefined;
856
+ /** May this command text REBIND `HOME` before a later word expands it? Read LEXICALLY, not by grammar: the text
857
+ * spells the name `HOME` somewhere other than as a plain read ({@link HOME_PLAIN_READ}) — after `\<newline>`
858
+ * continuations are joined and quotes (with the `$` of a `$'…'` / `$"…"` opener) and backslashes are removed, so
859
+ * `"HO"ME`, `HO\ME`, `HO$'ME'` and an `eval` string spell it the same as a bare word. That covers an assignment at ANY nesting depth (a `{ … }` group, a
860
+ * function body, a `case` arm, a loop), a declaration builtin's operand, a `for` / `select` variable, a `read` /
861
+ * `printf -v` / `mapfile` / `getopts` / `unset` target, a nameref value, an arithmetic or `${HOME:=…}` assignment,
862
+ * without naming any of them. A reader that substitutes the declared home for `$HOME` / `${HOME}` / a leading `~`
863
+ * asks this first; a text it answers yes for leaves those words unplaced (the fail-closed side).
864
+ * WHY not a grammar reading: every "where does the name stand" reading of this question (the leading assignment
865
+ * words, a declaration builtin's operands) has had a nesting level or a builtin it did not see.
866
+ * ACCEPTED COST: a text that merely spells the name while reading through the home — `echo HOME; cat ~/x`, a longer
867
+ * name ending in it (`MY_HOME=…`), a comment — asks instead of running.
868
+ * A name the shell COMPUTES spells no `HOME` to find, so four more readings count as a rebinding: an unquoted GLOB
869
+ * word whose pattern can match the name `HOME` or an array element `HOME[…]` of it ({@link globMayMatchHome} —
870
+ * `read H?ME`, `unset [H]OME`, `read H?ME[0]`, `unset *` glob against the working directory when an entry named
871
+ * `HOME` sits there); an unquoted EXTGLOB opener ({@link hasUnquotedExtglobOpener} — `read !(x)` matches nearly
872
+ * every name); an unquoted BRACE list ({@link wordHasBraceList} — `read H{O,X}ME` expands to a list whose first
873
+ * word is the name); an option word that carries a glob ({@link optionWordCarriesGlob} — `read -aH?ME`, where a
874
+ * matched entry named `-aHOME` hands the builtin `-a HOME`); and a `printf -v` name that is not a literal
875
+ * identifier ({@link printfNamesComputed} — `printf` opens no path, so its words are no candidate of their own).
876
+ * Elsewhere a computed word is its own unresolvable candidate, and an escape the lexer does not decode is
877
+ * unresolved on its own (asked). */
878
+ export declare function homeMayBeRebound(text: string): boolean;
867
879
  /**
868
880
  * {@link classifyCompoundReadonly} with the structured verdict. The name/operator/blocking arms
869
881
  * run FIRST and unchanged, so an existing demotion keeps its exact reason and never acquires the
@@ -949,16 +961,12 @@ export declare const POLL_LOOP_MAX_SLEEP_SECONDS = 600;
949
961
  * verdict carrying {@link CompoundReadonlyVerdict.undecidedPaths} (an unexpanded glob) rejects,
950
962
  * because the probe face is reason-only and a repeated read of a runtime-decided target must not
951
963
  * auto-allow.
952
- * G5 STRUCTURE SOUNDNESS (the parser sees what bash executes). Whitespace is normalized first: any
953
- * non-ASCII / control whitespace ({@link NON_ASCII_WHITESPACE}) rejects the whole command, so this
954
- * grammar's `[ \t]`/`\s` word split and bash's `$IFS` split cannot diverge (an NBSP-joined
955
- * `ls x` never reads as head `ls` here while bash runs one word). Quote pairing is then checked
956
- * whole-string ({@link pollLoopQuotesBalanced}); the var/iterable region admits no quote character
957
- * at all; so the `for … ; do … ; done` skeleton the anchored regex matches is exactly the one bash
958
- * parses — a `done` hidden in quotes either unbalances (reject) or leaves a body token that fails
959
- * the allowlist. Within the body, the quote-blind `;` split inherits the compound face's
960
- * soundness argument: with all expansion metacharacters banned, quotes only ever HIDE connectors,
961
- * so mis-splits are over-fine and the error direction is reject, never widen.
964
+ * G5 STRUCTURE SOUNDNESS (the parser sees what bash executes). Any non-ASCII / control whitespace
965
+ * ({@link NON_ASCII_WHITESPACE}) rejects the whole command up front, the reference implementation's
966
+ * form. The skeleton is then read off the ONE lexer's segments and words ({@link pollLoopShapeOf}) — the
967
+ * same reading bash gives the text: an unterminated quote is a parse failure (reject), a `done` hidden in
968
+ * quotes is a word that is not `done`, and the var/iterable words must be written bare. The body's
969
+ * segments are the lexer's, so a connector inside quotes is text, never a boundary.
962
970
  * G6 ENVIRONMENT INTEGRITY. The loop variable must be a SINGLE LOWERCASE LETTER: the `for`
963
971
  * assignment mutates a shell variable, and a name like `PATH`/`IFS`/`LD_PRELOAD`/`BASH_ENV`
964
972
  * would change how the body's commands RESOLVE (`for PATH in dir; do cat x; done` would run