@abelxiaoxing/cadence 1.0.14 → 1.1.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 (46) hide show
  1. package/README.md +29 -13
  2. package/agents/contract-reviewer.md +2 -2
  3. package/agents/design-explorer.md +1 -1
  4. package/agents/diagnosis-worker.md +6 -3
  5. package/agents/implementation-worker.md +14 -9
  6. package/config/.env.example +5 -17
  7. package/config/routes.example.json +42 -0
  8. package/package.json +3 -3
  9. package/prompts/abel-design.md +150 -228
  10. package/prompts/abel-diagnose.md +37 -18
  11. package/prompts/abel-implement.md +111 -178
  12. package/prompts/abel-init.md +36 -33
  13. package/skills/abel-workflow/SKILL.md +156 -168
  14. package/src/apply-transaction.ts +1427 -0
  15. package/src/artifact-store.ts +204 -0
  16. package/src/child-session.ts +136 -17
  17. package/src/contracts.ts +105 -696
  18. package/src/control-contracts.ts +259 -0
  19. package/src/delivery-compiler.ts +1088 -0
  20. package/src/file-snapshot.ts +5 -0
  21. package/src/implement-graph.ts +11 -4
  22. package/src/index.ts +1465 -133
  23. package/src/isolation-backend.ts +269 -0
  24. package/src/packet-runtime.ts +533 -0
  25. package/src/parent-provider.ts +23 -4
  26. package/src/route-policy.ts +483 -0
  27. package/src/run-state.ts +393 -0
  28. package/src/run-store.ts +918 -0
  29. package/src/safe-path.ts +11 -0
  30. package/src/scoped-tools.ts +36 -0
  31. package/src/state-root.ts +215 -0
  32. package/src/subagent-activity.ts +526 -27
  33. package/src/submit-tool.ts +274 -0
  34. package/src/task-ledger.ts +1380 -0
  35. package/src/verification-capability.ts +529 -42
  36. package/src/worker-broker.ts +703 -0
  37. package/src/workflow-engine.ts +7391 -0
  38. package/src/workspace-store.ts +544 -0
  39. package/src/candidate-preflight.ts +0 -1618
  40. package/src/drain.ts +0 -20
  41. package/src/patch.ts +0 -505
  42. package/src/result-store.ts +0 -312
  43. package/src/runtime.ts +0 -2436
  44. package/src/scheduler.ts +0 -384
  45. package/src/subagent-endpoint.ts +0 -158
  46. package/src/worker.ts +0 -204
@@ -1,4 +1,10 @@
1
- import { lstatSync, readFileSync, realpathSync } from "node:fs";
1
+ import {
2
+ lstatSync,
3
+ readdirSync,
4
+ readFileSync,
5
+ realpathSync,
6
+ type Stats,
7
+ } from "node:fs";
2
8
  import path from "node:path";
3
9
  import {
4
10
  type AtomicVerificationContract,
@@ -8,6 +14,7 @@ import {
8
14
  verificationInputPaths,
9
15
  verificationSteps,
10
16
  } from "./contracts.ts";
17
+ import { type Bound, isCurrent, snapshotFiles } from "./file-snapshot.ts";
11
18
  import { isSafeRegularFile, observeSafePath } from "./safe-path.ts";
12
19
 
13
20
  export type VerificationCapabilityDiagnostic =
@@ -32,16 +39,41 @@ export type VerificationCapabilityResult =
32
39
  }
33
40
  | { ok: false; diagnostic: VerificationCapabilityDiagnostic };
34
41
 
42
+ export interface CurrentVerificationCapability {
43
+ verificationId: string;
44
+ runnerBindings: VerificationRunnerBinding[];
45
+ inputSnapshot: Bound;
46
+ }
47
+
48
+ export type CurrentVerificationCapabilityResult =
49
+ | { ok: true; value: CurrentVerificationCapability }
50
+ | { ok: false; diagnostic: VerificationCapabilityDiagnostic };
51
+
35
52
  export interface VerificationRunnerBinding {
36
53
  command: string;
37
54
  executablePath: string;
55
+ fixedArgs?: string[];
38
56
  mountSource?: string;
39
57
  }
40
58
 
59
+ export interface VerificationRunnerEnvironment {
60
+ platform?: NodeJS.Platform;
61
+ path?: string;
62
+ pathExt?: string;
63
+ pathDelimiter?: string;
64
+ }
65
+
66
+ export interface VerificationCapabilityOptions {
67
+ dependencyOwner?: string;
68
+ runnerEnvironment?: VerificationRunnerEnvironment;
69
+ }
70
+
41
71
  const SHELL_OPERATOR = /[;&|`$<>\n\r\0]/u;
42
72
  const SIMPLE_TOKEN =
43
73
  /^(?:[a-z0-9][a-z0-9._:@/=-]*|--?[a-z0-9][a-z0-9._:@/=-]*)$/iu;
44
74
  const NETWORKING_RUNNERS = new Set(["bunx", "pnpx"]);
75
+ const WINDOWS_EXECUTABLE_EXTENSIONS = new Set([".exe", ".cmd"]);
76
+ const DEFAULT_WINDOWS_PATH_EXT = ".COM;.EXE;.BAT;.CMD";
45
77
  const SYSTEM_MOUNT_ROOTS = ["/usr", "/bin", "/lib", "/lib64", "/etc"];
46
78
  const BROAD_MOUNT_ROOTS = new Set([
47
79
  "/",
@@ -60,12 +92,17 @@ function capabilitySuccess(
60
92
  return {
61
93
  ok: true,
62
94
  verificationId,
63
- runnerBindings: runnerBindings.filter((binding) => {
64
- const key = `${binding.command}\0${binding.executablePath}\0${binding.mountSource ?? ""}`;
65
- if (seen.has(key)) return false;
66
- seen.add(key);
67
- return true;
68
- }),
95
+ runnerBindings: runnerBindings
96
+ .filter((binding) => {
97
+ const key = `${binding.command}\0${binding.executablePath}\0${JSON.stringify(binding.fixedArgs ?? [])}\0${binding.mountSource ?? ""}`;
98
+ if (seen.has(key)) return false;
99
+ seen.add(key);
100
+ return true;
101
+ })
102
+ .map((binding) => ({
103
+ ...binding,
104
+ ...(binding.fixedArgs ? { fixedArgs: [...binding.fixedArgs] } : {}),
105
+ })),
69
106
  };
70
107
  }
71
108
 
@@ -97,29 +134,138 @@ function localExecutable(
97
134
  root: string,
98
135
  executable: string,
99
136
  dependencyOwner = root,
137
+ environment: VerificationRunnerEnvironment = {},
100
138
  ): boolean {
101
139
  const dependencyRoot = path.resolve(dependencyOwner, "node_modules");
102
- const unresolved = path.resolve(dependencyRoot, ".bin", executable);
103
140
  try {
104
141
  const canonicalDependencyRoot = realpathSync(dependencyRoot);
105
- const canonical = realpathSync(unresolved);
106
- const stat = lstatSync(canonical);
107
- return (
108
- canonical !== canonicalDependencyRoot &&
109
- canonical.startsWith(`${canonicalDependencyRoot}${path.sep}`) &&
110
- stat.isFile() &&
111
- (stat.mode & 0o111) !== 0
112
- );
142
+ for (const name of localExecutableNames(executable, environment)) {
143
+ const unresolved = resolveCandidateName(
144
+ path.resolve(dependencyRoot, ".bin"),
145
+ name,
146
+ environment.platform ?? process.platform,
147
+ );
148
+ if (!unresolved) continue;
149
+ const canonical = realpathSync(unresolved);
150
+ const stat = lstatSync(canonical);
151
+ if (
152
+ canonical !== canonicalDependencyRoot &&
153
+ pathWithin(
154
+ canonicalDependencyRoot,
155
+ canonical,
156
+ environment.platform ?? process.platform,
157
+ ) &&
158
+ regularExecutableFile(stat, environment.platform ?? process.platform)
159
+ ) {
160
+ return true;
161
+ }
162
+ }
163
+ return false;
113
164
  } catch {
114
165
  return false;
115
166
  }
116
167
  }
117
168
 
169
+ function pathWithin(
170
+ root: string,
171
+ candidate: string,
172
+ platform: NodeJS.Platform,
173
+ ): boolean {
174
+ const normalizedRoot = platform === "win32" ? root.toLowerCase() : root;
175
+ const normalizedCandidate =
176
+ platform === "win32" ? candidate.toLowerCase() : candidate;
177
+ return (
178
+ normalizedCandidate === normalizedRoot ||
179
+ normalizedCandidate.startsWith(`${normalizedRoot}${path.sep}`)
180
+ );
181
+ }
182
+
183
+ function regularExecutableFile(
184
+ stat: Stats,
185
+ platform: NodeJS.Platform,
186
+ ): boolean {
187
+ return stat.isFile() && (platform === "win32" || (stat.mode & 0o111) !== 0);
188
+ }
189
+
190
+ function windowsPathExtensions(pathExt: string | undefined): string[] {
191
+ const seen = new Set<string>();
192
+ const extensions: string[] = [];
193
+ for (const entry of (pathExt ?? DEFAULT_WINDOWS_PATH_EXT).split(";")) {
194
+ const extension = entry.trim().toLowerCase();
195
+ if (!WINDOWS_EXECUTABLE_EXTENSIONS.has(extension) || seen.has(extension)) {
196
+ continue;
197
+ }
198
+ seen.add(extension);
199
+ extensions.push(extension);
200
+ }
201
+ return extensions;
202
+ }
203
+
204
+ function runnerNames(
205
+ command: string,
206
+ environment: VerificationRunnerEnvironment,
207
+ ): string[] {
208
+ if (!/^[a-z0-9][a-z0-9._-]*$/iu.test(command)) return [];
209
+ if ((environment.platform ?? process.platform) !== "win32") {
210
+ return [command];
211
+ }
212
+ const extension = path.extname(command).toLowerCase();
213
+ if (extension) {
214
+ return WINDOWS_EXECUTABLE_EXTENSIONS.has(extension) ? [command] : [];
215
+ }
216
+ return windowsPathExtensions(environment.pathExt).map(
217
+ (candidateExtension) => `${command}${candidateExtension}`,
218
+ );
219
+ }
220
+
221
+ function localExecutableNames(
222
+ executable: string,
223
+ environment: VerificationRunnerEnvironment,
224
+ ): string[] {
225
+ if (!/^[a-z0-9][a-z0-9._-]*$/iu.test(executable)) return [];
226
+ if ((environment.platform ?? process.platform) !== "win32") {
227
+ return [executable];
228
+ }
229
+ const extension = path.extname(executable).toLowerCase();
230
+ if (extension) {
231
+ return WINDOWS_EXECUTABLE_EXTENSIONS.has(extension) ? [executable] : [];
232
+ }
233
+ return [
234
+ executable,
235
+ ...windowsPathExtensions(environment.pathExt).map(
236
+ (candidateExtension) => `${executable}${candidateExtension}`,
237
+ ),
238
+ ];
239
+ }
240
+
241
+ function resolveCandidateName(
242
+ directory: string,
243
+ name: string,
244
+ platform: NodeJS.Platform,
245
+ ): string | null {
246
+ const exact = path.join(directory, name);
247
+ if (platform !== "win32") return exact;
248
+ try {
249
+ const matches = readdirSync(directory).filter(
250
+ (entry) => entry.toLowerCase() === name.toLowerCase(),
251
+ );
252
+ return matches.length === 1
253
+ ? path.join(directory, matches[0] as string)
254
+ : null;
255
+ } catch {
256
+ return null;
257
+ }
258
+ }
259
+
118
260
  function within(root: string, candidate: string): boolean {
119
261
  return candidate === root || candidate.startsWith(`${root}${path.sep}`);
120
262
  }
121
263
 
122
- function commonDirectory(left: string, right: string): string {
264
+ function commonDirectory(
265
+ left: string,
266
+ right: string,
267
+ platform: NodeJS.Platform = process.platform,
268
+ ): string {
123
269
  const leftParts = path.resolve(left).split(path.sep).filter(Boolean);
124
270
  const rightParts = path.resolve(right).split(path.sep).filter(Boolean);
125
271
  const common: string[] = [];
@@ -128,38 +274,305 @@ function commonDirectory(left: string, right: string): string {
128
274
  index < Math.min(leftParts.length, rightParts.length);
129
275
  index++
130
276
  ) {
131
- if (leftParts[index] !== rightParts[index]) break;
132
- common.push(leftParts[index] as string);
277
+ const leftPart = leftParts[index] as string;
278
+ const rightPart = rightParts[index] as string;
279
+ if (
280
+ (platform === "win32" ? leftPart.toLowerCase() : leftPart) !==
281
+ (platform === "win32" ? rightPart.toLowerCase() : rightPart)
282
+ ) {
283
+ break;
284
+ }
285
+ common.push(leftPart);
133
286
  }
134
287
  return path.join(path.parse(path.resolve(left)).root, ...common);
135
288
  }
136
289
 
137
290
  function unsafeMountSource(source: string): boolean {
138
291
  if (BROAD_MOUNT_ROOTS.has(source)) return true;
139
- const parts = source.split(path.sep).filter(Boolean);
292
+ if (path.resolve(source) === path.parse(path.resolve(source)).root)
293
+ return true;
294
+ const parts = path
295
+ .relative(path.parse(path.resolve(source)).root, path.resolve(source))
296
+ .split(path.sep)
297
+ .filter(Boolean)
298
+ .map((part) => part.toLowerCase());
140
299
  return (
141
300
  (parts[0] === "home" && parts.length <= 2) ||
142
- (parts[0] === "Users" && parts.length <= 2) ||
301
+ (parts[0] === "users" && parts.length <= 2) ||
143
302
  (parts[0] === "root" && parts.length <= 1)
144
303
  );
145
304
  }
146
305
 
306
+ function safeRunnerSource(
307
+ paths: readonly string[],
308
+ platform: NodeJS.Platform,
309
+ ): string | null {
310
+ let source = path.dirname(paths[0] as string);
311
+ for (const candidate of paths.slice(1)) {
312
+ source = commonDirectory(source, path.dirname(candidate), platform);
313
+ }
314
+ if (unsafeMountSource(source)) return null;
315
+ try {
316
+ const stat = lstatSync(source);
317
+ if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
318
+ } catch {
319
+ return null;
320
+ }
321
+ return paths.every((candidate) => pathWithin(source, candidate, platform))
322
+ ? source
323
+ : null;
324
+ }
325
+
326
+ function resolvedRunnerEnvironment(
327
+ environment: VerificationRunnerEnvironment,
328
+ ): Required<VerificationRunnerEnvironment> {
329
+ return {
330
+ platform: environment.platform ?? process.platform,
331
+ path: environment.path ?? process.env.PATH ?? "",
332
+ pathExt:
333
+ environment.pathExt ?? process.env.PATHEXT ?? DEFAULT_WINDOWS_PATH_EXT,
334
+ pathDelimiter: environment.pathDelimiter ?? path.delimiter,
335
+ };
336
+ }
337
+
338
+ function windowsShimBinding(
339
+ command: string,
340
+ launcherPath: string,
341
+ environment: Required<VerificationRunnerEnvironment>,
342
+ ): VerificationRunnerBinding | null {
343
+ const name = path.basename(command, path.extname(command)).toLowerCase();
344
+ if (name !== "npm" && name !== "npx") return null;
345
+ const node = resolveVerificationRunner("node", environment);
346
+ if (
347
+ !node ||
348
+ node.fixedArgs ||
349
+ path.extname(node.executablePath).toLowerCase() !== ".exe"
350
+ ) {
351
+ return null;
352
+ }
353
+ const cli = path.join(
354
+ path.dirname(launcherPath),
355
+ "node_modules",
356
+ "npm",
357
+ "bin",
358
+ `${name}-cli.js`,
359
+ );
360
+ try {
361
+ const canonicalLauncher = realpathSync(launcherPath);
362
+ const canonicalCli = realpathSync(cli);
363
+ const cliStat = lstatSync(canonicalCli);
364
+ if (!cliStat.isFile()) return null;
365
+ const source = safeRunnerSource(
366
+ [node.executablePath, canonicalLauncher, canonicalCli],
367
+ environment.platform,
368
+ );
369
+ if (!source) return null;
370
+ return {
371
+ command,
372
+ executablePath: node.executablePath,
373
+ fixedArgs: [canonicalCli],
374
+ };
375
+ } catch {
376
+ return null;
377
+ }
378
+ }
379
+
380
+ function localPackageDirectories(dependencyRoot: string): string[] {
381
+ const packages: string[] = [];
382
+ try {
383
+ for (const entry of readdirSync(dependencyRoot, { withFileTypes: true })) {
384
+ if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
385
+ const candidate = path.join(dependencyRoot, entry.name);
386
+ if (!entry.name.startsWith("@")) {
387
+ packages.push(candidate);
388
+ continue;
389
+ }
390
+ for (const scoped of readdirSync(candidate, { withFileTypes: true })) {
391
+ if (scoped.isDirectory() && !scoped.name.startsWith(".")) {
392
+ packages.push(path.join(candidate, scoped.name));
393
+ }
394
+ }
395
+ }
396
+ } catch {
397
+ return [];
398
+ }
399
+ return packages;
400
+ }
401
+
402
+ function localPackageBinTargets(
403
+ dependencyOwner: string,
404
+ executable: string,
405
+ platform: NodeJS.Platform,
406
+ ): string[] {
407
+ const dependencyRoot = path.resolve(dependencyOwner, "node_modules");
408
+ let canonicalDependencyRoot: string;
409
+ try {
410
+ canonicalDependencyRoot = realpathSync(dependencyRoot);
411
+ } catch {
412
+ return [];
413
+ }
414
+ const targets: string[] = [];
415
+ for (const packageDirectory of localPackageDirectories(dependencyRoot)) {
416
+ try {
417
+ const directoryStat = lstatSync(packageDirectory);
418
+ if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink()) {
419
+ continue;
420
+ }
421
+ const canonicalPackage = realpathSync(packageDirectory);
422
+ if (
423
+ !pathWithin(canonicalDependencyRoot, canonicalPackage, platform) ||
424
+ canonicalPackage === canonicalDependencyRoot
425
+ ) {
426
+ continue;
427
+ }
428
+ const manifestPath = path.join(canonicalPackage, "package.json");
429
+ const manifestStat = lstatSync(manifestPath);
430
+ if (!manifestStat.isFile() || manifestStat.isSymbolicLink()) continue;
431
+ const manifest = JSON.parse(
432
+ readFileSync(manifestPath, "utf8"),
433
+ ) as unknown;
434
+ if (
435
+ !manifest ||
436
+ typeof manifest !== "object" ||
437
+ Array.isArray(manifest)
438
+ ) {
439
+ continue;
440
+ }
441
+ const record = manifest as Record<string, unknown>;
442
+ const packageName =
443
+ typeof record.name === "string"
444
+ ? (record.name.split("/").pop() ?? "")
445
+ : "";
446
+ const bin = record.bin;
447
+ let relative: unknown;
448
+ if (
449
+ typeof bin === "string" &&
450
+ (platform === "win32"
451
+ ? packageName.toLowerCase() === executable.toLowerCase()
452
+ : packageName === executable)
453
+ ) {
454
+ relative = bin;
455
+ } else if (bin && typeof bin === "object" && !Array.isArray(bin)) {
456
+ const entries = Object.entries(bin as Record<string, unknown>).filter(
457
+ ([name]) =>
458
+ platform === "win32"
459
+ ? name.toLowerCase() === executable.toLowerCase()
460
+ : name === executable,
461
+ );
462
+ if (entries.length === 1) relative = entries[0]?.[1];
463
+ }
464
+ if (
465
+ typeof relative !== "string" ||
466
+ relative.length === 0 ||
467
+ relative.includes("\0")
468
+ ) {
469
+ continue;
470
+ }
471
+ const unresolved = path.resolve(canonicalPackage, relative);
472
+ if (!pathWithin(canonicalPackage, unresolved, platform)) continue;
473
+ const canonical = realpathSync(unresolved);
474
+ const stat = lstatSync(canonical);
475
+ if (
476
+ stat.isFile() &&
477
+ pathWithin(canonicalPackage, canonical, platform) &&
478
+ pathWithin(canonicalDependencyRoot, canonical, platform)
479
+ ) {
480
+ targets.push(canonical);
481
+ }
482
+ } catch {}
483
+ }
484
+ return [...new Set(targets)];
485
+ }
486
+
487
+ function resolveLocalVerificationRunner(
488
+ dependencyOwner: string,
489
+ executable: string,
490
+ runnerEnvironment: VerificationRunnerEnvironment,
491
+ ): VerificationRunnerBinding | null {
492
+ const environment = resolvedRunnerEnvironment(runnerEnvironment);
493
+ if (environment.platform !== "win32") return null;
494
+ const targets = localPackageBinTargets(
495
+ dependencyOwner,
496
+ executable,
497
+ environment.platform,
498
+ );
499
+ if (targets.length !== 1) return null;
500
+ const target = targets[0] as string;
501
+ const extension = path.extname(target).toLowerCase();
502
+ if (extension === ".exe") {
503
+ return { command: executable, executablePath: target };
504
+ }
505
+ if (![".js", ".cjs", ".mjs"].includes(extension)) return null;
506
+ const node = resolveVerificationRunner("node", environment);
507
+ if (!node || node.fixedArgs) return null;
508
+ return {
509
+ command: executable,
510
+ executablePath: node.executablePath,
511
+ fixedArgs: [target],
512
+ };
513
+ }
514
+
147
515
  export function resolveVerificationRunner(
148
516
  command: string,
517
+ runnerEnvironment: VerificationRunnerEnvironment = {},
149
518
  ): VerificationRunnerBinding | null {
519
+ const environment = resolvedRunnerEnvironment(runnerEnvironment);
520
+ const directories = environment.path
521
+ .split(environment.pathDelimiter)
522
+ .filter(Boolean);
523
+ const seenDirectories = new Set<string>();
150
524
  const candidates = [
151
- ...(command === "bun" && process.versions.bun ? [process.execPath] : []),
152
- ...(process.env.PATH ?? "")
153
- .split(path.delimiter)
154
- .filter(Boolean)
155
- .map((directory) => path.join(directory, command)),
525
+ ...(command === "bun" &&
526
+ process.versions.bun &&
527
+ environment.platform === process.platform
528
+ ? [process.execPath]
529
+ : []),
530
+ ...directories.flatMap((directory) => {
531
+ const resolvedDirectory = path.resolve(directory);
532
+ const key =
533
+ environment.platform === "win32"
534
+ ? resolvedDirectory.toLowerCase()
535
+ : resolvedDirectory;
536
+ if (seenDirectories.has(key)) return [];
537
+ seenDirectories.add(key);
538
+ return runnerNames(command, environment).flatMap((name) => {
539
+ const candidate = resolveCandidateName(
540
+ resolvedDirectory,
541
+ name,
542
+ environment.platform,
543
+ );
544
+ return candidate ? [candidate] : [];
545
+ });
546
+ }),
156
547
  ];
548
+ const seenCandidates = new Set<string>();
157
549
  for (const candidate of candidates) {
158
550
  try {
159
551
  const executablePath = path.resolve(candidate);
552
+ const candidateKey =
553
+ environment.platform === "win32"
554
+ ? executablePath.toLowerCase()
555
+ : executablePath;
556
+ if (seenCandidates.has(candidateKey)) continue;
557
+ seenCandidates.add(candidateKey);
160
558
  const canonical = realpathSync(executablePath);
161
559
  const stat = lstatSync(canonical);
162
- if (!stat.isFile() || (stat.mode & 0o111) === 0) continue;
560
+ if (!regularExecutableFile(stat, environment.platform)) continue;
561
+ if (environment.platform === "win32") {
562
+ const extension = path.extname(executablePath).toLowerCase();
563
+ if (extension === ".cmd") {
564
+ const shim = windowsShimBinding(command, executablePath, environment);
565
+ if (shim) return shim;
566
+ continue;
567
+ }
568
+ if (extension !== ".exe") continue;
569
+ if (
570
+ !safeRunnerSource([executablePath, canonical], environment.platform)
571
+ ) {
572
+ continue;
573
+ }
574
+ return { command, executablePath };
575
+ }
163
576
  const systemVisible = SYSTEM_MOUNT_ROOTS.some(
164
577
  (root) => within(root, executablePath) && within(root, canonical),
165
578
  );
@@ -167,6 +580,7 @@ export function resolveVerificationRunner(
167
580
  const mountSource = commonDirectory(
168
581
  path.dirname(executablePath),
169
582
  path.dirname(canonical),
583
+ environment.platform,
170
584
  );
171
585
  if (unsafeMountSource(mountSource)) continue;
172
586
  const sourceStat = lstatSync(mountSource);
@@ -196,6 +610,7 @@ function validateScriptCommand(
196
610
  verificationId: string,
197
611
  command: string,
198
612
  dependencyOwner: string,
613
+ runnerEnvironment: VerificationRunnerEnvironment,
199
614
  ): VerificationCapabilityResult {
200
615
  if (
201
616
  command.length === 0 ||
@@ -234,14 +649,17 @@ function validateScriptCommand(
234
649
  }
235
650
  if (executable === "npx") {
236
651
  const local = tokens[1] === "--no-install" ? tokens[2] : undefined;
237
- if (!local || !localExecutable(root, local, dependencyOwner)) {
652
+ if (
653
+ !local ||
654
+ !localExecutable(root, local, dependencyOwner, runnerEnvironment)
655
+ ) {
238
656
  return adapterFailure(
239
657
  verificationId,
240
658
  "local-executable-missing",
241
659
  "npx requires --no-install and an installed local executable",
242
660
  );
243
661
  }
244
- const runner = resolveVerificationRunner("npx");
662
+ const runner = resolveVerificationRunner("npx", runnerEnvironment);
245
663
  return runner
246
664
  ? capabilitySuccess(verificationId, [runner])
247
665
  : adapterFailure(
@@ -251,7 +669,7 @@ function validateScriptCommand(
251
669
  );
252
670
  }
253
671
  if (["node", "bun"].includes(executable)) {
254
- const runner = resolveVerificationRunner(executable);
672
+ const runner = resolveVerificationRunner(executable, runnerEnvironment);
255
673
  return runner
256
674
  ? capabilitySuccess(verificationId, [runner])
257
675
  : adapterFailure(
@@ -260,7 +678,7 @@ function validateScriptCommand(
260
678
  `approved runner ${executable} is unavailable or cannot be mounted safely`,
261
679
  );
262
680
  }
263
- return localExecutable(root, executable, dependencyOwner)
681
+ return localExecutable(root, executable, dependencyOwner, runnerEnvironment)
264
682
  ? capabilitySuccess(verificationId)
265
683
  : adapterFailure(
266
684
  verificationId,
@@ -276,8 +694,12 @@ function validatePackageScript(
276
694
  script: string,
277
695
  approvedCommand: string,
278
696
  dependencyOwner = root,
697
+ runnerEnvironment: VerificationRunnerEnvironment = {},
279
698
  ): VerificationCapabilityResult {
280
- const packageRunner = resolveVerificationRunner(packageManager);
699
+ const packageRunner = resolveVerificationRunner(
700
+ packageManager,
701
+ runnerEnvironment,
702
+ );
281
703
  if (!packageRunner) {
282
704
  return adapterFailure(
283
705
  verificationId,
@@ -310,6 +732,7 @@ function validatePackageScript(
310
732
  verificationId,
311
733
  actual,
312
734
  dependencyOwner,
735
+ runnerEnvironment,
313
736
  );
314
737
  return scriptCapability.ok
315
738
  ? capabilitySuccess(verificationId, [
@@ -323,6 +746,7 @@ function validateAtomicCapability(
323
746
  root: string,
324
747
  step: AtomicVerificationContract,
325
748
  dependencyOwner: string,
749
+ runnerEnvironment: VerificationRunnerEnvironment,
326
750
  ): VerificationCapabilityResult {
327
751
  if (step.kind === "package-script") {
328
752
  return validatePackageScript(
@@ -332,6 +756,7 @@ function validateAtomicCapability(
332
756
  step.script,
333
757
  step.command,
334
758
  dependencyOwner,
759
+ runnerEnvironment,
335
760
  );
336
761
  }
337
762
  const runner = step.runner;
@@ -343,11 +768,12 @@ function validateAtomicCapability(
343
768
  runner.script,
344
769
  runner.command,
345
770
  dependencyOwner,
771
+ runnerEnvironment,
346
772
  );
347
773
  if (!script.ok) return script;
348
774
  if (
349
775
  step.kind === "vitest" &&
350
- !localExecutable(root, "vitest", dependencyOwner)
776
+ !localExecutable(root, "vitest", dependencyOwner, runnerEnvironment)
351
777
  ) {
352
778
  return adapterFailure(
353
779
  step.id,
@@ -358,7 +784,7 @@ function validateAtomicCapability(
358
784
  return script;
359
785
  }
360
786
  if (runner.kind === "node") {
361
- const node = resolveVerificationRunner("node");
787
+ const node = resolveVerificationRunner("node", runnerEnvironment);
362
788
  return node
363
789
  ? capabilitySuccess(step.id, [node])
364
790
  : adapterFailure(
@@ -367,7 +793,14 @@ function validateAtomicCapability(
367
793
  "Node is unavailable or cannot be mounted safely",
368
794
  );
369
795
  }
370
- if (!localExecutable(root, runner.executable, dependencyOwner)) {
796
+ if (
797
+ !localExecutable(
798
+ root,
799
+ runner.executable,
800
+ dependencyOwner,
801
+ runnerEnvironment,
802
+ )
803
+ ) {
371
804
  return adapterFailure(
372
805
  step.id,
373
806
  "local-executable-missing",
@@ -375,7 +808,7 @@ function validateAtomicCapability(
375
808
  );
376
809
  }
377
810
  if (runner.kind === "npx") {
378
- const npx = resolveVerificationRunner("npx");
811
+ const npx = resolveVerificationRunner("npx", runnerEnvironment);
379
812
  return npx
380
813
  ? capabilitySuccess(step.id, [npx])
381
814
  : adapterFailure(
@@ -384,23 +817,70 @@ function validateAtomicCapability(
384
817
  "npx is unavailable or cannot be mounted safely",
385
818
  );
386
819
  }
820
+ if ((runnerEnvironment.platform ?? process.platform) === "win32") {
821
+ const binding = resolveLocalVerificationRunner(
822
+ dependencyOwner,
823
+ runner.executable,
824
+ runnerEnvironment,
825
+ );
826
+ return binding
827
+ ? capabilitySuccess(step.id, [binding])
828
+ : adapterFailure(
829
+ step.id,
830
+ "local-executable-missing",
831
+ `local executable ${runner.executable} has no safe direct Windows binding`,
832
+ );
833
+ }
387
834
  return capabilitySuccess(step.id);
388
835
  }
389
836
 
390
837
  export function validateVerificationCapability(
391
838
  root: string,
392
839
  value: unknown,
393
- options: {
394
- dependencyOwner?: string;
395
- } = {},
840
+ options: VerificationCapabilityOptions = {},
396
841
  ): VerificationCapabilityResult {
397
842
  return validateCapability(root, value, options, true);
398
843
  }
399
844
 
845
+ /** Validate one exact adapter and bind all of its current file inputs. */
846
+ export function bindCurrentVerificationCapability(
847
+ root: string,
848
+ value: unknown,
849
+ options: VerificationCapabilityOptions = {},
850
+ ): CurrentVerificationCapabilityResult {
851
+ const capability = validateVerificationCapability(root, value, options);
852
+ if (!capability.ok) return capability;
853
+ const validation = validateVerificationContract(value);
854
+ if (!validation.ok) {
855
+ throw new Error("verification-capability-invariant");
856
+ }
857
+ return {
858
+ ok: true,
859
+ value: {
860
+ verificationId: capability.verificationId,
861
+ runnerBindings: capability.runnerBindings.map((binding) => ({
862
+ ...binding,
863
+ ...(binding.fixedArgs ? { fixedArgs: [...binding.fixedArgs] } : {}),
864
+ })),
865
+ inputSnapshot: snapshotFiles(
866
+ root,
867
+ verificationInputPaths(validation.value),
868
+ ),
869
+ },
870
+ };
871
+ }
872
+
873
+ export function isVerificationCapabilityCurrent(
874
+ root: string,
875
+ capability: CurrentVerificationCapability,
876
+ ): boolean {
877
+ return isCurrent(root, capability.inputSnapshot);
878
+ }
879
+
400
880
  export function validateVerificationAdapterCapability(
401
881
  root: string,
402
882
  value: unknown,
403
- options: { dependencyOwner?: string } = {},
883
+ options: VerificationCapabilityOptions = {},
404
884
  ): VerificationCapabilityResult {
405
885
  return validateCapability(root, value, options, false);
406
886
  }
@@ -408,7 +888,7 @@ export function validateVerificationAdapterCapability(
408
888
  function validateCapability(
409
889
  root: string,
410
890
  value: unknown,
411
- options: { dependencyOwner?: string },
891
+ options: VerificationCapabilityOptions,
412
892
  requireInputAvailability: boolean,
413
893
  ): VerificationCapabilityResult {
414
894
  const validation = validateVerificationContract(value);
@@ -435,6 +915,7 @@ function validateCapability(
435
915
  root,
436
916
  validation.value,
437
917
  dependencyOwner,
918
+ options.runnerEnvironment ?? {},
438
919
  requireInputAvailability,
439
920
  );
440
921
  }
@@ -443,6 +924,7 @@ function validateAcceptedVerificationCapability(
443
924
  root: string,
444
925
  verification: StructuredVerificationContract,
445
926
  dependencyOwner: string,
927
+ runnerEnvironment: VerificationRunnerEnvironment,
446
928
  requireInputAvailability: boolean,
447
929
  ): VerificationCapabilityResult {
448
930
  if (requireInputAvailability) {
@@ -458,7 +940,12 @@ function validateAcceptedVerificationCapability(
458
940
  }
459
941
  const runnerBindings: VerificationRunnerBinding[] = [];
460
942
  for (const step of verificationSteps(verification)) {
461
- const capability = validateAtomicCapability(root, step, dependencyOwner);
943
+ const capability = validateAtomicCapability(
944
+ root,
945
+ step,
946
+ dependencyOwner,
947
+ runnerEnvironment,
948
+ );
462
949
  if (!capability.ok) return capability;
463
950
  runnerBindings.push(...capability.runnerBindings);
464
951
  }