@sema-agent/core 7.47.0 → 7.48.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 (44) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/core/allow-entry.js +1 -1
  3. package/dist/core/checkpoint-store.d.ts +1 -1
  4. package/dist/core/mcp-server-spec.d.ts +1 -1
  5. package/dist/core/runner/park-commit.d.ts +1 -1
  6. package/dist/core/safety-axis-vocab.d.ts +1 -1
  7. package/dist/core/safety-merge-corpus.d.ts +1 -1
  8. package/dist/core/session-policy-store.d.ts +6 -3
  9. package/dist/core/session-policy-store.js +28 -10
  10. package/dist/core/shell-wrapper-table.d.ts +2 -2
  11. package/dist/core/shell-wrapper-table.js +1 -1
  12. package/dist/core/tool-name-aliases.d.ts +9 -8
  13. package/dist/core/tool-name-aliases.js +2 -0
  14. package/dist/core/tool-policy.js +4 -5
  15. package/dist/orchestration/workflow.js +2 -2
  16. package/dist/prompt-assembly/types.d.ts +1 -1
  17. package/dist/shell/lex.js +30 -4
  18. package/dist/tools/fs/bash-program-position.d.ts +7 -0
  19. package/dist/tools/fs/bash-program-position.js +5 -2
  20. package/dist/tools/fs/index.d.ts +3 -2
  21. package/dist/tools/fs/index.js +2 -2
  22. package/dist/tools/fs/program-read-model.d.ts +60 -5
  23. package/dist/tools/fs/program-read-model.js +100 -36
  24. package/dist/tools/fs/program-read-model.rows.js +13 -5
  25. package/dist/tools/fs/read-deny-home.d.ts +20 -1
  26. package/dist/tools/fs/read-deny-home.js +28 -7
  27. package/dist/tools/fs/read-deny.d.ts +13 -6
  28. package/dist/tools/fs/read-deny.js +12 -3
  29. package/dist/tools/fs/removal-targets.js +12 -3
  30. package/dist/tools/fs/shell-read-argv.d.ts +4 -7
  31. package/dist/tools/fs/shell-read-argv.js +5 -7
  32. package/dist/tools/fs/shell-read-bindings.d.ts +34 -9
  33. package/dist/tools/fs/shell-read-bindings.js +166 -60
  34. package/dist/tools/fs/shell-read-establish.js +3 -4
  35. package/dist/tools/fs/shell-read-plan.d.ts +1 -14
  36. package/dist/tools/fs/shell-read-plan.js +102 -101
  37. package/dist/tools/fs/shell-read-start.d.ts +57 -0
  38. package/dist/tools/fs/shell-read-start.js +92 -0
  39. package/dist/tools/fs/shell-read-word.d.ts +35 -6
  40. package/dist/tools/fs/shell-read-word.js +85 -9
  41. package/dist/tools/fs/shell-reentry-text.d.ts +23 -21
  42. package/dist/tools/fs/shell-reentry-text.js +31 -26
  43. package/package.json +1 -1
  44. package/wire-changes/core@7.48.0.json +252 -0
@@ -10,11 +10,9 @@ import { type Read } from "./shell-read-word.js";
10
10
  import { type BindingTable, type FloorValue } from "./shell-read-bindings.js";
11
11
  import { subcommandOf, type ArgRole, type ProgramRow } from "./program-read-model.js";
12
12
  import type { ReadCandidate, ReadCandidateProvenance } from "./shell-read-plan.js";
13
- /** A candidate before the threading fills its bases. `alternatives`: the other spellings its word may expand to (a name
14
- * bound to several values), each with the pattern its names are matched by — one candidate each once the segment's reads
15
- * are read. */
13
+ /** A candidate before the threading fills its bases. `floors`: the spellings its word's names left for the deny judge
14
+ * alone (a word whose names are unknown here). */
16
15
  export type Bare = Omit<ReadCandidate, "bases"> & {
17
- readonly alternatives?: readonly Pick<ReadCandidate, "spelled" | "pattern">[];
18
16
  readonly floors?: readonly FloorValue[];
19
17
  };
20
18
  /** What reading a segment's words found beside its candidates. */
@@ -36,9 +34,8 @@ export declare function pathSubcommandOf(row: ProgramRow | undefined, args: read
36
34
  /** The sentence of inline program text the station does not read. */
37
35
  export declare const programTextSentence: (language: string) => string;
38
36
  /** One candidate without its bases (filled by the threading pass): the word's spelling — or `spelled`, a caller's
39
- * reading of PART of it (an option payload, a `key=value` value) — with the word's own expansion facts. A word whose
40
- * names may hold several values offers each as its whole spelling; a PART of such a word is not followed through them
41
- * (it is unresolvable). */
37
+ * reading of PART of it (an option payload, a `key=value` value) — with the word's own expansion facts. The word is read
38
+ * in one reading of the segment's names (`segmentReadingsOf`): its names hold one value each, or are unknown. */
42
39
  export declare function bare(r: Read, provenance: ReadCandidateProvenance, spelled?: string): Bare;
43
40
  /** The reads one value of a declared role hands over: a candidate per file it names, or — a value the program expands
44
41
  * past the listed bound — the word itself, unresolvable. A mount source is a read the daemon makes of the whole tree
@@ -1,5 +1,5 @@
1
1
  import { literalTildeSpelling } from "../../shell/lex.js";
2
- import { expandsOf, patternOf, readOfText, rewritten, spellingOf } from "./shell-read-word.js";
2
+ import { expandsOf, partPattern, readOfText, rewritten } from "./shell-read-word.js";
3
3
  import { arithmeticSubstitutions } from "./shell-read-bindings.js";
4
4
  import { argvProgramOf, attachedOptionPayloads, isPathShapedToken } from "./bash-readonly-classifier.js";
5
5
  import { assignmentArgumentsAreNotPaths, evaluatesOperands, messageWords, moduleTailOf, objectPathsOf, opensNoOperand, programRow, programTextMayOpen, programTextOpensOf, readArgs, selectsContentsTraversal, slotValueCandidates, subcommandOf, valuesAreLiteral, walksCwdWithoutOperand, wordsReadBySomeImplementations, } from "./program-read-model.js";
@@ -10,16 +10,13 @@ export function pathSubcommandOf(row, args) {
10
10
  export const programTextSentence = (language) => `runs inline ${language} program text, which may open files this station does not read`;
11
11
  export function bare(r, provenance, spelled = r.folded) {
12
12
  const whole = spelled === r.folded;
13
- const alternatives = r.values !== undefined && r.values.length > 1 ? r.values.slice(1).map((v) => { const pattern = patternOf(v); return { spelled: spellingOf(v), ...(pattern !== undefined ? { pattern } : {}) }; }) : undefined;
14
- if (!whole && alternatives !== undefined)
15
- return { spelled, raw: r.raw, provenance, expands: "unresolvable" };
13
+ const pattern = whole ? r.pattern : partPattern(r, spelled);
16
14
  return {
17
15
  spelled,
18
16
  raw: r.raw,
19
17
  provenance,
20
18
  expands: r.expands,
21
- ...(whole && r.pattern !== undefined ? { pattern: r.pattern } : {}),
22
- ...(whole && alternatives !== undefined ? { alternatives } : {}),
19
+ ...(pattern !== undefined ? { pattern } : {}),
23
20
  ...(whole && r.homeFloor !== undefined ? { floorSpelled: r.homeFloor } : {}),
24
21
  ...(whole && r.floorValues !== undefined ? { floors: r.floorValues } : {}),
25
22
  };
@@ -64,8 +61,9 @@ export function pushAssignment(reads, w, provenance) {
64
61
  if (!settled && unsettled !== undefined)
65
62
  reads.push(unsettled);
66
63
  const substitutes = a.value.expansions.some((p) => p.expansion?.kind === "command" || p.expansion?.kind === "process" || p.expansion?.kind === "arithmetic");
64
+ const pattern = named === undefined ? undefined : partPattern({ folded: w.folded, ...(w.judged !== undefined ? { pattern: w.judged } : {}) }, named);
67
65
  if (named !== undefined && !substitutes)
68
- reads.push({ spelled: named, raw: w.raw, provenance, expands: expandsOf(w) });
66
+ reads.push({ spelled: named, raw: w.raw, provenance, expands: expandsOf(w), ...(pattern !== undefined ? { pattern } : {}) });
69
67
  if (a.value.folded.includes(":"))
70
68
  for (const m of a.value.folded.split(":"))
71
69
  if (m !== "") {
@@ -36,10 +36,12 @@ export type BoundValue = {
36
36
  readonly spelled: string;
37
37
  readonly glob?: never;
38
38
  readonly parts?: never;
39
+ readonly fields?: readonly string[];
39
40
  } | {
40
41
  readonly spelled: string;
41
42
  readonly glob: true;
42
43
  readonly parts: readonly ValuePart[];
44
+ readonly fields?: never;
43
45
  };
44
46
  /** One run of a pattern {@link BoundValue}: literal text, or a pattern — the shell expands its characters — `text` as
45
47
  * written (what a shell re-reading it matches by), `judged` as a reader judges the names it matches. */
@@ -56,7 +58,14 @@ export type ValuePart = {
56
58
  export interface FloorValue {
57
59
  readonly spelled: string;
58
60
  readonly glob?: true;
61
+ /** A pattern's spelling as a reader judges the names it matches, where it is not `spelled` ({@link judgedSpellingOf}). */
62
+ readonly pattern?: string;
59
63
  }
64
+ /** A pattern value's names as a reader judges them, where that is not its spelling: its runs, a class the judge widens
65
+ * as `?` (the lexer's `WordExpansion.pattern.judged`), a leading `~` the literal tilde it is. */
66
+ export declare function judgedSpellingOf(v: BoundValue): string | undefined;
67
+ /** A value as a deny-floor spelling, its judged spelling with it. */
68
+ export declare function floorOf(v: BoundValue): FloorValue;
60
69
  /** What a name holds at one point of a text: its possible values, or `"unknown"`. */
61
70
  type NameState = readonly BoundValue[] | "unknown";
62
71
  /**
@@ -133,25 +142,36 @@ export declare function dropDefinitionHeads(tokens: LexedToken[]): readonly numb
133
142
  * where it refers to no name (it reads as written), else {@link valueOf}. `table.stateOf` is read after `overlay` (the
134
143
  * names the same segment bound earlier).
135
144
  */
136
- export declare function wordValues(word: Pick<LexedWord, "pieces">, table: BindingTable | undefined, quoted: boolean, overlay?: ReadonlyMap<string, NameState>): readonly BoundValue[] | "unknown" | undefined;
145
+ export declare function wordValues(word: Pick<LexedWord, "pieces">, table: BindingTable | undefined, quoted: boolean, overlay?: ReadonlyMap<string, NameState>, split?: boolean): readonly BoundValue[] | "unknown" | undefined;
146
+ /** The names a word's expansions read as values — a bare `$NAME` / `${NAME}`, and one written inside a bracket the shell
147
+ * forms its class from after expanding it ({@link valueOf}) — in the order they stand. */
148
+ export declare function namesIn(word: Pick<LexedWord, "pieces">): string[];
137
149
  /**
138
150
  * What a VALUE word holds (an assignment's value, a `for` list word, a word handed to a program) — THE piece walk, the one
139
- * producer of {@link BoundValue}s: every combination of the values its pieces take — a literal piece its text, a glob
140
- * piece its pattern (as the program receives it, as written for a shell re-reading it, and as a reader judges it — the
141
- * lexer's three texts, never re-cut here), `~` and `$NAME` the values the table gives:
142
- * · its values — a piece the shell globs makes the value a pattern;
151
+ * producer of {@link BoundValue}s: every combination of the values its pieces take, each field's pattern formed once the
152
+ * whole field has expanded ({@link formedField}) — a literal piece its text, a glob piece its pattern as written (a bracket
153
+ * holding an expansion its brackets and its body, the body's names substituted in place), `~` and `$NAME` the values the
154
+ * table gives:
155
+ * · its values — a field the shell globs makes the value a pattern;
143
156
  * · `"unknown"` — a name it refers to is not settled here, it carries an expansion no name answers (a modifier, a
144
- * substitution, a brace list, `~user`, an expansion inside a bracket pattern), an UNQUOTED expansion's value is not one
145
- * word or `IFS` may have changed (`quoted` = the whole word is read as one value: an assignment's value, which is never
146
- * split), or it would list more than {@link WORD_VALUES_MAX} values.
157
+ * substitution, a brace list, `~user`), an UNQUOTED expansion's value is no {@link READABLE_VALUE} (or holds
158
+ * whitespace outside `split`), `IFS` may have changed (`quoted` = the whole word is read as one value: an assignment's
159
+ * value, which is never split), a field is not formed or a split one forms a pattern, or it would list more than
160
+ * {@link WORD_VALUES_MAX} values.
161
+ * `split` = the word is read as the ARGUMENTS it becomes: an unquoted value is split into its words on the default `IFS`
162
+ * (a value then carries its {@link BoundValue} `fields`).
147
163
  */
148
- export declare function valueOf(word: Pick<LexedWord, "pieces">, table: BindingTable | undefined, quoted: boolean, overlay?: ReadonlyMap<string, NameState>): NameState;
164
+ export declare function valueOf(word: Pick<LexedWord, "pieces">, table: BindingTable | undefined, quoted: boolean, overlay?: ReadonlyMap<string, NameState>, split?: boolean): NameState;
149
165
  /**
150
166
  * The spellings a word the table leaves unknown may still take through the deny floors of the names it refers to
151
167
  * ({@link BindingTable.floorOf}) — `${A[k]}`, `${A[@]}`, `$A` reading an array's elements — or through their values where
152
168
  * known: for the deny judge alone. Empty where some name it refers to leaves neither.
153
169
  */
154
170
  export declare function wordFloorValues(word: Pick<LexedWord, "pieces">, table: BindingTable | undefined): readonly FloorValue[];
171
+ /** Where a segment's argv heads a `for` / `select` loop: the index of that bare word, at the segment's start or past the
172
+ * bare control words in front of it (`do for x in …`, `then for …` — a loop nested in a body or a branch); `undefined`
173
+ * for any other segment. */
174
+ export declare function loopListHeadAt(argv: readonly LexedWord[]): number | undefined;
155
175
  /**
156
176
  * What ONE segment does to the names (see the module header). `argv` = the segment's argv projection (structure
157
177
  * dropped); `readValue` folds a value word against the table (a reader that answers `unknown` gives the names alone).
@@ -180,6 +200,9 @@ export type NameReader = (name: string) => NameState;
180
200
  export interface InheritedNames {
181
201
  readonly attributes: readonly NameAttribute[];
182
202
  readonly references: boolean;
203
+ /** The deny floors a name starts the text with ({@link BindingTable.floorOf}): what the words handing it this text as
204
+ * written may name there — or, for a substitution body, the floors its segment reads. */
205
+ readonly floors?: ReadonlyMap<string, readonly FloorValue[]>;
183
206
  }
184
207
  /** What the thread answers for one reader: what a name may hold, and whether a name carries the integer attribute. */
185
208
  export interface BindingTable {
@@ -235,6 +258,8 @@ export declare class BindingThread {
235
258
  private readonly blocks;
236
259
  /** Segments that are a pipeline member or run in the background. */
237
260
  private readonly detached;
261
+ /** The floors the names start the text with ({@link InheritedNames.floors}). */
262
+ private readonly startFloors;
238
263
  constructor(segments: readonly LexedSegment[],
239
264
  /** What each segment binds, its names alone (a value is the reader's to read). */
240
265
  named: readonly BindingEffect[],
@@ -1,9 +1,19 @@
1
1
  import { argvOf, lexShell, literalTildeSpelling, substitutionsInHeredocBody } from "../../shell/lex.js";
2
- import { readProgramPosition } from "./bash-program-position.js";
2
+ import { isShellSyntaxWord, LOOP_LIST_HEADS, readProgramPosition } from "./bash-program-position.js";
3
3
  import { declaredAttributes, programRow, readArgRoles } from "./program-read-model.js";
4
4
  import { codeBodies } from "./shell-reentry-text.js";
5
+ export function judgedSpellingOf(v) {
6
+ if (v.glob !== true)
7
+ return undefined;
8
+ const judged = literalTildeSpelling(v.parts.map((p) => (p.pattern ? p.judged : p.text)).join(""));
9
+ return judged === literalTildeSpelling(v.spelled) ? undefined : judged;
10
+ }
11
+ export function floorOf(v) {
12
+ const pattern = judgedSpellingOf(v);
13
+ return v.glob === true ? { spelled: v.spelled, glob: true, ...(pattern !== undefined ? { pattern } : {}) } : { spelled: v.spelled };
14
+ }
5
15
  function sameValue(a, b) {
6
- if (a.spelled !== b.spelled)
16
+ if (a.spelled !== b.spelled || a.fields?.length !== b.fields?.length)
7
17
  return false;
8
18
  if (a.glob !== true || b.glob !== true)
9
19
  return a.glob === b.glob;
@@ -203,14 +213,51 @@ function blockStructure(segments) {
203
213
  return { words: words ?? depth, after: depth, lowest, switches, ...(body !== undefined ? { body } : {}) };
204
214
  });
205
215
  }
206
- const ONE_WORD = /^[A-Za-z0-9_./+@:%=,-]+$/;
216
+ const READABLE_VALUE = /^[^\\~()]*$/;
207
217
  const WORD_VALUES_MAX = 64;
208
- export function wordValues(word, table, quoted, overlay = new Map()) {
209
- if (!word.pieces.some((p) => p.expansion?.kind === "parameter" || p.expansion?.kind === "tilde"))
218
+ const quotedRunsOf = (v) => v.glob === true ? v.parts.map((r) => (r.pattern ? { text: r.text, mode: "set", judged: r.judged } : { text: r.text, mode: "inactive" })) : [{ text: v.spelled, mode: "inactive" }];
219
+ function formedField(f) {
220
+ const runs = f.runs;
221
+ if (!runs.some((r) => r.mode === "active" && /[[\]*?]/.test(r.text))) {
222
+ if (!runs.some((r) => r.mode === "raw" || r.mode === "set"))
223
+ return { spelled: f.text };
224
+ return { spelled: f.text, glob: true, parts: joinRuns([], runs.map((r) => (r.mode === "raw" || r.mode === "set" ? { text: r.text, pattern: true, judged: r.judged } : { text: r.text, pattern: false }))) };
225
+ }
226
+ if (runs.some((r) => r.mode === "active" && /[ \t\n\\]/.test(r.text)) || (runs.some((r) => r.mode === "set") && runs.some((r) => r.mode === "active" && /[[\]]/.test(r.text))))
210
227
  return undefined;
211
- return valueOf(word, table, quoted, overlay);
228
+ const escaped = runs.map((r) => (r.mode === "raw" || r.mode === "set" ? r.text : r.text.replace(r.mode === "active" ? /[^A-Za-z0-9_./*?[\]!^:=,+@%-]/g : /[^A-Za-z0-9_./]/g, (c) => `\\${c}`))).join("");
229
+ const lexed = lexShell(`: ${escaped}`);
230
+ const words = "parseFailure" in lexed || lexed.segments.length !== 1 ? undefined : argvOf(lexed.segments[0]);
231
+ if (words?.length !== 2)
232
+ return undefined;
233
+ let parts = [];
234
+ for (const p of words[1].pieces) {
235
+ if (p.expansion?.pattern !== undefined)
236
+ parts = joinRuns(parts, [{ text: p.expansion.pattern.written, pattern: true, judged: p.expansion.pattern.judged }]);
237
+ else if (p.expansion === undefined && p.literal !== undefined)
238
+ parts = joinRuns(parts, [{ text: p.literal, pattern: false }]);
239
+ else
240
+ return undefined;
241
+ }
242
+ return parts.some((r) => r.pattern) ? { spelled: f.text, glob: true, parts } : { spelled: f.text };
243
+ }
244
+ export function wordValues(word, table, quoted, overlay = new Map(), split = false) {
245
+ if (!word.pieces.some((p) => p.expansion?.kind === "parameter" || p.expansion?.kind === "tilde") && namesIn(word).length === 0)
246
+ return undefined;
247
+ return valueOf(word, table, quoted, overlay, split);
248
+ }
249
+ export function namesIn(word) {
250
+ return word.pieces.flatMap((p) => {
251
+ const e = p.expansion;
252
+ if (e?.kind === "parameter" && e.name !== undefined)
253
+ return e.body === undefined || e.body === e.name ? [e.name] : [];
254
+ return e?.kind === "glob" && e.pattern === undefined ? (bracketBodyOf(e.body) ?? []).flatMap((w) => namesIn(w)) : [];
255
+ });
256
+ }
257
+ function bracketBodyOf(body) {
258
+ const lexed = body === undefined ? undefined : lexShell(`""${body}`);
259
+ return lexed === undefined || "parseFailure" in lexed ? undefined : lexed.segments.flatMap((s) => argvOf(s));
212
260
  }
213
- const runsOf = (v) => (v.glob === true ? v.parts : [{ text: v.spelled, pattern: false }]);
214
261
  function joinRuns(runs, more) {
215
262
  const out = [...runs];
216
263
  for (const r of more) {
@@ -224,51 +271,96 @@ function joinRuns(runs, more) {
224
271
  }
225
272
  return out;
226
273
  }
227
- export function valueOf(word, table, quoted, overlay = new Map()) {
274
+ export function valueOf(word, table, quoted, overlay = new Map(), split = false) {
228
275
  const stateOf = (name) => (table === undefined ? "unknown" : overlay.get(name) ?? table.stateOf(name));
229
- let combos = [{ text: "", glob: false, parts: [] }];
230
- for (const p of word.pieces) {
231
- const e = p.expansion;
232
- let options;
233
- if (e === undefined) {
234
- if (p.literal === undefined)
235
- return "unknown";
236
- options = [{ spelled: p.literal }];
237
- }
238
- else if (e.kind === "glob") {
239
- if (e.pattern === undefined)
240
- return "unknown";
241
- options = [{ spelled: e.pattern.folded, glob: true, parts: [{ text: e.pattern.written, pattern: true, judged: e.pattern.judged }] }];
276
+ const EMPTY = { text: "", runs: [] };
277
+ let combos = [{ field: EMPTY, closed: [], started: false, split: false }];
278
+ const activeRun = (text) => ({ text, mode: "active" });
279
+ const splitInto = (c, value) => {
280
+ const words = value.split(/[ \t\n]+/).filter((w) => w !== "");
281
+ if (words.length === 0)
282
+ return value === "" ? { ...c, split: true } : { ...c, field: EMPTY, started: false, closed: c.started ? [...c.closed, c.field] : c.closed, split: true };
283
+ const closed = [...c.closed];
284
+ if (/^[ \t\n]/.test(value) && c.started)
285
+ closed.push(c.field);
286
+ let field = closed.length > c.closed.length ? { text: words[0], runs: [activeRun(words[0])] } : { text: c.field.text + words[0], runs: [...c.field.runs, activeRun(words[0])] };
287
+ for (let i = 1; i < words.length; i++) {
288
+ closed.push(field);
289
+ field = { text: words[i], runs: [activeRun(words[i])] };
242
290
  }
243
- else if (e.kind === "tilde") {
244
- if (e.body !== "~")
245
- return "unknown";
246
- const home = stateOf("HOME");
247
- if (home === "unknown")
248
- return "unknown";
249
- options = home;
250
- }
251
- else if (e.kind === "parameter" && e.name !== undefined && (e.body === undefined || e.body === e.name)) {
252
- const state = stateOf(e.name);
253
- if (state === "unknown")
254
- return "unknown";
255
- if (p.quotedBy === undefined && !quoted && (table?.ifsTouched === true || state.some((v) => v.glob === true || !ONE_WORD.test(v.spelled))))
256
- return "unknown";
257
- options = state;
258
- }
259
- else
260
- return "unknown";
261
- const next = [];
262
- for (const c of combos)
263
- for (const o of options)
264
- next.push({ text: c.text + o.spelled, glob: c.glob || o.glob === true, parts: joinRuns(c.parts, runsOf(o)) });
291
+ if (!/[ \t\n]$/.test(value))
292
+ return { ...c, field, started: true, closed, split: true };
293
+ closed.push(field);
294
+ return { ...c, field: EMPTY, started: false, closed, split: true };
295
+ };
296
+ const extend = (options) => {
297
+ const next = combos.flatMap((c) => options.map((o) => ({ ...c, field: { text: c.field.text + o.text, runs: [...c.field.runs, ...o.runs] }, started: true })));
265
298
  if (next.length > WORD_VALUES_MAX)
266
- return "unknown";
299
+ return false;
267
300
  combos = next;
268
- }
301
+ return true;
302
+ };
303
+ const unquotedKind = (v) => v.glob === true || !READABLE_VALUE.test(v.spelled) ? undefined : !/[ \t\n]/.test(v.spelled) && v.spelled !== "" ? "run" : split ? "split" : undefined;
304
+ const walk = (pieces, unquoted) => {
305
+ for (const p of pieces) {
306
+ const e = p.expansion;
307
+ const bare = unquoted && p.quotedBy === undefined;
308
+ if (e === undefined) {
309
+ if (p.literal === undefined || !extend([{ text: p.literal, runs: [{ text: p.literal, mode: bare ? "active" : "inactive" }] }]))
310
+ return false;
311
+ }
312
+ else if (e.kind === "glob") {
313
+ const formed = e.pattern;
314
+ if (formed !== undefined) {
315
+ if (!extend([{ text: formed.folded, runs: [{ text: formed.written, mode: "raw", judged: formed.judged }] }]))
316
+ return false;
317
+ continue;
318
+ }
319
+ const words = bracketBodyOf(e.body);
320
+ if (words?.length !== 1 || !extend([{ text: "[", runs: [activeRun("[")] }]) || !walk(words[0].pieces, true) || !extend([{ text: "]", runs: [activeRun("]")] }]))
321
+ return false;
322
+ }
323
+ else if (e.kind === "tilde") {
324
+ const home = e.body === "~" ? stateOf("HOME") : "unknown";
325
+ if (home === "unknown" || !extend(home.map((v) => ({ text: v.spelled, runs: quotedRunsOf(v) }))))
326
+ return false;
327
+ }
328
+ else if (e.kind === "parameter" && e.name !== undefined && (e.body === undefined || e.body === e.name)) {
329
+ const state = stateOf(e.name);
330
+ if (state === "unknown")
331
+ return false;
332
+ if (!bare) {
333
+ if (!extend(state.map((v) => ({ text: v.spelled, runs: quotedRunsOf(v) }))))
334
+ return false;
335
+ continue;
336
+ }
337
+ const kinds = state.map(unquotedKind);
338
+ if (table?.ifsTouched === true || kinds.includes(undefined))
339
+ return false;
340
+ if (!kinds.includes("split")) {
341
+ if (!extend(state.map((v) => ({ text: v.spelled, runs: [activeRun(v.spelled)] }))))
342
+ return false;
343
+ continue;
344
+ }
345
+ const next = combos.flatMap((c) => state.map((v) => splitInto(c, v.spelled)));
346
+ if (next.length > WORD_VALUES_MAX)
347
+ return false;
348
+ combos = next;
349
+ }
350
+ else
351
+ return false;
352
+ }
353
+ return true;
354
+ };
355
+ if (!walk(word.pieces, !quoted))
356
+ return "unknown";
269
357
  const out = [];
270
358
  for (const c of combos) {
271
- const v = c.glob ? { spelled: c.text, glob: true, parts: c.parts } : { spelled: c.text };
359
+ const fields = (c.split ? (c.started ? [...c.closed, c.field] : c.closed) : [c.field]).map(formedField);
360
+ const formed = fields.filter((f) => f !== undefined && !(c.split && f.glob === true));
361
+ if (formed.length !== fields.length)
362
+ return "unknown";
363
+ const v = !c.split ? formed[0] : formed.length !== 1 ? { spelled: formed.map((f) => f.spelled).join(" "), fields: formed.map((f) => f.spelled) } : { spelled: formed[0].spelled };
272
364
  if (!out.some((x) => sameValue(x, v)))
273
365
  out.push(v);
274
366
  }
@@ -288,22 +380,29 @@ export function wordFloorValues(word, table) {
288
380
  }
289
381
  else if (e.kind === "parameter" && e.name !== undefined) {
290
382
  const state = table.stateOf(e.name);
291
- options = state !== "unknown" && (e.body === undefined || e.body === e.name) ? state : table.floorOf(e.name);
383
+ options = state !== "unknown" && (e.body === undefined || e.body === e.name) ? state.map(floorOf) : table.floorOf(e.name);
292
384
  }
293
385
  else
294
386
  return [];
295
387
  if (options.length === 0)
296
388
  return [];
297
389
  const next = [];
298
- for (const c of combos)
299
- for (const o of options)
300
- next.push(c.glob === true || o.glob === true ? { spelled: c.spelled + o.spelled, glob: true } : { spelled: c.spelled + o.spelled });
390
+ for (const c of combos) {
391
+ for (const o of options) {
392
+ const pattern = c.pattern !== undefined || o.pattern !== undefined ? (c.pattern ?? c.spelled) + (o.pattern ?? o.spelled) : undefined;
393
+ next.push(c.glob === true || o.glob === true ? { spelled: c.spelled + o.spelled, glob: true, ...(pattern !== undefined ? { pattern } : {}) } : { spelled: c.spelled + o.spelled });
394
+ }
395
+ }
301
396
  if (next.length > WORD_VALUES_MAX)
302
397
  return [];
303
398
  combos = next;
304
399
  }
305
400
  return combos;
306
401
  }
402
+ export function loopListHeadAt(argv) {
403
+ const at = isShellSyntaxWord(argv[0], LOOP_LIST_HEADS) ? 0 : readProgramPosition(argv).written;
404
+ return at !== undefined && isShellSyntaxWord(argv[at], LOOP_LIST_HEADS) ? at : undefined;
405
+ }
307
406
  export function bindingEffectOf(segment, argv, shape, readValue) {
308
407
  const bindings = [];
309
408
  let all = shape.definesFunction ? "reset" : undefined;
@@ -353,21 +452,21 @@ export function bindingEffectOf(segment, argv, shape, readValue) {
353
452
  kill(name);
354
453
  return { bindings, all };
355
454
  }
356
- const head = argv[0];
357
- const headWord = head !== undefined && head.raw === head.folded ? head.folded : undefined;
358
- if (headWord === "for" || headWord === "select") {
455
+ const loopAt = loopListHeadAt(argv);
456
+ if (loopAt !== undefined) {
457
+ const headWord = argv[loopAt].folded;
359
458
  if (tokens.some((t) => t.kind === "group"))
360
459
  unknownAll();
361
- const name = argv[1] !== undefined ? nameOf(argv[1]) : undefined;
460
+ const name = argv[loopAt + 1] !== undefined ? nameOf(argv[loopAt + 1]) : undefined;
362
461
  if (name === undefined || name === "computed") {
363
462
  unknownAll();
364
463
  return { bindings, all };
365
464
  }
366
- const inAt = argv.findIndex((w, i) => i >= 2 && w.raw === "in");
465
+ const inAt = argv.findIndex((w, i) => i >= loopAt + 2 && w.raw === "in");
367
466
  let state = "unknown";
368
- if (headWord === "for" && inAt === 2 && argv.length > 3) {
467
+ if (headWord === "for" && inAt === loopAt + 2 && argv.length > loopAt + 3) {
369
468
  const values = [];
370
- for (const w of argv.slice(3)) {
469
+ for (const w of argv.slice(loopAt + 3)) {
371
470
  const v = readValue(w, false, overlay);
372
471
  if (v === "unknown") {
373
472
  values.length = 0;
@@ -381,7 +480,7 @@ export function bindingEffectOf(segment, argv, shape, readValue) {
381
480
  bind({ name, state, loopVar: true });
382
481
  return { bindings, all };
383
482
  }
384
- if (headWord === "coproc" && argv[1] !== undefined)
483
+ if (argv[0]?.raw === "coproc" && argv[1] !== undefined)
385
484
  killNamed(nameOf(argv[1]));
386
485
  const pos = readProgramPosition(argv);
387
486
  const program = pos.program;
@@ -490,7 +589,7 @@ function compoundFloor(raw, readValue, overlay) {
490
589
  return compoundElementWords(raw).flatMap((w) => {
491
590
  const v = w.tail === true ? "unknown" : readValue(w, true, overlay);
492
591
  if (v !== "unknown")
493
- return [...v];
592
+ return v.map(floorOf);
494
593
  const text = w.pieces.some((q) => q.expansion?.kind === "tilde") ? w.folded : literalTildeSpelling(w.folded);
495
594
  return [w.pieces.some((q) => q.expansion?.kind === "glob") ? { spelled: text, glob: true } : { spelled: text }];
496
595
  });
@@ -550,6 +649,7 @@ export class BindingThread {
550
649
  unreadable;
551
650
  blocks;
552
651
  detached;
652
+ startFloors;
553
653
  constructor(segments, named, initial, boundAnywhere, loops, homeStartsTouched = false, inherited = NOTHING_INHERITED) {
554
654
  this.segments = segments;
555
655
  this.named = named;
@@ -557,6 +657,7 @@ export class BindingThread {
557
657
  this.boundAnywhere = boundAnywhere;
558
658
  this.loops = loops;
559
659
  this.homeStartsTouched = homeStartsTouched;
660
+ this.startFloors = inherited.floors ?? new Map();
560
661
  this.blocks = blockStructure(segments);
561
662
  this.detached = segments.map((s, i) => s.connector === "|" || segments[i + 1]?.connector === "|" || segments[i + 1]?.connector === "&");
562
663
  this.unreadable = [...(inherited.references ? [{ at: -1 }] : []), ...named.flatMap((effect, at) => this.unreadableBy(effect, at))];
@@ -564,9 +665,14 @@ export class BindingThread {
564
665
  }
565
666
  inheritedAt(j) {
566
667
  const holds = (at) => j === undefined || this.mayRunBefore(at, j);
668
+ const floors = new Map(this.startFloors);
669
+ for (const e of this.entries)
670
+ if (e.name !== undefined && e.floor !== undefined && (j === undefined || e.at < j))
671
+ floors.set(e.name, [...(floors.get(e.name) ?? []), ...e.floor]);
567
672
  return {
568
673
  attributes: this.attributesAt.filter((x) => holds(x.at)).map(({ name, kind }) => (name !== undefined ? { name, kind } : { kind })),
569
674
  references: this.unreadable.some((u) => u.name === undefined && u.body === undefined && holds(u.at)),
675
+ ...(floors.size > 0 ? { floors } : {}),
570
676
  };
571
677
  }
572
678
  unreadableBy(effect, at) {
@@ -688,7 +794,7 @@ export class BindingThread {
688
794
  isInteger: (name) => SHELL_INTEGER_VARIABLES.has(name) || given(name, j, true),
689
795
  ifsTouched: touched("IFS"),
690
796
  homeTouched: this.homeStartsTouched || touched("HOME"),
691
- floorOf: (name) => this.entries.filter((e) => e.at < j && e.name === name).flatMap((e) => e.floor ?? []),
797
+ floorOf: (name) => [...(this.startFloors.get(name) ?? []), ...this.entries.filter((e) => e.at < j && e.name === name).flatMap((e) => e.floor ?? [])],
692
798
  inherited: this.inheritedAt(j),
693
799
  };
694
800
  }
@@ -57,11 +57,10 @@ function parseBracketExpression(pattern, start) {
57
57
  return { source: anyChar ? "[^/]" : `[${negate ? "^" : ""}${body}]`, end: i };
58
58
  }
59
59
  if (ch === "[" && (pattern[i + 1] === ":" || pattern[i + 1] === "." || pattern[i + 1] === "=")) {
60
- const close = pattern.indexOf(`${pattern[i + 1]}]`, i + 2);
61
- if (close < 0)
62
- return undefined;
63
60
  anyChar = true;
64
- i = close + 1;
61
+ const close = pattern.indexOf(`${pattern[i + 1]}]`, i + 2);
62
+ if (close >= 0 && (pattern[i + 1] === ":" ? /^[A-Za-z]+$/.test(pattern.slice(i + 2, close)) : close === i + 3))
63
+ i = close + 1;
65
64
  continue;
66
65
  }
67
66
  body += ch === "-" ? "-" : ch.replace(/[\]\\^]/g, "\\$&");
@@ -30,9 +30,9 @@
30
30
  * Every ambiguity resolves toward MORE candidates, never fewer.
31
31
  */
32
32
  import { type LenientConnector, type LexedRedirect, type LexedWord } from "../../shell/lex.js";
33
- import { type BindingTable, type NameReader } from "./shell-read-bindings.js";
34
33
  import { type ShellScanDecline } from "./bash-program-position.js";
35
34
  import { type StdinText } from "./shell-reentry-text.js";
35
+ import { type PlanStart } from "./shell-read-start.js";
36
36
  /** WHY a word is a read candidate — a closed set, registered in `docs/CLOSED-SETS.md`. */
37
37
  export declare const READ_CANDIDATE_PROVENANCES: readonly ["operand", "option_value", "stdin_redirect", "write_redirect_target", "env_assignment", "unlisted_word", "traversal_root", "cd_target", "program_path", "script_slot_file", "mount_source", "object_path"];
38
38
  export type ReadCandidateProvenance = (typeof READ_CANDIDATE_PROVENANCES)[number];
@@ -142,18 +142,5 @@ export interface ShellReadPlan {
142
142
  readonly strayRedirection: boolean;
143
143
  };
144
144
  }
145
- /** Where a nested text starts its names: a substitution body inherits the table of the segment it sits in (a subshell
146
- * holds every variable); a text the shell runs later or elsewhere (a code body, `eval`'s text, a child shell's `-c`)
147
- * starts from the text's own start — every name unknown, `HOME` the declared home unless the enclosing text binds it
148
- * anywhere. `boundAnywhere` answers for the enclosing texts. */
149
- interface PlanStart {
150
- readonly initial: NameReader;
151
- readonly boundAnywhere: (name: string) => boolean;
152
- /** `HOME` may already hold something other than the declared home where the nested text starts. */
153
- readonly homeTouched: boolean;
154
- /** The attributes and name references in force where the nested text starts. */
155
- readonly inherited: BindingTable["inherited"];
156
- }
157
145
  /** Plan every read candidate of one command (the contract is this module's header). */
158
146
  export declare function planShellReads(command: string, decl: ShellReadDeclarations, depth?: number, start?: PlanStart): ShellReadPlan;
159
- export {};