@abelxiaoxing/cadence 1.0.14 → 1.1.1

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 (48) hide show
  1. package/README.md +39 -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 +206 -228
  10. package/prompts/abel-diagnose.md +39 -18
  11. package/prompts/abel-implement.md +122 -178
  12. package/prompts/abel-init.md +36 -33
  13. package/skills/abel-workflow/SKILL.md +190 -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 +121 -696
  18. package/src/control-contracts.ts +246 -0
  19. package/src/delivery-compiler.ts +1118 -0
  20. package/src/design-control.ts +846 -0
  21. package/src/design-journal.ts +1569 -0
  22. package/src/file-snapshot.ts +5 -0
  23. package/src/implement-graph.ts +11 -4
  24. package/src/index.ts +1833 -118
  25. package/src/isolation-backend.ts +269 -0
  26. package/src/packet-runtime.ts +533 -0
  27. package/src/parent-provider.ts +23 -4
  28. package/src/route-policy.ts +483 -0
  29. package/src/run-state.ts +430 -0
  30. package/src/run-store.ts +990 -0
  31. package/src/safe-path.ts +11 -0
  32. package/src/scoped-tools.ts +36 -0
  33. package/src/state-root.ts +215 -0
  34. package/src/subagent-activity.ts +526 -27
  35. package/src/submit-tool.ts +274 -0
  36. package/src/task-ledger.ts +1380 -0
  37. package/src/verification-capability.ts +560 -45
  38. package/src/worker-broker.ts +703 -0
  39. package/src/workflow-engine.ts +7706 -0
  40. package/src/workspace-store.ts +544 -0
  41. package/src/candidate-preflight.ts +0 -1618
  42. package/src/drain.ts +0 -20
  43. package/src/patch.ts +0 -505
  44. package/src/result-store.ts +0 -312
  45. package/src/runtime.ts +0 -2436
  46. package/src/scheduler.ts +0 -384
  47. package/src/subagent-endpoint.ts +0 -158
  48. 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,69 +134,473 @@ 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 pathApi(
170
+ platform: NodeJS.Platform,
171
+ ...values: readonly string[]
172
+ ): typeof path.posix {
173
+ if (platform !== "win32") return path.posix;
174
+ if (
175
+ process.platform === "win32" ||
176
+ values.some(
177
+ (value) => /^[a-z]:[\\/]/iu.test(value) || value.startsWith("\\\\"),
178
+ )
179
+ ) {
180
+ return path.win32;
181
+ }
182
+ // Tests may model Windows executable semantics over a POSIX fixture tree.
183
+ return path.posix;
184
+ }
185
+
186
+ function pathWithin(
187
+ root: string,
188
+ candidate: string,
189
+ platform: NodeJS.Platform,
190
+ ): boolean {
191
+ const platformPath = pathApi(platform, root, candidate);
192
+ const normalizedRoot = platformPath.resolve(root);
193
+ const relative = platformPath.relative(
194
+ normalizedRoot,
195
+ platformPath.resolve(candidate),
196
+ );
197
+ return (
198
+ relative === "" ||
199
+ (relative !== ".." &&
200
+ !relative.startsWith(`..${platformPath.sep}`) &&
201
+ !platformPath.isAbsolute(relative))
202
+ );
203
+ }
204
+
205
+ function regularExecutableFile(
206
+ stat: Stats,
207
+ platform: NodeJS.Platform,
208
+ ): boolean {
209
+ return stat.isFile() && (platform === "win32" || (stat.mode & 0o111) !== 0);
210
+ }
211
+
212
+ function windowsPathExtensions(pathExt: string | undefined): string[] {
213
+ const seen = new Set<string>();
214
+ const extensions: string[] = [];
215
+ for (const entry of (pathExt ?? DEFAULT_WINDOWS_PATH_EXT).split(";")) {
216
+ const extension = entry.trim().toLowerCase();
217
+ if (!WINDOWS_EXECUTABLE_EXTENSIONS.has(extension) || seen.has(extension)) {
218
+ continue;
219
+ }
220
+ seen.add(extension);
221
+ extensions.push(extension);
222
+ }
223
+ return extensions;
224
+ }
225
+
226
+ function runnerNames(
227
+ command: string,
228
+ environment: VerificationRunnerEnvironment,
229
+ ): string[] {
230
+ if (!/^[a-z0-9][a-z0-9._-]*$/iu.test(command)) return [];
231
+ if ((environment.platform ?? process.platform) !== "win32") {
232
+ return [command];
233
+ }
234
+ const extension = path.extname(command).toLowerCase();
235
+ if (extension) {
236
+ return WINDOWS_EXECUTABLE_EXTENSIONS.has(extension) ? [command] : [];
237
+ }
238
+ return windowsPathExtensions(environment.pathExt).map(
239
+ (candidateExtension) => `${command}${candidateExtension}`,
240
+ );
241
+ }
242
+
243
+ function localExecutableNames(
244
+ executable: string,
245
+ environment: VerificationRunnerEnvironment,
246
+ ): string[] {
247
+ if (!/^[a-z0-9][a-z0-9._-]*$/iu.test(executable)) return [];
248
+ if ((environment.platform ?? process.platform) !== "win32") {
249
+ return [executable];
250
+ }
251
+ const extension = path.extname(executable).toLowerCase();
252
+ if (extension) {
253
+ return WINDOWS_EXECUTABLE_EXTENSIONS.has(extension) ? [executable] : [];
254
+ }
255
+ return [
256
+ executable,
257
+ ...windowsPathExtensions(environment.pathExt).map(
258
+ (candidateExtension) => `${executable}${candidateExtension}`,
259
+ ),
260
+ ];
261
+ }
262
+
263
+ function resolveCandidateName(
264
+ directory: string,
265
+ name: string,
266
+ platform: NodeJS.Platform,
267
+ ): string | null {
268
+ const exact = path.join(directory, name);
269
+ if (platform !== "win32") return exact;
270
+ try {
271
+ const matches = readdirSync(directory).filter(
272
+ (entry) => entry.toLowerCase() === name.toLowerCase(),
273
+ );
274
+ return matches.length === 1
275
+ ? path.join(directory, matches[0] as string)
276
+ : null;
277
+ } catch {
278
+ return null;
279
+ }
280
+ }
281
+
118
282
  function within(root: string, candidate: string): boolean {
119
283
  return candidate === root || candidate.startsWith(`${root}${path.sep}`);
120
284
  }
121
285
 
122
- function commonDirectory(left: string, right: string): string {
123
- const leftParts = path.resolve(left).split(path.sep).filter(Boolean);
124
- const rightParts = path.resolve(right).split(path.sep).filter(Boolean);
286
+ export function commonDirectory(
287
+ left: string,
288
+ right: string,
289
+ platform: NodeJS.Platform = process.platform,
290
+ ): string {
291
+ const platformPath = pathApi(platform, left, right);
292
+ const leftResolved = platformPath.resolve(left);
293
+ const rightResolved = platformPath.resolve(right);
294
+ const leftRoot = platformPath.parse(leftResolved).root;
295
+ const rightRoot = platformPath.parse(rightResolved).root;
296
+ const comparable = (value: string) =>
297
+ platform === "win32" ? value.toLowerCase() : value;
298
+ if (comparable(leftRoot) !== comparable(rightRoot)) return leftRoot;
299
+
300
+ const parts = (value: string, root: string) =>
301
+ platformPath.relative(root, value).split(platformPath.sep).filter(Boolean);
302
+ const leftParts = parts(leftResolved, leftRoot);
303
+ const rightParts = parts(rightResolved, rightRoot);
125
304
  const common: string[] = [];
126
305
  for (
127
306
  let index = 0;
128
307
  index < Math.min(leftParts.length, rightParts.length);
129
308
  index++
130
309
  ) {
131
- if (leftParts[index] !== rightParts[index]) break;
132
- common.push(leftParts[index] as string);
310
+ const leftPart = leftParts[index] as string;
311
+ const rightPart = rightParts[index] as string;
312
+ if (comparable(leftPart) !== comparable(rightPart)) break;
313
+ common.push(leftPart);
133
314
  }
134
- return path.join(path.parse(path.resolve(left)).root, ...common);
315
+ return platformPath.join(leftRoot, ...common);
135
316
  }
136
317
 
137
318
  function unsafeMountSource(source: string): boolean {
138
319
  if (BROAD_MOUNT_ROOTS.has(source)) return true;
139
- const parts = source.split(path.sep).filter(Boolean);
320
+ if (path.resolve(source) === path.parse(path.resolve(source)).root)
321
+ return true;
322
+ const parts = path
323
+ .relative(path.parse(path.resolve(source)).root, path.resolve(source))
324
+ .split(path.sep)
325
+ .filter(Boolean)
326
+ .map((part) => part.toLowerCase());
140
327
  return (
141
328
  (parts[0] === "home" && parts.length <= 2) ||
142
- (parts[0] === "Users" && parts.length <= 2) ||
329
+ (parts[0] === "users" && parts.length <= 2) ||
143
330
  (parts[0] === "root" && parts.length <= 1)
144
331
  );
145
332
  }
146
333
 
334
+ function safeRunnerSource(
335
+ paths: readonly string[],
336
+ platform: NodeJS.Platform,
337
+ ): string | null {
338
+ let source = path.dirname(paths[0] as string);
339
+ for (const candidate of paths.slice(1)) {
340
+ source = commonDirectory(source, path.dirname(candidate), platform);
341
+ }
342
+ if (unsafeMountSource(source)) return null;
343
+ try {
344
+ const stat = lstatSync(source);
345
+ if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
346
+ } catch {
347
+ return null;
348
+ }
349
+ return paths.every((candidate) => pathWithin(source, candidate, platform))
350
+ ? source
351
+ : null;
352
+ }
353
+
354
+ function resolvedRunnerEnvironment(
355
+ environment: VerificationRunnerEnvironment,
356
+ ): Required<VerificationRunnerEnvironment> {
357
+ return {
358
+ platform: environment.platform ?? process.platform,
359
+ path: environment.path ?? process.env.PATH ?? "",
360
+ pathExt:
361
+ environment.pathExt ?? process.env.PATHEXT ?? DEFAULT_WINDOWS_PATH_EXT,
362
+ pathDelimiter: environment.pathDelimiter ?? path.delimiter,
363
+ };
364
+ }
365
+
366
+ function windowsShimBinding(
367
+ command: string,
368
+ launcherPath: string,
369
+ environment: Required<VerificationRunnerEnvironment>,
370
+ ): VerificationRunnerBinding | null {
371
+ const name = path.basename(command, path.extname(command)).toLowerCase();
372
+ if (name !== "npm" && name !== "npx") return null;
373
+ const node = resolveVerificationRunner("node", environment);
374
+ if (
375
+ !node ||
376
+ node.fixedArgs ||
377
+ path.extname(node.executablePath).toLowerCase() !== ".exe"
378
+ ) {
379
+ return null;
380
+ }
381
+ const cli = path.join(
382
+ path.dirname(launcherPath),
383
+ "node_modules",
384
+ "npm",
385
+ "bin",
386
+ `${name}-cli.js`,
387
+ );
388
+ try {
389
+ const canonicalLauncher = realpathSync(launcherPath);
390
+ const canonicalCli = realpathSync(cli);
391
+ const cliStat = lstatSync(canonicalCli);
392
+ if (!cliStat.isFile()) return null;
393
+ const source = safeRunnerSource(
394
+ [node.executablePath, canonicalLauncher, canonicalCli],
395
+ environment.platform,
396
+ );
397
+ if (!source) return null;
398
+ return {
399
+ command,
400
+ executablePath: node.executablePath,
401
+ fixedArgs: [canonicalCli],
402
+ };
403
+ } catch {
404
+ return null;
405
+ }
406
+ }
407
+
408
+ function localPackageDirectories(dependencyRoot: string): string[] {
409
+ const packages: string[] = [];
410
+ try {
411
+ for (const entry of readdirSync(dependencyRoot, { withFileTypes: true })) {
412
+ if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
413
+ const candidate = path.join(dependencyRoot, entry.name);
414
+ if (!entry.name.startsWith("@")) {
415
+ packages.push(candidate);
416
+ continue;
417
+ }
418
+ for (const scoped of readdirSync(candidate, { withFileTypes: true })) {
419
+ if (scoped.isDirectory() && !scoped.name.startsWith(".")) {
420
+ packages.push(path.join(candidate, scoped.name));
421
+ }
422
+ }
423
+ }
424
+ } catch {
425
+ return [];
426
+ }
427
+ return packages;
428
+ }
429
+
430
+ function localPackageBinTargets(
431
+ dependencyOwner: string,
432
+ executable: string,
433
+ platform: NodeJS.Platform,
434
+ ): string[] {
435
+ const dependencyRoot = path.resolve(dependencyOwner, "node_modules");
436
+ let canonicalDependencyRoot: string;
437
+ try {
438
+ canonicalDependencyRoot = realpathSync(dependencyRoot);
439
+ } catch {
440
+ return [];
441
+ }
442
+ const targets: string[] = [];
443
+ for (const packageDirectory of localPackageDirectories(dependencyRoot)) {
444
+ try {
445
+ const directoryStat = lstatSync(packageDirectory);
446
+ if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink()) {
447
+ continue;
448
+ }
449
+ const canonicalPackage = realpathSync(packageDirectory);
450
+ if (
451
+ !pathWithin(canonicalDependencyRoot, canonicalPackage, platform) ||
452
+ canonicalPackage === canonicalDependencyRoot
453
+ ) {
454
+ continue;
455
+ }
456
+ const manifestPath = path.join(canonicalPackage, "package.json");
457
+ const manifestStat = lstatSync(manifestPath);
458
+ if (!manifestStat.isFile() || manifestStat.isSymbolicLink()) continue;
459
+ const manifest = JSON.parse(
460
+ readFileSync(manifestPath, "utf8"),
461
+ ) as unknown;
462
+ if (
463
+ !manifest ||
464
+ typeof manifest !== "object" ||
465
+ Array.isArray(manifest)
466
+ ) {
467
+ continue;
468
+ }
469
+ const record = manifest as Record<string, unknown>;
470
+ const packageName =
471
+ typeof record.name === "string"
472
+ ? (record.name.split("/").pop() ?? "")
473
+ : "";
474
+ const bin = record.bin;
475
+ let relative: unknown;
476
+ if (
477
+ typeof bin === "string" &&
478
+ (platform === "win32"
479
+ ? packageName.toLowerCase() === executable.toLowerCase()
480
+ : packageName === executable)
481
+ ) {
482
+ relative = bin;
483
+ } else if (bin && typeof bin === "object" && !Array.isArray(bin)) {
484
+ const entries = Object.entries(bin as Record<string, unknown>).filter(
485
+ ([name]) =>
486
+ platform === "win32"
487
+ ? name.toLowerCase() === executable.toLowerCase()
488
+ : name === executable,
489
+ );
490
+ if (entries.length === 1) relative = entries[0]?.[1];
491
+ }
492
+ if (
493
+ typeof relative !== "string" ||
494
+ relative.length === 0 ||
495
+ relative.includes("\0")
496
+ ) {
497
+ continue;
498
+ }
499
+ const unresolved = path.resolve(canonicalPackage, relative);
500
+ if (!pathWithin(canonicalPackage, unresolved, platform)) continue;
501
+ const canonical = realpathSync(unresolved);
502
+ const stat = lstatSync(canonical);
503
+ if (
504
+ stat.isFile() &&
505
+ pathWithin(canonicalPackage, canonical, platform) &&
506
+ pathWithin(canonicalDependencyRoot, canonical, platform)
507
+ ) {
508
+ targets.push(canonical);
509
+ }
510
+ } catch {}
511
+ }
512
+ return [...new Set(targets)];
513
+ }
514
+
515
+ function resolveLocalVerificationRunner(
516
+ dependencyOwner: string,
517
+ executable: string,
518
+ runnerEnvironment: VerificationRunnerEnvironment,
519
+ ): VerificationRunnerBinding | null {
520
+ const environment = resolvedRunnerEnvironment(runnerEnvironment);
521
+ if (environment.platform !== "win32") return null;
522
+ const targets = localPackageBinTargets(
523
+ dependencyOwner,
524
+ executable,
525
+ environment.platform,
526
+ );
527
+ if (targets.length !== 1) return null;
528
+ const target = targets[0] as string;
529
+ const extension = path.extname(target).toLowerCase();
530
+ if (extension === ".exe") {
531
+ return { command: executable, executablePath: target };
532
+ }
533
+ if (![".js", ".cjs", ".mjs"].includes(extension)) return null;
534
+ const node = resolveVerificationRunner("node", environment);
535
+ if (!node || node.fixedArgs) return null;
536
+ return {
537
+ command: executable,
538
+ executablePath: node.executablePath,
539
+ fixedArgs: [target],
540
+ };
541
+ }
542
+
147
543
  export function resolveVerificationRunner(
148
544
  command: string,
545
+ runnerEnvironment: VerificationRunnerEnvironment = {},
149
546
  ): VerificationRunnerBinding | null {
547
+ const environment = resolvedRunnerEnvironment(runnerEnvironment);
548
+ const directories = environment.path
549
+ .split(environment.pathDelimiter)
550
+ .filter(Boolean);
551
+ const seenDirectories = new Set<string>();
150
552
  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)),
553
+ ...(command === "bun" &&
554
+ process.versions.bun &&
555
+ environment.platform === process.platform
556
+ ? [process.execPath]
557
+ : []),
558
+ ...directories.flatMap((directory) => {
559
+ const resolvedDirectory = path.resolve(directory);
560
+ const key =
561
+ environment.platform === "win32"
562
+ ? resolvedDirectory.toLowerCase()
563
+ : resolvedDirectory;
564
+ if (seenDirectories.has(key)) return [];
565
+ seenDirectories.add(key);
566
+ return runnerNames(command, environment).flatMap((name) => {
567
+ const candidate = resolveCandidateName(
568
+ resolvedDirectory,
569
+ name,
570
+ environment.platform,
571
+ );
572
+ return candidate ? [candidate] : [];
573
+ });
574
+ }),
156
575
  ];
576
+ const seenCandidates = new Set<string>();
157
577
  for (const candidate of candidates) {
158
578
  try {
159
579
  const executablePath = path.resolve(candidate);
580
+ const candidateKey =
581
+ environment.platform === "win32"
582
+ ? executablePath.toLowerCase()
583
+ : executablePath;
584
+ if (seenCandidates.has(candidateKey)) continue;
585
+ seenCandidates.add(candidateKey);
160
586
  const canonical = realpathSync(executablePath);
161
587
  const stat = lstatSync(canonical);
162
- if (!stat.isFile() || (stat.mode & 0o111) === 0) continue;
588
+ if (!regularExecutableFile(stat, environment.platform)) continue;
589
+ if (environment.platform === "win32") {
590
+ const extension = path.extname(executablePath).toLowerCase();
591
+ if (extension === ".cmd") {
592
+ const shim = windowsShimBinding(command, executablePath, environment);
593
+ if (shim) return shim;
594
+ continue;
595
+ }
596
+ if (extension !== ".exe") continue;
597
+ if (
598
+ !safeRunnerSource([executablePath, canonical], environment.platform)
599
+ ) {
600
+ continue;
601
+ }
602
+ return { command, executablePath };
603
+ }
163
604
  const systemVisible = SYSTEM_MOUNT_ROOTS.some(
164
605
  (root) => within(root, executablePath) && within(root, canonical),
165
606
  );
@@ -167,6 +608,7 @@ export function resolveVerificationRunner(
167
608
  const mountSource = commonDirectory(
168
609
  path.dirname(executablePath),
169
610
  path.dirname(canonical),
611
+ environment.platform,
170
612
  );
171
613
  if (unsafeMountSource(mountSource)) continue;
172
614
  const sourceStat = lstatSync(mountSource);
@@ -196,6 +638,7 @@ function validateScriptCommand(
196
638
  verificationId: string,
197
639
  command: string,
198
640
  dependencyOwner: string,
641
+ runnerEnvironment: VerificationRunnerEnvironment,
199
642
  ): VerificationCapabilityResult {
200
643
  if (
201
644
  command.length === 0 ||
@@ -234,14 +677,17 @@ function validateScriptCommand(
234
677
  }
235
678
  if (executable === "npx") {
236
679
  const local = tokens[1] === "--no-install" ? tokens[2] : undefined;
237
- if (!local || !localExecutable(root, local, dependencyOwner)) {
680
+ if (
681
+ !local ||
682
+ !localExecutable(root, local, dependencyOwner, runnerEnvironment)
683
+ ) {
238
684
  return adapterFailure(
239
685
  verificationId,
240
686
  "local-executable-missing",
241
687
  "npx requires --no-install and an installed local executable",
242
688
  );
243
689
  }
244
- const runner = resolveVerificationRunner("npx");
690
+ const runner = resolveVerificationRunner("npx", runnerEnvironment);
245
691
  return runner
246
692
  ? capabilitySuccess(verificationId, [runner])
247
693
  : adapterFailure(
@@ -251,7 +697,7 @@ function validateScriptCommand(
251
697
  );
252
698
  }
253
699
  if (["node", "bun"].includes(executable)) {
254
- const runner = resolveVerificationRunner(executable);
700
+ const runner = resolveVerificationRunner(executable, runnerEnvironment);
255
701
  return runner
256
702
  ? capabilitySuccess(verificationId, [runner])
257
703
  : adapterFailure(
@@ -260,7 +706,7 @@ function validateScriptCommand(
260
706
  `approved runner ${executable} is unavailable or cannot be mounted safely`,
261
707
  );
262
708
  }
263
- return localExecutable(root, executable, dependencyOwner)
709
+ return localExecutable(root, executable, dependencyOwner, runnerEnvironment)
264
710
  ? capabilitySuccess(verificationId)
265
711
  : adapterFailure(
266
712
  verificationId,
@@ -276,8 +722,12 @@ function validatePackageScript(
276
722
  script: string,
277
723
  approvedCommand: string,
278
724
  dependencyOwner = root,
725
+ runnerEnvironment: VerificationRunnerEnvironment = {},
279
726
  ): VerificationCapabilityResult {
280
- const packageRunner = resolveVerificationRunner(packageManager);
727
+ const packageRunner = resolveVerificationRunner(
728
+ packageManager,
729
+ runnerEnvironment,
730
+ );
281
731
  if (!packageRunner) {
282
732
  return adapterFailure(
283
733
  verificationId,
@@ -310,6 +760,7 @@ function validatePackageScript(
310
760
  verificationId,
311
761
  actual,
312
762
  dependencyOwner,
763
+ runnerEnvironment,
313
764
  );
314
765
  return scriptCapability.ok
315
766
  ? capabilitySuccess(verificationId, [
@@ -323,6 +774,7 @@ function validateAtomicCapability(
323
774
  root: string,
324
775
  step: AtomicVerificationContract,
325
776
  dependencyOwner: string,
777
+ runnerEnvironment: VerificationRunnerEnvironment,
326
778
  ): VerificationCapabilityResult {
327
779
  if (step.kind === "package-script") {
328
780
  return validatePackageScript(
@@ -332,6 +784,7 @@ function validateAtomicCapability(
332
784
  step.script,
333
785
  step.command,
334
786
  dependencyOwner,
787
+ runnerEnvironment,
335
788
  );
336
789
  }
337
790
  const runner = step.runner;
@@ -343,11 +796,12 @@ function validateAtomicCapability(
343
796
  runner.script,
344
797
  runner.command,
345
798
  dependencyOwner,
799
+ runnerEnvironment,
346
800
  );
347
801
  if (!script.ok) return script;
348
802
  if (
349
803
  step.kind === "vitest" &&
350
- !localExecutable(root, "vitest", dependencyOwner)
804
+ !localExecutable(root, "vitest", dependencyOwner, runnerEnvironment)
351
805
  ) {
352
806
  return adapterFailure(
353
807
  step.id,
@@ -358,7 +812,7 @@ function validateAtomicCapability(
358
812
  return script;
359
813
  }
360
814
  if (runner.kind === "node") {
361
- const node = resolveVerificationRunner("node");
815
+ const node = resolveVerificationRunner("node", runnerEnvironment);
362
816
  return node
363
817
  ? capabilitySuccess(step.id, [node])
364
818
  : adapterFailure(
@@ -367,7 +821,14 @@ function validateAtomicCapability(
367
821
  "Node is unavailable or cannot be mounted safely",
368
822
  );
369
823
  }
370
- if (!localExecutable(root, runner.executable, dependencyOwner)) {
824
+ if (
825
+ !localExecutable(
826
+ root,
827
+ runner.executable,
828
+ dependencyOwner,
829
+ runnerEnvironment,
830
+ )
831
+ ) {
371
832
  return adapterFailure(
372
833
  step.id,
373
834
  "local-executable-missing",
@@ -375,7 +836,7 @@ function validateAtomicCapability(
375
836
  );
376
837
  }
377
838
  if (runner.kind === "npx") {
378
- const npx = resolveVerificationRunner("npx");
839
+ const npx = resolveVerificationRunner("npx", runnerEnvironment);
379
840
  return npx
380
841
  ? capabilitySuccess(step.id, [npx])
381
842
  : adapterFailure(
@@ -384,23 +845,70 @@ function validateAtomicCapability(
384
845
  "npx is unavailable or cannot be mounted safely",
385
846
  );
386
847
  }
848
+ if ((runnerEnvironment.platform ?? process.platform) === "win32") {
849
+ const binding = resolveLocalVerificationRunner(
850
+ dependencyOwner,
851
+ runner.executable,
852
+ runnerEnvironment,
853
+ );
854
+ return binding
855
+ ? capabilitySuccess(step.id, [binding])
856
+ : adapterFailure(
857
+ step.id,
858
+ "local-executable-missing",
859
+ `local executable ${runner.executable} has no safe direct Windows binding`,
860
+ );
861
+ }
387
862
  return capabilitySuccess(step.id);
388
863
  }
389
864
 
390
865
  export function validateVerificationCapability(
391
866
  root: string,
392
867
  value: unknown,
393
- options: {
394
- dependencyOwner?: string;
395
- } = {},
868
+ options: VerificationCapabilityOptions = {},
396
869
  ): VerificationCapabilityResult {
397
870
  return validateCapability(root, value, options, true);
398
871
  }
399
872
 
873
+ /** Validate one exact adapter and bind all of its current file inputs. */
874
+ export function bindCurrentVerificationCapability(
875
+ root: string,
876
+ value: unknown,
877
+ options: VerificationCapabilityOptions = {},
878
+ ): CurrentVerificationCapabilityResult {
879
+ const capability = validateVerificationCapability(root, value, options);
880
+ if (!capability.ok) return capability;
881
+ const validation = validateVerificationContract(value);
882
+ if (!validation.ok) {
883
+ throw new Error("verification-capability-invariant");
884
+ }
885
+ return {
886
+ ok: true,
887
+ value: {
888
+ verificationId: capability.verificationId,
889
+ runnerBindings: capability.runnerBindings.map((binding) => ({
890
+ ...binding,
891
+ ...(binding.fixedArgs ? { fixedArgs: [...binding.fixedArgs] } : {}),
892
+ })),
893
+ inputSnapshot: snapshotFiles(
894
+ root,
895
+ verificationInputPaths(validation.value),
896
+ ),
897
+ },
898
+ };
899
+ }
900
+
901
+ export function isVerificationCapabilityCurrent(
902
+ root: string,
903
+ capability: CurrentVerificationCapability,
904
+ ): boolean {
905
+ return isCurrent(root, capability.inputSnapshot);
906
+ }
907
+
400
908
  export function validateVerificationAdapterCapability(
401
909
  root: string,
402
910
  value: unknown,
403
- options: { dependencyOwner?: string } = {},
911
+ options: VerificationCapabilityOptions = {},
404
912
  ): VerificationCapabilityResult {
405
913
  return validateCapability(root, value, options, false);
406
914
  }
@@ -408,7 +916,7 @@ export function validateVerificationAdapterCapability(
408
916
  function validateCapability(
409
917
  root: string,
410
918
  value: unknown,
411
- options: { dependencyOwner?: string },
919
+ options: VerificationCapabilityOptions,
412
920
  requireInputAvailability: boolean,
413
921
  ): VerificationCapabilityResult {
414
922
  const validation = validateVerificationContract(value);
@@ -435,6 +943,7 @@ function validateCapability(
435
943
  root,
436
944
  validation.value,
437
945
  dependencyOwner,
946
+ options.runnerEnvironment ?? {},
438
947
  requireInputAvailability,
439
948
  );
440
949
  }
@@ -443,6 +952,7 @@ function validateAcceptedVerificationCapability(
443
952
  root: string,
444
953
  verification: StructuredVerificationContract,
445
954
  dependencyOwner: string,
955
+ runnerEnvironment: VerificationRunnerEnvironment,
446
956
  requireInputAvailability: boolean,
447
957
  ): VerificationCapabilityResult {
448
958
  if (requireInputAvailability) {
@@ -458,7 +968,12 @@ function validateAcceptedVerificationCapability(
458
968
  }
459
969
  const runnerBindings: VerificationRunnerBinding[] = [];
460
970
  for (const step of verificationSteps(verification)) {
461
- const capability = validateAtomicCapability(root, step, dependencyOwner);
971
+ const capability = validateAtomicCapability(
972
+ root,
973
+ step,
974
+ dependencyOwner,
975
+ runnerEnvironment,
976
+ );
462
977
  if (!capability.ok) return capability;
463
978
  runnerBindings.push(...capability.runnerBindings);
464
979
  }