@loucompanion/forge-bridge 0.1.3-dev.9bc5014a2270 → 0.1.3-dev.a58d7cd1092f

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 (168) hide show
  1. package/README.md +127 -0
  2. package/dist/bridge/bridge.base.d.ts +8 -1
  3. package/dist/bridge/bridge.service.d.ts +93 -3
  4. package/dist/bridge/bridge.service.js +1358 -47
  5. package/dist/bridge/bridge.types.d.ts +202 -1
  6. package/dist/bridge/runner-host-capabilities.d.ts +15 -0
  7. package/dist/bridge/runner-host-capabilities.js +93 -0
  8. package/dist/cleanup/cleanup.service.d.ts +14 -2
  9. package/dist/cleanup/cleanup.service.js +496 -3
  10. package/dist/cleanup/cleanup.types.d.ts +10 -0
  11. package/dist/cleanup/index.d.ts +1 -1
  12. package/dist/cleanup/index.js +1 -1
  13. package/dist/cleanup/internals.d.ts +2 -2
  14. package/dist/cleanup/internals.js +1 -1
  15. package/dist/cli/cli.help.js +47 -1
  16. package/dist/cli/cli.service.d.ts +45 -2
  17. package/dist/cli/cli.service.js +378 -26
  18. package/dist/cli/local-config.d.ts +7 -0
  19. package/dist/cli/local-config.js +51 -3
  20. package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
  21. package/dist/runtime/aspnet-runtime-prebuild.js +340 -0
  22. package/dist/runtime/attempt-node-runtime-launcher.d.ts +17 -0
  23. package/dist/runtime/attempt-node-runtime-launcher.js +105 -0
  24. package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
  25. package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
  26. package/dist/runtime/attempt-process-supervisor.d.ts +97 -0
  27. package/dist/runtime/attempt-process-supervisor.js +386 -0
  28. package/dist/runtime/docker-engine-probe.d.ts +83 -0
  29. package/dist/runtime/docker-engine-probe.js +300 -0
  30. package/dist/runtime/generated-output-reset.d.ts +10 -0
  31. package/dist/runtime/generated-output-reset.js +103 -0
  32. package/dist/runtime/host-listener-inspector.d.ts +46 -0
  33. package/dist/runtime/host-listener-inspector.js +349 -0
  34. package/dist/runtime/host-port-broker.d.ts +76 -0
  35. package/dist/runtime/host-port-broker.js +333 -0
  36. package/dist/runtime/host-runtime-contract.d.ts +47 -0
  37. package/dist/runtime/host-runtime-contract.js +700 -0
  38. package/dist/runtime/host-runtime-lease.d.ts +197 -0
  39. package/dist/runtime/host-runtime-lease.js +2203 -0
  40. package/dist/runtime/host-runtime-promotion.d.ts +23 -0
  41. package/dist/runtime/host-runtime-promotion.js +159 -0
  42. package/dist/runtime/memory-capacity.d.ts +68 -0
  43. package/dist/runtime/memory-capacity.js +121 -0
  44. package/dist/runtime/node-host-runtime-adapter.d.ts +54 -0
  45. package/dist/runtime/node-host-runtime-adapter.js +284 -0
  46. package/dist/runtime/playwright-host-runtime-adapter.d.ts +21 -0
  47. package/dist/runtime/playwright-host-runtime-adapter.js +231 -0
  48. package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
  49. package/dist/runtime/runtime-isolation-lease.js +158 -0
  50. package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
  51. package/dist/runtime/workspace-resource-estimator.js +174 -0
  52. package/dist/sandbox/command-runner.d.ts +4 -0
  53. package/dist/sandbox/command-runner.js +47 -0
  54. package/dist/sandbox/contract.d.ts +5 -0
  55. package/dist/sandbox/contract.js +276 -0
  56. package/dist/sandbox/event-budget.d.ts +11 -0
  57. package/dist/sandbox/event-budget.js +27 -0
  58. package/dist/sandbox/image-policy.d.ts +35 -0
  59. package/dist/sandbox/image-policy.js +247 -0
  60. package/dist/sandbox/index.d.ts +5 -0
  61. package/dist/sandbox/index.js +5 -0
  62. package/dist/sandbox/lease-watchdog.d.ts +18 -0
  63. package/dist/sandbox/lease-watchdog.js +67 -0
  64. package/dist/sandbox/preflight.d.ts +84 -0
  65. package/dist/sandbox/preflight.js +521 -0
  66. package/dist/sandbox/process.d.ts +20 -0
  67. package/dist/sandbox/process.js +209 -0
  68. package/dist/sandbox/provider-auth/codex.d.ts +26 -0
  69. package/dist/sandbox/provider-auth/codex.js +212 -0
  70. package/dist/sandbox/repository-lock.d.ts +5 -0
  71. package/dist/sandbox/repository-lock.js +121 -0
  72. package/dist/sandbox/runtime.d.ts +297 -0
  73. package/dist/sandbox/runtime.js +2338 -0
  74. package/dist/sandbox/sandbox.types.d.ts +93 -0
  75. package/dist/sandbox/sandbox.types.js +1 -0
  76. package/dist/sandbox/smoke.d.ts +76 -0
  77. package/dist/sandbox/smoke.js +380 -0
  78. package/dist/sandbox/worker-supervisor.d.ts +13 -0
  79. package/dist/sandbox/worker-supervisor.js +36 -0
  80. package/dist/sandbox/worker.d.ts +1 -0
  81. package/dist/sandbox/worker.js +225 -0
  82. package/dist/sandbox/workspace.service.d.ts +13 -0
  83. package/dist/sandbox/workspace.service.js +72 -0
  84. package/dist/session/managed-attempt-tools.d.ts +21 -0
  85. package/dist/session/managed-attempt-tools.js +912 -0
  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 +335 -0
  90. package/dist/session/phase-completion.artifacts.d.ts +17 -0
  91. package/dist/session/phase-completion.artifacts.js +256 -26
  92. package/dist/session/phase-completion.d.ts +12 -2
  93. package/dist/session/phase-completion.helpers.d.ts +5 -0
  94. package/dist/session/phase-completion.helpers.js +51 -10
  95. package/dist/session/phase-completion.js +14 -3
  96. package/dist/session/phase-completion.prompts.d.ts +3 -3
  97. package/dist/session/phase-completion.prompts.js +24 -8
  98. package/dist/session/phase-completion.runtime.d.ts +13 -1
  99. package/dist/session/phase-completion.runtime.js +189 -8
  100. package/dist/session/phase-completion.schemas.js +117 -4
  101. package/dist/session/phase-completion.validation.d.ts +4 -1
  102. package/dist/session/phase-completion.validation.js +153 -22
  103. package/dist/session/pre-publication-validation.d.ts +46 -0
  104. package/dist/session/pre-publication-validation.js +208 -0
  105. package/dist/session/provider-cli/ci-repair-readiness.d.ts +16 -0
  106. package/dist/session/provider-cli/ci-repair-readiness.js +110 -0
  107. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +3 -2
  108. package/dist/session/provider-cli/provider-cli.artifacts.js +65 -7
  109. package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
  110. package/dist/session/provider-cli/provider-cli.deterministic.d.ts +2 -0
  111. package/dist/session/provider-cli/provider-cli.deterministic.js +66 -9
  112. package/dist/session/provider-cli/provider-cli.process.d.ts +26 -0
  113. package/dist/session/provider-cli/provider-cli.process.js +474 -23
  114. package/dist/session/provider-cli/provider-cli.prompts.d.ts +16 -2
  115. package/dist/session/provider-cli/provider-cli.prompts.js +224 -24
  116. package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
  117. package/dist/session/provider-cli/provider-cli.runtime.d.ts +1 -1
  118. package/dist/session/provider-cli/provider-cli.runtime.js +6 -3
  119. package/dist/session/provider-cli/provider-cli.service.d.ts +26 -3
  120. package/dist/session/provider-cli/provider-cli.service.js +646 -90
  121. package/dist/session/provider-cli/provider-cli.types.d.ts +5 -0
  122. package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
  123. package/dist/session/provider-cli/provider-output-state.js +91 -16
  124. package/dist/session/provider-cli/provider-usage.d.ts +7 -0
  125. package/dist/session/provider-cli/provider-usage.js +28 -5
  126. package/dist/session/session-redaction.d.ts +3 -0
  127. package/dist/session/session-redaction.js +38 -0
  128. package/dist/session/session.base.d.ts +6 -1
  129. package/dist/session/session.service.d.ts +8 -2
  130. package/dist/session/session.service.js +530 -42
  131. package/dist/session/session.types.d.ts +124 -1
  132. package/dist/session/session.types.js +231 -4
  133. package/dist/shared/format/format-bytes.d.ts +1 -0
  134. package/dist/shared/format/format-bytes.js +9 -0
  135. package/dist/shared/path/path.d.ts +1 -0
  136. package/dist/shared/path/path.js +21 -1
  137. package/dist/shared/process/dotnet-toolchain.d.ts +14 -0
  138. package/dist/shared/process/dotnet-toolchain.js +179 -0
  139. package/dist/shared/process/host-process-recovery.d.ts +12 -0
  140. package/dist/shared/process/host-process-recovery.js +78 -0
  141. package/dist/shared/process/host-process-supervisor.d.ts +59 -0
  142. package/dist/shared/process/host-process-supervisor.js +330 -0
  143. package/dist/shared/process/process.d.ts +1 -0
  144. package/dist/shared/process/process.js +1 -0
  145. package/dist/shared/process/windows-launch.d.ts +1 -1
  146. package/dist/shared/process/windows-launch.js +6 -3
  147. package/dist/shared/provider-capabilities.d.ts +1 -0
  148. package/dist/shared/provider-capabilities.js +3 -0
  149. package/dist/shared/redaction/redaction.js +16 -4
  150. package/dist/version.d.ts +1 -1
  151. package/dist/version.js +1 -1
  152. package/dist/worktree/artifact-injector.d.ts +3 -2
  153. package/dist/worktree/artifact-injector.js +43 -4
  154. package/dist/worktree/dependency-warmer.d.ts +36 -2
  155. package/dist/worktree/dependency-warmer.js +347 -11
  156. package/dist/worktree/index.d.ts +2 -1
  157. package/dist/worktree/index.js +1 -0
  158. package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
  159. package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
  160. package/dist/worktree/worktree-paths.d.ts +4 -0
  161. package/dist/worktree/worktree-paths.js +30 -0
  162. package/dist/worktree/worktree.base.d.ts +3 -0
  163. package/dist/worktree/worktree.service.d.ts +13 -2
  164. package/dist/worktree/worktree.service.js +238 -47
  165. package/dist/worktree/worktree.types.d.ts +5 -1
  166. package/package.json +8 -4
  167. package/runtime/windows-job-supervisor.ps1 +444 -0
  168. package/runtime/windows-listener-owner.ps1 +33 -0
@@ -0,0 +1,912 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { resetGeneratedOutputs } from "../runtime/generated-output-reset.js";
5
+ import { ManagedBuildCoordinator } from "./managed-build-coordinator.js";
6
+ import { managedBuildConfiguration, publishManagedBuildReceipt, verifyManagedBuildReceipt, } from "./managed-build-receipt.js";
7
+ import { resolveWindowsCommandFromPath, StreamingProviderProcessRunner } from "./provider-cli/provider-cli.process.js";
8
+ const managedCommandNames = new Set(["forge.run_managed_command", "run_managed_command"]);
9
+ const startManagedCommandNames = new Set(["forge.start_managed_command", "start_managed_command"]);
10
+ const getManagedCommandNames = new Set(["forge.get_managed_command", "get_managed_command"]);
11
+ const waitManagedCommandNames = new Set(["forge.wait_managed_command", "wait_managed_command"]);
12
+ const startManagedVerificationNames = new Set(["forge.start_managed_verification", "start_managed_verification"]);
13
+ const resetGeneratedOutputNames = new Set(["forge.reset_generated_outputs", "reset_generated_outputs"]);
14
+ const maximumCapturedCharacters = 200_000;
15
+ const maximumReturnedOutputCharacters = 4_000;
16
+ const maximumReportedFailedTests = 50;
17
+ // Keep a wait slice comfortably below the MCP transport deadline. The managed
18
+ // process remains supervised and its durable receipt continues to update after
19
+ // this call returns, so a shorter slice avoids converting healthy long-running
20
+ // commands into ambiguous MCP failures.
21
+ const maximumSynchronousWaitMs = 45_000;
22
+ const managedCommandRetryAfterMs = 60_000;
23
+ const managedCommandLaunchTimeoutMs = 60_000;
24
+ const managedCommandProgressPersistIntervalMs = 5_000;
25
+ const managedCommandReceiptPollIntervalMs = 1_000;
26
+ const managedCommandReceiptReplaceAttempts = 40;
27
+ const managedCommandReceiptReplaceRetryMs = 25;
28
+ const terminalManagedCommandStatuses = new Set([
29
+ "pass",
30
+ "fail",
31
+ "timed_out",
32
+ "timeout_termination_failed",
33
+ "runner_error",
34
+ ]);
35
+ const managedVerificationBuildCoverage = "forge_dotnet_test_graph_v1";
36
+ const managedCommandTasks = new Map();
37
+ export const managedAttemptToolNames = [
38
+ "forge.run_managed_command",
39
+ "run_managed_command",
40
+ "forge.start_managed_command",
41
+ "start_managed_command",
42
+ "forge.get_managed_command",
43
+ "get_managed_command",
44
+ "forge.wait_managed_command",
45
+ "wait_managed_command",
46
+ "forge.start_managed_verification",
47
+ "start_managed_verification",
48
+ "forge.reset_generated_outputs",
49
+ "reset_generated_outputs",
50
+ ];
51
+ export function isManagedAttemptTool(name) {
52
+ return managedCommandNames.has(name)
53
+ || startManagedCommandNames.has(name)
54
+ || getManagedCommandNames.has(name)
55
+ || waitManagedCommandNames.has(name)
56
+ || startManagedVerificationNames.has(name)
57
+ || resetGeneratedOutputNames.has(name);
58
+ }
59
+ export function managedAttemptToolDefinition(name) {
60
+ if (startManagedVerificationNames.has(name)) {
61
+ return {
62
+ name,
63
+ description: "Start a canonical Forge verification profile with runner-owned cwd, arguments, attempt output, timeout, and process supervision. Prefer this over reconstructing broad .NET commands. Continue independent work, then use forge.wait_managed_command for a transport-safe terminal receipt.",
64
+ inputSchema: {
65
+ type: "object",
66
+ additionalProperties: false,
67
+ required: ["profile"],
68
+ properties: {
69
+ profile: {
70
+ type: "string",
71
+ enum: ["dotnet_unit", "dotnet_contract", "dotnet_api_component", "dotnet_orchestration", "dotnet_infrastructure", "dotnet_solution"],
72
+ },
73
+ shard_count: { type: "integer", minimum: 1, maximum: 32, default: 1 },
74
+ shard_index: { type: "integer", minimum: 0, maximum: 31, default: 0 },
75
+ run_all_shards: { type: "boolean", default: false },
76
+ max_parallelism: { type: "integer", minimum: 1, maximum: 4, default: 2 },
77
+ no_build: { type: "boolean", default: false },
78
+ timeout_ms: { type: "integer", minimum: 1000, maximum: 7200000 },
79
+ },
80
+ },
81
+ };
82
+ }
83
+ if (managedCommandNames.has(name) || startManagedCommandNames.has(name)) {
84
+ return {
85
+ name,
86
+ description: startManagedCommandNames.has(name)
87
+ ? "Start a long command in an attempt-owned process tree and return a durable command ID immediately. Continue independent work, then use forge.wait_managed_command for a transport-safe terminal receipt."
88
+ : "Run a short command in an attempt-owned process tree. Commands with a timeout above four minutes automatically start asynchronously; continue independent work, then use forge.wait_managed_command for the durable result.",
89
+ inputSchema: {
90
+ type: "object",
91
+ additionalProperties: false,
92
+ required: ["command", "args"],
93
+ properties: {
94
+ command: { type: "string", minLength: 1 },
95
+ args: { type: "array", items: { type: "string" } },
96
+ cwd: { type: "string", description: "Worktree-relative directory; defaults to the worktree root." },
97
+ timeout_ms: { type: "integer", minimum: 1000, maximum: 7200000, default: 2700000 },
98
+ },
99
+ },
100
+ };
101
+ }
102
+ if (getManagedCommandNames.has(name)) {
103
+ return {
104
+ name,
105
+ description: "Read the durable status and final receipt for a command started by Forge's managed command runner.",
106
+ inputSchema: {
107
+ type: "object",
108
+ additionalProperties: false,
109
+ required: ["command_id"],
110
+ properties: {
111
+ command_id: { type: "string", minLength: 1 },
112
+ },
113
+ },
114
+ };
115
+ }
116
+ if (waitManagedCommandNames.has(name)) {
117
+ return {
118
+ name,
119
+ description: "Wait up to 45 seconds for a managed command terminal receipt. If it remains active, the call succeeds with command_continues=true and retry_after_ms; continue independent work or wait again later. Live progress remains in the durable receipt/UI; do not poll get_managed_command.",
120
+ inputSchema: {
121
+ type: "object",
122
+ additionalProperties: false,
123
+ required: ["command_id"],
124
+ properties: {
125
+ command_id: { type: "string", minLength: 1 },
126
+ wait_timeout_ms: { type: "integer", minimum: 1000, maximum: 45000, default: 45000 },
127
+ },
128
+ },
129
+ };
130
+ }
131
+ return {
132
+ name,
133
+ description: "Quarantine known untracked generated test-output directories into this attempt's private output directory.",
134
+ inputSchema: { type: "object", additionalProperties: false, properties: {} },
135
+ };
136
+ }
137
+ export async function callManagedAttemptTool(name, input, worktreePath) {
138
+ if (resetGeneratedOutputNames.has(name)) {
139
+ const outputDirectory = requiredAttemptEnvironment("FORGE_TEST_OUTPUT_DIR");
140
+ const result = await resetGeneratedOutputs(worktreePath, outputDirectory);
141
+ return {
142
+ content: [{
143
+ type: "text",
144
+ text: result.quarantinedPaths.length === 0
145
+ ? "Forge found no stale generated output in the source worktree."
146
+ : `Forge quarantined ${result.quarantinedPaths.length} generated-output director${result.quarantinedPaths.length === 1 ? "y" : "ies"}: ${result.quarantinedPaths.join(", ")}.`,
147
+ }],
148
+ structuredContent: {
149
+ status: "pass",
150
+ quarantined_paths: result.quarantinedPaths,
151
+ quarantine_directory: result.quarantineDirectory,
152
+ },
153
+ };
154
+ }
155
+ if (getManagedCommandNames.has(name)) {
156
+ return await readManagedCommand(input);
157
+ }
158
+ if (waitManagedCommandNames.has(name)) {
159
+ return await waitManagedCommand(input);
160
+ }
161
+ if (startManagedVerificationNames.has(name)) {
162
+ return await startManagedVerification(input, worktreePath);
163
+ }
164
+ if (!managedCommandNames.has(name) && !startManagedCommandNames.has(name)) {
165
+ throw new Error(`Unknown managed attempt tool: ${name}`);
166
+ }
167
+ return await launchManagedCommand(asRecord(input), worktreePath, startManagedCommandNames.has(name));
168
+ }
169
+ async function launchManagedCommand(record, worktreePath, forceAsync, verificationProfile, buildProducingOverride, buildReceiptReuse) {
170
+ const command = requiredInputString(record, "command");
171
+ const args = requiredStringArray(record, "args");
172
+ const timeoutMs = optionalTimeout(record.timeout_ms);
173
+ const cwd = await containedWorkingDirectory(worktreePath, record.cwd);
174
+ await validatePlaywrightCommandProject(command, args, cwd);
175
+ const logDirectory = requiredAttemptEnvironment("FORGE_LOG_DIR");
176
+ await fs.mkdir(path.join(logDirectory, "managed-commands"), { recursive: true, mode: 0o700 });
177
+ const commandId = randomUUID();
178
+ const metadataPath = path.join(logDirectory, "managed-commands", `${commandId}.json`);
179
+ const receiptPath = managedCommandReceiptPath(logDirectory, commandId);
180
+ const invocation = {
181
+ commandId,
182
+ command,
183
+ args,
184
+ cwd,
185
+ timeoutMs,
186
+ logDirectory,
187
+ metadataPath,
188
+ receiptPath,
189
+ startedAt: Date.now(),
190
+ verificationProfile,
191
+ worktreePath,
192
+ buildProducing: buildProducingOverride ?? isBuildProducingManagedCommand(command, args),
193
+ buildReceiptReuse,
194
+ };
195
+ if (forceAsync || timeoutMs > maximumSynchronousWaitMs) {
196
+ const started = managedCommandResult({
197
+ ...invocation,
198
+ timedOut: false,
199
+ status: "starting",
200
+ exitCode: null,
201
+ stdout: "",
202
+ stderr: "",
203
+ });
204
+ await persistManagedCommandReceipt(receiptPath, started.structuredContent);
205
+ const task = executeManagedCommand(invocation);
206
+ managedCommandTasks.set(commandId, task);
207
+ void task.finally(() => managedCommandTasks.delete(commandId)).catch(() => undefined);
208
+ return {
209
+ ...managedCommandPublicResult(started),
210
+ content: [{
211
+ type: "text",
212
+ text: `Forge managed command ${commandId} started asynchronously. Continue independent work, then use forge.wait_managed_command with this command_id for a transport-safe terminal receipt.`,
213
+ }],
214
+ };
215
+ }
216
+ return await executeManagedCommand(invocation);
217
+ }
218
+ async function startManagedVerification(input, worktreePath) {
219
+ const record = asRecord(input);
220
+ const profile = requiredInputString(record, "profile");
221
+ const shardCount = optionalBoundedInteger(record.shard_count, "shard_count", 1, 32, 1);
222
+ const shardIndex = optionalBoundedInteger(record.shard_index, "shard_index", 0, 31, 0);
223
+ if (shardIndex >= shardCount) {
224
+ throw new Error("managed_verification_invalid: shard_index must be less than shard_count.");
225
+ }
226
+ const noBuild = optionalBoolean(record.no_build, "no_build", false);
227
+ const runAllShards = optionalBoolean(record.run_all_shards, "run_all_shards", false);
228
+ const maxParallelism = optionalBoundedInteger(record.max_parallelism, "max_parallelism", 1, 4, 2);
229
+ if (runAllShards && shardCount < 2) {
230
+ throw new Error("managed_verification_invalid: run_all_shards requires shard_count of at least 2.");
231
+ }
232
+ if (runAllShards && shardIndex !== 0) {
233
+ throw new Error("managed_verification_invalid: shard_index must be 0 when run_all_shards is enabled.");
234
+ }
235
+ if (runAllShards && noBuild) {
236
+ throw new Error("managed_verification_invalid: run_all_shards owns the single prerequisite build and cannot be combined with no_build.");
237
+ }
238
+ const outputDirectory = requiredAttemptEnvironment("FORGE_TEST_OUTPUT_DIR");
239
+ const profileCommand = buildManagedVerificationCommand(profile, outputDirectory, shardCount, shardIndex, noBuild, runAllShards, maxParallelism);
240
+ const buildReceiptReuse = noBuild
241
+ ? await verifyManagedBuildReceipt({
242
+ worktreePath,
243
+ configuration: managedBuildConfiguration(profileCommand.command, profileCommand.args),
244
+ requiredCoverage: managedVerificationBuildCoverage,
245
+ })
246
+ : undefined;
247
+ return await launchManagedCommand({
248
+ command: profileCommand.command,
249
+ args: profileCommand.args,
250
+ timeout_ms: record.timeout_ms ?? profileCommand.timeoutMs,
251
+ }, worktreePath, true, profile, !noBuild, buildReceiptReuse);
252
+ }
253
+ export function buildManagedVerificationCommand(profile, outputDirectory, shardCount, shardIndex, noBuild, runAllShards = false, maxParallelism = 2) {
254
+ if (profile === "dotnet_solution") {
255
+ if (runAllShards) {
256
+ throw new Error("managed_verification_invalid: dotnet_solution does not support run_all_shards; select a test tier instead.");
257
+ }
258
+ if (shardCount !== 1 || shardIndex !== 0) {
259
+ throw new Error("managed_verification_invalid: dotnet_solution does not accept sharding; select a test tier instead.");
260
+ }
261
+ const command = preferredPowerShellExecutable();
262
+ const args = [
263
+ "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "scripts/run-dotnet-test-suite.ps1",
264
+ "-ResultsDirectory", outputDirectory,
265
+ "-MaxParallelism", String(maxParallelism),
266
+ ];
267
+ if (noBuild)
268
+ args.push("-NoBuild");
269
+ return { command, args, timeoutMs: 2_700_000 };
270
+ }
271
+ const tiers = {
272
+ dotnet_unit: "Unit",
273
+ dotnet_contract: "Contract",
274
+ dotnet_api_component: "ApiComponent",
275
+ dotnet_orchestration: "Orchestration",
276
+ dotnet_infrastructure: "Infrastructure",
277
+ };
278
+ const tier = tiers[profile];
279
+ if (!tier)
280
+ throw new Error(`managed_verification_invalid: unsupported profile ${profile}.`);
281
+ const command = preferredPowerShellExecutable();
282
+ if (runAllShards) {
283
+ return {
284
+ command,
285
+ args: [
286
+ "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "scripts/run-dotnet-test-tier-set.ps1",
287
+ "-Tier", tier,
288
+ "-ResultsDirectory", outputDirectory,
289
+ "-ShardCount", String(shardCount),
290
+ "-MaxParallelism", String(maxParallelism),
291
+ ],
292
+ timeoutMs: tier === "Unit" || tier === "Contract" ? 900_000 : 2_700_000,
293
+ };
294
+ }
295
+ const args = [
296
+ "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "scripts/run-dotnet-test-tier.ps1",
297
+ "-Tier", tier,
298
+ "-ResultsDirectory", outputDirectory,
299
+ "-ShardCount", String(shardCount),
300
+ "-ShardIndex", String(shardIndex),
301
+ ];
302
+ if (noBuild)
303
+ args.push("-NoBuild");
304
+ return { command, args, timeoutMs: tier === "Unit" || tier === "Contract" ? 900_000 : 2_700_000 };
305
+ }
306
+ export function preferredPowerShellExecutable(platform = process.platform, pathValue = process.env.PATH ?? "") {
307
+ if (platform !== "win32")
308
+ return "pwsh";
309
+ return resolveWindowsCommandFromPath("pwsh.exe", pathValue) ?? "powershell.exe";
310
+ }
311
+ export function validatePlaywrightProjectName(project, availableProjects) {
312
+ if (!availableProjects.includes(project)) {
313
+ throw new Error(`managed_command_capability_unavailable: Playwright project '${project}' is not advertised by the selected config. Available projects: ${availableProjects.join(", ") || "none"}.`);
314
+ }
315
+ }
316
+ async function validatePlaywrightCommandProject(command, args, cwd) {
317
+ const invocation = [path.basename(command).toLowerCase(), ...args.map(argument => argument.toLowerCase())];
318
+ if (!invocation.some(argument => argument.includes("playwright")))
319
+ return;
320
+ const project = optionValue(args, "--project");
321
+ if (!project)
322
+ return;
323
+ const configuredPath = optionValue(args, "--config") ?? process.env.FORGE_PLAYWRIGHT_CONFIG;
324
+ const candidates = configuredPath
325
+ ? [path.resolve(cwd, configuredPath)]
326
+ : ["playwright.config.ts", "playwright.config.js", "playwright.config.mjs"].map(file => path.join(cwd, file));
327
+ for (const candidate of candidates) {
328
+ try {
329
+ const source = await fs.readFile(candidate, "utf8");
330
+ const projects = playwrightProjectNamesFromConfigSource(source);
331
+ validatePlaywrightProjectName(project, projects);
332
+ return;
333
+ }
334
+ catch (error) {
335
+ if (error.code !== "ENOENT")
336
+ throw error;
337
+ }
338
+ }
339
+ throw new Error("managed_command_capability_unavailable: Playwright config was not found; project selection cannot be validated.");
340
+ }
341
+ export function playwrightProjectNamesFromConfigSource(source) {
342
+ const marker = source.match(/^\/\/ forge-playwright-projects: (\[[^\r\n]*\])$/mu)?.[1];
343
+ if (!marker) {
344
+ return [...source.matchAll(/\bname\s*:\s*["']([^"']+)["']/gu)].map(match => match[1]);
345
+ }
346
+ try {
347
+ const parsed = JSON.parse(marker);
348
+ return Array.isArray(parsed)
349
+ ? parsed.filter((value) => typeof value === "string" && value.trim().length > 0)
350
+ : [];
351
+ }
352
+ catch {
353
+ return [];
354
+ }
355
+ }
356
+ function optionValue(args, name) {
357
+ const direct = args.find(argument => argument.startsWith(`${name}=`));
358
+ if (direct)
359
+ return direct.slice(name.length + 1).trim() || null;
360
+ const index = args.indexOf(name);
361
+ return index >= 0 ? args[index + 1]?.trim() || null : null;
362
+ }
363
+ async function executeManagedCommand(invocation) {
364
+ if (!invocation.buildProducing)
365
+ return await executeManagedCommandProcess(invocation);
366
+ return await new ManagedBuildCoordinator().withBuildSlot(invocation.worktreePath, async (buildQueueWaitMs) => await executeManagedCommandProcess({ ...invocation, buildQueueWaitMs }), async () => {
367
+ const waiting = managedCommandResult({
368
+ ...invocation,
369
+ timedOut: false,
370
+ status: "waiting_for_build_slot",
371
+ exitCode: null,
372
+ stdout: "",
373
+ stderr: "",
374
+ });
375
+ await persistManagedCommandReceipt(invocation.receiptPath, waiting.structuredContent);
376
+ });
377
+ }
378
+ async function executeManagedCommandProcess(invocation) {
379
+ const environment = inheritedEnvironment({
380
+ FORGE_RUNTIME_MODE: "host",
381
+ FORGE_RESOURCE_LEASE_ID: requiredAttemptEnvironment("FORGE_RESOURCE_LEASE_ID"),
382
+ FORGE_DISPATCH_ATTEMPT_ID: requiredAttemptEnvironment("FORGE_DISPATCH_ATTEMPT_ID"),
383
+ // This nested command owns a distinct supervisor identity. A command-local
384
+ // fence avoids exposing the parent attempt's fencing capability to Codex's
385
+ // MCP subprocess configuration while preserving exact-tree cleanup proof.
386
+ FORGE_FENCING_TOKEN: randomUUID(),
387
+ FORGE_LOG_DIR: invocation.logDirectory,
388
+ FORGE_TEST_OUTPUT_DIR: requiredAttemptEnvironment("FORGE_TEST_OUTPUT_DIR"),
389
+ FORGE_PROCESS_SUPERVISOR_ID: `managed-${invocation.commandId.replaceAll("-", "")}`,
390
+ FORGE_PROCESS_SUPERVISOR_METADATA_PATH: invocation.metadataPath,
391
+ });
392
+ let stdout = "";
393
+ let stderr = "";
394
+ const controller = new AbortController();
395
+ let timedOut = false;
396
+ let timer = null;
397
+ let outputRevision = 0;
398
+ let lastOutputAt = null;
399
+ let lastOutputStream = null;
400
+ let lastProgressPersistedAt = 0;
401
+ let progressPersistence = Promise.resolve();
402
+ const launchTimer = setTimeout(() => {
403
+ timedOut = true;
404
+ controller.abort(new Error(`managed_command_launch_timeout: process supervisor was not ready within ${managedCommandLaunchTimeoutMs}ms.`));
405
+ }, managedCommandLaunchTimeoutMs);
406
+ try {
407
+ const result = await new StreamingProviderProcessRunner().run({
408
+ command: invocation.command,
409
+ args: invocation.args,
410
+ cwd: invocation.cwd,
411
+ env: environment,
412
+ captureOutput: false,
413
+ }, {
414
+ onStarted: async () => {
415
+ clearTimeout(launchTimer);
416
+ const running = managedCommandResult({
417
+ ...invocation,
418
+ timedOut: false,
419
+ status: "running",
420
+ exitCode: null,
421
+ stdout,
422
+ stderr,
423
+ outputRevision,
424
+ lastOutputAt,
425
+ lastOutputStream,
426
+ });
427
+ await persistManagedCommandReceipt(invocation.receiptPath, running.structuredContent);
428
+ timer = setTimeout(() => {
429
+ timedOut = true;
430
+ controller.abort(new Error(`managed_command_timeout: exceeded ${invocation.timeoutMs}ms.`));
431
+ }, invocation.timeoutMs);
432
+ },
433
+ onStdoutLine: (line) => {
434
+ stdout = appendBounded(stdout, line);
435
+ outputRevision += 1;
436
+ lastOutputAt = Date.now();
437
+ lastOutputStream = "stdout";
438
+ progressPersistence = persistManagedCommandProgressIfDue(invocation, () => ({ stdout, stderr, outputRevision, lastOutputAt, lastOutputStream }), progressPersistence, () => lastProgressPersistedAt, value => { lastProgressPersistedAt = value; });
439
+ return progressPersistence;
440
+ },
441
+ onStderrLine: (line) => {
442
+ stderr = appendBounded(stderr, line);
443
+ outputRevision += 1;
444
+ lastOutputAt = Date.now();
445
+ lastOutputStream = "stderr";
446
+ progressPersistence = persistManagedCommandProgressIfDue(invocation, () => ({ stdout, stderr, outputRevision, lastOutputAt, lastOutputStream }), progressPersistence, () => lastProgressPersistedAt, value => { lastProgressPersistedAt = value; });
447
+ return progressPersistence;
448
+ },
449
+ }, controller.signal);
450
+ await progressPersistence;
451
+ const status = timedOut ? "timed_out" : result.exitCode === 0 ? "pass" : "fail";
452
+ const receiptCoverage = managedBuildReceiptCoverage(invocation);
453
+ const buildReceipt = status === "pass" && receiptCoverage
454
+ ? await publishManagedBuildReceipt({
455
+ worktreePath: invocation.worktreePath,
456
+ command: invocation.command,
457
+ args: invocation.args,
458
+ configuration: managedBuildConfiguration(invocation.command, invocation.args),
459
+ coverage: receiptCoverage,
460
+ })
461
+ : invocation.buildReceiptReuse;
462
+ const receipt = managedCommandResult({
463
+ ...invocation,
464
+ timedOut,
465
+ status,
466
+ exitCode: result.exitCode,
467
+ stdout,
468
+ stderr,
469
+ outputRevision,
470
+ lastOutputAt,
471
+ lastOutputStream,
472
+ buildReceipt,
473
+ buildReceiptReused: invocation.buildReceiptReuse !== undefined,
474
+ });
475
+ await persistManagedCommandReceipt(invocation.receiptPath, receipt.structuredContent);
476
+ return managedCommandPublicResult(receipt);
477
+ }
478
+ catch (error) {
479
+ const message = error instanceof Error ? error.message : String(error);
480
+ stderr = appendBounded(stderr, message);
481
+ outputRevision += 1;
482
+ lastOutputAt = Date.now();
483
+ lastOutputStream = "stderr";
484
+ const receipt = managedCommandResult({
485
+ ...invocation,
486
+ timedOut,
487
+ status: timedOut ? "timeout_termination_failed" : "runner_error",
488
+ exitCode: null,
489
+ stdout,
490
+ stderr,
491
+ outputRevision,
492
+ lastOutputAt,
493
+ lastOutputStream,
494
+ });
495
+ await persistManagedCommandReceipt(invocation.receiptPath, receipt.structuredContent);
496
+ return managedCommandPublicResult(receipt);
497
+ }
498
+ finally {
499
+ clearTimeout(launchTimer);
500
+ if (timer)
501
+ clearTimeout(timer);
502
+ }
503
+ }
504
+ async function readManagedCommand(input) {
505
+ const commandId = validatedCommandId(asRecord(input));
506
+ const receiptPath = managedCommandReceiptPath(requiredAttemptEnvironment("FORGE_LOG_DIR"), commandId);
507
+ let receipt;
508
+ try {
509
+ receipt = JSON.parse(await fs.readFile(receiptPath, "utf8"));
510
+ }
511
+ catch (error) {
512
+ if (error.code === "ENOENT") {
513
+ throw new Error(`managed_command_not_found: no durable receipt exists for ${commandId}.`, { cause: error });
514
+ }
515
+ throw error;
516
+ }
517
+ const status = typeof receipt.status === "string" ? receipt.status : "unknown";
518
+ const stdout = typeof receipt.stdout === "string" ? receipt.stdout : "";
519
+ const stderr = typeof receipt.stderr === "string" ? receipt.stderr : "";
520
+ return managedCommandPublicResult({
521
+ content: [{
522
+ type: "text",
523
+ text: terminalManagedCommandStatuses.has(status)
524
+ ? terminalManagedCommandText({
525
+ commandId,
526
+ status,
527
+ exitCode: typeof receipt.exit_code === "number" ? receipt.exit_code : null,
528
+ elapsedMs: typeof receipt.elapsed_ms === "number" ? receipt.elapsed_ms : null,
529
+ stdout,
530
+ stderr,
531
+ })
532
+ : `Forge managed command ${commandId} is ${status}. Continue independent work and use forge.wait_managed_command when ready for its terminal receipt.`,
533
+ }],
534
+ structuredContent: receipt,
535
+ isError: terminalManagedCommandStatuses.has(status) && status !== "pass",
536
+ });
537
+ }
538
+ async function waitManagedCommand(input) {
539
+ const record = asRecord(input);
540
+ const commandId = validatedCommandId(record);
541
+ const requestedWaitTimeoutMs = optionalWaitTimeout(record.wait_timeout_ms);
542
+ const waitTimeoutMs = Math.min(requestedWaitTimeoutMs, maximumSynchronousWaitMs);
543
+ const deadline = Date.now() + waitTimeoutMs;
544
+ const receiptPath = managedCommandReceiptPath(requiredAttemptEnvironment("FORGE_LOG_DIR"), commandId);
545
+ const task = managedCommandTasks.get(commandId);
546
+ if (task) {
547
+ const result = await Promise.race([
548
+ task,
549
+ delay(waitTimeoutMs).then(() => null),
550
+ ]);
551
+ if (result)
552
+ return result;
553
+ }
554
+ while (Date.now() < deadline) {
555
+ const result = await readManagedCommand({ command_id: commandId });
556
+ if (terminalManagedCommandStatuses.has(String(result.structuredContent.status))) {
557
+ return result;
558
+ }
559
+ await delay(Math.min(managedCommandReceiptPollIntervalMs, Math.max(1, deadline - Date.now())));
560
+ }
561
+ const receipt = await readManagedCommand({ command_id: commandId });
562
+ return {
563
+ ...receipt,
564
+ content: [{
565
+ type: "text",
566
+ text: `Forge is still supervising managed command ${commandId} after this ${waitTimeoutMs}ms transport-safe wait. Continue independent work or call forge.wait_managed_command again after ${managedCommandRetryAfterMs}ms. Its durable receipt remains at ${receiptPath}.`,
567
+ }],
568
+ structuredContent: {
569
+ ...receipt.structuredContent,
570
+ command_continues: true,
571
+ terminal: false,
572
+ wait_timeout_ms: waitTimeoutMs,
573
+ requested_wait_timeout_ms: requestedWaitTimeoutMs,
574
+ retry_after_ms: managedCommandRetryAfterMs,
575
+ },
576
+ isError: false,
577
+ };
578
+ }
579
+ function managedCommandReceiptPath(logDirectory, commandId) {
580
+ return path.join(logDirectory, "managed-commands", `${commandId}.receipt.json`);
581
+ }
582
+ async function persistManagedCommandReceipt(receiptPath, receipt) {
583
+ const temporaryPath = `${receiptPath}.${randomUUID()}.tmp`;
584
+ await fs.writeFile(temporaryPath, `${JSON.stringify(receipt, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
585
+ try {
586
+ for (let attempt = 1;; attempt += 1) {
587
+ try {
588
+ await fs.rename(temporaryPath, receiptPath);
589
+ break;
590
+ }
591
+ catch (error) {
592
+ const code = error.code;
593
+ if (attempt >= managedCommandReceiptReplaceAttempts
594
+ || (code !== "EPERM" && code !== "EBUSY" && code !== "EACCES")) {
595
+ throw error;
596
+ }
597
+ await delay(managedCommandReceiptReplaceRetryMs);
598
+ }
599
+ }
600
+ }
601
+ finally {
602
+ await fs.rm(temporaryPath, { force: true }).catch(() => undefined);
603
+ }
604
+ }
605
+ function managedCommandResult(values) {
606
+ const elapsedMs = Date.now() - values.startedAt;
607
+ const isTerminal = terminalManagedCommandStatuses.has(values.status);
608
+ const diagnostics = isTerminal
609
+ ? managedCommandDiagnostics(values.stdout, values.stderr)
610
+ : undefined;
611
+ return {
612
+ content: [{
613
+ type: "text",
614
+ text: isTerminal
615
+ ? terminalManagedCommandText({
616
+ commandId: values.commandId,
617
+ status: values.status,
618
+ exitCode: values.exitCode,
619
+ elapsedMs,
620
+ stdout: values.stdout,
621
+ stderr: values.stderr,
622
+ }, diagnostics)
623
+ : `Forge managed command ${values.commandId} is ${values.status} after ${elapsedMs}ms.`,
624
+ }],
625
+ structuredContent: {
626
+ command_id: values.commandId,
627
+ status: values.status,
628
+ command: [values.command, ...values.args],
629
+ cwd: values.cwd,
630
+ timeout_ms: values.timeoutMs,
631
+ elapsed_ms: elapsedMs,
632
+ started_at: new Date(values.startedAt).toISOString(),
633
+ timed_out: values.timedOut,
634
+ exit_code: values.exitCode,
635
+ stdout: values.stdout,
636
+ stderr: values.stderr,
637
+ ...(diagnostics ? { diagnostics } : {}),
638
+ output_truncated: values.stdout.length >= maximumCapturedCharacters
639
+ || values.stderr.length >= maximumCapturedCharacters,
640
+ output_revision: values.outputRevision ?? 0,
641
+ last_output_at: values.lastOutputAt ? new Date(values.lastOutputAt).toISOString() : null,
642
+ last_output_stream: values.lastOutputStream ?? null,
643
+ supervisor_metadata_path: values.metadataPath,
644
+ receipt_path: values.receiptPath,
645
+ verification_profile: values.verificationProfile ?? null,
646
+ verification_profile_runner_authored: values.verificationProfile !== undefined,
647
+ build_producing: values.buildProducing,
648
+ build_coordination: values.buildProducing ? "per_worktree_exclusive" : null,
649
+ build_queue_wait_ms: values.buildQueueWaitMs ?? 0,
650
+ build_receipt_reused: values.buildReceiptReused ?? false,
651
+ build_receipt: values.buildReceipt ? {
652
+ fingerprint: values.buildReceipt.fingerprint,
653
+ receipt_path: values.buildReceipt.receiptPath,
654
+ producer_command: values.buildReceipt.producerCommand,
655
+ configuration: values.buildReceipt.configuration,
656
+ coverage: values.buildReceipt.coverage,
657
+ output_count: values.buildReceipt.outputCount,
658
+ } : null,
659
+ runtime_configuration: managedRuntimeConfiguration(),
660
+ process_tree_terminated: values.status === "timed_out" ? true : values.timedOut ? false : undefined,
661
+ },
662
+ isError: isTerminal && values.status !== "pass",
663
+ };
664
+ }
665
+ function managedRuntimeConfiguration() {
666
+ const providerOnlySpecs = process.env.FORGE_CI_REPAIR_PROVIDER_ONLY_SPECS?.trim();
667
+ let providerOnlySpecCount = 0;
668
+ if (providerOnlySpecs) {
669
+ try {
670
+ const parsed = JSON.parse(providerOnlySpecs);
671
+ providerOnlySpecCount = Array.isArray(parsed)
672
+ ? parsed.filter(value => typeof value === "string" && value.length > 0).length
673
+ : 0;
674
+ }
675
+ catch {
676
+ providerOnlySpecCount = 0;
677
+ }
678
+ }
679
+ return {
680
+ attempt_database_schema: process.env.FORGE_ATTEMPT_DATABASE_SCHEMA ?? null,
681
+ attempt_database_readiness_path: process.env.FORGE_ATTEMPT_DATABASE_READINESS_PATH ?? null,
682
+ playwright_config: process.env.FORGE_PLAYWRIGHT_CONFIG ?? null,
683
+ provider_only_spec_count: providerOnlySpecCount,
684
+ provider_only_spec_fingerprint: providerOnlySpecs
685
+ ? `sha256:${createHash("sha256").update(providerOnlySpecs).digest("hex")}`
686
+ : null,
687
+ };
688
+ }
689
+ function managedCommandPublicResult(result) {
690
+ const stdout = typeof result.structuredContent.stdout === "string" ? result.structuredContent.stdout : "";
691
+ const stderr = typeof result.structuredContent.stderr === "string" ? result.structuredContent.stderr : "";
692
+ const { stdout: _stdout, stderr: _stderr, ...receipt } = result.structuredContent;
693
+ return {
694
+ ...result,
695
+ structuredContent: {
696
+ ...receipt,
697
+ stdout_tail: tail(stdout, maximumReturnedOutputCharacters),
698
+ stderr_tail: tail(stderr, maximumReturnedOutputCharacters),
699
+ full_output_in_receipt: true,
700
+ },
701
+ };
702
+ }
703
+ function persistManagedCommandProgressIfDue(invocation, snapshot, previousPersistence, lastPersistedAt, setLastPersistedAt) {
704
+ const now = Date.now();
705
+ if (now - lastPersistedAt() < managedCommandProgressPersistIntervalMs)
706
+ return previousPersistence;
707
+ setLastPersistedAt(now);
708
+ const current = snapshot();
709
+ return previousPersistence.then(async () => {
710
+ const receipt = managedCommandResult({
711
+ ...invocation,
712
+ timedOut: false,
713
+ status: "running",
714
+ exitCode: null,
715
+ ...current,
716
+ });
717
+ await persistManagedCommandReceipt(invocation.receiptPath, receipt.structuredContent);
718
+ });
719
+ }
720
+ async function containedWorkingDirectory(worktreePath, input) {
721
+ const root = await fs.realpath(path.resolve(worktreePath));
722
+ const relative = input === undefined ? "." : requiredStandaloneString(input, "cwd");
723
+ if (path.isAbsolute(relative))
724
+ throw new Error("managed_command_invalid: cwd must be worktree-relative.");
725
+ const target = await fs.realpath(path.resolve(root, relative));
726
+ const containment = path.relative(root, target);
727
+ if (containment.startsWith(`..${path.sep}`) || path.isAbsolute(containment)) {
728
+ throw new Error("managed_command_invalid: cwd resolves outside the source worktree.");
729
+ }
730
+ return target;
731
+ }
732
+ function inheritedEnvironment(overrides) {
733
+ const env = {};
734
+ for (const [key, value] of Object.entries(process.env)) {
735
+ if (typeof value === "string" && !looksSensitiveEnvironmentName(key))
736
+ env[key] = value;
737
+ }
738
+ return { ...env, ...overrides };
739
+ }
740
+ export function isBuildProducingManagedCommand(command, args) {
741
+ const executable = path.basename(command).toLowerCase();
742
+ if (executable === "dotnet" || executable === "dotnet.exe") {
743
+ const verb = args.find(argument => !argument.startsWith("-"))?.toLowerCase();
744
+ if (!verb || !new Set(["build", "test", "restore", "publish", "pack", "msbuild"]).has(verb))
745
+ return false;
746
+ return verb === "restore" || !args.some(argument => argument.toLowerCase() === "--no-build");
747
+ }
748
+ if (["powershell", "powershell.exe", "pwsh", "pwsh.exe"].includes(executable)) {
749
+ const fileIndex = args.findIndex(argument => argument.toLowerCase() === "-file");
750
+ const script = fileIndex >= 0 ? path.basename(args[fileIndex + 1] ?? "").toLowerCase() : "";
751
+ if (script === "run-dotnet-test-tier-set.ps1" || script === "run-dotnet-test-suite.ps1") {
752
+ return !args.some(argument => argument.toLowerCase() === "-nobuild");
753
+ }
754
+ if (script === "run-dotnet-test-tier.ps1") {
755
+ return !args.some(argument => argument.toLowerCase() === "-nobuild");
756
+ }
757
+ }
758
+ return false;
759
+ }
760
+ function managedBuildReceiptCoverage(invocation) {
761
+ if (!invocation.buildProducing)
762
+ return null;
763
+ if (invocation.verificationProfile)
764
+ return managedVerificationBuildCoverage;
765
+ const executable = path.basename(invocation.command).toLowerCase();
766
+ if (executable === "dotnet" || executable === "dotnet.exe") {
767
+ const verb = invocation.args.find(argument => !argument.startsWith("-"))?.toLowerCase();
768
+ if (!verb || !new Set(["build", "test"]).has(verb))
769
+ return null;
770
+ const target = invocation.args.find(argument => /\.(?:sln|slnx|csproj)$/iu.test(argument));
771
+ if (target && (/\.(?:sln|slnx)$/iu.test(target) || /(?:^|[\\/])Forge\.Api\.Tests\.csproj$/iu.test(target))) {
772
+ return managedVerificationBuildCoverage;
773
+ }
774
+ return null;
775
+ }
776
+ if (["powershell", "powershell.exe", "pwsh", "pwsh.exe"].includes(executable)) {
777
+ const fileIndex = invocation.args.findIndex(argument => argument.toLowerCase() === "-file");
778
+ const script = fileIndex >= 0 ? path.basename(invocation.args[fileIndex + 1] ?? "").toLowerCase() : "";
779
+ if (["run-dotnet-test-tier.ps1", "run-dotnet-test-tier-set.ps1", "run-dotnet-test-suite.ps1"].includes(script)
780
+ && !invocation.args.some(argument => argument.toLowerCase() === "-nobuild")) {
781
+ return managedVerificationBuildCoverage;
782
+ }
783
+ }
784
+ return null;
785
+ }
786
+ function looksSensitiveEnvironmentName(name) {
787
+ const normalized = name.trim().toUpperCase();
788
+ return normalized.includes("SECRET")
789
+ || normalized.includes("TOKEN")
790
+ || normalized.includes("PASSWORD")
791
+ || normalized.includes("CREDENTIAL")
792
+ || normalized.endsWith("_API_KEY")
793
+ || normalized === "OPENAI_API_KEY";
794
+ }
795
+ function requiredAttemptEnvironment(name) {
796
+ const value = process.env[name]?.trim();
797
+ if (!value)
798
+ throw new Error(`managed_attempt_tool_unavailable: ${name} is not configured.`);
799
+ return value;
800
+ }
801
+ function asRecord(value) {
802
+ if (!value || typeof value !== "object" || Array.isArray(value))
803
+ throw new Error("managed_command_invalid: input must be an object.");
804
+ return value;
805
+ }
806
+ function requiredInputString(record, name) {
807
+ return requiredStandaloneString(record[name], name);
808
+ }
809
+ function validatedCommandId(record) {
810
+ const commandId = requiredInputString(record, "command_id");
811
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(commandId)) {
812
+ throw new Error("managed_command_invalid: command_id must be a UUID returned by Forge.");
813
+ }
814
+ return commandId;
815
+ }
816
+ function requiredStandaloneString(value, name) {
817
+ if (typeof value !== "string" || !value.trim())
818
+ throw new Error(`managed_command_invalid: ${name} is required.`);
819
+ return value.trim();
820
+ }
821
+ function requiredStringArray(record, name) {
822
+ const value = record[name];
823
+ if (!Array.isArray(value) || value.some(entry => typeof entry !== "string")) {
824
+ throw new Error(`managed_command_invalid: ${name} must be a string array.`);
825
+ }
826
+ return value;
827
+ }
828
+ function optionalTimeout(value) {
829
+ if (value === undefined)
830
+ return 2_700_000;
831
+ if (!Number.isSafeInteger(value) || value < 1_000 || value > 7_200_000) {
832
+ throw new Error("managed_command_invalid: timeout_ms must be an integer from 1000 through 7200000.");
833
+ }
834
+ return value;
835
+ }
836
+ function optionalWaitTimeout(value) {
837
+ if (value === undefined)
838
+ return maximumSynchronousWaitMs;
839
+ if (!Number.isSafeInteger(value) || value < 1_000 || value > maximumSynchronousWaitMs) {
840
+ throw new Error(`managed_command_invalid: wait_timeout_ms must be an integer from 1000 through ${maximumSynchronousWaitMs}.`);
841
+ }
842
+ return value;
843
+ }
844
+ function optionalBoundedInteger(value, name, minimum, maximum, fallback) {
845
+ if (value === undefined)
846
+ return fallback;
847
+ if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
848
+ throw new Error(`managed_verification_invalid: ${name} must be an integer from ${minimum} through ${maximum}.`);
849
+ }
850
+ return value;
851
+ }
852
+ function optionalBoolean(value, name, fallback) {
853
+ if (value === undefined)
854
+ return fallback;
855
+ if (typeof value !== "boolean")
856
+ throw new Error(`managed_verification_invalid: ${name} must be a boolean.`);
857
+ return value;
858
+ }
859
+ async function delay(ms) {
860
+ await new Promise(resolve => setTimeout(resolve, ms));
861
+ }
862
+ function appendBounded(current, line) {
863
+ const next = `${current}${line}\n`;
864
+ return next.length <= maximumCapturedCharacters
865
+ ? next
866
+ : next.slice(-maximumCapturedCharacters);
867
+ }
868
+ function managedCommandDiagnostics(stdout, stderr) {
869
+ const lines = `${stdout}\n${stderr}`
870
+ .split(/\r?\n/u)
871
+ .map(line => line.trim())
872
+ .filter(Boolean);
873
+ const failedTests = new Set();
874
+ for (const line of lines) {
875
+ const xunit = line.match(/^\[xUnit\.net[^\]]*\]\s+(.+?)\s+\[FAIL\]$/u);
876
+ if (xunit?.[1])
877
+ failedTests.add(xunit[1].trim());
878
+ const vstest = line.match(/^Failed\s+(.+?)(?:\s+\[(?:\d|<).*)?$/u);
879
+ if (vstest?.[1] && !vstest[1].startsWith("!"))
880
+ failedTests.add(vstest[1].trim());
881
+ }
882
+ const summaryLines = lines
883
+ .filter(line => /(?:Failed!|Passed!|Test Run|Tests?:\s|Test Files|\[FAIL\]|error\s+[A-Z]+\d+)/iu.test(line))
884
+ .slice(-20);
885
+ return {
886
+ failed_tests: [...failedTests].slice(0, maximumReportedFailedTests),
887
+ failed_test_count: failedTests.size,
888
+ summary_lines: summaryLines,
889
+ };
890
+ }
891
+ function terminalManagedCommandText(values, diagnostics = managedCommandDiagnostics(values.stdout, values.stderr)) {
892
+ const failedTests = diagnostics.failed_tests;
893
+ const summaryLines = diagnostics.summary_lines;
894
+ const duration = values.elapsedMs === null ? "an unknown duration" : `${values.elapsedMs}ms`;
895
+ const sections = [
896
+ `Forge managed command ${values.commandId} ${values.status} after ${duration} (exit ${values.exitCode ?? "unavailable"}).`,
897
+ ];
898
+ if (failedTests.length > 0) {
899
+ sections.push(`Failed tests (${diagnostics.failed_test_count}):\n${failedTests.map(test => `- ${test}`).join("\n")}`);
900
+ }
901
+ if (summaryLines.length > 0) {
902
+ sections.push(`Test summary:\n${summaryLines.join("\n")}`);
903
+ }
904
+ const stdoutTail = tail(values.stdout, maximumReturnedOutputCharacters);
905
+ const stderrTail = tail(values.stderr, maximumReturnedOutputCharacters);
906
+ sections.push(`stdout tail:\n${stdoutTail || "(empty)"}`);
907
+ sections.push(`stderr tail:\n${stderrTail || "(empty)"}`);
908
+ return sections.join("\n\n");
909
+ }
910
+ function tail(value, maximumCharacters) {
911
+ return value.length <= maximumCharacters ? value : value.slice(-maximumCharacters);
912
+ }