@loucompanion/forge-bridge 0.1.3-dev.ba7d36726eda → 0.1.3-dev.bca7d0ccee53

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 (73) hide show
  1. package/dist/bridge/bridge.base.d.ts +2 -1
  2. package/dist/bridge/bridge.service.d.ts +8 -0
  3. package/dist/bridge/bridge.service.js +184 -12
  4. package/dist/bridge/bridge.types.d.ts +37 -0
  5. package/dist/cli/cli.help.js +1 -1
  6. package/dist/cli/cli.service.js +1 -1
  7. package/dist/cli/local-config.js +2 -1
  8. package/dist/runtime/attempt-node-runtime-launcher.d.ts +3 -1
  9. package/dist/runtime/attempt-node-runtime-launcher.js +62 -11
  10. package/dist/runtime/attempt-process-supervisor-registry.js +1 -1
  11. package/dist/runtime/attempt-process-supervisor.d.ts +12 -0
  12. package/dist/runtime/attempt-process-supervisor.js +75 -15
  13. package/dist/runtime/generated-output-reset.d.ts +10 -0
  14. package/dist/runtime/generated-output-reset.js +103 -0
  15. package/dist/runtime/host-listener-inspector.d.ts +16 -0
  16. package/dist/runtime/host-listener-inspector.js +143 -4
  17. package/dist/runtime/host-runtime-contract.d.ts +9 -1
  18. package/dist/runtime/host-runtime-contract.js +411 -6
  19. package/dist/runtime/host-runtime-lease.d.ts +12 -1
  20. package/dist/runtime/host-runtime-lease.js +271 -50
  21. package/dist/runtime/host-runtime-promotion.d.ts +2 -0
  22. package/dist/runtime/host-runtime-promotion.js +35 -4
  23. package/dist/runtime/node-host-runtime-adapter.d.ts +7 -0
  24. package/dist/runtime/node-host-runtime-adapter.js +71 -8
  25. package/dist/runtime/playwright-host-runtime-adapter.js +84 -0
  26. package/dist/sandbox/preflight.d.ts +9 -0
  27. package/dist/sandbox/preflight.js +39 -4
  28. package/dist/sandbox/workspace.service.js +7 -2
  29. package/dist/session/managed-attempt-tools.d.ts +17 -0
  30. package/dist/session/managed-attempt-tools.js +712 -0
  31. package/dist/session/phase-completion.artifacts.js +9 -2
  32. package/dist/session/phase-completion.d.ts +11 -2
  33. package/dist/session/phase-completion.helpers.d.ts +5 -0
  34. package/dist/session/phase-completion.helpers.js +45 -9
  35. package/dist/session/phase-completion.js +12 -3
  36. package/dist/session/phase-completion.prompts.d.ts +3 -3
  37. package/dist/session/phase-completion.prompts.js +21 -6
  38. package/dist/session/phase-completion.runtime.d.ts +13 -1
  39. package/dist/session/phase-completion.runtime.js +183 -8
  40. package/dist/session/phase-completion.schemas.js +108 -3
  41. package/dist/session/phase-completion.validation.d.ts +4 -1
  42. package/dist/session/phase-completion.validation.js +144 -18
  43. package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
  44. package/dist/session/provider-cli/ci-repair-readiness.js +95 -0
  45. package/dist/session/provider-cli/provider-cli.deterministic.js +47 -1
  46. package/dist/session/provider-cli/provider-cli.process.d.ts +14 -0
  47. package/dist/session/provider-cli/provider-cli.process.js +176 -18
  48. package/dist/session/provider-cli/provider-cli.prompts.js +30 -8
  49. package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
  50. package/dist/session/provider-cli/provider-cli.service.d.ts +19 -2
  51. package/dist/session/provider-cli/provider-cli.service.js +390 -83
  52. package/dist/session/provider-cli/provider-cli.types.d.ts +3 -0
  53. package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
  54. package/dist/session/provider-cli/provider-output-state.js +30 -2
  55. package/dist/session/session.base.d.ts +2 -1
  56. package/dist/session/session.service.d.ts +2 -1
  57. package/dist/session/session.service.js +303 -30
  58. package/dist/session/session.types.d.ts +38 -0
  59. package/dist/session/session.types.js +17 -0
  60. package/dist/shared/process/host-process-supervisor.d.ts +18 -1
  61. package/dist/shared/process/host-process-supervisor.js +94 -7
  62. package/dist/shared/process/process.d.ts +1 -0
  63. package/dist/shared/process/process.js +1 -0
  64. package/dist/version.d.ts +1 -1
  65. package/dist/version.js +1 -1
  66. package/dist/worktree/artifact-injector.d.ts +3 -2
  67. package/dist/worktree/artifact-injector.js +43 -4
  68. package/dist/worktree/dependency-warmer.d.ts +10 -1
  69. package/dist/worktree/dependency-warmer.js +94 -5
  70. package/dist/worktree/worktree.service.d.ts +1 -0
  71. package/dist/worktree/worktree.service.js +12 -1
  72. package/package.json +1 -1
  73. package/runtime/windows-job-supervisor.ps1 +89 -20
@@ -11,6 +11,7 @@ import { HostListenerInspector } from "./host-listener-inspector.js";
11
11
  import { PlaywrightHostRuntimeAdapter } from "./playwright-host-runtime-adapter.js";
12
12
  import { HostParallelPromotionStore, configuredLaunchPortNames, hostRuntimeContractFingerprint, hostRuntimeParallelEligible, } from "./host-runtime-promotion.js";
13
13
  import { ExactHostProcessRecovery, } from "../shared/process/host-process-recovery.js";
14
+ import { resetGeneratedOutputs } from "./generated-output-reset.js";
14
15
  export class HostRuntimeLeaseManager {
15
16
  stateRoot;
16
17
  processSupervisors;
@@ -27,6 +28,7 @@ export class HostRuntimeLeaseManager {
27
28
  parallelProjectOwners = new Map();
28
29
  parallelProjectWaiters = [];
29
30
  degraded = new Map();
31
+ knownPromotions = new Map();
30
32
  portBroker;
31
33
  nodeAdapter;
32
34
  constructor(stateRoot, portBroker = new HostPortBroker(), nodeAdapter, processSupervisors = new AttemptProcessSupervisorRegistry(), processRunner = new StreamingProviderProcessRunner(), listenerInspector = new HostListenerInspector(), promotionStore = new HostParallelPromotionStore(stateRoot), playwrightAdapter = new PlaywrightHostRuntimeAdapter(), processRecovery = new ExactHostProcessRecovery()) {
@@ -41,6 +43,7 @@ export class HostRuntimeLeaseManager {
41
43
  this.nodeAdapter = nodeAdapter ?? new NodeHostRuntimeAdapter(portBroker);
42
44
  }
43
45
  async reap(options) {
46
+ await this.refreshKnownPromotions();
44
47
  const report = {
45
48
  examined: 0,
46
49
  reaped: 0,
@@ -50,8 +53,25 @@ export class HostRuntimeLeaseManager {
50
53
  };
51
54
  for (const ledger of await this.readLedgers()) {
52
55
  const lease = ledger.runtimeLease;
53
- if (lease.runnerId !== options.runnerId || this.active.has(lease.dispatchAttemptId))
56
+ if (lease.runnerId !== options.runnerId)
54
57
  continue;
58
+ const active = this.active.get(lease.dispatchAttemptId);
59
+ if (active) {
60
+ if (active.lease.state !== "degraded")
61
+ continue;
62
+ report.examined += 1;
63
+ try {
64
+ const released = await this.release(lease.dispatchAttemptId, lease.fencingToken);
65
+ if (released?.state === "released")
66
+ report.reaped += 1;
67
+ else
68
+ report.degraded += 1;
69
+ }
70
+ catch {
71
+ report.degraded += 1;
72
+ }
73
+ continue;
74
+ }
55
75
  const staleInstance = lease.runnerInstanceId !== options.runnerInstanceId;
56
76
  const expired = Date.parse(lease.expiresAt) <= (options.nowMs ?? Date.now());
57
77
  if (!staleInstance && !expired && lease.state !== "released" && lease.state !== "degraded")
@@ -91,6 +111,13 @@ export class HostRuntimeLeaseManager {
91
111
  }
92
112
  snapshot() {
93
113
  const projects = new Map();
114
+ for (const promotion of this.knownPromotions.values()) {
115
+ const project = capacityProject(projects, promotion.repoId);
116
+ project.safetyClass = conservativeSafetyClass(project.safetyClass, "host_parallel_safe");
117
+ project.runtimeContractFingerprint = promotion.runtimeContractFingerprint;
118
+ project.runtimeContractSource = promotion.runtimeContractSource ?? null;
119
+ project.promotedAt = promotion.observedAt;
120
+ }
94
121
  for (const [dispatchAttemptId, admission] of this.admissions) {
95
122
  const active = this.active.get(dispatchAttemptId);
96
123
  const project = capacityProject(projects, admission.repoId);
@@ -101,17 +128,21 @@ export class HostRuntimeLeaseManager {
101
128
  }
102
129
  else {
103
130
  project.activeAttempts += 1;
104
- const effectiveConcurrency = effectiveHostConcurrency(active.lease, active.runtimeContract);
131
+ const effectiveConcurrency = effectiveHostConcurrency(active.lease, active.runtimeContract, active.executionClass);
105
132
  project.safetyClass = conservativeSafetyClass(project.safetyClass, effectiveConcurrency === "host_parallel_safe"
106
133
  ? effectiveConcurrency
107
- : admission.parallelProjectKey && admission.provisionalPromotionFingerprint
108
- ? "host_parallel_candidate"
109
- : "host_serial_only");
134
+ : admission.executionClass === "analysis_only"
135
+ ? "host_parallel_safe"
136
+ : admission.parallelProjectKey && admission.provisionalPromotionFingerprint
137
+ ? "host_parallel_candidate"
138
+ : "host_serial_only");
110
139
  }
111
140
  }
112
141
  else {
113
142
  project.reservedAttempts += 1;
114
- project.safetyClass = conservativeSafetyClass(project.safetyClass, admission.provisionalPromotionFingerprint ? "host_parallel_candidate" : "host_serial_only");
143
+ project.safetyClass = conservativeSafetyClass(project.safetyClass, admission.executionClass === "analysis_only"
144
+ ? "host_parallel_safe"
145
+ : admission.provisionalPromotionFingerprint ? "host_parallel_candidate" : "host_serial_only");
115
146
  }
116
147
  }
117
148
  for (const [dispatchAttemptId, degraded] of this.degraded) {
@@ -179,37 +210,49 @@ export class HostRuntimeLeaseManager {
179
210
  }
180
211
  return existing.keys;
181
212
  }
182
- const promotion = await this.promotionStore.load(dispatch.repo.id);
183
- const projectKey = projectHostRuntimeSingletonKey(dispatch.repo.id);
184
- const keys = promotion ? [] : [projectKey];
213
+ const executionClass = runtimeExecutionClass(dispatch);
214
+ const promotion = executionClass === "managed_runtime"
215
+ ? await this.promotionStore.load(dispatch.repo.id)
216
+ : null;
217
+ if (promotion)
218
+ this.knownPromotions.set(promotion.repoId, promotion);
219
+ const projectKey = executionClass === "analysis_only"
220
+ ? projectHostAnalysisKey(dispatch.repo.id)
221
+ : projectHostRuntimeSingletonKey(dispatch.repo.id);
222
+ let keys = promotion || executionClass === "analysis_only" ? [] : [projectKey];
185
223
  const expiresAt = this.pendingRenewals.get(sandbox.dispatchAttemptId)?.fencingToken === owner.fencingToken
186
224
  ? this.pendingRenewals.get(sandbox.dispatchAttemptId).expiresAt
187
225
  : dispatch.lease.expiresAt;
188
226
  try {
189
- if (promotion) {
227
+ if (promotion || executionClass === "analysis_only") {
190
228
  await this.reserveParallelProject(projectKey, owner, expiresAt, signal);
191
229
  }
192
230
  else {
193
- await this.reserveSingletonKeys(keys, owner, expiresAt, signal);
231
+ keys = await this.reserveSingletonKeys(keys, owner, expiresAt, signal);
194
232
  }
195
233
  }
196
234
  catch (error) {
197
235
  this.clearPendingRenewal(owner);
198
236
  throw error;
199
237
  }
238
+ const admittedPromotion = promotion
239
+ ?? (executionClass === "managed_runtime" && keys.length === 0
240
+ ? this.knownPromotions.get(dispatch.repo.id) ?? null
241
+ : null);
200
242
  this.admissions.set(sandbox.dispatchAttemptId, {
201
243
  repoId: dispatch.repo.id,
202
244
  fencingToken: owner.fencingToken,
203
- provisionalPromotionFingerprint: promotion?.runtimeContractFingerprint ?? null,
245
+ executionClass,
246
+ provisionalPromotionFingerprint: admittedPromotion?.runtimeContractFingerprint ?? null,
204
247
  keys,
205
- parallelProjectKey: promotion ? projectKey : null,
248
+ parallelProjectKey: admittedPromotion || executionClass === "analysis_only" ? projectKey : null,
206
249
  });
207
250
  return keys;
208
251
  }
209
252
  async reserveSingletonKeys(keys, owner, expiresAt, signal) {
210
253
  const owned = keys.every(key => sameSingletonOwner(this.singletonOwners.get(key), owner));
211
254
  if (owned)
212
- return;
255
+ return keys;
213
256
  const staleFence = [...this.singletonOwners.values()].find(existing => existing.dispatchAttemptId === owner.dispatchAttemptId
214
257
  && existing.fencingToken !== owner.fencingToken);
215
258
  if (staleFence) {
@@ -220,13 +263,13 @@ export class HostRuntimeLeaseManager {
220
263
  throwIfAdmissionExpired(expiresAt);
221
264
  if (this.singletonKeysAvailable(keys)) {
222
265
  this.assignSingletonKeys(keys, owner);
223
- return;
266
+ return keys;
224
267
  }
225
- await new Promise((resolve, reject) => {
268
+ return new Promise((resolve, reject) => {
226
269
  const waiter = {
227
270
  owner,
228
271
  keys,
229
- resolve: () => resolve(),
272
+ resolve,
230
273
  reject,
231
274
  signal,
232
275
  expiresAt,
@@ -322,8 +365,9 @@ export class HostRuntimeLeaseManager {
322
365
  }
323
366
  return {
324
367
  lease: existing.lease,
325
- environment: hostRuntimeEnvironment(dispatch.session.id, existing.lease, existing.runtimeContract),
368
+ environment: hostRuntimeEnvironment(dispatch.session.id, existing.lease, existing.runtimeContract, existing.executionClass),
326
369
  runtimeContract: existing.runtimeContract,
370
+ executionClass: existing.executionClass,
327
371
  };
328
372
  }
329
373
  const admission = this.admissions.get(sandbox.dispatchAttemptId);
@@ -352,8 +396,8 @@ export class HostRuntimeLeaseManager {
352
396
  runtimeContract = await loadHostRuntimeContract(normalizedWorktreePath);
353
397
  }
354
398
  catch (error) {
355
- if (admission.provisionalPromotionFingerprint) {
356
- await this.promotionStore.invalidate(dispatch.repo.id).catch(() => undefined);
399
+ if (admission.executionClass === "managed_runtime" && admission.provisionalPromotionFingerprint) {
400
+ await this.invalidatePromotion(dispatch.repo.id).catch(() => undefined);
357
401
  }
358
402
  throw error;
359
403
  }
@@ -361,11 +405,13 @@ export class HostRuntimeLeaseManager {
361
405
  throw new Error("host_runtime_disabled: project runtime configuration requires Docker execution.");
362
406
  }
363
407
  const runtimeContractFingerprint = hostRuntimeContractFingerprint(runtimeContract);
364
- const promotion = await this.promotionStore.load(dispatch.repo.id);
408
+ const promotion = admission.executionClass === "managed_runtime"
409
+ ? await this.promotionStore.load(dispatch.repo.id)
410
+ : null;
365
411
  const promotionMatches = hostRuntimeParallelEligible(runtimeContract)
366
412
  && promotion?.runtimeContractFingerprint === runtimeContractFingerprint;
367
- if (admission.keys.length === 0 && !promotionMatches) {
368
- await this.promotionStore.invalidate(dispatch.repo.id);
413
+ if (admission.executionClass === "managed_runtime" && admission.keys.length === 0 && !promotionMatches) {
414
+ await this.invalidatePromotion(dispatch.repo.id);
369
415
  const projectKeys = hostRuntimeSingletonKeys(dispatch);
370
416
  if (admission.parallelProjectKey) {
371
417
  this.releaseParallelProjectNoDrain(admission.parallelProjectKey, owner);
@@ -374,8 +420,33 @@ export class HostRuntimeLeaseManager {
374
420
  const expiresAt = this.pendingRenewals.get(sandbox.dispatchAttemptId)?.fencingToken === owner.fencingToken
375
421
  ? this.pendingRenewals.get(sandbox.dispatchAttemptId).expiresAt
376
422
  : dispatch.lease.expiresAt;
377
- await this.reserveSingletonKeys(projectKeys, owner, expiresAt, signal);
378
- admission.keys = projectKeys;
423
+ admission.keys = await this.reserveSingletonKeys(projectKeys, owner, expiresAt, signal);
424
+ while (admission.keys.length === 0) {
425
+ const promoted = this.knownPromotions.get(dispatch.repo.id);
426
+ if (!promoted) {
427
+ throw new Error("host_runtime_parallel_promotion_missing: shared admission requires matching promotion evidence.");
428
+ }
429
+ if (promoted.runtimeContractFingerprint !== runtimeContractFingerprint) {
430
+ this.releaseParallelProjectNoDrain(projectKeys[0], owner);
431
+ await this.invalidatePromotion(dispatch.repo.id);
432
+ admission.keys = await this.reserveSingletonKeys(projectKeys, owner, expiresAt, signal);
433
+ continue;
434
+ }
435
+ admission.parallelProjectKey = projectKeys[0] ?? null;
436
+ admission.provisionalPromotionFingerprint = promoted.runtimeContractFingerprint;
437
+ }
438
+ }
439
+ const declaredSingletonKeys = runtimeContract.contract.runtime.singletonResources
440
+ .map(hostRuntimeDeclaredSingletonKey);
441
+ const missingSingletonKeys = declaredSingletonKeys
442
+ .filter(key => !admission.keys.includes(key));
443
+ if (missingSingletonKeys.length > 0) {
444
+ const expiresAt = this.pendingRenewals.get(sandbox.dispatchAttemptId)?.fencingToken === owner.fencingToken
445
+ ? this.pendingRenewals.get(sandbox.dispatchAttemptId).expiresAt
446
+ : dispatch.lease.expiresAt;
447
+ const reservedSingletonKeys = await this.reserveSingletonKeys(missingSingletonKeys, owner, expiresAt, signal);
448
+ admission.keys = [...admission.keys, ...reservedSingletonKeys]
449
+ .sort((left, right) => left.localeCompare(right));
379
450
  }
380
451
  const singletonKeys = admission.keys;
381
452
  if (!singletonKeys.every(key => sameSingletonOwner(this.singletonOwners.get(key), owner))) {
@@ -391,6 +462,7 @@ export class HostRuntimeLeaseManager {
391
462
  ensurePrivateDirectory(browserProfilePath),
392
463
  ensurePrivateDirectory(testOutputPath),
393
464
  ]);
465
+ await resetGeneratedOutputs(normalizedWorktreePath, testOutputPath);
394
466
  let lease = createRuntimeIsolationLease({
395
467
  leaseId: sandbox.executionLeaseId,
396
468
  dispatchAttemptId: sandbox.dispatchAttemptId,
@@ -440,6 +512,7 @@ export class HostRuntimeLeaseManager {
440
512
  attemptRoot,
441
513
  runtimeContract,
442
514
  runtimeContractFingerprint,
515
+ executionClass: admission.executionClass,
443
516
  parallelPromotionEligible: hostRuntimeParallelEligible(runtimeContract),
444
517
  processSupervisor,
445
518
  adapterCleanups: [],
@@ -449,8 +522,9 @@ export class HostRuntimeLeaseManager {
449
522
  this.pendingRenewals.delete(sandbox.dispatchAttemptId);
450
523
  return {
451
524
  lease,
452
- environment: hostRuntimeEnvironment(dispatch.session.id, lease, runtimeContract),
525
+ environment: hostRuntimeEnvironment(dispatch.session.id, lease, runtimeContract, admission.executionClass),
453
526
  runtimeContract,
527
+ executionClass: admission.executionClass,
454
528
  };
455
529
  }
456
530
  catch (error) {
@@ -499,34 +573,49 @@ export class HostRuntimeLeaseManager {
499
573
  if (!active || active.lease.fencingToken !== fencingToken || active.lease.state !== "active" || active.releasing) {
500
574
  throw new Error("host_runtime_project_unavailable: an active matching host runtime lease is required.");
501
575
  }
502
- const configuredPort = active.runtimeContract.contract.runtime.ports.find(port => port.launch !== null);
503
- if (!configuredPort?.launch) {
576
+ if (active.executionClass === "analysis_only") {
577
+ return {
578
+ lease: active.lease,
579
+ environment: hostRuntimeEnvironment(active.sessionId, active.lease, active.runtimeContract, active.executionClass),
580
+ runtimeContract: active.runtimeContract,
581
+ executionClass: active.executionClass,
582
+ };
583
+ }
584
+ const configuredPorts = active.runtimeContract.contract.runtime.ports.filter((port) => port.launch !== null);
585
+ if (configuredPorts.length === 0) {
504
586
  return {
505
587
  lease: active.lease,
506
- environment: hostRuntimeEnvironment(active.sessionId, active.lease, active.runtimeContract),
588
+ environment: hostRuntimeEnvironment(active.sessionId, active.lease, active.runtimeContract, active.executionClass),
507
589
  runtimeContract: active.runtimeContract,
590
+ executionClass: active.executionClass,
508
591
  };
509
592
  }
510
- const workingDirectory = await containedWorkingDirectory(active.lease.worktreePath, configuredPort.launch.workingDirectory);
511
593
  const owner = hostPortOwner(active.lease);
512
594
  const logDirectory = active.lease.resources.paths.find(entry => entry.role === "logs")?.path;
513
595
  if (!logDirectory)
514
596
  throw new Error("host_runtime_project_invalid: attempt log directory is unavailable.");
515
597
  const launcher = new AttemptNodeRuntimeLauncher(active.processSupervisor, owner, logDirectory, this.listenerInspector);
516
- const launched = await this.launchNodeRuntime(dispatchAttemptId, fencingToken, configuredPort.name, {
517
- command: configuredPort.launch.command,
518
- args: configuredPort.launch.args,
519
- cwd: workingDirectory,
520
- environment: hostRuntimeEnvironment(active.sessionId, active.lease, active.runtimeContract),
521
- }, launcher, signal);
522
- const promotedLease = await this.promoteVerifiedRuntime(active, launched.runtimeLease);
523
- const runtimeEnvironment = hostRuntimeEnvironment(active.sessionId, promotedLease, active.runtimeContract);
598
+ for (const configuredPort of configuredPorts) {
599
+ const workingDirectory = await containedWorkingDirectory(active.lease.worktreePath, configuredPort.launch.workingDirectory);
600
+ await this.launchNodeRuntime(dispatchAttemptId, fencingToken, configuredPort.name, {
601
+ command: configuredPort.launch.command,
602
+ args: configuredPort.launch.args,
603
+ cwd: workingDirectory,
604
+ environment: {
605
+ ...configuredPort.launch.environment,
606
+ ...hostRuntimeEnvironment(active.sessionId, active.lease, active.runtimeContract, active.executionClass),
607
+ },
608
+ }, launcher, signal);
609
+ }
610
+ const promotedLease = await this.promoteVerifiedRuntime(active, active.lease);
611
+ const runtimeEnvironment = hostRuntimeEnvironment(active.sessionId, promotedLease, active.runtimeContract, active.executionClass);
524
612
  const playwright = active.runtimeContract.contract.runtime.playwright;
525
613
  if (!playwright) {
526
614
  return {
527
615
  lease: promotedLease,
528
616
  environment: runtimeEnvironment,
529
617
  runtimeContract: active.runtimeContract,
618
+ executionClass: active.executionClass,
530
619
  };
531
620
  }
532
621
  const playwrightPort = promotedLease.resources.ports.find(port => port.name === playwright.baseUrlPort);
@@ -553,8 +642,15 @@ export class HostRuntimeLeaseManager {
553
642
  lease: promotedLease,
554
643
  environment: { ...runtimeEnvironment, ...preparedPlaywright.environment },
555
644
  runtimeContract: active.runtimeContract,
645
+ executionClass: active.executionClass,
556
646
  };
557
647
  }
648
+ providerProcessRunner(dispatchAttemptId, fencingToken) {
649
+ const active = this.active.get(dispatchAttemptId);
650
+ if (!active || active.lease.fencingToken !== fencingToken || active.releasing)
651
+ return null;
652
+ return new AttemptSupervisedProviderProcessRunner(active.processSupervisor, hostPortOwner(active.lease), this.listenerInspector, active.executionClass === "analysis_only");
653
+ }
558
654
  async promoteVerifiedRuntime(active, runtimeLease) {
559
655
  if (!active.parallelPromotionEligible)
560
656
  return runtimeLease;
@@ -573,10 +669,11 @@ export class HostRuntimeLeaseManager {
573
669
  active.lease = promotedLease;
574
670
  try {
575
671
  const promotion = await this.promotionStore.observe(active.repoId, active.runtimeContract, runtimeLease.dispatchAttemptId, verifiedPortNames);
672
+ this.knownPromotions.set(promotion.repoId, promotion);
576
673
  this.convertSingletonToParallelProject(projectKey, {
577
674
  dispatchAttemptId: runtimeLease.dispatchAttemptId,
578
675
  fencingToken: runtimeLease.fencingToken,
579
- });
676
+ }, promotion);
580
677
  const admission = this.admissions.get(runtimeLease.dispatchAttemptId);
581
678
  if (admission?.fencingToken === runtimeLease.fencingToken) {
582
679
  this.admissions.set(runtimeLease.dispatchAttemptId, {
@@ -589,7 +686,7 @@ export class HostRuntimeLeaseManager {
589
686
  return promotedLease;
590
687
  }
591
688
  catch {
592
- await this.promotionStore.invalidate(active.repoId).catch(() => undefined);
689
+ await this.invalidatePromotion(active.repoId).catch(() => undefined);
593
690
  active.lease = runtimeLease;
594
691
  await this.writeLedger(runtimeLease);
595
692
  return runtimeLease;
@@ -831,12 +928,25 @@ export class HostRuntimeLeaseManager {
831
928
  }
832
929
  this.drainAdmissionWaiters();
833
930
  }
834
- convertSingletonToParallelProject(key, owner) {
931
+ convertSingletonToParallelProject(key, owner, promotion) {
835
932
  if (!sameSingletonOwner(this.singletonOwners.get(key), owner)) {
836
933
  throw new Error("host_runtime_parallel_promotion_stale: matching exclusive project admission is required.");
837
934
  }
838
935
  this.assignParallelProject(key, owner);
839
936
  this.singletonOwners.delete(key);
937
+ for (let index = 0; index < this.singletonWaiters.length;) {
938
+ const waiter = this.singletonWaiters[index];
939
+ if (waiter.keys.length !== 1 || waiter.keys[0] !== key) {
940
+ index += 1;
941
+ continue;
942
+ }
943
+ this.singletonWaiters.splice(index, 1);
944
+ clearAdmissionDeadline(waiter);
945
+ waiter.signal?.removeEventListener("abort", waiter.abort);
946
+ this.assignParallelProject(key, waiter.owner);
947
+ this.knownPromotions.set(promotion.repoId, promotion);
948
+ waiter.resolve([]);
949
+ }
840
950
  this.drainAdmissionWaiters();
841
951
  }
842
952
  parallelProjectOwned(key, owner) {
@@ -961,11 +1071,91 @@ export class HostRuntimeLeaseManager {
961
1071
  waiter.resolve();
962
1072
  }
963
1073
  }
1074
+ async refreshKnownPromotions() {
1075
+ const promotions = await this.promotionStore.list();
1076
+ this.knownPromotions.clear();
1077
+ for (const promotion of promotions)
1078
+ this.knownPromotions.set(promotion.repoId, promotion);
1079
+ }
1080
+ async invalidatePromotion(repoId) {
1081
+ await this.promotionStore.invalidate(repoId);
1082
+ this.knownPromotions.delete(repoId.trim().toLowerCase());
1083
+ }
964
1084
  drainAdmissionWaiters() {
965
1085
  this.drainSingletonWaiters();
966
1086
  this.drainParallelProjectWaiters();
967
1087
  }
968
1088
  }
1089
+ class AttemptSupervisedProviderProcessRunner {
1090
+ supervisor;
1091
+ owner;
1092
+ listenerInspector;
1093
+ enforceNoListeners;
1094
+ constructor(supervisor, owner, listenerInspector, enforceNoListeners) {
1095
+ this.supervisor = supervisor;
1096
+ this.owner = owner;
1097
+ this.listenerInspector = listenerInspector;
1098
+ this.enforceNoListeners = enforceNoListeners;
1099
+ }
1100
+ async run(request, handlers = {}, signal) {
1101
+ const child = await this.supervisor.startChildReady("provider", request, handlers, signal, { maxStartAttempts: request.stdin === undefined ? 1 : 3, retryDelayMs: 100 });
1102
+ if (!this.enforceNoListeners)
1103
+ return await child.result;
1104
+ let settled = false;
1105
+ let inspectionFailure;
1106
+ const monitor = (async () => {
1107
+ while (!settled) {
1108
+ try {
1109
+ const ports = await this.listenerInspector.inspectOwnedLoopbackListeners(child, this.owner);
1110
+ if (ports.length > 0) {
1111
+ inspectionFailure = new Error(`host_analysis_listener_violation: analysis-only provider opened undeclared loopback listener(s): ${ports.join(", ")}.`);
1112
+ await child.stop(this.owner.fencingToken).catch(() => undefined);
1113
+ return;
1114
+ }
1115
+ }
1116
+ catch (error) {
1117
+ inspectionFailure = error;
1118
+ await child.stop(this.owner.fencingToken).catch(() => undefined);
1119
+ return;
1120
+ }
1121
+ await waitForAnalysisInspection(250, signal).catch(error => {
1122
+ if (!settled && !signal?.aborted)
1123
+ inspectionFailure = error;
1124
+ });
1125
+ }
1126
+ })();
1127
+ try {
1128
+ const result = await child.result;
1129
+ settled = true;
1130
+ await monitor;
1131
+ if (inspectionFailure)
1132
+ throw inspectionFailure;
1133
+ return result;
1134
+ }
1135
+ catch (error) {
1136
+ settled = true;
1137
+ await monitor.catch(() => undefined);
1138
+ if (inspectionFailure)
1139
+ throw inspectionFailure;
1140
+ throw error;
1141
+ }
1142
+ }
1143
+ }
1144
+ function waitForAnalysisInspection(milliseconds, signal) {
1145
+ if (signal?.aborted)
1146
+ return Promise.reject(cancellationError(signal));
1147
+ return new Promise((resolve, reject) => {
1148
+ const timer = setTimeout(() => {
1149
+ signal?.removeEventListener("abort", abort);
1150
+ resolve();
1151
+ }, milliseconds);
1152
+ const abort = () => {
1153
+ clearTimeout(timer);
1154
+ reject(cancellationError(signal));
1155
+ };
1156
+ signal?.addEventListener("abort", abort, { once: true });
1157
+ });
1158
+ }
969
1159
  export class HostRuntimeLeaseCleanupError extends Error {
970
1160
  lease;
971
1161
  constructor(lease, cause) {
@@ -980,11 +1170,11 @@ export class HostRuntimeAdmissionExpiredError extends Error {
980
1170
  this.name = "HostRuntimeAdmissionExpiredError";
981
1171
  }
982
1172
  }
983
- export function hostRuntimeEventResources(lease, runtimeContract) {
1173
+ export function hostRuntimeEventResources(lease, runtimeContract, executionClass = "managed_runtime") {
984
1174
  const requestedHostConcurrency = runtimeContract?.contract.runtime.hostConcurrency ?? "serial";
985
1175
  const configuredLaunches = runtimeContract?.contract.runtime.ports.filter(port => port.launch !== null) ?? [];
986
1176
  const verifiedLaunches = configuredLaunches.filter(configured => lease.resources.ports.some(port => port.name === configured.name));
987
- const effectiveConcurrency = effectiveHostConcurrency(lease, runtimeContract);
1177
+ const effectiveConcurrency = effectiveHostConcurrency(lease, runtimeContract, executionClass);
988
1178
  return {
989
1179
  agentContainer: null,
990
1180
  sidecarContainers: [],
@@ -994,7 +1184,7 @@ export function hostRuntimeEventResources(lease, runtimeContract) {
994
1184
  hostConcurrency: effectiveConcurrency,
995
1185
  requestedHostConcurrency,
996
1186
  hostParallelPromotion: effectiveConcurrency === "host_parallel_safe"
997
- ? "observed_probe_passed"
1187
+ ? executionClass === "analysis_only" ? "analysis_profile_enforced" : "observed_probe_passed"
998
1188
  : requestedHostConcurrency !== "parallel"
999
1189
  ? "not_requested"
1000
1190
  : configuredLaunches.length > 0 && verifiedLaunches.length === configuredLaunches.length
@@ -1002,6 +1192,7 @@ export function hostRuntimeEventResources(lease, runtimeContract) {
1002
1192
  : "pending_adapter_probe",
1003
1193
  verifiedRuntimePorts: verifiedLaunches.map(port => port.name),
1004
1194
  runtimeContractSource: runtimeContract?.source ?? "default",
1195
+ executionClass,
1005
1196
  requestedPorts: runtimeContract?.contract.runtime.ports.map(port => port.name) ?? [],
1006
1197
  singletonKeys: lease.resources.singletonKeys,
1007
1198
  processSupervisorId: lease.resources.processSupervisorId,
@@ -1011,7 +1202,7 @@ export function hostRuntimeEventResources(lease, runtimeContract) {
1011
1202
  export function hostRuntimeSingletonKeys(dispatch) {
1012
1203
  return [projectHostRuntimeSingletonKey(dispatch.repo.id)];
1013
1204
  }
1014
- function hostRuntimeEnvironment(sessionId, lease, runtimeContract) {
1205
+ function hostRuntimeEnvironment(sessionId, lease, runtimeContract, executionClass) {
1015
1206
  const pathFor = (role) => lease.resources.paths.find(entry => entry.role === role)?.path ?? "";
1016
1207
  const environment = {
1017
1208
  FORGE_SESSION_ID: sessionId,
@@ -1027,10 +1218,13 @@ function hostRuntimeEnvironment(sessionId, lease, runtimeContract) {
1027
1218
  FORGE_CACHE_DIR: pathFor("cache"),
1028
1219
  FORGE_BROWSER_PROFILE_DIR: pathFor("browser_profile"),
1029
1220
  FORGE_TEST_OUTPUT_DIR: pathFor("test_output"),
1221
+ FORGE_ATTEMPT_DATABASE_SCHEMA: `forge_attempt_${uuidHex(lease.dispatchAttemptId, "dispatch attempt")}`,
1222
+ FORGE_ATTEMPT_DATABASE_READINESS_PATH: path.join(pathFor("logs"), "attempt-database-readiness.json"),
1030
1223
  FORGE_PROCESS_SUPERVISOR_ID: lease.resources.processSupervisorId ?? "",
1031
1224
  FORGE_PROCESS_SUPERVISOR_METADATA_PATH: pathFor("process_supervisor_metadata"),
1032
- FORGE_HOST_CONCURRENCY: effectiveHostConcurrency(lease, runtimeContract),
1225
+ FORGE_HOST_CONCURRENCY: effectiveHostConcurrency(lease, runtimeContract, executionClass),
1033
1226
  FORGE_HOST_CONCURRENCY_REQUESTED: runtimeContract.contract.runtime.hostConcurrency,
1227
+ FORGE_RUNTIME_EXECUTION_CLASS: executionClass,
1034
1228
  FORGE_HOST_RUNTIME_CONTRACT_SOURCE: runtimeContract.source,
1035
1229
  FORGE_SINGLETON_KEYS: lease.resources.singletonKeys.join(","),
1036
1230
  TMPDIR: pathFor("temporary"),
@@ -1057,11 +1251,19 @@ function hostRuntimeEnvironment(sessionId, lease, runtimeContract) {
1057
1251
  }
1058
1252
  }
1059
1253
  }
1254
+ if (environment.FORGE_API_URL)
1255
+ environment.FORGE_E2E_API_URL = environment.FORGE_API_URL;
1256
+ if (environment.FORGE_WEB_URL)
1257
+ environment.FORGE_E2E_WEB_URL = environment.FORGE_WEB_URL;
1060
1258
  if (resolvedBaseUrls.length === 1)
1061
1259
  environment.FORGE_BASE_URL = resolvedBaseUrls[0];
1062
1260
  return environment;
1063
1261
  }
1064
- function effectiveHostConcurrency(lease, runtimeContract) {
1262
+ function effectiveHostConcurrency(lease, runtimeContract, executionClass = "managed_runtime") {
1263
+ if (executionClass === "analysis_only") {
1264
+ const analysisSingletonHeld = lease.resources.singletonKeys.length > 0;
1265
+ return analysisSingletonHeld ? "host_serial_only" : "host_parallel_safe";
1266
+ }
1065
1267
  if (!runtimeContract || !hostRuntimeParallelEligible(runtimeContract))
1066
1268
  return "host_serial_only";
1067
1269
  const requiredPorts = configuredLaunchPortNames(runtimeContract);
@@ -1073,6 +1275,18 @@ function effectiveHostConcurrency(lease, runtimeContract) {
1073
1275
  function projectHostRuntimeSingletonKey(repoId) {
1074
1276
  return `project:${repoId.toLowerCase()}:host-runtime`;
1075
1277
  }
1278
+ function projectHostAnalysisKey(repoId) {
1279
+ return `project:${repoId.toLowerCase()}:host-analysis`;
1280
+ }
1281
+ function hostRuntimeDeclaredSingletonKey(resource) {
1282
+ return `runtime:${resource}`;
1283
+ }
1284
+ function runtimeExecutionClass(dispatch) {
1285
+ return dispatch.runtimeIntent?.executionClass === "analysis_only"
1286
+ && dispatch.runtimeIntent.applicationRuntimeRequired === false
1287
+ ? "analysis_only"
1288
+ : "managed_runtime";
1289
+ }
1076
1290
  function capacityProject(projects, repoId) {
1077
1291
  const normalized = repoId.trim().toLowerCase();
1078
1292
  const existing = projects.get(normalized);
@@ -1085,6 +1299,9 @@ function capacityProject(projects, repoId) {
1085
1299
  reservedAttempts: 0,
1086
1300
  waitingAttempts: 0,
1087
1301
  degradedAttempts: 0,
1302
+ runtimeContractFingerprint: null,
1303
+ runtimeContractSource: null,
1304
+ promotedAt: null,
1088
1305
  };
1089
1306
  projects.set(normalized, created);
1090
1307
  return created;
@@ -1104,8 +1321,8 @@ function repoIdFromProjectSingletonKeys(keys) {
1104
1321
  }
1105
1322
  function repoIdFromProjectSingletonKey(key) {
1106
1323
  const prefix = "project:";
1107
- const suffix = ":host-runtime";
1108
- return key.startsWith(prefix) && key.endsWith(suffix)
1324
+ const suffix = [":host-runtime", ":host-analysis"].find(candidate => key.endsWith(candidate));
1325
+ return key.startsWith(prefix) && suffix
1109
1326
  ? key.slice(prefix.length, -suffix.length)
1110
1327
  : null;
1111
1328
  }
@@ -1185,13 +1402,17 @@ function validHostProcessSupervisorMetadata(value, lease) {
1185
1402
  && (metadata.rootIdentity == null
1186
1403
  || (typeof metadata.rootIdentity === "string" && metadata.rootIdentity.length <= 256))
1187
1404
  && /^[0-9a-f]{64}$/u.test(metadata.commandDigest)
1188
- && path.resolve(metadata.worktreePath) === path.resolve(lease.worktreePath)
1405
+ && containedMetadataWorktreePath(lease.worktreePath, metadata.worktreePath)
1189
1406
  && metadata.runtimeLeaseId === lease.executionLeaseId
1190
1407
  && metadata.dispatchAttemptId === lease.dispatchAttemptId
1191
1408
  && metadata.fencingToken === lease.fencingToken
1192
1409
  && Number.isSafeInteger(metadata.bridgePid)
1193
1410
  && metadata.bridgePid > 0;
1194
1411
  }
1412
+ function containedMetadataWorktreePath(worktreePath, metadataWorktreePath) {
1413
+ const relative = path.relative(path.resolve(worktreePath), path.resolve(metadataWorktreePath));
1414
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
1415
+ }
1195
1416
  function transitionHostLeaseForCleanup(lease, state) {
1196
1417
  if (lease.state === state || lease.state === "released")
1197
1418
  return lease;
@@ -3,6 +3,7 @@ export interface HostParallelPromotionRecord {
3
3
  readonly schemaVersion: 1;
4
4
  readonly repoId: string;
5
5
  readonly runtimeContractFingerprint: string;
6
+ readonly runtimeContractSource?: "project" | "derived";
6
7
  readonly observedAt: string;
7
8
  readonly observedDispatchAttemptId: string;
8
9
  readonly verifiedPortNames: readonly string[];
@@ -12,6 +13,7 @@ export declare class HostParallelPromotionStore {
12
13
  private readonly stateRoot;
13
14
  constructor(stateRoot: string);
14
15
  load(repoId: string): Promise<HostParallelPromotionRecord | null>;
16
+ list(): Promise<readonly HostParallelPromotionRecord[]>;
15
17
  observe(repoId: string, runtimeContract: LoadedHostRuntimeContract, dispatchAttemptId: string, verifiedPortNames: readonly string[]): Promise<HostParallelPromotionRecord>;
16
18
  invalidate(repoId: string): Promise<void>;
17
19
  private recordPath;