@loucompanion/forge-bridge 0.1.3-dev.fbe15023cdfe → 0.1.4-dev.bfa11ebe98c6

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 (160) hide show
  1. package/README.md +88 -2
  2. package/dist/bridge/bridge-chat.types.d.ts +111 -0
  3. package/dist/bridge/bridge-chat.types.js +401 -0
  4. package/dist/bridge/bridge.base.d.ts +4 -1
  5. package/dist/bridge/bridge.service.d.ts +72 -3
  6. package/dist/bridge/bridge.service.js +1562 -152
  7. package/dist/bridge/bridge.types.d.ts +46 -1
  8. package/dist/bridge/dispatch-result-outbox.d.ts +25 -0
  9. package/dist/bridge/dispatch-result-outbox.js +62 -0
  10. package/dist/bridge/dispatch-result-worker.d.ts +2 -0
  11. package/dist/bridge/dispatch-result-worker.js +122 -0
  12. package/dist/bridge/internals.d.ts +2 -0
  13. package/dist/bridge/internals.js +1 -0
  14. package/dist/bridge/readiness-result-outbox.d.ts +21 -0
  15. package/dist/bridge/readiness-result-outbox.js +54 -0
  16. package/dist/bridge/readiness-result-worker.d.ts +2 -0
  17. package/dist/bridge/readiness-result-worker.js +121 -0
  18. package/dist/bridge/runner-host-capabilities.d.ts +15 -0
  19. package/dist/bridge/runner-host-capabilities.js +93 -0
  20. package/dist/cleanup/cleanup.service.d.ts +1 -0
  21. package/dist/cleanup/cleanup.service.js +70 -9
  22. package/dist/cli/cli.help.js +2 -2
  23. package/dist/cli/cli.service.d.ts +14 -6
  24. package/dist/cli/cli.service.js +75 -16
  25. package/dist/cli/local-config.d.ts +1 -0
  26. package/dist/cli/local-config.js +4 -0
  27. package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
  28. package/dist/runtime/aspnet-runtime-prebuild.js +341 -0
  29. package/dist/runtime/attempt-node-runtime-launcher.d.ts +2 -1
  30. package/dist/runtime/attempt-node-runtime-launcher.js +57 -18
  31. package/dist/runtime/attempt-path-operation.d.ts +21 -0
  32. package/dist/runtime/attempt-path-operation.js +94 -0
  33. package/dist/runtime/attempt-private-directory.d.ts +6 -0
  34. package/dist/runtime/attempt-private-directory.js +54 -0
  35. package/dist/runtime/attempt-process-supervisor.d.ts +11 -0
  36. package/dist/runtime/attempt-process-supervisor.js +67 -20
  37. package/dist/runtime/canonical-source-file.d.ts +3 -0
  38. package/dist/runtime/canonical-source-file.js +54 -0
  39. package/dist/runtime/docker-engine-probe.d.ts +90 -0
  40. package/dist/runtime/docker-engine-probe.js +333 -0
  41. package/dist/runtime/generated-output-reset.d.ts +10 -0
  42. package/dist/runtime/generated-output-reset.js +62 -8
  43. package/dist/runtime/host-database-capability-boundary.d.ts +16 -0
  44. package/dist/runtime/host-database-capability-boundary.js +62 -0
  45. package/dist/runtime/host-diagnostics.d.ts +11 -0
  46. package/dist/runtime/host-diagnostics.js +191 -0
  47. package/dist/runtime/host-execution-root.d.ts +4 -0
  48. package/dist/runtime/host-execution-root.js +39 -0
  49. package/dist/runtime/host-listener-inspector.d.ts +8 -1
  50. package/dist/runtime/host-listener-inspector.js +121 -60
  51. package/dist/runtime/host-runtime-contract.d.ts +12 -1
  52. package/dist/runtime/host-runtime-contract.js +107 -53
  53. package/dist/runtime/host-runtime-lease.d.ts +90 -8
  54. package/dist/runtime/host-runtime-lease.js +1097 -108
  55. package/dist/runtime/host-runtime-promotion.d.ts +12 -3
  56. package/dist/runtime/host-runtime-promotion.js +59 -12
  57. package/dist/runtime/memory-capacity.d.ts +70 -0
  58. package/dist/runtime/memory-capacity.js +123 -0
  59. package/dist/runtime/native-test-postgres-fallback.d.ts +25 -0
  60. package/dist/runtime/native-test-postgres-fallback.js +91 -0
  61. package/dist/runtime/node-host-runtime-adapter.d.ts +3 -0
  62. package/dist/runtime/node-host-runtime-adapter.js +28 -11
  63. package/dist/runtime/pinned-directory-process.d.ts +9 -0
  64. package/dist/runtime/pinned-directory-process.js +61 -0
  65. package/dist/runtime/pinned-source-directory.d.ts +17 -0
  66. package/dist/runtime/pinned-source-directory.js +94 -0
  67. package/dist/runtime/playwright-host-runtime-adapter.d.ts +1 -0
  68. package/dist/runtime/playwright-host-runtime-adapter.js +53 -5
  69. package/dist/runtime/prepared-runtime-identity.d.ts +6 -0
  70. package/dist/runtime/prepared-runtime-identity.js +218 -0
  71. package/dist/runtime/prepared-runtime-readiness.d.ts +30 -0
  72. package/dist/runtime/prepared-runtime-readiness.js +285 -0
  73. package/dist/runtime/runtime-isolation-lease.js +4 -1
  74. package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
  75. package/dist/runtime/workspace-resource-estimator.js +174 -0
  76. package/dist/sandbox/preflight.d.ts +31 -1
  77. package/dist/sandbox/preflight.js +237 -51
  78. package/dist/sandbox/repository-lock.d.ts +2 -1
  79. package/dist/sandbox/repository-lock.js +17 -8
  80. package/dist/sandbox/runtime.d.ts +6 -1
  81. package/dist/sandbox/runtime.js +132 -103
  82. package/dist/sandbox/sandbox.types.d.ts +7 -0
  83. package/dist/sandbox/workspace.service.js +2 -0
  84. package/dist/session/managed-attempt-tools.d.ts +17 -1
  85. package/dist/session/managed-attempt-tools.js +481 -31
  86. package/dist/session/managed-build-coordinator.d.ts +5 -0
  87. package/dist/session/managed-build-coordinator.js +105 -0
  88. package/dist/session/managed-build-receipt.d.ts +21 -0
  89. package/dist/session/managed-build-receipt.js +337 -0
  90. package/dist/session/managed-verification-timeouts.d.ts +8 -0
  91. package/dist/session/managed-verification-timeouts.js +17 -0
  92. package/dist/session/phase-completion.artifacts.d.ts +17 -0
  93. package/dist/session/phase-completion.artifacts.js +224 -22
  94. package/dist/session/phase-completion.d.ts +11 -1
  95. package/dist/session/phase-completion.helpers.d.ts +5 -0
  96. package/dist/session/phase-completion.helpers.js +30 -7
  97. package/dist/session/phase-completion.js +14 -3
  98. package/dist/session/phase-completion.prompts.js +17 -18
  99. package/dist/session/phase-completion.runtime.d.ts +3 -1
  100. package/dist/session/phase-completion.runtime.js +118 -16
  101. package/dist/session/phase-completion.schemas.js +49 -2
  102. package/dist/session/phase-completion.validation.d.ts +1 -0
  103. package/dist/session/phase-completion.validation.js +111 -21
  104. package/dist/session/pre-publication-validation.d.ts +75 -0
  105. package/dist/session/pre-publication-validation.js +657 -0
  106. package/dist/session/provider-cli/bridge-cli.conformance-fixtures.d.ts +72 -0
  107. package/dist/session/provider-cli/bridge-cli.conformance-fixtures.js +462 -0
  108. package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
  109. package/dist/session/provider-cli/ci-repair-readiness.js +23 -3
  110. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +4 -2
  111. package/dist/session/provider-cli/provider-cli.artifacts.js +47 -13
  112. package/dist/session/provider-cli/provider-cli.deterministic.js +34 -2
  113. package/dist/session/provider-cli/provider-cli.process.d.ts +51 -0
  114. package/dist/session/provider-cli/provider-cli.process.js +422 -22
  115. package/dist/session/provider-cli/provider-cli.prompts.d.ts +5 -0
  116. package/dist/session/provider-cli/provider-cli.prompts.js +136 -16
  117. package/dist/session/provider-cli/provider-cli.repair-epoch.d.ts +6 -0
  118. package/dist/session/provider-cli/provider-cli.repair-epoch.js +11 -0
  119. package/dist/session/provider-cli/provider-cli.runtime.d.ts +8 -1
  120. package/dist/session/provider-cli/provider-cli.runtime.js +40 -3
  121. package/dist/session/provider-cli/provider-cli.service.d.ts +30 -5
  122. package/dist/session/provider-cli/provider-cli.service.js +771 -68
  123. package/dist/session/provider-cli/provider-cli.types.d.ts +2 -0
  124. package/dist/session/provider-cli/provider-output-state.d.ts +10 -0
  125. package/dist/session/provider-cli/provider-output-state.js +361 -17
  126. package/dist/session/provider-cli/provider-usage.d.ts +7 -0
  127. package/dist/session/provider-cli/provider-usage.js +28 -5
  128. package/dist/session/session-redaction.js +434 -9
  129. package/dist/session/session.service.js +145 -23
  130. package/dist/session/session.types.d.ts +147 -2
  131. package/dist/session/session.types.js +587 -7
  132. package/dist/session/unreferenced-exports.d.ts +12 -0
  133. package/dist/session/unreferenced-exports.js +59 -0
  134. package/dist/shared/format/format-bytes.d.ts +1 -0
  135. package/dist/shared/format/format-bytes.js +9 -0
  136. package/dist/shared/git/git.js +2 -1
  137. package/dist/shared/path/path.d.ts +1 -0
  138. package/dist/shared/path/path.js +18 -0
  139. package/dist/shared/process/dotnet-toolchain.d.ts +17 -0
  140. package/dist/shared/process/dotnet-toolchain.js +307 -0
  141. package/dist/shared/process/host-process-supervisor.d.ts +2 -0
  142. package/dist/shared/process/host-process-supervisor.js +57 -9
  143. package/dist/shared/process/process.d.ts +2 -0
  144. package/dist/shared/process/process.js +12 -7
  145. package/dist/shared/provider-capabilities.d.ts +3 -0
  146. package/dist/shared/provider-capabilities.js +85 -0
  147. package/dist/shared/redaction/redaction.d.ts +1 -0
  148. package/dist/shared/redaction/redaction.js +39 -3
  149. package/dist/version.d.ts +1 -1
  150. package/dist/version.js +1 -1
  151. package/dist/worktree/dependency-warmer.d.ts +41 -9
  152. package/dist/worktree/dependency-warmer.js +497 -22
  153. package/dist/worktree/index.d.ts +1 -1
  154. package/dist/worktree/story-qa-reference-hydrator.d.ts +4 -0
  155. package/dist/worktree/story-qa-reference-hydrator.js +253 -0
  156. package/dist/worktree/worktree.service.d.ts +7 -0
  157. package/dist/worktree/worktree.service.js +253 -21
  158. package/dist/worktree/worktree.types.d.ts +7 -1
  159. package/package.json +7 -5
  160. package/runtime/windows-job-supervisor.ps1 +380 -14
@@ -10,6 +10,7 @@ export declare class ManagedWorktreeCleanupService implements CleanupService {
10
10
  run(request: CleanupRequest): Promise<CleanupResult>;
11
11
  private runLocked;
12
12
  private listWorktrees;
13
+ private staleRuntimeReadinessPaths;
13
14
  private scanManagedCandidates;
14
15
  private readRuntimeLeases;
15
16
  private worktreeMetadataRoot;
@@ -4,9 +4,10 @@ import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { RepositoryCoordinatorLock } from "../sandbox/repository-lock.js";
6
6
  import { GitClient } from "../shared/git/git.js";
7
- import { isSubPath, resolveExistingDirectoryPath } from "../shared/path/path.js";
7
+ import { isSubPath, resolveExistingDirectoryPath, resolvePathIdentity } from "../shared/path/path.js";
8
8
  import { validRuntimeIsolationLease } from "../runtime/runtime-isolation-lease.js";
9
9
  import { defaultWorktreeCoordinatorStateRoot, resolveManagedWorktreeRootForRepo, } from "../worktree/worktree-paths.js";
10
+ const staleRuntimeReadinessAgeMs = 30 * 60 * 1000;
10
11
  export class ManagedWorktreeCleanupService {
11
12
  stateRoot;
12
13
  git;
@@ -17,8 +18,13 @@ export class ManagedWorktreeCleanupService {
17
18
  this.coordinator = coordinator;
18
19
  }
19
20
  async run(request) {
20
- const repoPath = await resolveExistingDirectoryPath(request.repoRoot);
21
- const worktreeRoot = resolveManagedWorktreeRootForRepo(repoPath, request.allowedRepoRoot);
21
+ const [repoPath, allowedRepoRoot] = await Promise.all([
22
+ resolveExistingDirectoryPath(request.repoRoot),
23
+ request.allowedRepoRoot
24
+ ? resolvePathIdentity(request.allowedRepoRoot)
25
+ : Promise.resolve(undefined),
26
+ ]);
27
+ const worktreeRoot = resolveManagedWorktreeRootForRepo(repoPath, allowedRepoRoot);
22
28
  return await this.coordinator.withLock(repoPath, async () => {
23
29
  return await this.runLocked(repoPath, worktreeRoot, request);
24
30
  });
@@ -39,11 +45,13 @@ export class ManagedWorktreeCleanupService {
39
45
  this.listWorktrees(repoPath),
40
46
  this.readRuntimeLeases(),
41
47
  ]);
42
- const metadataRoot = await this.worktreeMetadataRoot(repoPath);
43
- const activeWorktrees = activePaths(listedWorktrees);
44
- const activeRuntimePaths = runtimeLeases
48
+ const activeRuntimePaths = await Promise.all(runtimeLeases
45
49
  .filter(lease => lease.state !== "released")
46
- .map(lease => path.resolve(lease.worktreePath));
50
+ .map(lease => resolveRuntimeLeasePath(lease.worktreePath)));
51
+ const metadataRoot = await this.worktreeMetadataRoot(repoPath);
52
+ const staleReadinessPaths = await this.staleRuntimeReadinessPaths(listedWorktrees, activeRuntimePaths);
53
+ const activeWorktrees = activePaths(listedWorktrees)
54
+ .filter(activePath => !staleReadinessPaths.some(stalePath => samePath(stalePath, activePath)));
47
55
  if (request.mode === "all") {
48
56
  const protectedPaths = uniquePaths([
49
57
  ...activeWorktrees.filter(activePath => isSubPath(safeRoot, activePath)),
@@ -88,9 +96,14 @@ export class ManagedWorktreeCleanupService {
88
96
  this.listWorktrees(repoPath),
89
97
  this.readRuntimeLeases(),
90
98
  ]);
99
+ const currentRuntimePaths = await Promise.all(currentLeases
100
+ .filter(lease => lease.state !== "released")
101
+ .map(lease => resolveRuntimeLeasePath(lease.worktreePath)));
102
+ const currentStaleReadinessPaths = await this.staleRuntimeReadinessPaths(currentWorktrees, currentRuntimePaths);
91
103
  const currentlyProtected = [
92
- ...activePaths(currentWorktrees),
93
- ...currentLeases.filter(lease => lease.state !== "released").map(lease => path.resolve(lease.worktreePath)),
104
+ ...activePaths(currentWorktrees)
105
+ .filter(activePath => !currentStaleReadinessPaths.some(stalePath => samePath(stalePath, activePath))),
106
+ ...currentRuntimePaths,
94
107
  ];
95
108
  if (worktreePathState(candidate, currentlyProtected) !== "none") {
96
109
  continue;
@@ -118,6 +131,24 @@ export class ManagedWorktreeCleanupService {
118
131
  const output = await this.git.run(repoPath, ["worktree", "list", "--porcelain", "-z"]);
119
132
  return parseWorktreeListing(repoPath, output);
120
133
  }
134
+ async staleRuntimeReadinessPaths(worktrees, runtimePaths) {
135
+ const stale = [];
136
+ for (const worktree of worktrees) {
137
+ if (worktree.prunable || !isRuntimeReadinessWorktree(worktree))
138
+ continue;
139
+ if (worktreePathState(worktree.worktreePath, runtimePaths) !== "none")
140
+ continue;
141
+ const stat = await fs.stat(worktree.worktreePath).catch(error => {
142
+ if (isEnoent(error))
143
+ return null;
144
+ throw error;
145
+ });
146
+ if (stat && Date.now() - stat.mtimeMs >= staleRuntimeReadinessAgeMs) {
147
+ stale.push(worktree.worktreePath);
148
+ }
149
+ }
150
+ return stale;
151
+ }
121
152
  async scanManagedCandidates(metadataRoot, worktreeRoot, activeWorktrees, runtimePaths) {
122
153
  const candidates = [];
123
154
  let protectedWorktrees = 0;
@@ -215,6 +246,28 @@ export class ManagedWorktreeCleanupService {
215
246
  return path.join(await fs.realpath(commonDirPath), "worktrees");
216
247
  }
217
248
  }
249
+ async function resolveRuntimeLeasePath(leasePath) {
250
+ const resolved = path.resolve(leasePath);
251
+ const before = await fs.lstat(resolved).catch(error => {
252
+ if (isEnoent(error))
253
+ return null;
254
+ throw error;
255
+ });
256
+ if (!before)
257
+ return await resolvePathIdentity(resolved);
258
+ if (before.isSymbolicLink() || !before.isDirectory()) {
259
+ throw new Error(`Refusing managed worktree cleanup because runtime lease path ${resolved} is redirected or is not a directory.`);
260
+ }
261
+ const real = await fs.realpath(resolved);
262
+ const after = await fs.lstat(resolved);
263
+ if (after.isSymbolicLink()
264
+ || !after.isDirectory()
265
+ || (before.dev !== 0 && before.ino !== 0 && after.dev !== 0 && after.ino !== 0
266
+ && (before.dev !== after.dev || before.ino !== after.ino))) {
267
+ throw new Error(`Refusing managed worktree cleanup because runtime lease path ${resolved} changed while being resolved.`);
268
+ }
269
+ return real;
270
+ }
218
271
  async function readRuntimeLedger(target, entryName) {
219
272
  const pathStat = await fs.lstat(target).catch(error => {
220
273
  if (isEnoent(error))
@@ -399,16 +452,24 @@ function parseWorktreeListing(repoPath, output) {
399
452
  current = {
400
453
  worktreePath: path.resolve(path.isAbsolute(rawPath) ? rawPath : path.join(repoPath, rawPath)),
401
454
  prunable: false,
455
+ branchName: null,
402
456
  };
403
457
  }
404
458
  else if (field.startsWith("prunable") && current) {
405
459
  current.prunable = true;
406
460
  }
461
+ else if (field.startsWith("branch ") && current) {
462
+ current.branchName = field.slice("branch ".length).trim();
463
+ }
407
464
  }
408
465
  if (current)
409
466
  worktrees.push(current);
410
467
  return worktrees;
411
468
  }
469
+ function isRuntimeReadinessWorktree(worktree) {
470
+ return worktree.branchName?.startsWith("refs/heads/runtime-readiness-") === true
471
+ || path.basename(worktree.worktreePath).startsWith("runtime-readiness-");
472
+ }
412
473
  function activePaths(worktrees) {
413
474
  return worktrees.filter(entry => !entry.prunable).map(entry => entry.worktreePath);
414
475
  }
@@ -60,7 +60,7 @@ export function commandHelpText(command) {
60
60
  " --repo-root <path> Folder where bridge-managed repos/worktrees live.",
61
61
  " --name <name> Display name for this runner.",
62
62
  " --fingerprint <value> Stable device fingerprint override.",
63
- " --cli-provider <list> Comma-separated providers, e.g. codex,cursor.",
63
+ " --cli-provider <list> Comma-separated providers, e.g. codex,cursor,claude.",
64
64
  " --cleanup-worktrees Enable managed worktree cleanup.",
65
65
  " --no-cleanup-worktrees Disable managed worktree cleanup.",
66
66
  "",
@@ -81,7 +81,7 @@ export function commandHelpText(command) {
81
81
  " --runner-id <id> Runner id; saved config is used when omitted.",
82
82
  " --device-token <token> Device token; saved config is used when omitted.",
83
83
  " --repo-root <path> Override repo/worktree folder for this run.",
84
- " --cli-provider <list> Comma-separated providers, e.g. codex,cursor.",
84
+ " --cli-provider <list> Comma-separated providers, e.g. codex,cursor,claude.",
85
85
  " --heartbeat-interval-ms <n> Heartbeat interval in milliseconds.",
86
86
  " --cleanup-worktrees Enable managed worktree cleanup for this run.",
87
87
  " --no-cleanup-worktrees Disable managed worktree cleanup for this run.",
@@ -1,15 +1,20 @@
1
+ import { type DotNetToolchainSelection } from "../shared/process/dotnet-toolchain.js";
1
2
  import type { BridgeService } from "../bridge/bridge.base.js";
2
3
  import type { BridgeServiceConfig } from "../bridge/bridge.types.js";
3
4
  import type { CleanupMode } from "../cleanup/cleanup.types.js";
4
5
  import type { CleanupService } from "../cleanup/cleanup.base.js";
5
6
  import type { SessionService } from "../session/session.base.js";
6
7
  import type { CliIo, CliService } from "./cli.base.js";
8
+ type DotNetToolchainConfigurator = (repoRoots: readonly string[], options?: {
9
+ readonly cloneFolders?: readonly string[];
10
+ }) => Promise<DotNetToolchainSelection | null>;
7
11
  export declare class ForgeBridgeCliService implements CliService {
8
12
  private readonly sessions;
9
13
  private readonly bridgeFactory;
10
14
  private readonly pairRunner;
11
15
  private readonly cleanupFactory;
12
- constructor(sessions?: SessionService, bridgeFactory?: (config: BridgeServiceConfig) => BridgeService, pairRunner?: PairRunnerClient, cleanupFactory?: CleanupServiceFactory);
16
+ private readonly dotNetToolchainConfigurator;
17
+ constructor(sessions?: SessionService, bridgeFactory?: (config: BridgeServiceConfig) => BridgeService, pairRunner?: PairRunnerClient, cleanupFactory?: CleanupServiceFactory, dotNetToolchainConfigurator?: DotNetToolchainConfigurator);
13
18
  run(argv: readonly string[], io?: CliIo): Promise<number>;
14
19
  }
15
20
  export declare function parseArgs(argv: readonly string[]): {
@@ -21,11 +26,7 @@ export declare function parseArgs(argv: readonly string[]): {
21
26
  readonly ok: true;
22
27
  readonly command: "mcp";
23
28
  readonly argv: readonly string[];
24
- } | {
25
- readonly ok: true;
26
- readonly command: "connect";
27
- readonly config: BridgeServiceConfig;
28
- } | ({
29
+ } | ConnectArgs | ({
29
30
  readonly ok: true;
30
31
  readonly command: "pair";
31
32
  } & PairArgs) | {
@@ -89,6 +90,12 @@ export declare function parseArgs(argv: readonly string[]): {
89
90
  readonly ok: false;
90
91
  readonly error: string;
91
92
  };
93
+ interface ConnectArgs {
94
+ readonly ok: true;
95
+ readonly command: "connect";
96
+ readonly config: BridgeServiceConfig;
97
+ readonly implicitCloneDestination: boolean;
98
+ }
92
99
  interface PairArgs {
93
100
  readonly serverUrl: string;
94
101
  readonly code?: string;
@@ -104,6 +111,7 @@ interface PairRunnerClaim {
104
111
  readonly deviceToken: string;
105
112
  }
106
113
  type PairRunnerClient = (args: PairArgs) => Promise<PairRunnerClaim>;
114
+ export declare const claudeDefaultModels: readonly string[];
107
115
  export declare function parseCodexModelCatalog(raw: string): string[];
108
116
  export declare function parseCursorModelList(raw: string): string[];
109
117
  type CleanupServiceFactory = (stateRoot: string) => CleanupService;
@@ -6,6 +6,7 @@ import { createInterface } from "node:readline/promises";
6
6
  import { promisify } from "node:util";
7
7
  import { resolveCursorAgentDirectLaunch } from "../shared/cursor-agent/cursor-agent.launch.js";
8
8
  import { buildWindowsCmdLaunch } from "../shared/process/windows-launch.js";
9
+ import { configureDotNetToolchain } from "../shared/process/dotnet-toolchain.js";
9
10
  import { supportsDraftImageAttachmentsFor } from "../shared/provider-capabilities.js";
10
11
  import { redactText } from "../shared/redaction/redaction.js";
11
12
  import { defaultBridgeServiceConfig, ForgeBridgeService } from "../bridge/bridge.service.js";
@@ -19,7 +20,7 @@ import { DockerPreflight } from "../sandbox/preflight.js";
19
20
  import { DockerSandboxRuntime } from "../sandbox/runtime.js";
20
21
  import { DockerSandboxSmoke } from "../sandbox/smoke.js";
21
22
  import { parseCompletionMcpArgs, runCompletionMcpServer } from "../session/phase-completion.js";
22
- import { failureResult, parseRunOnceDispatchPacket } from "../session/session.types.js";
23
+ import { failureFactsFromError, failureResult, parseRunOnceDispatchPacket } from "../session/session.types.js";
23
24
  import { commandHelpText, helpText } from "./cli.help.js";
24
25
  const apiKeySecretGrantEnv = "FORGE_API_KEY_SECRET_GRANT";
25
26
  const execFileAsync = promisify(execFile);
@@ -28,11 +29,13 @@ export class ForgeBridgeCliService {
28
29
  bridgeFactory;
29
30
  pairRunner;
30
31
  cleanupFactory;
31
- constructor(sessions = new BridgeSessionService(), bridgeFactory = (config) => new ForgeBridgeService(config), pairRunner = claimBridgeRunner, cleanupFactory = stateRoot => new ManagedWorktreeCleanupService(stateRoot)) {
32
+ dotNetToolchainConfigurator;
33
+ constructor(sessions = new BridgeSessionService(), bridgeFactory = (config) => new ForgeBridgeService(config), pairRunner = claimBridgeRunner, cleanupFactory = stateRoot => new ManagedWorktreeCleanupService(stateRoot), dotNetToolchainConfigurator = configureDotNetToolchain) {
32
34
  this.sessions = sessions;
33
35
  this.bridgeFactory = bridgeFactory;
34
36
  this.pairRunner = pairRunner;
35
37
  this.cleanupFactory = cleanupFactory;
38
+ this.dotNetToolchainConfigurator = dotNetToolchainConfigurator;
36
39
  }
37
40
  async run(argv, io = { stdout: process.stdout, stderr: process.stderr }) {
38
41
  if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
@@ -67,11 +70,26 @@ export class ForgeBridgeCliService {
67
70
  io.stderr.write(`forge-bridge connect requires ${missing[0]}.\n`);
68
71
  return 2;
69
72
  }
73
+ let dotNetToolchain;
74
+ try {
75
+ dotNetToolchain = await this.dotNetToolchainConfigurator(await dotNetPreflightRoots(config, args.implicitCloneDestination), { cloneFolders: [config.defaultRepoRoot.trim()].filter(Boolean) });
76
+ }
77
+ catch (error) {
78
+ io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
79
+ return 1;
80
+ }
81
+ if (dotNetToolchain) {
82
+ io.stdout.write(`Using .NET SDK ${dotNetToolchain.sdkVersion}: ${dotNetToolchain.executablePath}\n`);
83
+ }
70
84
  const controller = new AbortController();
71
85
  const stop = () => controller.abort("signal received");
72
86
  process.once("SIGINT", stop);
73
87
  process.once("SIGTERM", stop);
74
88
  try {
89
+ if (config.cleanupWorktrees && config.repoMappings.some(mapping => mapping.localPath?.trim())) {
90
+ io.stdout.write("Cleaning stale managed worktrees before accepting dispatches.\n");
91
+ await cleanupRepos(config, "stale", true, this.cleanupFactory(config.sandboxStateRoot), io);
92
+ }
75
93
  io.stdout.write(connectStartupText(config));
76
94
  await this.bridgeFactory(config).run(controller.signal);
77
95
  return 0;
@@ -216,7 +234,7 @@ export class ForgeBridgeCliService {
216
234
  }
217
235
  catch (error) {
218
236
  const summary = redactText(error instanceof Error ? error.message : String(error), redactionSecrets);
219
- await writeJsonAtomic(args.resultPath, failureResult(summary)).catch(() => undefined);
237
+ await writeJsonAtomic(args.resultPath, redactRunOnceResultPacket(failureResult(summary, [], failureFactsFromError(error, { source: "bridge" })), redactionSecrets)).catch(() => undefined);
220
238
  io.stderr.write(`${summary}\n`);
221
239
  return 1;
222
240
  }
@@ -582,6 +600,28 @@ async function reapSandboxes(io) {
582
600
  await preflight.release();
583
601
  }
584
602
  }
603
+ async function dotNetPreflightRoots(config, implicitCloneDestination) {
604
+ const roots = config.repoMappings.map((mapping) => mapping.localPath?.trim())
605
+ .filter((repoRoot) => Boolean(repoRoot));
606
+ const defaultRoot = config.defaultRepoRoot.trim();
607
+ if (!defaultRoot)
608
+ return roots;
609
+ if (implicitCloneDestination) {
610
+ try {
611
+ // The built-in clone destination need not exist on first connect. lstat
612
+ // deliberately retains dangling links for the strict SDK scanner to reject.
613
+ // Explicit mappings (including this same path) remain required inputs.
614
+ await fs.lstat(defaultRoot);
615
+ }
616
+ catch (error) {
617
+ if (error.code === "ENOENT")
618
+ return roots;
619
+ throw error;
620
+ }
621
+ }
622
+ roots.push(defaultRoot);
623
+ return roots;
624
+ }
585
625
  function parseConnectArgs(argv) {
586
626
  let serverUrl = null;
587
627
  let runnerId = null;
@@ -651,7 +691,10 @@ function parseConnectArgs(argv) {
651
691
  return {
652
692
  ok: true,
653
693
  command: "connect",
654
- config: configuredBridgeServiceConfig(overrides, localConfig)
694
+ config: configuredBridgeServiceConfig(overrides, localConfig),
695
+ implicitCloneDestination: repoRoot === null
696
+ && process.env.FORGE_REPO_ROOT === undefined
697
+ && localConfig.defaultRepoRoot === undefined,
655
698
  };
656
699
  }
657
700
  function parsePairArgs(argv) {
@@ -714,7 +757,7 @@ function parsePairArgs(argv) {
714
757
  name: name?.trim() || defaultName,
715
758
  fingerprint: fingerprint?.trim() || `local-bridge:${os.hostname()}:${user}`,
716
759
  repoRoot: repoRoot?.trim() ? path.resolve(repoRoot) : undefined,
717
- cleanupWorktrees: cleanupWorktrees ?? localConfig.cleanupWorktrees ?? false,
760
+ cleanupWorktrees: cleanupWorktrees ?? defaultConfig.cleanupWorktrees,
718
761
  cliProviders: cliProviders.length > 0 ? cliProviders : localConfig.cliProviders && localConfig.cliProviders.length > 0 ? [...localConfig.cliProviders] : ["codex"],
719
762
  projectLabels: projectLabels.length > 0 ? projectLabels : localConfig.projectLabels ? [...localConfig.projectLabels] : []
720
763
  };
@@ -869,6 +912,7 @@ function connectStartupText(config) {
869
912
  ` repo folder: ${config.defaultRepoRoot || "not configured"}`,
870
913
  ` providers: ${formatList(config.cliProviders)}`,
871
914
  ` cleanup worktrees: ${config.cleanupWorktrees ? "enabled" : "disabled"}`,
915
+ ` Docker Desktop auto-recovery: ${config.dockerDesktopRecoveryEnabled ? "enabled" : "disabled"}`,
872
916
  ` open public sandbox networking: ${config.allowOpenPublicSandbox ? "enabled" : "disabled"}`,
873
917
  "",
874
918
  "Press Ctrl+C to stop.",
@@ -900,7 +944,7 @@ async function writeJsonAtomic(outputPath, value) {
900
944
  await fs.rename(tempPath, outputPath);
901
945
  }
902
946
  async function writeStatus(config, checkUpdates, io) {
903
- const detected = await detectCliProviders([...new Set([...config.cliProviders, "codex", "cursor"])]);
947
+ const detected = await detectCliProviders([...new Set([...config.cliProviders, "codex", "cursor", "claude"])]);
904
948
  const repos = config.repoMappings.filter((mapping) => mapping.localPath?.trim());
905
949
  io.stdout.write(`Forge bridge ${config.version}\n`);
906
950
  io.stdout.write(`Config: ${resolveLocalConfigPath()}\n`);
@@ -973,7 +1017,7 @@ async function detectCliProviders(providers) {
973
1017
  let detected = null;
974
1018
  for (const probe of cliProbeCommandsFor(provider)) {
975
1019
  try {
976
- detected = await execVersionCommand(probe);
1020
+ detected = await execVersionCommand(probe, probe.timeoutMs);
977
1021
  break;
978
1022
  }
979
1023
  catch {
@@ -997,6 +1041,13 @@ function cliProbeCommandsFor(provider) {
997
1041
  { command: "codex", args: ["--version"] }
998
1042
  ];
999
1043
  }
1044
+ if (normalized === "claude") {
1045
+ return [
1046
+ // Claude Code's Node startup can exceed the default 2s probe on a cold Windows host.
1047
+ { command: "claude", args: ["--version"], timeoutMs: 8000 },
1048
+ { command: "claude", args: ["-v"], timeoutMs: 8000 }
1049
+ ];
1050
+ }
1000
1051
  if (normalized === "cursor") {
1001
1052
  return [
1002
1053
  { command: "cursor-agent", args: ["-v"] },
@@ -1010,22 +1061,22 @@ function cliProbeCommandsFor(provider) {
1010
1061
  { command: provider, args: ["-V"] }
1011
1062
  ];
1012
1063
  }
1013
- async function execVersionCommand(probe, timeout = 2000) {
1064
+ async function execVersionCommand(probe, timeout = 2000, maxBuffer) {
1014
1065
  const directLaunch = process.platform === "win32"
1015
1066
  ? await resolveCursorAgentDirectLaunch(probe.command, probe.args)
1016
1067
  : null;
1017
1068
  if (directLaunch) {
1018
- return await execFileAsync(directLaunch.command, [...directLaunch.args], { timeout });
1069
+ return await execFileAsync(directLaunch.command, [...directLaunch.args], { timeout, maxBuffer });
1019
1070
  }
1020
1071
  try {
1021
- return await execFileAsync(probe.command, probe.args, { timeout });
1072
+ return await execFileAsync(probe.command, probe.args, { timeout, maxBuffer });
1022
1073
  }
1023
1074
  catch (error) {
1024
1075
  if (process.platform !== "win32") {
1025
1076
  throw error;
1026
1077
  }
1027
1078
  const launch = buildWindowsCmdLaunch(probe.command, probe.args);
1028
- return await execFileAsync(launch.command, [...launch.args], { timeout });
1079
+ return await execFileAsync(launch.command, [...launch.args], { timeout, maxBuffer });
1029
1080
  }
1030
1081
  }
1031
1082
  function versionLine(output) {
@@ -1040,19 +1091,26 @@ async function detectCliModels(provider) {
1040
1091
  const normalized = provider.toLowerCase();
1041
1092
  try {
1042
1093
  if (normalized === "codex") {
1043
- const result = await execFileAsync("codex", ["debug", "models"], { timeout: 5000, maxBuffer: 10 * 1024 * 1024 });
1094
+ // Windows installs codex as a .cmd shim, which execFile cannot launch directly.
1095
+ const result = await execVersionCommand({ command: "codex", args: ["debug", "models"] }, 10_000, 10 * 1024 * 1024);
1044
1096
  return parseCodexModelCatalog(result.stdout);
1045
1097
  }
1046
1098
  if (normalized === "cursor") {
1047
1099
  const result = await execVersionCommand({ command: "cursor-agent", args: ["models"] }, 10_000);
1048
1100
  return parseCursorModelList(`${result.stdout || result.stderr}`);
1049
1101
  }
1102
+ if (normalized === "claude") {
1103
+ // Claude Code has no model-listing command; it accepts these aliases and full model IDs.
1104
+ const configured = splitCsv(process.env.FORGE_CLAUDE_MODELS ?? "");
1105
+ return configured.length > 0 ? configured : [...claudeDefaultModels];
1106
+ }
1050
1107
  }
1051
1108
  catch {
1052
1109
  return [];
1053
1110
  }
1054
1111
  return [];
1055
1112
  }
1113
+ export const claudeDefaultModels = ["sonnet", "opus", "haiku"];
1056
1114
  export function parseCodexModelCatalog(raw) {
1057
1115
  try {
1058
1116
  const parsed = JSON.parse(raw);
@@ -1105,8 +1163,8 @@ function uniqueModels(models) {
1105
1163
  function isRecord(value) {
1106
1164
  return typeof value === "object" && value !== null;
1107
1165
  }
1108
- async function withDetectedCliProviders(config) {
1109
- const detected = await detectCliProviders(configuredProviderCandidates(config.cliProviders));
1166
+ async function withDetectedCliProviders(config, candidates = config.cliProviders) {
1167
+ const detected = await detectCliProviders(uniqueProviders(candidates));
1110
1168
  const found = detected.filter((result) => result.found);
1111
1169
  if (found.length === 0) {
1112
1170
  return config;
@@ -1130,6 +1188,7 @@ async function withDetectedCliProviders(config) {
1130
1188
  };
1131
1189
  }
1132
1190
  async function withDetectedPairCliProviders(args) {
1191
+ const configured = configuredProviderCandidates(args.cliProviders);
1133
1192
  const detectedConfig = await withDetectedCliProviders({
1134
1193
  ...defaultBridgeServiceConfig({
1135
1194
  serverUrl: args.serverUrl,
@@ -1139,14 +1198,14 @@ async function withDetectedPairCliProviders(args) {
1139
1198
  cleanupWorktrees: args.cleanupWorktrees,
1140
1199
  defaultRepoRoot: args.repoRoot ?? defaultBridgeServiceConfig().defaultRepoRoot
1141
1200
  })
1142
- });
1201
+ }, configured);
1143
1202
  return {
1144
1203
  ...args,
1145
1204
  cliProviders: detectedConfig.cliProviders
1146
1205
  };
1147
1206
  }
1148
1207
  function configuredProviderCandidates(providers) {
1149
- return uniqueProviders([...providers, "codex", "cursor"]);
1208
+ return uniqueProviders([...providers, "codex", "cursor", "claude"]);
1150
1209
  }
1151
1210
  function uniqueProviders(providers) {
1152
1211
  return providers
@@ -8,6 +8,7 @@ export interface BridgeLocalConfig {
8
8
  readonly defaultRepoRoot?: string;
9
9
  readonly cleanupWorktrees?: boolean;
10
10
  readonly dockerEnabled?: boolean;
11
+ readonly dockerDesktopRecoveryEnabled?: boolean;
11
12
  readonly allowOpenPublicSandbox?: boolean;
12
13
  readonly cliProviders?: readonly string[];
13
14
  readonly providerStatuses?: Readonly<Record<string, BridgeProviderStatus>>;
@@ -48,6 +48,9 @@ export function applyLocalConfigDefaults(config, local) {
48
48
  dockerEnabled: process.env.FORGE_DOCKER_SANDBOX === undefined
49
49
  ? local.dockerEnabled ?? config.dockerEnabled
50
50
  : config.dockerEnabled,
51
+ dockerDesktopRecoveryEnabled: process.env.FORGE_DOCKER_DESKTOP_AUTO_RECOVERY === undefined
52
+ ? local.dockerDesktopRecoveryEnabled ?? config.dockerDesktopRecoveryEnabled
53
+ : config.dockerDesktopRecoveryEnabled,
51
54
  allowOpenPublicSandbox: process.env.FORGE_ALLOW_OPEN_PUBLIC_SANDBOX === undefined
52
55
  ? local.allowOpenPublicSandbox ?? config.allowOpenPublicSandbox
53
56
  : config.allowOpenPublicSandbox,
@@ -126,6 +129,7 @@ function normalizeLocalConfig(config) {
126
129
  defaultRepoRoot: cleanString(config.defaultRepoRoot),
127
130
  cleanupWorktrees: config.cleanupWorktrees,
128
131
  dockerEnabled: config.dockerEnabled,
132
+ dockerDesktopRecoveryEnabled: config.dockerDesktopRecoveryEnabled,
129
133
  allowOpenPublicSandbox: config.allowOpenPublicSandbox,
130
134
  cliProviders: cleanList(config.cliProviders),
131
135
  providerStatuses: normalizeProviderStatuses(config.providerStatuses),
@@ -0,0 +1,36 @@
1
+ export interface AspNetRuntimePrebuildPlan {
2
+ readonly fingerprint: string;
3
+ readonly receiptPath: string;
4
+ readonly command: string;
5
+ readonly args: readonly string[];
6
+ readonly cwd: string;
7
+ readonly environment: Readonly<Record<string, string>>;
8
+ readonly projectPath: string;
9
+ readonly worktreePath: string;
10
+ readonly inputCommitSha: string;
11
+ }
12
+ export interface AspNetRuntimePrebuildResult {
13
+ readonly cacheHit: boolean;
14
+ readonly receiptPath: string;
15
+ readonly fingerprint: string;
16
+ }
17
+ export interface AspNetRuntimeBuildExecutionResult {
18
+ readonly exitCode: number;
19
+ readonly stdoutTail: string;
20
+ readonly stderrTail: string;
21
+ readonly stdoutLogPath: string;
22
+ readonly stderrLogPath: string;
23
+ }
24
+ export type AspNetRuntimeBuildExecutor = (plan: AspNetRuntimePrebuildPlan, signal?: AbortSignal) => Promise<AspNetRuntimeBuildExecutionResult>;
25
+ export declare function createAspNetRuntimePrebuildPlan(options: {
26
+ readonly command: string;
27
+ readonly args: readonly string[];
28
+ readonly cwd: string;
29
+ readonly worktreePath: string;
30
+ readonly environment?: Readonly<Record<string, string>>;
31
+ readonly inputCommitSha: string;
32
+ readonly runtimeContractFingerprint: string;
33
+ }): AspNetRuntimePrebuildPlan | null;
34
+ export declare function ensureAspNetRuntimePrebuilt(plan: AspNetRuntimePrebuildPlan, execute: AspNetRuntimeBuildExecutor, signal?: AbortSignal): Promise<AspNetRuntimePrebuildResult>;
35
+ export declare function withAspNetPrebuiltRunArguments(command: string, args: readonly string[]): readonly string[];
36
+ export declare function resolveWorktreeCommitSha(worktreePath: string, supplied?: string | null): Promise<string>;