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

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 (162) 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 +1563 -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-database-broker.d.ts +89 -0
  30. package/dist/runtime/attempt-database-broker.js +390 -0
  31. package/dist/runtime/attempt-node-runtime-launcher.d.ts +2 -1
  32. package/dist/runtime/attempt-node-runtime-launcher.js +57 -18
  33. package/dist/runtime/attempt-path-operation.d.ts +21 -0
  34. package/dist/runtime/attempt-path-operation.js +94 -0
  35. package/dist/runtime/attempt-private-directory.d.ts +6 -0
  36. package/dist/runtime/attempt-private-directory.js +54 -0
  37. package/dist/runtime/attempt-process-supervisor.d.ts +11 -0
  38. package/dist/runtime/attempt-process-supervisor.js +67 -20
  39. package/dist/runtime/canonical-source-file.d.ts +3 -0
  40. package/dist/runtime/canonical-source-file.js +54 -0
  41. package/dist/runtime/docker-engine-probe.d.ts +90 -0
  42. package/dist/runtime/docker-engine-probe.js +333 -0
  43. package/dist/runtime/generated-output-reset.d.ts +10 -0
  44. package/dist/runtime/generated-output-reset.js +62 -8
  45. package/dist/runtime/host-database-capability-boundary.d.ts +16 -0
  46. package/dist/runtime/host-database-capability-boundary.js +62 -0
  47. package/dist/runtime/host-diagnostics.d.ts +11 -0
  48. package/dist/runtime/host-diagnostics.js +191 -0
  49. package/dist/runtime/host-execution-root.d.ts +4 -0
  50. package/dist/runtime/host-execution-root.js +39 -0
  51. package/dist/runtime/host-listener-inspector.d.ts +8 -1
  52. package/dist/runtime/host-listener-inspector.js +121 -60
  53. package/dist/runtime/host-runtime-contract.d.ts +12 -1
  54. package/dist/runtime/host-runtime-contract.js +168 -55
  55. package/dist/runtime/host-runtime-lease.d.ts +92 -8
  56. package/dist/runtime/host-runtime-lease.js +1141 -114
  57. package/dist/runtime/host-runtime-promotion.d.ts +12 -3
  58. package/dist/runtime/host-runtime-promotion.js +59 -12
  59. package/dist/runtime/memory-capacity.d.ts +70 -0
  60. package/dist/runtime/memory-capacity.js +123 -0
  61. package/dist/runtime/native-test-postgres-fallback.d.ts +25 -0
  62. package/dist/runtime/native-test-postgres-fallback.js +91 -0
  63. package/dist/runtime/node-host-runtime-adapter.d.ts +3 -0
  64. package/dist/runtime/node-host-runtime-adapter.js +28 -11
  65. package/dist/runtime/pinned-directory-process.d.ts +9 -0
  66. package/dist/runtime/pinned-directory-process.js +61 -0
  67. package/dist/runtime/pinned-source-directory.d.ts +17 -0
  68. package/dist/runtime/pinned-source-directory.js +94 -0
  69. package/dist/runtime/playwright-host-runtime-adapter.d.ts +1 -0
  70. package/dist/runtime/playwright-host-runtime-adapter.js +53 -5
  71. package/dist/runtime/prepared-runtime-identity.d.ts +6 -0
  72. package/dist/runtime/prepared-runtime-identity.js +218 -0
  73. package/dist/runtime/prepared-runtime-readiness.d.ts +30 -0
  74. package/dist/runtime/prepared-runtime-readiness.js +285 -0
  75. package/dist/runtime/runtime-isolation-lease.js +4 -1
  76. package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
  77. package/dist/runtime/workspace-resource-estimator.js +174 -0
  78. package/dist/sandbox/preflight.d.ts +31 -1
  79. package/dist/sandbox/preflight.js +237 -51
  80. package/dist/sandbox/repository-lock.d.ts +2 -1
  81. package/dist/sandbox/repository-lock.js +17 -8
  82. package/dist/sandbox/runtime.d.ts +6 -1
  83. package/dist/sandbox/runtime.js +132 -103
  84. package/dist/sandbox/sandbox.types.d.ts +7 -0
  85. package/dist/sandbox/workspace.service.js +2 -0
  86. package/dist/session/managed-attempt-tools.d.ts +17 -1
  87. package/dist/session/managed-attempt-tools.js +485 -31
  88. package/dist/session/managed-build-coordinator.d.ts +5 -0
  89. package/dist/session/managed-build-coordinator.js +105 -0
  90. package/dist/session/managed-build-receipt.d.ts +21 -0
  91. package/dist/session/managed-build-receipt.js +337 -0
  92. package/dist/session/managed-verification-timeouts.d.ts +8 -0
  93. package/dist/session/managed-verification-timeouts.js +17 -0
  94. package/dist/session/phase-completion.artifacts.d.ts +17 -0
  95. package/dist/session/phase-completion.artifacts.js +224 -22
  96. package/dist/session/phase-completion.d.ts +11 -1
  97. package/dist/session/phase-completion.helpers.d.ts +5 -0
  98. package/dist/session/phase-completion.helpers.js +30 -7
  99. package/dist/session/phase-completion.js +14 -3
  100. package/dist/session/phase-completion.prompts.js +17 -18
  101. package/dist/session/phase-completion.runtime.d.ts +3 -1
  102. package/dist/session/phase-completion.runtime.js +126 -17
  103. package/dist/session/phase-completion.schemas.js +49 -2
  104. package/dist/session/phase-completion.validation.d.ts +1 -0
  105. package/dist/session/phase-completion.validation.js +111 -21
  106. package/dist/session/pre-publication-validation.d.ts +75 -0
  107. package/dist/session/pre-publication-validation.js +661 -0
  108. package/dist/session/provider-cli/bridge-cli.conformance-fixtures.d.ts +72 -0
  109. package/dist/session/provider-cli/bridge-cli.conformance-fixtures.js +462 -0
  110. package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
  111. package/dist/session/provider-cli/ci-repair-readiness.js +23 -3
  112. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +4 -2
  113. package/dist/session/provider-cli/provider-cli.artifacts.js +47 -13
  114. package/dist/session/provider-cli/provider-cli.deterministic.js +34 -2
  115. package/dist/session/provider-cli/provider-cli.process.d.ts +51 -0
  116. package/dist/session/provider-cli/provider-cli.process.js +422 -22
  117. package/dist/session/provider-cli/provider-cli.prompts.d.ts +5 -0
  118. package/dist/session/provider-cli/provider-cli.prompts.js +136 -16
  119. package/dist/session/provider-cli/provider-cli.repair-epoch.d.ts +6 -0
  120. package/dist/session/provider-cli/provider-cli.repair-epoch.js +11 -0
  121. package/dist/session/provider-cli/provider-cli.runtime.d.ts +8 -1
  122. package/dist/session/provider-cli/provider-cli.runtime.js +43 -3
  123. package/dist/session/provider-cli/provider-cli.service.d.ts +30 -5
  124. package/dist/session/provider-cli/provider-cli.service.js +771 -68
  125. package/dist/session/provider-cli/provider-cli.types.d.ts +2 -0
  126. package/dist/session/provider-cli/provider-output-state.d.ts +10 -0
  127. package/dist/session/provider-cli/provider-output-state.js +361 -17
  128. package/dist/session/provider-cli/provider-usage.d.ts +7 -0
  129. package/dist/session/provider-cli/provider-usage.js +28 -5
  130. package/dist/session/session-redaction.js +434 -9
  131. package/dist/session/session.service.js +145 -23
  132. package/dist/session/session.types.d.ts +147 -2
  133. package/dist/session/session.types.js +587 -7
  134. package/dist/session/unreferenced-exports.d.ts +12 -0
  135. package/dist/session/unreferenced-exports.js +59 -0
  136. package/dist/shared/format/format-bytes.d.ts +1 -0
  137. package/dist/shared/format/format-bytes.js +9 -0
  138. package/dist/shared/git/git.js +2 -1
  139. package/dist/shared/path/path.d.ts +1 -0
  140. package/dist/shared/path/path.js +18 -0
  141. package/dist/shared/process/dotnet-toolchain.d.ts +17 -0
  142. package/dist/shared/process/dotnet-toolchain.js +307 -0
  143. package/dist/shared/process/host-process-supervisor.d.ts +2 -0
  144. package/dist/shared/process/host-process-supervisor.js +57 -9
  145. package/dist/shared/process/process.d.ts +2 -0
  146. package/dist/shared/process/process.js +12 -7
  147. package/dist/shared/provider-capabilities.d.ts +3 -0
  148. package/dist/shared/provider-capabilities.js +85 -0
  149. package/dist/shared/redaction/redaction.d.ts +1 -0
  150. package/dist/shared/redaction/redaction.js +39 -3
  151. package/dist/version.d.ts +1 -1
  152. package/dist/version.js +1 -1
  153. package/dist/worktree/dependency-warmer.d.ts +41 -9
  154. package/dist/worktree/dependency-warmer.js +497 -22
  155. package/dist/worktree/index.d.ts +1 -1
  156. package/dist/worktree/story-qa-reference-hydrator.d.ts +4 -0
  157. package/dist/worktree/story-qa-reference-hydrator.js +253 -0
  158. package/dist/worktree/worktree.service.d.ts +7 -0
  159. package/dist/worktree/worktree.service.js +253 -21
  160. package/dist/worktree/worktree.types.d.ts +7 -1
  161. package/package.json +7 -5
  162. package/runtime/windows-job-supervisor.ps1 +380 -14
@@ -0,0 +1,12 @@
1
+ export type GitRunner = (args: readonly string[]) => Promise<{
2
+ exitCode: number;
3
+ stdout?: string;
4
+ }>;
5
+ export type UnreferencedExport = {
6
+ readonly name: string;
7
+ readonly path: string;
8
+ readonly testOnly: boolean;
9
+ };
10
+ export declare function isTestPath(file: string): boolean;
11
+ export declare function addedExports(diff: string): Map<string, string>;
12
+ export declare function findUnreferencedNewExports(git: GitRunner, worktreePath: string, baselineRevision: string | undefined): Promise<UnreferencedExport[]>;
@@ -0,0 +1,59 @@
1
+ // Finds exports added by this change that no production code uses. An export reached only from
2
+ // tests is code the current Story does not actually deliver: reviewers flag it and the Story
3
+ // cannot fix it without changing scope, so publication stops before the PR is opened.
4
+ import fs from "node:fs/promises";
5
+ import path from "node:path";
6
+ const SourceFile = /\.(?:[cm]?[jt]sx?)$/u;
7
+ const TestFile = /(?:^|\/)(?:test|tests|__tests__|e2e)\/|\.(?:test|spec)\.[cm]?[jt]sx?$/u;
8
+ const ExportDeclaration = /^\+\s*export\s+(?:default\s+)?(?:declare\s+)?(?:abstract\s+)?(?:async\s+)?(?:function\*?|const|let|var|class|interface|type|enum)\s+([A-Za-z_$][\w$]*)/u;
9
+ const Lines = /\r?\n/u;
10
+ const MaximumNames = 200;
11
+ export function isTestPath(file) {
12
+ return TestFile.test(file);
13
+ }
14
+ export function addedExports(diff) {
15
+ const added = new Map();
16
+ let file = null;
17
+ for (const line of diff.split(Lines)) {
18
+ if (line.startsWith("+++ ")) {
19
+ const target = line.slice(4).trim();
20
+ file = target === "/dev/null" ? null : target.replace(/^b\//u, "");
21
+ continue;
22
+ }
23
+ if (!file || !SourceFile.test(file) || isTestPath(file) || file.endsWith(".d.ts"))
24
+ continue;
25
+ const match = ExportDeclaration.exec(line);
26
+ if (match && added.size < MaximumNames)
27
+ added.set(match[1], file);
28
+ }
29
+ return added;
30
+ }
31
+ export async function findUnreferencedNewExports(git, worktreePath, baselineRevision) {
32
+ const diff = await git(["diff", "--no-color", "--no-ext-diff", "-U0", baselineRevision?.trim() || "HEAD", "--"]);
33
+ if (diff.exitCode !== 0)
34
+ return [];
35
+ // New files are untracked until Bridge commits them, so git diff alone would miss their exports.
36
+ const untracked = await git(["ls-files", "--others", "--exclude-standard"]);
37
+ const sections = [diff.stdout ?? ""];
38
+ for (const file of (untracked.exitCode === 0 ? untracked.stdout ?? "" : "").split(Lines).filter(row => SourceFile.test(row) && !isTestPath(row))) {
39
+ const content = await fs.readFile(path.join(worktreePath, file), "utf8").catch(() => "");
40
+ sections.push(`+++ b/${file}`, ...content.split(Lines).map(line => `+${line}`));
41
+ }
42
+ const found = [];
43
+ for (const [name, file] of addedExports(sections.join("\n"))) {
44
+ const grep = await git(["grep", "--untracked", "-I", "-c", "-w", "-F", "-e", name, "--"]);
45
+ // git grep exits 1 for no match; any other failure skips the name rather than guessing.
46
+ if (grep.exitCode !== 0 && grep.exitCode !== 1)
47
+ continue;
48
+ const counts = (grep.stdout ?? "").split(Lines).filter(Boolean).map(row => {
49
+ const separator = row.lastIndexOf(":");
50
+ return { file: row.slice(0, separator).replaceAll("\\", "/"), count: Number(row.slice(separator + 1)) };
51
+ });
52
+ const ownUses = counts.find(row => row.file === file)?.count ?? 0;
53
+ const elsewhere = counts.filter(row => row.file !== file);
54
+ if (ownUses > 1 || elsewhere.some(row => !isTestPath(row.file)))
55
+ continue;
56
+ found.push({ name, path: file, testOnly: elsewhere.length > 0 });
57
+ }
58
+ return found;
59
+ }
@@ -0,0 +1 @@
1
+ export declare function formatBytes(bytes: number): string;
@@ -0,0 +1,9 @@
1
+ export function formatBytes(bytes) {
2
+ if (!Number.isFinite(bytes) || bytes <= 0)
3
+ return "0 B";
4
+ const units = ["B", "KiB", "MiB", "GiB", "TiB"];
5
+ const power = Math.max(0, Math.min(units.length - 1, Math.floor(Math.log(bytes) / Math.log(1024))));
6
+ const value = bytes / 1024 ** power;
7
+ const precision = power === 0 || value >= 100 ? 0 : 1;
8
+ return `${value.toFixed(precision)} ${units[power]}`;
9
+ }
@@ -8,7 +8,8 @@ export class GitClient {
8
8
  const result = await this.processRunner.run({
9
9
  command: "git",
10
10
  args,
11
- cwd
11
+ cwd,
12
+ ...(args.includes("-z") ? { rawOutput: true } : {}),
12
13
  });
13
14
  if (result.exitCode !== 0) {
14
15
  throw new Error(`git ${args.join(" ")} failed: ${result.stderr.trim() || result.stdout.trim()}`);
@@ -1,4 +1,5 @@
1
1
  export declare function isSubPath(root: string, candidate: string): boolean;
2
2
  export declare function resolveExistingDirectoryPath(inputPath: string): Promise<string>;
3
+ export declare function resolvePathIdentity(inputPath: string): Promise<string>;
3
4
  export declare function ensureDirectory(inputPath: string): Promise<string>;
4
5
  export declare function safeSegment(value: string, fallback: string): string;
@@ -15,6 +15,24 @@ export async function resolveExistingDirectoryPath(inputPath) {
15
15
  }
16
16
  return await fs.realpath(inputPath);
17
17
  }
18
+ export async function resolvePathIdentity(inputPath) {
19
+ let existingAncestor = path.resolve(inputPath);
20
+ const missingSegments = [];
21
+ while (true) {
22
+ try {
23
+ return path.join(await fs.realpath(existingAncestor), ...missingSegments);
24
+ }
25
+ catch (error) {
26
+ if (error.code !== "ENOENT")
27
+ throw error;
28
+ const parent = path.dirname(existingAncestor);
29
+ if (parent === existingAncestor)
30
+ throw error;
31
+ missingSegments.unshift(path.basename(existingAncestor));
32
+ existingAncestor = parent;
33
+ }
34
+ }
35
+ }
18
36
  export async function ensureDirectory(inputPath) {
19
37
  await fs.mkdir(inputPath, { recursive: true });
20
38
  return await resolveExistingDirectoryPath(inputPath);
@@ -0,0 +1,17 @@
1
+ export interface DotNetToolchainSelection {
2
+ readonly executablePath: string;
3
+ readonly rootPath: string;
4
+ readonly sdkVersion: string;
5
+ readonly requiredMajor: number;
6
+ }
7
+ export interface DotNetToolchainOptions {
8
+ readonly environment?: NodeJS.ProcessEnv;
9
+ readonly platform?: NodeJS.Platform;
10
+ readonly homeDirectory?: string;
11
+ readonly fileExists?: (candidate: string) => Promise<boolean>;
12
+ readonly listSdks?: (candidate: string) => Promise<string>;
13
+ readonly resolveSdk?: (candidate: string, repositoryRoot: string) => Promise<string>;
14
+ /** Supplied roots that hold many repositories, such as the bridge clone folder. */
15
+ readonly cloneFolders?: readonly string[];
16
+ }
17
+ export declare function configureDotNetToolchain(repoRoots: readonly string[], options?: DotNetToolchainOptions): Promise<DotNetToolchainSelection | null>;
@@ -0,0 +1,307 @@
1
+ import { execFile } from "node:child_process";
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { promisify } from "node:util";
6
+ const execFileAsync = promisify(execFile);
7
+ const skippedDirectories = new Set([".git", "bin", "node_modules", "obj"]);
8
+ const maximumDiscoveryDepth = 32;
9
+ const maximumDiscoveryEntries = 20_000;
10
+ const maximumProjectFiles = 256;
11
+ const maximumInvocationContexts = 256;
12
+ export async function configureDotNetToolchain(repoRoots, options = {}) {
13
+ const environment = options.environment ?? process.env;
14
+ const platform = options.platform ?? process.platform;
15
+ const requirements = await discoverSdkRequirements(repoRoots, options.cloneFolders ?? []);
16
+ if (requirements.length === 0)
17
+ return null;
18
+ const requiredMajor = Math.max(...requirements.map((requirement) => requirement.minimumMajor));
19
+ const policyGroups = groupPolicyRequirements(requirements, platform);
20
+ const explicit = environmentValue(environment, "FORGE_DOTNET_PATH");
21
+ if (explicit && !path.isAbsolute(explicit)) {
22
+ throw new Error("dotnet_toolchain_invalid: FORGE_DOTNET_PATH must be an absolute executable path.");
23
+ }
24
+ const candidates = dotNetCandidates(environment, platform, options.homeDirectory ?? os.homedir(), requiredMajor);
25
+ const fileExists = options.fileExists ?? isFile;
26
+ const listSdks = options.listSdks ?? probeSdks;
27
+ const resolveSdk = options.resolveSdk ?? probeSelectedSdk;
28
+ const probed = [];
29
+ for (const candidate of candidates) {
30
+ if (!await fileExists(candidate))
31
+ continue;
32
+ try {
33
+ const output = await listSdks(candidate);
34
+ probed.push(`${candidate} (${sdkVersions(output).join(", ") || "no SDKs"})`);
35
+ // Project, solution, and repository-root commands can resolve different
36
+ // SDKs. Preserve all invocation contexts as well as nested project policies;
37
+ // only identical policy identities may share a probe.
38
+ const resolvedVersions = [];
39
+ for (const requirement of policyGroups) {
40
+ const resolved = (await resolveSdk(candidate, requirement.directory)).trim();
41
+ if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/u.test(resolved)) {
42
+ throw new Error(`No valid SDK selected for ${requirement.directory}`);
43
+ }
44
+ if ((majorVersion(resolved) ?? 0) < requirement.minimumMajor) {
45
+ throw new Error(`SDK ${resolved} cannot satisfy .NET ${requirement.minimumMajor} in ${requirement.directory}`);
46
+ }
47
+ resolvedVersions.push(resolved);
48
+ }
49
+ // Detect a policy added, removed, or changed during discovery/probing.
50
+ // In particular, do not reuse a parent's probe after a nested override appears.
51
+ for (const requirement of requirements) {
52
+ const policy = await nearestSdkPolicy(requirement.directory);
53
+ if (policy?.path !== requirement.policy?.path || policy?.contents !== requirement.policy?.contents) {
54
+ throw new Error(`SDK policy changed during resolution for ${requirement.directory}; retry after repository changes finish`);
55
+ }
56
+ }
57
+ const selectedVersion = resolvedVersions.sort((a, b) => a.localeCompare(b, "en", { numeric: true })).at(-1);
58
+ const rootPath = path.dirname(candidate);
59
+ prependPath(environment, rootPath, platform);
60
+ setEnvironmentValue(environment, "DOTNET_ROOT", rootPath);
61
+ if (platform === "win32")
62
+ setEnvironmentValue(environment, "DOTNET_ROOT_X64", rootPath);
63
+ setEnvironmentValue(environment, "FORGE_DOTNET_PATH", candidate);
64
+ return { executablePath: candidate, rootPath, sdkVersion: selectedVersion, requiredMajor: requiredMajor || majorVersion(selectedVersion) };
65
+ }
66
+ catch (error) {
67
+ probed.push(`${candidate} (${error instanceof Error ? error.message : String(error)})`);
68
+ }
69
+ }
70
+ const detail = probed.length > 0 ? ` Probed: ${probed.join("; ")}.` : "";
71
+ throw new Error(`dotnet_toolchain_unavailable: repositories require ${requiredMajor ? `.NET SDK ${requiredMajor}.x` : "a .NET SDK"}, but no compatible dotnet executable was found.`
72
+ + ` Set FORGE_DOTNET_PATH to the absolute executable path.${detail}`);
73
+ }
74
+ async function discoverSdkRequirements(repoRoots, cloneFolders) {
75
+ const { projects, invocationDirectories } = await findDotNetInputs(repoRoots, cloneFolders);
76
+ const requirements = [];
77
+ for (const project of projects) {
78
+ const source = await fs.readFile(project, "utf8");
79
+ const directory = path.dirname(project);
80
+ const policy = await nearestSdkPolicy(directory);
81
+ const majors = [policy?.major ?? 0];
82
+ for (const match of source.matchAll(/<TargetFrameworks?>\s*([^<]+)\s*<\/TargetFrameworks?>/giu)) {
83
+ for (const framework of match[1]?.split(";") ?? []) {
84
+ const target = /^net(\d+)\./iu.exec(framework.trim());
85
+ if (target?.[1])
86
+ majors.push(Number.parseInt(target[1], 10));
87
+ }
88
+ }
89
+ requirements.push({ directory, policy, minimumMajor: Math.max(...majors) });
90
+ }
91
+ for (const directory of invocationDirectories) {
92
+ const policy = await nearestSdkPolicy(directory);
93
+ requirements.push({ directory, policy, minimumMajor: policy?.major ?? 0 });
94
+ }
95
+ return requirements;
96
+ }
97
+ async function nearestSdkPolicy(projectDirectory) {
98
+ for (let directory = projectDirectory;; directory = path.dirname(directory)) {
99
+ const policyPath = path.join(directory, "global.json");
100
+ try {
101
+ const contents = await fs.readFile(policyPath, "utf8");
102
+ let major = null;
103
+ try {
104
+ const parsed = JSON.parse(contents);
105
+ major = typeof parsed.sdk?.version === "string" ? majorVersion(parsed.sdk.version) : null;
106
+ }
107
+ catch {
108
+ // The actual SDK resolver diagnoses malformed policies; never bypass one.
109
+ }
110
+ return { path: policyPath, contents, major };
111
+ }
112
+ catch (error) {
113
+ if (error.code !== "ENOENT")
114
+ throw error;
115
+ }
116
+ if (path.dirname(directory) === directory)
117
+ return null;
118
+ }
119
+ }
120
+ function groupPolicyRequirements(requirements, platform) {
121
+ const groups = new Map();
122
+ for (const requirement of requirements) {
123
+ const key = requirement.policy ? canonicalPath(requirement.policy.path, platform) : "no-global-json";
124
+ const previous = groups.get(key);
125
+ if (previous && previous.policy?.contents !== requirement.policy?.contents) {
126
+ throw new Error(`dotnet_toolchain_changed: SDK policy changed during discovery: ${requirement.policy?.path}`);
127
+ }
128
+ groups.set(key, previous
129
+ ? { ...previous, minimumMajor: Math.max(previous.minimumMajor, requirement.minimumMajor) }
130
+ : requirement);
131
+ }
132
+ return [...groups.values()];
133
+ }
134
+ async function findDotNetInputs(repoRoots, cloneFolders) {
135
+ const projects = [];
136
+ const invocationDirectories = new Map();
137
+ const visitedDirectories = new Set();
138
+ const roots = [...new Set(repoRoots.map((candidate) => path.resolve(candidate)))];
139
+ const explicitRoots = new Set(roots.map((root) => canonicalPath(root, process.platform)));
140
+ const cloneFolderRoots = new Set(cloneFolders.map((folder) => canonicalPath(path.resolve(folder), process.platform)));
141
+ // The entry bound applies per repository. A clone folder is not one: each of
142
+ // its child directories is bounded as its own repository root.
143
+ let entryCount = 0;
144
+ async function visitRoot(root) {
145
+ entryCount = 0;
146
+ await visit(root, 0, root, new Set());
147
+ }
148
+ function addInvocationDirectory(directory, root) {
149
+ const identity = canonicalPath(directory, process.platform);
150
+ if (invocationDirectories.has(identity))
151
+ return;
152
+ if (invocationDirectories.size >= maximumInvocationContexts)
153
+ throw discoveryLimit("invocation contexts", maximumInvocationContexts, root);
154
+ invocationDirectories.set(identity, directory);
155
+ }
156
+ async function visit(directory, depth, root, ancestors) {
157
+ const directoryIdentity = canonicalPath(directory, process.platform);
158
+ if (visitedDirectories.has(directoryIdentity))
159
+ return;
160
+ if (depth > maximumDiscoveryDepth)
161
+ throw discoveryLimit("depth", maximumDiscoveryDepth, root);
162
+ const physicalDirectory = await fs.realpath(directory);
163
+ if (ancestors.has(physicalDirectory))
164
+ throw new Error(`dotnet_toolchain_discovery: directory cycle at ${directory}`);
165
+ const nextAncestors = new Set([...ancestors, physicalDirectory]);
166
+ const cloneFolder = depth === 0 && cloneFolderRoots.has(directoryIdentity);
167
+ visitedDirectories.add(directoryIdentity);
168
+ const entries = [];
169
+ for await (const entry of await fs.opendir(directory)) {
170
+ if (++entryCount > maximumDiscoveryEntries)
171
+ throw discoveryLimit("entries", maximumDiscoveryEntries, root);
172
+ entries.push(entry);
173
+ }
174
+ // Stable traversal also makes the representative policy probe deterministic.
175
+ entries.sort((a, b) => a.name.localeCompare(b.name, "en"));
176
+ for (const entry of entries) {
177
+ // Hidden folders (.git, .vs, .forge-worktrees, ...) are tool state, never
178
+ // repository sources; an explicitly supplied hidden root is still visited.
179
+ if (skippedDirectories.has(entry.name) || entry.name.startsWith(".") && !entry.isFile())
180
+ continue;
181
+ const entryPath = path.join(directory, entry.name);
182
+ const linked = entry.isSymbolicLink() ? await fs.stat(entryPath) : null;
183
+ if (linked?.isDirectory()) {
184
+ if (cloneFolder || explicitRoots.has(canonicalPath(entryPath, process.platform))) {
185
+ await visitRoot(entryPath);
186
+ continue;
187
+ }
188
+ throw new Error(`dotnet_toolchain_discovery: symbolic/reparse directory ${entryPath} must be supplied as an explicit repository root, not traversed from ${root}`);
189
+ }
190
+ if (entry.isDirectory()) {
191
+ if (cloneFolder)
192
+ await visitRoot(entryPath);
193
+ else
194
+ await visit(entryPath, depth + 1, root, nextAncestors);
195
+ }
196
+ else if (entry.isFile() || linked?.isFile()) {
197
+ const extension = path.extname(entry.name).toLowerCase();
198
+ if (extension === ".csproj") {
199
+ if (projects.length >= maximumProjectFiles)
200
+ throw discoveryLimit("projects", maximumProjectFiles, root);
201
+ projects.push(entryPath);
202
+ }
203
+ // A solution's SDK resolver starts at its directory. A global.json also
204
+ // defines a CLI invocation context even when every child project overrides
205
+ // it (or the repository contains only SDK-based tools).
206
+ if ([".sln", ".slnx", ".slnf"].includes(extension)
207
+ || canonicalPath(entry.name, process.platform) === "global.json") {
208
+ addInvocationDirectory(directory, root);
209
+ }
210
+ }
211
+ }
212
+ }
213
+ for (const root of roots) {
214
+ await visitRoot(root);
215
+ }
216
+ // Supplied roots are also supported command working directories. A parent's
217
+ // policy cannot be dropped merely because every project/solution below it has
218
+ // its own override. Ignore unrelated roots without any discovered .NET inputs.
219
+ const inputDirectories = [...projects.map((project) => path.dirname(project)), ...invocationDirectories.values()];
220
+ for (const root of roots) {
221
+ if (inputDirectories.some((directory) => isWithinDirectory(root, directory))) {
222
+ addInvocationDirectory(root, root);
223
+ }
224
+ }
225
+ return { projects, invocationDirectories: [...invocationDirectories.values()] };
226
+ }
227
+ function isWithinDirectory(root, directory) {
228
+ const relative = path.relative(root, directory);
229
+ return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
230
+ }
231
+ function discoveryLimit(bound, maximum, root) {
232
+ return new Error(`dotnet_toolchain_discovery: ${bound} limit ${maximum} exceeded under ${root}; configure narrower explicit repository roots`);
233
+ }
234
+ function dotNetCandidates(environment, platform, homeDirectory, requiredMajor) {
235
+ const executable = platform === "win32" ? "dotnet.exe" : "dotnet";
236
+ const explicit = environmentValue(environment, "FORGE_DOTNET_PATH");
237
+ if (explicit)
238
+ return [explicit];
239
+ const dotNetRoot = environmentValue(environment, "DOTNET_ROOT");
240
+ const pathValue = environmentValue(environment, "PATH") ?? "";
241
+ const programFiles = environmentValue(environment, "ProgramFiles");
242
+ const candidates = [
243
+ dotNetRoot ? path.join(dotNetRoot, executable) : null,
244
+ path.join(homeDirectory, ".dotnet", executable),
245
+ requiredMajor > 0 ? path.join(homeDirectory, `.dotnet${requiredMajor}`, executable) : null,
246
+ ...(platform === "win32" && programFiles ? [path.join(programFiles, "dotnet", executable)] : []),
247
+ ...pathValue.split(path.delimiter).filter(Boolean).map((directory) => path.join(directory, executable)),
248
+ ].filter((candidate) => Boolean(candidate));
249
+ return [...new Map(candidates.map((candidate) => [canonicalPath(candidate, platform), path.resolve(candidate)])).values()];
250
+ }
251
+ async function probeSdks(candidate) {
252
+ const { stdout } = await execFileAsync(candidate, ["--list-sdks"], {
253
+ encoding: "utf8",
254
+ timeout: 10_000,
255
+ windowsHide: true,
256
+ });
257
+ return stdout;
258
+ }
259
+ async function probeSelectedSdk(candidate, repositoryRoot) {
260
+ const { stdout } = await execFileAsync(candidate, ["--version"], {
261
+ cwd: repositoryRoot,
262
+ encoding: "utf8",
263
+ timeout: 10_000,
264
+ windowsHide: true,
265
+ });
266
+ return stdout;
267
+ }
268
+ function sdkVersions(output) {
269
+ return output.split(/\r?\n/gu)
270
+ .map((line) => /^(\d+\.\d+\.\d+(?:[-+][^\s]+)?)/u.exec(line.trim())?.[1] ?? null)
271
+ .filter((version) => version !== null);
272
+ }
273
+ function majorVersion(version) {
274
+ const match = /^(\d+)\./u.exec(version.trim());
275
+ return match?.[1] ? Number.parseInt(match[1], 10) : null;
276
+ }
277
+ async function isFile(candidate) {
278
+ try {
279
+ return (await fs.stat(candidate)).isFile();
280
+ }
281
+ catch {
282
+ return false;
283
+ }
284
+ }
285
+ function prependPath(environment, directory, platform) {
286
+ const current = environmentValue(environment, "PATH") ?? "";
287
+ const normalizedDirectory = canonicalPath(directory, platform);
288
+ const remaining = current.split(path.delimiter)
289
+ .filter(Boolean)
290
+ .filter((entry) => canonicalPath(entry, platform) !== normalizedDirectory);
291
+ setEnvironmentValue(environment, "PATH", [directory, ...remaining].join(path.delimiter));
292
+ }
293
+ function environmentValue(environment, name) {
294
+ const key = Object.keys(environment).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
295
+ return key ? environment[key] : undefined;
296
+ }
297
+ function setEnvironmentValue(environment, name, value) {
298
+ const keys = Object.keys(environment).filter((candidate) => candidate.toLowerCase() === name.toLowerCase());
299
+ const target = keys[0] ?? name;
300
+ for (const duplicate of keys.slice(1))
301
+ delete environment[duplicate];
302
+ environment[target] = value;
303
+ }
304
+ function canonicalPath(candidate, platform) {
305
+ const normalized = path.normalize(candidate);
306
+ return platform === "win32" ? normalized.toLowerCase() : normalized;
307
+ }
@@ -13,7 +13,9 @@ export interface HostProcessSupervisedLaunch {
13
13
  readonly supervisorId: string;
14
14
  readonly strategy: HostProcessSupervisorStrategy;
15
15
  readonly metadataPath: string;
16
+ readonly memoryTelemetryPath?: string;
16
17
  readonly payloadPath?: string;
18
+ readonly payloadSha256?: string;
17
19
  readonly readyPath?: string;
18
20
  readonly startGrantPath?: string;
19
21
  readonly commandDigest: string;
@@ -22,7 +22,8 @@ export function resolveHostProcessSupervisedLaunch(request, resolvedCommand, res
22
22
  const payloadPath = `${metadataPath}.launch.json`;
23
23
  const readyPath = `${metadataPath}.provider-ready.json`;
24
24
  const startGrantPath = `${metadataPath}.provider-start-granted`;
25
- fs.writeFileSync(payloadPath, `${JSON.stringify({
25
+ const memoryTelemetryPath = `${metadataPath}.memory.json`;
26
+ const serializedPayload = `${JSON.stringify({
26
27
  command: resolvedCommand,
27
28
  args: resolvedArgs,
28
29
  cwd: request.cwd,
@@ -30,7 +31,10 @@ export function resolveHostProcessSupervisedLaunch(request, resolvedCommand, res
30
31
  supervisorId,
31
32
  readyPath,
32
33
  startGrantPath,
33
- })}\n`, {
34
+ memoryTelemetryPath,
35
+ })}\n`;
36
+ const payloadSha256 = createHash("sha256").update(serializedPayload).digest("hex");
37
+ fs.writeFileSync(payloadPath, serializedPayload, {
34
38
  encoding: "utf8",
35
39
  flag: "wx",
36
40
  mode: 0o600,
@@ -49,12 +53,16 @@ export function resolveHostProcessSupervisedLaunch(request, resolvedCommand, res
49
53
  "Outer",
50
54
  "-PayloadPath",
51
55
  payloadPath,
56
+ "-PayloadSha256",
57
+ payloadSha256,
52
58
  ],
53
59
  detached: false,
54
60
  supervisorId,
55
61
  strategy: "windows_job_object",
56
62
  metadataPath,
63
+ memoryTelemetryPath,
57
64
  payloadPath,
65
+ payloadSha256,
58
66
  readyPath,
59
67
  startGrantPath,
60
68
  commandDigest,
@@ -147,12 +155,7 @@ export function captureHostProcessIdentity(pid, platform = process.platform) {
147
155
  }
148
156
  export async function terminateHostProcessTree(child, launch, graceMs = 2_000) {
149
157
  if (!launch) {
150
- try {
151
- child.kill("SIGTERM");
152
- }
153
- catch {
154
- // The process already settled.
155
- }
158
+ await terminateUnsupervisedProcessTree(child, graceMs);
156
159
  return;
157
160
  }
158
161
  if (launch.strategy === "windows_job_object") {
@@ -189,9 +192,48 @@ export async function terminateHostProcessTree(child, launch, graceMs = 2_000) {
189
192
  const processGroupId = child.pid;
190
193
  if (!processGroupId || !signalUnixProcessGroup(processGroupId, "SIGTERM"))
191
194
  return;
192
- await new Promise(resolve => setTimeout(resolve, graceMs));
195
+ await waitForUnixProcessGroupExit(processGroupId, graceMs);
193
196
  if (unixProcessGroupExists(processGroupId)) {
194
197
  signalUnixProcessGroup(processGroupId, "SIGKILL");
198
+ await waitForUnixProcessGroupExit(processGroupId, graceMs);
199
+ }
200
+ if (unixProcessGroupExists(processGroupId)) {
201
+ throw new Error("host_process_supervisor_termination_failed: supervised Unix process group did not stop.");
202
+ }
203
+ }
204
+ async function terminateUnsupervisedProcessTree(child, graceMs) {
205
+ const rootPid = child.pid ?? 0;
206
+ if (!Number.isSafeInteger(rootPid) || rootPid <= 0) {
207
+ try {
208
+ child.kill("SIGTERM");
209
+ }
210
+ catch {
211
+ // The process already settled before a PID was assigned.
212
+ }
213
+ return;
214
+ }
215
+ if (process.platform === "win32") {
216
+ // Without a Job Object there is no trustworthy ancestry record after the
217
+ // root has already exited. Never target a potentially reused Windows PID.
218
+ if (child.exitCode !== null || child.signalCode !== null)
219
+ return;
220
+ const rootIdentity = captureHostProcessIdentity(rootPid, "win32");
221
+ forceWindowsProcessTree(rootPid);
222
+ await waitForExactProcessExit(rootPid, rootIdentity, graceMs, "win32");
223
+ if (exactProcessExists(rootPid, rootIdentity, "win32")) {
224
+ throw new Error("host_process_supervisor_termination_failed: unsupervised Windows provider tree did not stop.");
225
+ }
226
+ return;
227
+ }
228
+ if (!signalUnixProcessGroup(rootPid, "SIGTERM"))
229
+ return;
230
+ await waitForUnixProcessGroupExit(rootPid, graceMs);
231
+ if (unixProcessGroupExists(rootPid)) {
232
+ signalUnixProcessGroup(rootPid, "SIGKILL");
233
+ await waitForUnixProcessGroupExit(rootPid, graceMs);
234
+ }
235
+ if (unixProcessGroupExists(rootPid)) {
236
+ throw new Error("host_process_supervisor_termination_failed: unsupervised Unix provider tree did not stop.");
195
237
  }
196
238
  }
197
239
  function requiredEnvironment(env, name) {
@@ -234,6 +276,12 @@ function unixProcessGroupExists(processGroupId) {
234
276
  throw error;
235
277
  }
236
278
  }
279
+ async function waitForUnixProcessGroupExit(processGroupId, timeoutMs) {
280
+ const deadline = Date.now() + timeoutMs;
281
+ while (Date.now() < deadline && unixProcessGroupExists(processGroupId)) {
282
+ await new Promise(resolve => setTimeout(resolve, 25));
283
+ }
284
+ }
237
285
  function readSupervisorMetadata(metadataPath) {
238
286
  try {
239
287
  return JSON.parse(fs.readFileSync(metadataPath, "utf8"));
@@ -3,6 +3,8 @@ export interface ProcessRunRequest {
3
3
  readonly args: readonly string[];
4
4
  readonly cwd: string;
5
5
  readonly env?: NodeJS.ProcessEnv;
6
+ readonly rawOutput?: boolean;
7
+ readonly windowsVerbatimArguments?: boolean;
6
8
  }
7
9
  export interface ProcessRunResult {
8
10
  readonly exitCode: number;
@@ -6,22 +6,27 @@ export class NodeProcessRunner {
6
6
  const child = spawn(request.command, request.args, {
7
7
  cwd: request.cwd,
8
8
  env: request.env,
9
- stdio: ["ignore", "pipe", "pipe"]
9
+ stdio: ["ignore", "pipe", "pipe"],
10
+ windowsVerbatimArguments: request.windowsVerbatimArguments,
10
11
  });
11
12
  let stdout = "";
12
13
  let stderr = "";
13
14
  child.on("error", reject);
14
- const stdoutReader = readline.createInterface({ input: child.stdout });
15
- const stderrReader = readline.createInterface({ input: child.stderr });
16
- stdoutReader.on("line", (line) => {
15
+ const stdoutReader = request.rawOutput ? undefined : readline.createInterface({ input: child.stdout });
16
+ const stderrReader = request.rawOutput ? undefined : readline.createInterface({ input: child.stderr });
17
+ if (request.rawOutput) {
18
+ child.stdout.setEncoding("utf8").on("data", chunk => { stdout += chunk; });
19
+ child.stderr.setEncoding("utf8").on("data", chunk => { stderr += chunk; });
20
+ }
21
+ stdoutReader?.on("line", (line) => {
17
22
  stdout += `${line}\n`;
18
23
  });
19
- stderrReader.on("line", (line) => {
24
+ stderrReader?.on("line", (line) => {
20
25
  stderr += `${line}\n`;
21
26
  });
22
27
  child.on("close", (code) => {
23
- stdoutReader.close();
24
- stderrReader.close();
28
+ stdoutReader?.close();
29
+ stderrReader?.close();
25
30
  resolve({
26
31
  exitCode: code ?? 1,
27
32
  stdout,
@@ -1 +1,4 @@
1
+ import { type BridgeChatCapability } from "../bridge/bridge-chat.types.js";
2
+ import type { BridgeProviderStatus } from "../bridge/bridge.types.js";
1
3
  export declare function supportsDraftImageAttachmentsFor(cliProviders: readonly string[]): boolean;
4
+ export declare function bridgeChatCapabilityFor(cliProviders: readonly string[], providerStatuses: Readonly<Record<string, BridgeProviderStatus>>, baseCapability?: BridgeChatCapability | null): BridgeChatCapability;