@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
@@ -1,17 +1,31 @@
1
- import { randomUUID } from "node:crypto";
1
+ import { createHash, randomUUID } from "node:crypto";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { resetGeneratedOutputs } from "../runtime/generated-output-reset.js";
5
- import { StreamingProviderProcessRunner } from "./provider-cli/provider-cli.process.js";
5
+ import { DockerEngineProbe } from "../runtime/docker-engine-probe.js";
6
+ import { attemptDatabaseCapabilityEnvironment, attemptDatabaseCapabilitySecrets, } from "../runtime/attempt-database-broker.js";
7
+ import { assertHostDatabaseCapabilityBoundary } from "../runtime/host-database-capability-boundary.js";
8
+ import { nativeTestPostgresFallbackConfigEnvironment, nativeTestPostgresFallbackDatabaseMode, nativeTestPostgresFallbackEnvironment, readNativeTestPostgresFallback, } from "../runtime/native-test-postgres-fallback.js";
9
+ import { redactText } from "../shared/redaction/redaction.js";
10
+ import { ManagedBuildCoordinator } from "./managed-build-coordinator.js";
11
+ import { managedBuildConfiguration, publishManagedBuildReceipt, verifyManagedBuildReceipt, } from "./managed-build-receipt.js";
12
+ import { managedLongTierTimeoutMs, managedMaximumCommandTimeoutMs, managedVerificationTierTimeoutMs, } from "./managed-verification-timeouts.js";
13
+ import { resolveWindowsCommandFromPath, StreamingProviderProcessRunner } from "./provider-cli/provider-cli.process.js";
6
14
  const managedCommandNames = new Set(["forge.run_managed_command", "run_managed_command"]);
7
15
  const startManagedCommandNames = new Set(["forge.start_managed_command", "start_managed_command"]);
8
16
  const getManagedCommandNames = new Set(["forge.get_managed_command", "get_managed_command"]);
9
17
  const waitManagedCommandNames = new Set(["forge.wait_managed_command", "wait_managed_command"]);
18
+ const startManagedVerificationNames = new Set(["forge.start_managed_verification", "start_managed_verification"]);
10
19
  const resetGeneratedOutputNames = new Set(["forge.reset_generated_outputs", "reset_generated_outputs"]);
11
20
  const maximumCapturedCharacters = 200_000;
12
- const maximumReturnedOutputCharacters = 12_000;
21
+ const maximumReturnedOutputCharacters = 4_000;
13
22
  const maximumReportedFailedTests = 50;
14
- const maximumSynchronousWaitMs = 240_000;
23
+ // Keep a wait slice comfortably below the MCP transport deadline. The managed
24
+ // process remains supervised and its durable receipt continues to update after
25
+ // this call returns, so a shorter slice avoids converting healthy long-running
26
+ // commands into ambiguous MCP failures.
27
+ const maximumSynchronousWaitMs = 45_000;
28
+ const managedCommandRetryAfterMs = 60_000;
15
29
  const managedCommandLaunchTimeoutMs = 60_000;
16
30
  const managedCommandProgressPersistIntervalMs = 5_000;
17
31
  const managedCommandReceiptPollIntervalMs = 1_000;
@@ -24,6 +38,7 @@ const terminalManagedCommandStatuses = new Set([
24
38
  "timeout_termination_failed",
25
39
  "runner_error",
26
40
  ]);
41
+ const managedVerificationBuildCoverage = "forge_dotnet_test_graph_v1";
27
42
  const managedCommandTasks = new Map();
28
43
  export const managedAttemptToolNames = [
29
44
  "forge.run_managed_command",
@@ -34,6 +49,8 @@ export const managedAttemptToolNames = [
34
49
  "get_managed_command",
35
50
  "forge.wait_managed_command",
36
51
  "wait_managed_command",
52
+ "forge.start_managed_verification",
53
+ "start_managed_verification",
37
54
  "forge.reset_generated_outputs",
38
55
  "reset_generated_outputs",
39
56
  ];
@@ -42,15 +59,39 @@ export function isManagedAttemptTool(name) {
42
59
  || startManagedCommandNames.has(name)
43
60
  || getManagedCommandNames.has(name)
44
61
  || waitManagedCommandNames.has(name)
62
+ || startManagedVerificationNames.has(name)
45
63
  || resetGeneratedOutputNames.has(name);
46
64
  }
47
65
  export function managedAttemptToolDefinition(name) {
66
+ if (startManagedVerificationNames.has(name)) {
67
+ return {
68
+ name,
69
+ 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.",
70
+ inputSchema: {
71
+ type: "object",
72
+ additionalProperties: false,
73
+ required: ["profile"],
74
+ properties: {
75
+ profile: {
76
+ type: "string",
77
+ enum: ["dotnet_unit", "dotnet_contract", "dotnet_api_component", "dotnet_orchestration", "dotnet_infrastructure", "dotnet_solution", "dotnet_affected"],
78
+ },
79
+ shard_count: { type: "integer", minimum: 1, maximum: 32, default: 1 },
80
+ shard_index: { type: "integer", minimum: 0, maximum: 31, default: 0 },
81
+ run_all_shards: { type: "boolean", default: false },
82
+ max_parallelism: { type: "integer", minimum: 1, maximum: 4, default: 2 },
83
+ no_build: { type: "boolean", default: false },
84
+ timeout_ms: { type: "integer", minimum: 1000, maximum: 7200000 },
85
+ },
86
+ },
87
+ };
88
+ }
48
89
  if (managedCommandNames.has(name) || startManagedCommandNames.has(name)) {
49
90
  return {
50
91
  name,
51
92
  description: startManagedCommandNames.has(name)
52
- ? "Start a long command in an attempt-owned process tree and return a durable command ID immediately. Continue independent work, then call forge.wait_managed_command once for its terminal receipt."
53
- : "Run a short command in an attempt-owned process tree. Commands with a timeout above four minutes automatically start asynchronously; continue independent work, then call forge.wait_managed_command once for the durable result.",
93
+ ? "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."
94
+ : "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.",
54
95
  inputSchema: {
55
96
  type: "object",
56
97
  additionalProperties: false,
@@ -81,14 +122,14 @@ export function managedAttemptToolDefinition(name) {
81
122
  if (waitManagedCommandNames.has(name)) {
82
123
  return {
83
124
  name,
84
- description: "Wait for a command started by Forge's managed command runner to reach a terminal state. Use this once after completing work that is independent of the command; Forge streams durable progress while this call waits.",
125
+ 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.",
85
126
  inputSchema: {
86
127
  type: "object",
87
128
  additionalProperties: false,
88
129
  required: ["command_id"],
89
130
  properties: {
90
131
  command_id: { type: "string", minLength: 1 },
91
- wait_timeout_ms: { type: "integer", minimum: 1000, maximum: 7200000, default: 7200000 },
132
+ wait_timeout_ms: { type: "integer", minimum: 1000, maximum: 45000, default: 45000 },
92
133
  },
93
134
  },
94
135
  };
@@ -113,6 +154,7 @@ export async function callManagedAttemptTool(name, input, worktreePath) {
113
154
  structuredContent: {
114
155
  status: "pass",
115
156
  quarantined_paths: result.quarantinedPaths,
157
+ retained_paths: result.retainedPaths,
116
158
  quarantine_directory: result.quarantineDirectory,
117
159
  },
118
160
  };
@@ -123,14 +165,20 @@ export async function callManagedAttemptTool(name, input, worktreePath) {
123
165
  if (waitManagedCommandNames.has(name)) {
124
166
  return await waitManagedCommand(input);
125
167
  }
168
+ if (startManagedVerificationNames.has(name)) {
169
+ return await startManagedVerification(input, worktreePath);
170
+ }
126
171
  if (!managedCommandNames.has(name) && !startManagedCommandNames.has(name)) {
127
172
  throw new Error(`Unknown managed attempt tool: ${name}`);
128
173
  }
129
- const record = asRecord(input);
174
+ return await launchManagedCommand(asRecord(input), worktreePath, startManagedCommandNames.has(name));
175
+ }
176
+ async function launchManagedCommand(record, worktreePath, forceAsync, verificationProfile, buildProducingOverride, buildReceiptReuse) {
130
177
  const command = requiredInputString(record, "command");
131
178
  const args = requiredStringArray(record, "args");
132
179
  const timeoutMs = optionalTimeout(record.timeout_ms);
133
180
  const cwd = await containedWorkingDirectory(worktreePath, record.cwd);
181
+ await validatePlaywrightCommandProject(command, args, cwd);
134
182
  const logDirectory = requiredAttemptEnvironment("FORGE_LOG_DIR");
135
183
  await fs.mkdir(path.join(logDirectory, "managed-commands"), { recursive: true, mode: 0o700 });
136
184
  const commandId = randomUUID();
@@ -146,8 +194,12 @@ export async function callManagedAttemptTool(name, input, worktreePath) {
146
194
  metadataPath,
147
195
  receiptPath,
148
196
  startedAt: Date.now(),
197
+ verificationProfile,
198
+ worktreePath,
199
+ buildProducing: buildProducingOverride ?? isBuildProducingManagedCommand(command, args),
200
+ buildReceiptReuse,
149
201
  };
150
- if (startManagedCommandNames.has(name) || timeoutMs > maximumSynchronousWaitMs) {
202
+ if (forceAsync || timeoutMs > maximumSynchronousWaitMs) {
151
203
  const started = managedCommandResult({
152
204
  ...invocation,
153
205
  timedOut: false,
@@ -161,16 +213,198 @@ export async function callManagedAttemptTool(name, input, worktreePath) {
161
213
  managedCommandTasks.set(commandId, task);
162
214
  void task.finally(() => managedCommandTasks.delete(commandId)).catch(() => undefined);
163
215
  return {
164
- ...started,
216
+ ...managedCommandPublicResult(started),
165
217
  content: [{
166
218
  type: "text",
167
- text: `Forge managed command ${commandId} started asynchronously. Continue independent work, then call forge.wait_managed_command once with this command_id for the terminal receipt.`,
219
+ 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.`,
168
220
  }],
169
221
  };
170
222
  }
171
223
  return await executeManagedCommand(invocation);
172
224
  }
225
+ async function startManagedVerification(input, worktreePath) {
226
+ const record = asRecord(input);
227
+ const profile = requiredInputString(record, "profile");
228
+ const shardCount = optionalBoundedInteger(record.shard_count, "shard_count", 1, 32, 1);
229
+ const shardIndex = optionalBoundedInteger(record.shard_index, "shard_index", 0, 31, 0);
230
+ if (shardIndex >= shardCount) {
231
+ throw new Error("managed_verification_invalid: shard_index must be less than shard_count.");
232
+ }
233
+ const noBuild = optionalBoolean(record.no_build, "no_build", false);
234
+ const runAllShards = optionalBoolean(record.run_all_shards, "run_all_shards", false);
235
+ const maxParallelism = optionalBoundedInteger(record.max_parallelism, "max_parallelism", 1, 4, 2);
236
+ if (runAllShards && shardCount < 2) {
237
+ throw new Error("managed_verification_invalid: run_all_shards requires shard_count of at least 2.");
238
+ }
239
+ if (runAllShards && shardIndex !== 0) {
240
+ throw new Error("managed_verification_invalid: shard_index must be 0 when run_all_shards is enabled.");
241
+ }
242
+ if (runAllShards && noBuild) {
243
+ throw new Error("managed_verification_invalid: run_all_shards owns the single prerequisite build and cannot be combined with no_build.");
244
+ }
245
+ const outputDirectory = requiredAttemptEnvironment("FORGE_TEST_OUTPUT_DIR");
246
+ const profileCommand = buildManagedVerificationCommand(profile, outputDirectory, shardCount, shardIndex, noBuild, runAllShards, maxParallelism);
247
+ const buildReceiptReuse = noBuild
248
+ ? await verifyManagedBuildReceipt({
249
+ worktreePath,
250
+ configuration: managedBuildConfiguration(profileCommand.command, profileCommand.args),
251
+ requiredCoverage: managedVerificationBuildCoverage,
252
+ })
253
+ : undefined;
254
+ return await launchManagedCommand({
255
+ command: profileCommand.command,
256
+ args: profileCommand.args,
257
+ timeout_ms: record.timeout_ms ?? profileCommand.timeoutMs,
258
+ }, worktreePath, true, profile, !noBuild, buildReceiptReuse);
259
+ }
260
+ export function buildManagedVerificationCommand(profile, outputDirectory, shardCount, shardIndex, noBuild, runAllShards = false, maxParallelism = 2) {
261
+ if (profile === "dotnet_affected") {
262
+ // The tests an uncommitted change can affect: one build, Unit and Contract whole, then
263
+ // only the affected heavier-tier classes. The script owns its build and its selection.
264
+ if (runAllShards || shardCount !== 1 || shardIndex !== 0 || noBuild) {
265
+ throw new Error("managed_verification_invalid: dotnet_affected selects its own tests and owns its build; it accepts no sharding or no_build.");
266
+ }
267
+ return {
268
+ command: process.execPath,
269
+ args: ["scripts/run-affected-dotnet-tests.mjs", "--results-directory", outputDirectory],
270
+ // A change that reaches tests without naming a type runs the heavier tiers whole.
271
+ timeoutMs: managedMaximumCommandTimeoutMs,
272
+ };
273
+ }
274
+ if (profile === "dotnet_solution") {
275
+ if (runAllShards) {
276
+ throw new Error("managed_verification_invalid: dotnet_solution does not support run_all_shards; select a test tier instead.");
277
+ }
278
+ if (shardCount !== 1 || shardIndex !== 0) {
279
+ throw new Error("managed_verification_invalid: dotnet_solution does not accept sharding; select a test tier instead.");
280
+ }
281
+ const command = preferredPowerShellExecutable();
282
+ const args = [
283
+ "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "scripts/run-dotnet-test-suite.ps1",
284
+ "-ResultsDirectory", outputDirectory,
285
+ "-MaxParallelism", String(maxParallelism),
286
+ ];
287
+ if (noBuild)
288
+ args.push("-NoBuild");
289
+ return { command, args, timeoutMs: managedLongTierTimeoutMs };
290
+ }
291
+ const tiers = {
292
+ dotnet_unit: "Unit",
293
+ dotnet_contract: "Contract",
294
+ dotnet_api_component: "ApiComponent",
295
+ dotnet_orchestration: "Orchestration",
296
+ dotnet_infrastructure: "Infrastructure",
297
+ };
298
+ const tier = tiers[profile];
299
+ if (!tier)
300
+ throw new Error(`managed_verification_invalid: unsupported profile ${profile}.`);
301
+ const command = preferredPowerShellExecutable();
302
+ if (runAllShards) {
303
+ return {
304
+ command,
305
+ args: [
306
+ "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "scripts/run-dotnet-test-tier-set.ps1",
307
+ "-Tier", tier,
308
+ "-ResultsDirectory", outputDirectory,
309
+ "-ShardCount", String(shardCount),
310
+ "-MaxParallelism", String(maxParallelism),
311
+ ],
312
+ timeoutMs: managedVerificationTierTimeoutMs(tier, { shardCount, maxParallelism }),
313
+ };
314
+ }
315
+ const args = [
316
+ "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "scripts/run-dotnet-test-tier.ps1",
317
+ "-Tier", tier,
318
+ "-ResultsDirectory", outputDirectory,
319
+ "-ShardCount", String(shardCount),
320
+ "-ShardIndex", String(shardIndex),
321
+ ];
322
+ if (noBuild)
323
+ args.push("-NoBuild");
324
+ return { command, args, timeoutMs: managedVerificationTierTimeoutMs(tier) };
325
+ }
326
+ export function preferredPowerShellExecutable(platform = process.platform, pathValue = process.env.PATH ?? "") {
327
+ if (platform !== "win32")
328
+ return "pwsh";
329
+ return resolveWindowsCommandFromPath("pwsh.exe", pathValue) ?? "powershell.exe";
330
+ }
331
+ export function validatePlaywrightProjectName(project, availableProjects) {
332
+ if (!availableProjects.includes(project)) {
333
+ throw new Error(`managed_command_capability_unavailable: Playwright project '${project}' is not advertised by the selected config. Available projects: ${availableProjects.join(", ") || "none"}.`);
334
+ }
335
+ }
336
+ async function validatePlaywrightCommandProject(command, args, cwd) {
337
+ const invocation = [path.basename(command).toLowerCase(), ...args.map(argument => argument.toLowerCase())];
338
+ if (!invocation.some(argument => argument.includes("playwright")))
339
+ return;
340
+ const project = optionValue(args, "--project");
341
+ if (!project)
342
+ return;
343
+ const configuredPath = optionValue(args, "--config") ?? process.env.FORGE_PLAYWRIGHT_CONFIG;
344
+ const candidates = configuredPath
345
+ ? [path.resolve(cwd, configuredPath)]
346
+ : ["playwright.config.ts", "playwright.config.js", "playwright.config.mjs"].map(file => path.join(cwd, file));
347
+ for (const candidate of candidates) {
348
+ try {
349
+ const source = await fs.readFile(candidate, "utf8");
350
+ const projects = playwrightProjectNamesFromConfigSource(source);
351
+ validatePlaywrightProjectName(project, projects);
352
+ return;
353
+ }
354
+ catch (error) {
355
+ if (error.code !== "ENOENT")
356
+ throw error;
357
+ }
358
+ }
359
+ throw new Error("managed_command_capability_unavailable: Playwright config was not found; project selection cannot be validated.");
360
+ }
361
+ export function playwrightProjectNamesFromConfigSource(source) {
362
+ const marker = source.match(/^\/\/ forge-playwright-projects: (\[[^\r\n]*\])$/mu)?.[1];
363
+ if (!marker) {
364
+ return [...source.matchAll(/\bname\s*:\s*["']([^"']+)["']/gu)].map(match => match[1]);
365
+ }
366
+ try {
367
+ const parsed = JSON.parse(marker);
368
+ return Array.isArray(parsed)
369
+ ? parsed.filter((value) => typeof value === "string" && value.trim().length > 0)
370
+ : [];
371
+ }
372
+ catch {
373
+ return [];
374
+ }
375
+ }
376
+ function optionValue(args, name) {
377
+ const direct = args.find(argument => argument.startsWith(`${name}=`));
378
+ if (direct)
379
+ return direct.slice(name.length + 1).trim() || null;
380
+ const index = args.indexOf(name);
381
+ return index >= 0 ? args[index + 1]?.trim() || null : null;
382
+ }
173
383
  async function executeManagedCommand(invocation) {
384
+ if (!invocation.buildProducing)
385
+ return await executeManagedCommandProcess(invocation);
386
+ return await new ManagedBuildCoordinator().withBuildSlot(invocation.worktreePath, async (buildQueueWaitMs) => await executeManagedCommandProcess({ ...invocation, buildQueueWaitMs }), async () => {
387
+ const waiting = managedCommandResult({
388
+ ...invocation,
389
+ timedOut: false,
390
+ status: "waiting_for_build_slot",
391
+ exitCode: null,
392
+ stdout: "",
393
+ stderr: "",
394
+ });
395
+ await persistManagedCommandReceipt(invocation.receiptPath, waiting.structuredContent);
396
+ });
397
+ }
398
+ async function executeManagedCommandProcess(invocation) {
399
+ assertHostDatabaseCapabilityBoundary();
400
+ const databaseEnvironment = invocation.verificationProfile
401
+ ? await managedVerificationDatabaseEnvironment()
402
+ : {};
403
+ const exactSecrets = [
404
+ ...Object.values(databaseEnvironment).filter(value => value !== nativeTestPostgresFallbackDatabaseMode),
405
+ // The attempt's brokered database login reaches managed commands; their output never shows it.
406
+ ...attemptDatabaseCapabilitySecrets(process.env[attemptDatabaseCapabilityEnvironment]),
407
+ ];
174
408
  const environment = inheritedEnvironment({
175
409
  FORGE_RUNTIME_MODE: "host",
176
410
  FORGE_RESOURCE_LEASE_ID: requiredAttemptEnvironment("FORGE_RESOURCE_LEASE_ID"),
@@ -183,6 +417,17 @@ async function executeManagedCommand(invocation) {
183
417
  FORGE_TEST_OUTPUT_DIR: requiredAttemptEnvironment("FORGE_TEST_OUTPUT_DIR"),
184
418
  FORGE_PROCESS_SUPERVISOR_ID: `managed-${invocation.commandId.replaceAll("-", "")}`,
185
419
  FORGE_PROCESS_SUPERVISOR_METADATA_PATH: invocation.metadataPath,
420
+ ...(invocation.verificationProfile
421
+ ? {
422
+ FORGE_MANAGED_VERIFICATION_PROFILE: invocation.verificationProfile,
423
+ // Managed verification executes mutable worktree scripts, SDK targets,
424
+ // and test assemblies. It uses an attempt-owned container, or the
425
+ // dedicated native test cluster while Docker is unavailable; it never
426
+ // receives a capability for a server that holds real data.
427
+ FORGE_MANAGED_VERIFICATION_DATABASE_MODE: "container_only",
428
+ ...databaseEnvironment,
429
+ }
430
+ : {}),
186
431
  });
187
432
  let stdout = "";
188
433
  let stderr = "";
@@ -191,8 +436,29 @@ async function executeManagedCommand(invocation) {
191
436
  let timer = null;
192
437
  let outputRevision = 0;
193
438
  let lastOutputAt = null;
439
+ let lastOutputStream = null;
194
440
  let lastProgressPersistedAt = 0;
195
441
  let progressPersistence = Promise.resolve();
442
+ let progressHeartbeat = null;
443
+ let progressFinished = false;
444
+ const persistProgress = (force = false) => {
445
+ if (progressFinished)
446
+ return progressPersistence;
447
+ const next = persistManagedCommandProgressIfDue(invocation, () => ({ stdout, stderr, outputRevision, lastOutputAt, lastOutputStream }), progressPersistence, () => lastProgressPersistedAt, value => { lastProgressPersistedAt = value; }, force);
448
+ // A heartbeat write is best-effort: one failed write (a locked or full disk) must
449
+ // not poison the chain, freeze later heartbeats, or fail a command that succeeded.
450
+ progressPersistence = force ? next.catch(() => undefined) : next;
451
+ return progressPersistence;
452
+ };
453
+ // Stop progress writes and let any in flight land before a terminal receipt is
454
+ // written, so a late "running" snapshot can never replace it.
455
+ const settleProgress = async () => {
456
+ progressFinished = true;
457
+ if (progressHeartbeat)
458
+ clearInterval(progressHeartbeat);
459
+ progressHeartbeat = null;
460
+ await progressPersistence;
461
+ };
196
462
  const launchTimer = setTimeout(() => {
197
463
  timedOut = true;
198
464
  controller.abort(new Error(`managed_command_launch_timeout: process supervisor was not ready within ${managedCommandLaunchTimeoutMs}ms.`));
@@ -216,30 +482,51 @@ async function executeManagedCommand(invocation) {
216
482
  stderr,
217
483
  outputRevision,
218
484
  lastOutputAt,
485
+ lastOutputStream,
219
486
  });
220
487
  await persistManagedCommandReceipt(invocation.receiptPath, running.structuredContent);
221
488
  timer = setTimeout(() => {
222
489
  timedOut = true;
223
490
  controller.abort(new Error(`managed_command_timeout: exceeded ${invocation.timeoutMs}ms.`));
224
491
  }, invocation.timeoutMs);
492
+ // Output alone does not keep the receipt current: a test tier can print a burst
493
+ // and then run quietly for many minutes. Without this heartbeat the elapsed time
494
+ // and the burst's last lines freeze, and a waiting agent sees a hung command.
495
+ // The run can already have failed while the "running" write above was pending.
496
+ if (progressFinished)
497
+ return;
498
+ progressHeartbeat = setInterval(() => {
499
+ void persistProgress(true);
500
+ }, managedCommandProgressPersistIntervalMs);
501
+ progressHeartbeat.unref();
225
502
  },
226
503
  onStdoutLine: (line) => {
227
- stdout = appendBounded(stdout, line);
504
+ stdout = appendBounded(stdout, redactText(line, exactSecrets));
228
505
  outputRevision += 1;
229
506
  lastOutputAt = Date.now();
230
- progressPersistence = persistManagedCommandProgressIfDue(invocation, () => ({ stdout, stderr, outputRevision, lastOutputAt }), progressPersistence, () => lastProgressPersistedAt, value => { lastProgressPersistedAt = value; });
231
- return progressPersistence;
507
+ lastOutputStream = "stdout";
508
+ return persistProgress();
232
509
  },
233
510
  onStderrLine: (line) => {
234
- stderr = appendBounded(stderr, line);
511
+ stderr = appendBounded(stderr, redactText(line, exactSecrets));
235
512
  outputRevision += 1;
236
513
  lastOutputAt = Date.now();
237
- progressPersistence = persistManagedCommandProgressIfDue(invocation, () => ({ stdout, stderr, outputRevision, lastOutputAt }), progressPersistence, () => lastProgressPersistedAt, value => { lastProgressPersistedAt = value; });
238
- return progressPersistence;
514
+ lastOutputStream = "stderr";
515
+ return persistProgress();
239
516
  },
240
517
  }, controller.signal);
241
- await progressPersistence;
518
+ await settleProgress();
242
519
  const status = timedOut ? "timed_out" : result.exitCode === 0 ? "pass" : "fail";
520
+ const receiptCoverage = managedBuildReceiptCoverage(invocation);
521
+ const buildReceipt = status === "pass" && receiptCoverage
522
+ ? await publishManagedBuildReceipt({
523
+ worktreePath: invocation.worktreePath,
524
+ command: invocation.command,
525
+ args: invocation.args,
526
+ configuration: managedBuildConfiguration(invocation.command, invocation.args),
527
+ coverage: receiptCoverage,
528
+ })
529
+ : invocation.buildReceiptReuse;
243
530
  const receipt = managedCommandResult({
244
531
  ...invocation,
245
532
  timedOut,
@@ -249,13 +536,21 @@ async function executeManagedCommand(invocation) {
249
536
  stderr,
250
537
  outputRevision,
251
538
  lastOutputAt,
539
+ lastOutputStream,
540
+ buildReceipt,
541
+ buildReceiptReused: invocation.buildReceiptReuse !== undefined,
252
542
  });
253
543
  await persistManagedCommandReceipt(invocation.receiptPath, receipt.structuredContent);
254
- return receipt;
544
+ return managedCommandPublicResult(receipt);
255
545
  }
256
546
  catch (error) {
257
- const message = error instanceof Error ? error.message : String(error);
547
+ // The failure may be a progress write itself; it is reported below.
548
+ await settleProgress().catch(() => undefined);
549
+ const message = redactText(error instanceof Error ? error.message : String(error), exactSecrets);
258
550
  stderr = appendBounded(stderr, message);
551
+ outputRevision += 1;
552
+ lastOutputAt = Date.now();
553
+ lastOutputStream = "stderr";
259
554
  const receipt = managedCommandResult({
260
555
  ...invocation,
261
556
  timedOut,
@@ -265,14 +560,17 @@ async function executeManagedCommand(invocation) {
265
560
  stderr,
266
561
  outputRevision,
267
562
  lastOutputAt,
563
+ lastOutputStream,
268
564
  });
269
565
  await persistManagedCommandReceipt(invocation.receiptPath, receipt.structuredContent);
270
- return receipt;
566
+ return managedCommandPublicResult(receipt);
271
567
  }
272
568
  finally {
273
569
  clearTimeout(launchTimer);
274
570
  if (timer)
275
571
  clearTimeout(timer);
572
+ if (progressHeartbeat)
573
+ clearInterval(progressHeartbeat);
276
574
  }
277
575
  }
278
576
  async function readManagedCommand(input) {
@@ -291,7 +589,7 @@ async function readManagedCommand(input) {
291
589
  const status = typeof receipt.status === "string" ? receipt.status : "unknown";
292
590
  const stdout = typeof receipt.stdout === "string" ? receipt.stdout : "";
293
591
  const stderr = typeof receipt.stderr === "string" ? receipt.stderr : "";
294
- return {
592
+ return managedCommandPublicResult({
295
593
  content: [{
296
594
  type: "text",
297
595
  text: terminalManagedCommandStatuses.has(status)
@@ -307,12 +605,13 @@ async function readManagedCommand(input) {
307
605
  }],
308
606
  structuredContent: receipt,
309
607
  isError: terminalManagedCommandStatuses.has(status) && status !== "pass",
310
- };
608
+ });
311
609
  }
312
610
  async function waitManagedCommand(input) {
313
611
  const record = asRecord(input);
314
612
  const commandId = validatedCommandId(record);
315
- const waitTimeoutMs = optionalWaitTimeout(record.wait_timeout_ms);
613
+ const requestedWaitTimeoutMs = optionalWaitTimeout(record.wait_timeout_ms);
614
+ const waitTimeoutMs = Math.min(requestedWaitTimeoutMs, maximumSynchronousWaitMs);
316
615
  const deadline = Date.now() + waitTimeoutMs;
317
616
  const receiptPath = managedCommandReceiptPath(requiredAttemptEnvironment("FORGE_LOG_DIR"), commandId);
318
617
  const task = managedCommandTasks.get(commandId);
@@ -332,12 +631,35 @@ async function waitManagedCommand(input) {
332
631
  await delay(Math.min(managedCommandReceiptPollIntervalMs, Math.max(1, deadline - Date.now())));
333
632
  }
334
633
  const receipt = await readManagedCommand({ command_id: commandId });
634
+ // Only a running receipt is refreshed on a heartbeat; queued or starting receipts are
635
+ // written once, so their elapsed time says nothing about progress.
636
+ const elapsedMs = receipt.structuredContent.elapsed_ms;
637
+ const timeoutMs = receipt.structuredContent.timeout_ms;
638
+ const lastOutputAt = Date.parse(String(receipt.structuredContent.last_output_at));
639
+ const progress = receipt.structuredContent.status === "running" && typeof elapsedMs === "number"
640
+ ? ` It has run for ${Math.round(elapsedMs / 1000)}s`
641
+ + (Number.isFinite(lastOutputAt) ? `, last output ${Math.max(0, Math.round((Date.now() - lastOutputAt) / 1000))}s ago` : ", with no output yet")
642
+ + `. Forge refreshes this receipt every ${managedCommandProgressPersistIntervalMs / 1000}s, so a rising elapsed time means the command is still alive even when it is quiet.`
643
+ // Agents otherwise abandon long verification tiers and record a blocker that is not one.
644
+ + (typeof timeoutMs === "number"
645
+ ? ` Forge itself ends it at its ${Math.round(timeoutMs / 60_000)}-minute limit if it hangs, so keep waiting; a running command is not a blocker.`
646
+ : "")
647
+ : "";
335
648
  return {
336
649
  ...receipt,
337
650
  content: [{
338
651
  type: "text",
339
- text: `Forge is still supervising managed command ${commandId} after waiting ${waitTimeoutMs}ms. Its durable receipt remains at ${receiptPath}.`,
652
+ text: `Forge is still supervising managed command ${commandId} after this ${waitTimeoutMs}ms transport-safe wait.${progress} Continue independent work or call forge.wait_managed_command again after ${managedCommandRetryAfterMs}ms. Its durable receipt remains at ${receiptPath}.`,
340
653
  }],
654
+ structuredContent: {
655
+ ...receipt.structuredContent,
656
+ command_continues: true,
657
+ terminal: false,
658
+ wait_timeout_ms: waitTimeoutMs,
659
+ requested_wait_timeout_ms: requestedWaitTimeoutMs,
660
+ retry_after_ms: managedCommandRetryAfterMs,
661
+ },
662
+ isError: false,
341
663
  };
342
664
  }
343
665
  function managedCommandReceiptPath(logDirectory, commandId) {
@@ -403,16 +725,70 @@ function managedCommandResult(values) {
403
725
  || values.stderr.length >= maximumCapturedCharacters,
404
726
  output_revision: values.outputRevision ?? 0,
405
727
  last_output_at: values.lastOutputAt ? new Date(values.lastOutputAt).toISOString() : null,
728
+ last_output_stream: values.lastOutputStream ?? null,
406
729
  supervisor_metadata_path: values.metadataPath,
407
730
  receipt_path: values.receiptPath,
731
+ verification_profile: values.verificationProfile ?? null,
732
+ verification_profile_runner_authored: values.verificationProfile !== undefined,
733
+ build_producing: values.buildProducing,
734
+ build_coordination: values.buildProducing ? "per_worktree_exclusive" : null,
735
+ build_queue_wait_ms: values.buildQueueWaitMs ?? 0,
736
+ build_receipt_reused: values.buildReceiptReused ?? false,
737
+ build_receipt: values.buildReceipt ? {
738
+ fingerprint: values.buildReceipt.fingerprint,
739
+ receipt_path: values.buildReceipt.receiptPath,
740
+ producer_command: values.buildReceipt.producerCommand,
741
+ configuration: values.buildReceipt.configuration,
742
+ coverage: values.buildReceipt.coverage,
743
+ output_count: values.buildReceipt.outputCount,
744
+ } : null,
745
+ runtime_configuration: managedRuntimeConfiguration(),
408
746
  process_tree_terminated: values.status === "timed_out" ? true : values.timedOut ? false : undefined,
409
747
  },
410
748
  isError: isTerminal && values.status !== "pass",
411
749
  };
412
750
  }
413
- function persistManagedCommandProgressIfDue(invocation, snapshot, previousPersistence, lastPersistedAt, setLastPersistedAt) {
751
+ function managedRuntimeConfiguration() {
752
+ const providerOnlySpecs = process.env.FORGE_CI_REPAIR_PROVIDER_ONLY_SPECS?.trim();
753
+ let providerOnlySpecCount = 0;
754
+ if (providerOnlySpecs) {
755
+ try {
756
+ const parsed = JSON.parse(providerOnlySpecs);
757
+ providerOnlySpecCount = Array.isArray(parsed)
758
+ ? parsed.filter(value => typeof value === "string" && value.length > 0).length
759
+ : 0;
760
+ }
761
+ catch {
762
+ providerOnlySpecCount = 0;
763
+ }
764
+ }
765
+ return {
766
+ attempt_database_schema: process.env.FORGE_ATTEMPT_DATABASE_SCHEMA ?? null,
767
+ attempt_database_readiness_path: process.env.FORGE_ATTEMPT_DATABASE_READINESS_PATH ?? null,
768
+ playwright_config: process.env.FORGE_PLAYWRIGHT_CONFIG ?? null,
769
+ provider_only_spec_count: providerOnlySpecCount,
770
+ provider_only_spec_fingerprint: providerOnlySpecs
771
+ ? `sha256:${createHash("sha256").update(providerOnlySpecs).digest("hex")}`
772
+ : null,
773
+ };
774
+ }
775
+ function managedCommandPublicResult(result) {
776
+ const stdout = typeof result.structuredContent.stdout === "string" ? result.structuredContent.stdout : "";
777
+ const stderr = typeof result.structuredContent.stderr === "string" ? result.structuredContent.stderr : "";
778
+ const { stdout: _stdout, stderr: _stderr, ...receipt } = result.structuredContent;
779
+ return {
780
+ ...result,
781
+ structuredContent: {
782
+ ...receipt,
783
+ stdout_tail: tail(stdout, maximumReturnedOutputCharacters),
784
+ stderr_tail: tail(stderr, maximumReturnedOutputCharacters),
785
+ full_output_in_receipt: true,
786
+ },
787
+ };
788
+ }
789
+ function persistManagedCommandProgressIfDue(invocation, snapshot, previousPersistence, lastPersistedAt, setLastPersistedAt, force = false) {
414
790
  const now = Date.now();
415
- if (now - lastPersistedAt() < managedCommandProgressPersistIntervalMs)
791
+ if (!force && now - lastPersistedAt() < managedCommandProgressPersistIntervalMs)
416
792
  return previousPersistence;
417
793
  setLastPersistedAt(now);
418
794
  const current = snapshot();
@@ -447,12 +823,75 @@ function inheritedEnvironment(overrides) {
447
823
  }
448
824
  return { ...env, ...overrides };
449
825
  }
826
+ /**
827
+ * Runner-authored verification normally uses its attempt-owned Testcontainers
828
+ * database. Only when the Docker engine is unavailable does it switch to the
829
+ * operator-provisioned native test cluster, which holds no real data.
830
+ */
831
+ export async function managedVerificationDatabaseEnvironment(dockerAvailable = async () => (await new DockerEngineProbe().refresh(true)).available, readFallback = () => {
832
+ const configPath = process.env[nativeTestPostgresFallbackConfigEnvironment]?.trim();
833
+ return configPath ? readNativeTestPostgresFallback(configPath) : null;
834
+ }) {
835
+ const fallback = readFallback();
836
+ if (!fallback || await dockerAvailable())
837
+ return {};
838
+ return nativeTestPostgresFallbackEnvironment(fallback);
839
+ }
840
+ export function isBuildProducingManagedCommand(command, args) {
841
+ const executable = path.basename(command).toLowerCase();
842
+ if (executable === "dotnet" || executable === "dotnet.exe") {
843
+ const verb = args.find(argument => !argument.startsWith("-"))?.toLowerCase();
844
+ if (!verb || !new Set(["build", "test", "restore", "publish", "pack", "msbuild"]).has(verb))
845
+ return false;
846
+ return verb === "restore" || !args.some(argument => argument.toLowerCase() === "--no-build");
847
+ }
848
+ if (["powershell", "powershell.exe", "pwsh", "pwsh.exe"].includes(executable)) {
849
+ const fileIndex = args.findIndex(argument => argument.toLowerCase() === "-file");
850
+ const script = fileIndex >= 0 ? path.basename(args[fileIndex + 1] ?? "").toLowerCase() : "";
851
+ if (script === "run-dotnet-test-tier-set.ps1" || script === "run-dotnet-test-suite.ps1") {
852
+ return !args.some(argument => argument.toLowerCase() === "-nobuild");
853
+ }
854
+ if (script === "run-dotnet-test-tier.ps1") {
855
+ return !args.some(argument => argument.toLowerCase() === "-nobuild");
856
+ }
857
+ }
858
+ return false;
859
+ }
860
+ function managedBuildReceiptCoverage(invocation) {
861
+ if (!invocation.buildProducing)
862
+ return null;
863
+ if (invocation.verificationProfile)
864
+ return managedVerificationBuildCoverage;
865
+ const executable = path.basename(invocation.command).toLowerCase();
866
+ if (executable === "dotnet" || executable === "dotnet.exe") {
867
+ const verb = invocation.args.find(argument => !argument.startsWith("-"))?.toLowerCase();
868
+ if (!verb || !new Set(["build", "test"]).has(verb))
869
+ return null;
870
+ const target = invocation.args.find(argument => /\.(?:sln|slnx|csproj)$/iu.test(argument));
871
+ if (target && (/\.(?:sln|slnx)$/iu.test(target) || /(?:^|[\\/])Forge\.Api\.Tests\.csproj$/iu.test(target))) {
872
+ return managedVerificationBuildCoverage;
873
+ }
874
+ return null;
875
+ }
876
+ if (["powershell", "powershell.exe", "pwsh", "pwsh.exe"].includes(executable)) {
877
+ const fileIndex = invocation.args.findIndex(argument => argument.toLowerCase() === "-file");
878
+ const script = fileIndex >= 0 ? path.basename(invocation.args[fileIndex + 1] ?? "").toLowerCase() : "";
879
+ if (["run-dotnet-test-tier.ps1", "run-dotnet-test-tier-set.ps1", "run-dotnet-test-suite.ps1"].includes(script)
880
+ && !invocation.args.some(argument => argument.toLowerCase() === "-nobuild")) {
881
+ return managedVerificationBuildCoverage;
882
+ }
883
+ }
884
+ return null;
885
+ }
450
886
  function looksSensitiveEnvironmentName(name) {
451
887
  const normalized = name.trim().toUpperCase();
452
888
  return normalized.includes("SECRET")
453
889
  || normalized.includes("TOKEN")
454
890
  || normalized.includes("PASSWORD")
455
891
  || normalized.includes("CREDENTIAL")
892
+ || normalized.includes("CONNECTION_STRING")
893
+ || normalized.startsWith("CONNECTIONSTRINGS__")
894
+ || normalized.startsWith("CONNECTIONSTRINGS:")
456
895
  || normalized.endsWith("_API_KEY")
457
896
  || normalized === "OPENAI_API_KEY";
458
897
  }
@@ -499,12 +938,27 @@ function optionalTimeout(value) {
499
938
  }
500
939
  function optionalWaitTimeout(value) {
501
940
  if (value === undefined)
502
- return 7_200_000;
503
- if (!Number.isSafeInteger(value) || value < 1_000 || value > 7_200_000) {
504
- throw new Error("managed_command_invalid: wait_timeout_ms must be an integer from 1000 through 7200000.");
941
+ return maximumSynchronousWaitMs;
942
+ if (!Number.isSafeInteger(value) || value < 1_000 || value > maximumSynchronousWaitMs) {
943
+ throw new Error(`managed_command_invalid: wait_timeout_ms must be an integer from 1000 through ${maximumSynchronousWaitMs}.`);
505
944
  }
506
945
  return value;
507
946
  }
947
+ function optionalBoundedInteger(value, name, minimum, maximum, fallback) {
948
+ if (value === undefined)
949
+ return fallback;
950
+ if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
951
+ throw new Error(`managed_verification_invalid: ${name} must be an integer from ${minimum} through ${maximum}.`);
952
+ }
953
+ return value;
954
+ }
955
+ function optionalBoolean(value, name, fallback) {
956
+ if (value === undefined)
957
+ return fallback;
958
+ if (typeof value !== "boolean")
959
+ throw new Error(`managed_verification_invalid: ${name} must be a boolean.`);
960
+ return value;
961
+ }
508
962
  async function delay(ms) {
509
963
  await new Promise(resolve => setTimeout(resolve, ms));
510
964
  }