@sema-agent/core 7.24.3 → 7.25.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 (106) hide show
  1. package/CHANGELOG.md +101 -1
  2. package/dist/agents/peer-session-drain.d.ts +16 -1
  3. package/dist/agents/peer-session-drain.js +16 -1
  4. package/dist/agents/send-message-tool.d.ts +10 -2
  5. package/dist/agents/send-message-tool.js +18 -7
  6. package/dist/agents/subagent.js +13 -10
  7. package/dist/core/background-agent-store.d.ts +3 -3
  8. package/dist/core/checkpoint-store.d.ts +1 -0
  9. package/dist/core/delegation-frames.d.ts +5 -0
  10. package/dist/core/gate-fold.js +85 -0
  11. package/dist/core/gate-lanes.js +23 -1
  12. package/dist/core/gate-outcome.d.ts +8 -2
  13. package/dist/core/gate-outcome.js +3 -1
  14. package/dist/core/hooks.d.ts +29 -0
  15. package/dist/core/hooks.js +2 -0
  16. package/dist/core/mailbox-store.d.ts +5 -0
  17. package/dist/core/mailbox-store.js +1 -1
  18. package/dist/core/parent-spec-seat.d.ts +7 -1
  19. package/dist/core/parent-spec-seat.js +2 -0
  20. package/dist/core/permission-rule-consent.js +29 -47
  21. package/dist/core/permission-rule-provider.js +4 -3
  22. package/dist/core/permission-rule-store.d.ts +143 -7
  23. package/dist/core/permission-rule-store.js +165 -15
  24. package/dist/core/permission-rules.js +14 -10
  25. package/dist/core/rule-denial-sentence.d.ts +75 -0
  26. package/dist/core/rule-denial-sentence.js +16 -0
  27. package/dist/core/runner/contracts.d.ts +36 -2
  28. package/dist/core/runner/gate-exit.d.ts +14 -0
  29. package/dist/core/runner/gate-exit.js +4 -0
  30. package/dist/core/runner/prepare-delegation-surface.d.ts +4 -4
  31. package/dist/core/runner/prepare-delegation-surface.js +2 -1
  32. package/dist/core/runner/prepare-gate-stations.d.ts +4 -0
  33. package/dist/core/runner/prepare-gate-stations.js +7 -4
  34. package/dist/core/runner/prepare-hands-readface.d.ts +8 -4
  35. package/dist/core/runner/prepare-hands-readface.js +25 -10
  36. package/dist/core/runner/prepare-inherited-gate.d.ts +23 -0
  37. package/dist/core/runner/prepare-inherited-gate.js +11 -6
  38. package/dist/core/runner/prepare-policy-chain.d.ts +15 -1
  39. package/dist/core/runner/prepare-policy-chain.js +42 -22
  40. package/dist/core/runner/prepare-suspend-saga.d.ts +5 -2
  41. package/dist/core/runner/prepare-suspend-saga.js +4 -3
  42. package/dist/core/runner/prepare-task.js +10 -9
  43. package/dist/core/runner/prepare-wiring-manifest.d.ts +11 -4
  44. package/dist/core/runner/prepare-wiring-manifest.js +8 -4
  45. package/dist/core/runner/resume-apply.js +2 -0
  46. package/dist/core/runner/run-harness-handlers.js +1 -0
  47. package/dist/core/runner/run-identity-wiring.d.ts +2 -2
  48. package/dist/core/runner/run-leg.js +28 -1
  49. package/dist/core/runner/run-notification-lane.d.ts +2 -2
  50. package/dist/core/runner/run-notification-lane.js +2 -1
  51. package/dist/core/runner/run-settle-and-teardown.js +1 -0
  52. package/dist/core/runner/session-rule-policy.d.ts +73 -2
  53. package/dist/core/runner/session-rule-policy.js +121 -21
  54. package/dist/core/session-policy-store.d.ts +27 -3
  55. package/dist/core/session-policy-store.js +12 -4
  56. package/dist/core/store-contracts/background-agent-store-contract.js +10 -0
  57. package/dist/core/store-contracts/peer-directory-contract.d.ts +23 -0
  58. package/dist/core/store-contracts/peer-directory-contract.js +82 -0
  59. package/dist/core/task-event.d.ts +6 -0
  60. package/dist/core/task-notification.d.ts +79 -4
  61. package/dist/core/task-notification.js +90 -1
  62. package/dist/core/task-registry-agent.js +1 -2
  63. package/dist/core/task-registry-monitor.js +2 -1
  64. package/dist/core/task-registry-shared.d.ts +53 -5
  65. package/dist/core/task-registry-shared.js +13 -2
  66. package/dist/core/task-registry.js +8 -5
  67. package/dist/core/task-spec.d.ts +12 -5
  68. package/dist/core/tool-catalog-entries.js +1 -0
  69. package/dist/core/tool-spec.d.ts +1 -1
  70. package/dist/core/wiring-manifest.d.ts +2 -2
  71. package/dist/engine/loop/agent-loop.js +1 -1
  72. package/dist/engine/loop/types.d.ts +4 -0
  73. package/dist/index.d.ts +6 -3
  74. package/dist/index.js +4 -3
  75. package/dist/orchestration/workflow.js +12 -3
  76. package/dist/prompts/coordinator.d.ts +2 -1
  77. package/dist/stores/file/adoption/adopt.js +23 -1
  78. package/dist/stores/file/permission-rule-store.js +3 -1
  79. package/dist/stores/file/session-policy-store.d.ts +31 -0
  80. package/dist/stores/file/session-policy-store.js +87 -7
  81. package/dist/tools/fs/bash-program-position.d.ts +4 -26
  82. package/dist/tools/fs/bash-program-position.js +0 -11
  83. package/dist/tools/fs/bash-readonly-classifier.d.ts +20 -309
  84. package/dist/tools/fs/bash-readonly-classifier.js +28 -766
  85. package/dist/tools/fs/fs-bash.d.ts +43 -207
  86. package/dist/tools/fs/fs-bash.js +20 -341
  87. package/dist/tools/fs/index.d.ts +12 -13
  88. package/dist/tools/fs/index.js +3 -4
  89. package/dist/tools/fs/read-deny.d.ts +25 -0
  90. package/dist/tools/fs/read-deny.js +19 -0
  91. package/dist/tools/fs/safety.js +2 -6
  92. package/dist/tools/fs/shell-read-check.d.ts +99 -0
  93. package/dist/tools/fs/shell-read-check.js +229 -0
  94. package/dist/tools/fs/shell-read-establish.d.ts +70 -115
  95. package/dist/tools/fs/shell-read-establish.js +216 -102
  96. package/dist/tools/fs/shell-read-judge.d.ts +231 -0
  97. package/dist/tools/fs/shell-read-judge.js +170 -0
  98. package/dist/tools/fs/shell-read-plan.d.ts +3 -0
  99. package/dist/tools/fs/shell-read-plan.js +82 -19
  100. package/dist/tools/monitor.d.ts +5 -5
  101. package/dist/tools/monitor.js +3 -5
  102. package/dist/tools/task-list.d.ts +4 -0
  103. package/package.json +1 -1
  104. package/test/export-surface.snapshot.json +19 -3
  105. package/wire-changes/core@7.24.4.json +54 -0
  106. package/wire-changes/core@7.25.0.json +671 -0
@@ -1,5 +1,6 @@
1
- import { normalizeAbsPathLexically, withinAnyRoot } from "./safety.js";
1
+ import { normalizeAbsPathLexically, pathFamilyOf } from "./safety.js";
2
2
  import { boundaryIsTheSandbox, canonicalPathIsAuthoritative } from "../../core/remote-env.js";
3
+ import { resolveOperandSpellings } from "./bash-readonly-classifier.js";
3
4
  const GLOB_METACHAR_RE = /[*?[]/;
4
5
  export const MAX_GLOB_DIRS = 256;
5
6
  export const MAX_GLOB_MATCHES = 8192;
@@ -69,26 +70,75 @@ function globSegmentMatches(re, pattern, name) {
69
70
  return false;
70
71
  return re.test(name);
71
72
  }
72
- function joinCandidate(dir, name) {
73
- return `${dir.replace(/[\\/]+$/, "")}/${name}`;
74
- }
75
- function pathSegmentsFrom(p, from) {
73
+ function pathSegmentsFrom(p, from, posix = false) {
76
74
  const segs = [];
75
+ const isSep = (c) => c === "/" || (!posix && c === "\\");
77
76
  let scan = from;
78
77
  while (scan < p.length) {
79
- while (scan < p.length && (p[scan] === "/" || p[scan] === "\\"))
78
+ while (scan < p.length && isSep(p[scan]))
80
79
  scan++;
81
80
  const s = scan;
82
- while (scan < p.length && p[scan] !== "/" && p[scan] !== "\\")
81
+ while (scan < p.length && !isSep(p[scan]))
83
82
  scan++;
84
83
  if (scan > s)
85
84
  segs.push({ text: p.slice(s, scan), start: s });
86
85
  }
87
86
  return segs;
88
87
  }
89
- export function createCanonicalReadBoundary(env, readRoots) {
88
+ function basesFor(candidate, boundary) {
89
+ if (candidate.bases === "unmodelled")
90
+ return "unmodelled";
91
+ if (candidate.bases.length > 0)
92
+ return candidate.bases;
93
+ return [boundary.cwd ?? boundary.roots[0] ?? ""];
94
+ }
95
+ function lexicalReadsOf(candidate, boundary) {
96
+ if (candidate.expands === "unresolvable") {
97
+ return [{ candidate, established: "unestablishable", spellings: [candidate.spelled], cause: `"${candidate.raw}" is rewritten by the shell before the program sees it` }];
98
+ }
99
+ const bases = basesFor(candidate, boundary);
100
+ if (bases === "unmodelled") {
101
+ return [{ candidate, established: "unestablishable", spellings: [candidate.spelled], cause: `"${candidate.raw}" is relative to a working directory this command's text does not settle` }];
102
+ }
103
+ const out = [];
104
+ const seen = new Set();
105
+ for (const base of bases) {
106
+ const sp = resolveOperandSpellings(base === "" ? undefined : base, candidate.spelled, boundary.homeDir);
107
+ if (sp === undefined) {
108
+ const key = `?${candidate.spelled}`;
109
+ if (!seen.has(key)) {
110
+ seen.add(key);
111
+ out.push({ candidate, established: "unestablishable", spellings: [candidate.spelled], cause: `"${candidate.raw}" cannot be placed against ${base === "" ? "no working directory" : `"${base}"`}` });
112
+ }
113
+ continue;
114
+ }
115
+ if (seen.has(sp.folded))
116
+ continue;
117
+ seen.add(sp.folded);
118
+ out.push({ candidate, established: "lexical", target: sp.folded, spellings: sp.spelled !== undefined ? [sp.folded, sp.spelled] : [sp.folded] });
119
+ }
120
+ return out;
121
+ }
122
+ function judgedCandidatesOf(segment) {
123
+ return segment.reads.filter((c) => c.provenance !== "write_redirect_target");
124
+ }
125
+ export function establishShellReadsLexically(plan, boundary) {
126
+ const reads = new Map();
127
+ const nested = new Map();
128
+ for (const segment of plan.segments) {
129
+ reads.set(segment, judgedCandidatesOf(segment).flatMap((c) => lexicalReadsOf(c, boundary)));
130
+ for (const n of segment.nested)
131
+ nested.set(n, establishShellReadsLexically(n, boundary));
132
+ }
133
+ return { plan, reads, nested, roots: boundary.roots };
134
+ }
135
+ export function createShellReadEstablisher(env, readRoots) {
90
136
  const lexicalOnly = boundaryIsTheSandbox(env);
91
137
  const resolverIsAuthoritative = canonicalPathIsAuthoritative(env);
138
+ const family = pathFamilyOf({ root: readRoots[0] ?? "/" });
139
+ const SEP = family === "posix" ? /\// : /[\\/]/;
140
+ const TRAILING_SEP = family === "posix" ? /\/+$/ : /[\\/]+$/;
141
+ const joinUnder = (dir, name) => `${dir.replace(TRAILING_SEP, "")}/${name}`;
92
142
  let rootsOnce;
93
143
  const canonicalRoots = async (signal) => {
94
144
  rootsOnce ??= (async () => {
@@ -102,7 +152,7 @@ export function createCanonicalReadBoundary(env, readRoots) {
102
152
  return await rootsOnce;
103
153
  };
104
154
  const resolveWithoutParentSteps = async (p, signal) => {
105
- const parts = p.split(/[\\/]/);
155
+ const parts = p.split(SEP);
106
156
  const tail = [];
107
157
  let allAbsent = true;
108
158
  while (parts.length > 0) {
@@ -110,7 +160,7 @@ export function createCanonicalReadBoundary(env, readRoots) {
110
160
  if (canon.ok) {
111
161
  if (tail.length === 0)
112
162
  return { value: canon.value, exact: true };
113
- return { value: [canon.value.replace(/[\\/]+$/, ""), ...[...tail].reverse()].join("/"), exact: false, missing: allAbsent };
163
+ return { value: [canon.value.replace(TRAILING_SEP, ""), ...[...tail].reverse()].join("/"), exact: false, missing: allAbsent };
114
164
  }
115
165
  if (canon.error.code !== "not_found" && canon.error.code !== "not_directory")
116
166
  allAbsent = false;
@@ -120,7 +170,7 @@ export function createCanonicalReadBoundary(env, readRoots) {
120
170
  };
121
171
  const parentOf = (canonical) => normalizeAbsPathLexically(`${canonical}/..`);
122
172
  const resolve = async (p, signal) => {
123
- const parts = p.split(/[\\/]/);
173
+ const parts = p.split(SEP);
124
174
  const first = parts.findIndex((seg) => seg === "..");
125
175
  if (first < 0)
126
176
  return await resolveWithoutParentSteps(p, signal);
@@ -132,7 +182,7 @@ export function createCanonicalReadBoundary(env, readRoots) {
132
182
  const flush = async () => {
133
183
  if (pending.length === 0)
134
184
  return true;
135
- const next = await resolveWithoutParentSteps([head.value.replace(/[\\/]+$/, ""), ...pending].join("/"), signal);
185
+ const next = await resolveWithoutParentSteps([head.value.replace(TRAILING_SEP, ""), ...pending].join("/"), signal);
136
186
  pending = [];
137
187
  if (next === undefined)
138
188
  return false;
@@ -170,40 +220,37 @@ export function createCanonicalReadBoundary(env, readRoots) {
170
220
  };
171
221
  const expandGlobCandidate = async (p, roots, signal) => {
172
222
  const head = literalRootHead(p, roots);
173
- let segs = pathSegmentsFrom(p, head?.length ?? 0);
223
+ let segs = pathSegmentsFrom(p, head?.length ?? 0, family === "posix");
174
224
  let first = segs.findIndex((s) => GLOB_METACHAR_RE.test(s.text));
175
225
  if (first < 0 && head !== undefined && GLOB_METACHAR_RE.test(head)) {
176
- segs = pathSegmentsFrom(p, 0);
226
+ segs = pathSegmentsFrom(p, 0, family === "posix");
177
227
  first = segs.findIndex((s) => GLOB_METACHAR_RE.test(s.text));
178
228
  }
179
229
  if (first < 0)
180
230
  return undefined;
181
- let frontier = [p.slice(0, segs[first].start).replace(/[\\/]+$/, "") || "/"];
231
+ let frontier = [p.slice(0, segs[first].start).replace(TRAILING_SEP, "") || "/"];
182
232
  let dirsUsed = 0;
183
233
  let matches = 0;
184
234
  const canonicalOfMatch = new Map();
185
235
  for (let j = first; j < segs.length; j++) {
186
236
  const seg = segs[j].text;
187
237
  if (!GLOB_METACHAR_RE.test(seg)) {
188
- frontier = frontier.map((d) => joinCandidate(d, seg));
238
+ frontier = frontier.map((d) => joinUnder(d, seg));
189
239
  continue;
190
240
  }
191
241
  const re = compileGlobSegment(seg);
192
- if (re === undefined) {
242
+ if (re === undefined)
193
243
  return { unverifiable: `the pattern "${seg}" cannot be expanded to verify what it would read` };
194
- }
195
244
  const isLast = j === segs.length - 1;
196
245
  const next = [];
197
246
  for (const dir of frontier) {
198
- if (++dirsUsed > MAX_GLOB_DIRS) {
247
+ if (++dirsUsed > MAX_GLOB_DIRS)
199
248
  return { unverifiable: `"${p}" expands over more than ${MAX_GLOB_DIRS} directories, too many to enumerate to verify` };
200
- }
201
249
  const canonDir = await resolve(dir, signal);
202
250
  if (canonDir !== undefined && canonDir.missing === true)
203
251
  continue;
204
- if (canonDir !== undefined && !canonDir.exact) {
252
+ if (canonDir !== undefined && !canonDir.exact)
205
253
  return { unverifiable: `"${dir}" cannot be resolved to verify what "${seg}" would expand to` };
206
- }
207
254
  const listed = await env.listDir(canonDir?.value ?? dir, signal);
208
255
  if (!listed.ok) {
209
256
  if (listed.error.code === "not_found" || listed.error.code === "not_directory")
@@ -213,18 +260,17 @@ export function createCanonicalReadBoundary(env, readRoots) {
213
260
  if (seg.startsWith(".")) {
214
261
  for (const dot of [".", ".."])
215
262
  if (re.test(dot))
216
- next.push(joinCandidate(dir, dot));
263
+ next.push(joinUnder(dir, dot));
217
264
  }
218
265
  const canonDirHere = isLast ? canonDir?.value : undefined;
219
266
  for (const entry of listed.value) {
220
267
  if (!globSegmentMatches(re, seg, entry.name))
221
268
  continue;
222
- if (++matches > MAX_GLOB_MATCHES) {
269
+ if (++matches > MAX_GLOB_MATCHES)
223
270
  return { unverifiable: `"${p}" expands to more than ${MAX_GLOB_MATCHES} names, too many to enumerate to verify` };
224
- }
225
- const candidate = joinCandidate(dir, entry.name);
271
+ const candidate = joinUnder(dir, entry.name);
226
272
  if (canonDirHere !== undefined && entry.kind !== "symlink")
227
- canonicalOfMatch.set(candidate, joinCandidate(canonDirHere, entry.name));
273
+ canonicalOfMatch.set(candidate, joinUnder(canonDirHere, entry.name));
228
274
  next.push(candidate);
229
275
  }
230
276
  }
@@ -234,93 +280,161 @@ export function createCanonicalReadBoundary(env, readRoots) {
234
280
  }
235
281
  return { paths: frontier, canonical: canonicalOfMatch };
236
282
  };
237
- return {
238
- escapesAfterResolution: async (paths, ask, signal) => {
239
- const deny = ask.deny;
240
- if (lexicalOnly || (paths.literal.length === 0 && paths.patterns.length === 0))
241
- return { escaping: [] };
242
- const roots = await canonicalRoots(signal);
243
- const escaping = [];
244
- let unresolvable;
245
- let denied;
246
- const denyHit = (p) => {
247
- if (deny === undefined || denied !== undefined)
248
- return false;
249
- const folded = normalizeAbsPathLexically(p);
250
- if (ask.denyAnswered.has(folded))
251
- return false;
252
- const pattern = deny(folded);
253
- if (pattern === null)
254
- return false;
255
- denied = { path: folded, pattern };
256
- return true;
257
- };
258
- const judge = async (p, canonicalKnown, origin) => {
259
- if (denyHit(p))
260
- return;
261
- const real = canonicalKnown !== undefined ? { value: canonicalKnown, exact: true } : await resolve(p, signal);
262
- if (real === undefined) {
263
- if (resolverIsAuthoritative)
264
- unresolvable ??= `"${p}" cannot be resolved to verify where it reads`;
265
- return;
283
+ const enumerateTraversal = async (rootCanonical, signal) => {
284
+ const members = [];
285
+ const stack = [{ dir: rootCanonical, spell: rootCanonical }];
286
+ const visited = new Set([rootCanonical]);
287
+ let dirsUsed = 0;
288
+ while (stack.length > 0) {
289
+ const { dir, spell } = stack.pop();
290
+ if (++dirsUsed > MAX_GLOB_DIRS)
291
+ return { unverifiable: `"${rootCanonical}" holds more than ${MAX_GLOB_DIRS} directories, too many to enumerate to verify what a recursive read would open` };
292
+ const listed = await env.listDir(dir, signal);
293
+ if (!listed.ok) {
294
+ if (listed.error.code === "not_directory" || listed.error.code === "not_found")
295
+ continue;
296
+ return { unverifiable: `"${dir}" cannot be enumerated to verify what a recursive read would open (${listed.error.code})` };
297
+ }
298
+ for (const entry of listed.value) {
299
+ if (members.length >= MAX_GLOB_MATCHES)
300
+ return { unverifiable: `"${rootCanonical}" holds more than ${MAX_GLOB_MATCHES} entries, too many to enumerate to verify what a recursive read would open` };
301
+ const path = joinUnder(spell, entry.name);
302
+ const canonicalPath = joinUnder(dir, entry.name);
303
+ if (entry.kind === "symlink") {
304
+ const canon = await env.canonicalPath(canonicalPath, signal);
305
+ members.push({ path, ...(canon.ok ? { canonical: canon.value } : {}), symlink: true });
306
+ if (canon.ok && !visited.has(canon.value)) {
307
+ visited.add(canon.value);
308
+ stack.push({ dir: canon.value, spell: path });
309
+ }
310
+ continue;
266
311
  }
267
- if (!real.exact && real.missing !== true) {
268
- unresolvable ??= `"${p}" cannot be resolved to verify where it reads`;
269
- return;
312
+ if (entry.kind === "directory") {
313
+ if (!visited.has(canonicalPath)) {
314
+ visited.add(canonicalPath);
315
+ stack.push({ dir: canonicalPath, spell: path });
316
+ }
317
+ continue;
270
318
  }
271
- if (denyHit(real.value))
319
+ members.push({ path, canonical: canonicalPath, symlink: false });
320
+ }
321
+ }
322
+ return { members };
323
+ };
324
+ const canonicalReadOf = async (candidate, spelling, spellings, established, origin, canonicalKnown, signal) => {
325
+ const real = canonicalKnown !== undefined ? { value: canonicalKnown, exact: true } : await resolve(spelling, signal);
326
+ if (real === undefined) {
327
+ if (resolverIsAuthoritative)
328
+ return { candidate, established: "unestablishable", spellings, ...(origin !== undefined ? { origin } : {}), cause: `"${spelling}" cannot be resolved to verify where it reads` };
329
+ return { candidate, established: "lexical", target: normalizeAbsPathLexically(spelling), spellings, ...(origin !== undefined ? { origin } : {}) };
330
+ }
331
+ if (!real.exact && real.missing !== true) {
332
+ return { candidate, established: "unestablishable", spellings, ...(origin !== undefined ? { origin } : {}), cause: `"${spelling}" cannot be resolved to verify where it reads` };
333
+ }
334
+ return { candidate, established, target: real.value, spellings, ...(origin !== undefined ? { origin } : {}) };
335
+ };
336
+ const establishSegment = async (segment, boundary, roots, signal) => {
337
+ const out = [];
338
+ for (const candidate of judgedCandidatesOf(segment)) {
339
+ const traversal = candidate.provenance === "traversal_root";
340
+ const land = async (read, spellings, folded) => {
341
+ out.push(read);
342
+ if (!traversal || (read.established !== "canonical" && read.established !== "expansion") || read.target === undefined)
272
343
  return;
273
- if (ask.containmentAnswered.has(normalizeAbsPathLexically(origin ?? p)))
344
+ const walkedTree = await enumerateTraversal(read.target, signal);
345
+ if ("unverifiable" in walkedTree) {
346
+ out.push({ candidate, established: "unestablishable", spellings, origin: folded, cause: walkedTree.unverifiable });
274
347
  return;
275
- if (ask.containment && !withinAnyRoot(roots, real.value) && !escaping.includes(real.value))
276
- escaping.push(real.value);
348
+ }
349
+ for (const m of walkedTree.members)
350
+ out.push(await canonicalReadOf(candidate, m.path, [m.path], "expansion", folded, m.canonical, signal));
277
351
  };
278
- for (const p of paths.literal) {
279
- if (denied !== undefined)
280
- break;
281
- await judge(p);
282
- }
283
- for (const p of paths.patterns) {
284
- if (denied !== undefined)
285
- break;
286
- const expanded = await expandGlobCandidate(p, roots, signal);
287
- if (expanded !== undefined && "unverifiable" in expanded) {
288
- unresolvable ??= expanded.unverifiable;
352
+ for (const lexical of lexicalReadsOf(candidate, boundary)) {
353
+ if (lexical.established !== "lexical") {
354
+ out.push(lexical);
289
355
  continue;
290
356
  }
291
- if (expanded !== undefined && expanded.paths.length > 0) {
292
- for (const t of expanded.paths) {
293
- if (denied !== undefined)
294
- break;
295
- await judge(t, expanded.canonical.get(t), p);
357
+ const folded = lexical.target;
358
+ const spellings = lexical.spellings;
359
+ const walked = spellings.find((s) => s !== folded) ?? folded;
360
+ if (candidate.expands === "glob") {
361
+ let matched = false;
362
+ for (const p of spellings) {
363
+ const expanded = await expandGlobCandidate(p, roots, signal);
364
+ if (expanded !== undefined && "unverifiable" in expanded) {
365
+ out.push({ candidate, established: "unestablishable", spellings, origin: folded, cause: expanded.unverifiable });
366
+ matched = true;
367
+ continue;
368
+ }
369
+ if (expanded !== undefined && expanded.paths.length > 0) {
370
+ matched = true;
371
+ for (const t of expanded.paths)
372
+ await land(await canonicalReadOf(candidate, t, [t, folded], "expansion", folded, expanded.canonical.get(t), signal), spellings, folded);
373
+ }
296
374
  }
375
+ if (!matched)
376
+ await land(await canonicalReadOf(candidate, walked, spellings, "canonical", undefined, undefined, signal), spellings, folded);
377
+ continue;
297
378
  }
298
- else
299
- await judge(p);
379
+ await land(await canonicalReadOf(candidate, walked, spellings, "canonical", undefined, undefined, signal), spellings, folded);
300
380
  }
301
- if (denied !== undefined)
302
- return { escaping, denied };
303
- return unresolvable === undefined ? { escaping } : { escaping, unverifiable: unresolvable };
304
- },
305
- canonicalOf: async (path, signal) => {
381
+ }
382
+ return out;
383
+ };
384
+ const establishPlan = async (plan, boundary, roots, signal) => {
385
+ const reads = new Map();
386
+ const nested = new Map();
387
+ for (const segment of plan.segments) {
388
+ reads.set(segment, await establishSegment(segment, boundary, roots, signal));
389
+ for (const n of segment.nested)
390
+ nested.set(n, await establishPlan(n, boundary, roots, signal));
391
+ }
392
+ return { plan, reads, nested, roots };
393
+ };
394
+ return {
395
+ lexicalOnly,
396
+ establish: async (plan, boundary, signal) => {
306
397
  if (lexicalOnly)
307
- return undefined;
308
- const real = await resolve(path, signal);
309
- return real === undefined || (!real.exact && real.missing !== true) ? undefined : real.value;
310
- },
311
- allResolveInside: async (paths, signal) => {
312
- if (lexicalOnly || paths.length === 0)
313
- return false;
398
+ return establishShellReadsLexically(plan, boundary);
314
399
  const roots = await canonicalRoots(signal);
315
- for (const p of paths) {
316
- const real = await resolve(p, signal);
317
- if (real === undefined || (!real.exact && real.missing !== true))
318
- return false;
319
- if (!withinAnyRoot(roots, real.value))
320
- return false;
321
- }
322
- return true;
400
+ return await establishPlan(plan, { ...boundary, roots }, roots, signal);
401
+ },
402
+ reestablish: async (previous, boundary, signal) => {
403
+ if (lexicalOnly)
404
+ return { changed: [] };
405
+ const again = await establishPlan(previous.plan, { ...boundary, roots: previous.roots }, previous.roots, signal);
406
+ const settled = (e) => {
407
+ const out = [];
408
+ const walk = (p) => {
409
+ for (const reads of p.reads.values())
410
+ for (const r of reads)
411
+ if ((r.established === "canonical" || r.established === "expansion") && r.target !== undefined)
412
+ out.push(r.target);
413
+ for (const n of p.nested.values())
414
+ walk(n);
415
+ };
416
+ walk(e);
417
+ return out.sort();
418
+ };
419
+ const before = settled(previous);
420
+ const after = new Set(settled(again));
421
+ const beforeSet = new Set(before);
422
+ const moved = before.filter((t) => !after.has(t));
423
+ const appeared = [...after].filter((t) => !beforeSet.has(t));
424
+ if (moved.length === 0 && appeared.length === 0)
425
+ return { changed: [] };
426
+ const changed = [];
427
+ const collect = (p) => {
428
+ for (const reads of p.reads.values())
429
+ for (const r of reads)
430
+ if (r.target !== undefined && (moved.includes(r.target) || appeared.includes(r.target)))
431
+ changed.push(r);
432
+ for (const n of p.nested.values())
433
+ collect(n);
434
+ };
435
+ collect(previous);
436
+ collect(again);
437
+ return { changed };
323
438
  },
324
439
  };
325
440
  }
326
- export const NOTHING_ANSWERED = new Set();