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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/README.md +78 -0
  2. package/dist/bridge/bridge.base.d.ts +6 -1
  3. package/dist/bridge/bridge.service.d.ts +60 -3
  4. package/dist/bridge/bridge.service.js +970 -24
  5. package/dist/bridge/bridge.types.d.ts +172 -1
  6. package/dist/cleanup/cleanup.service.d.ts +13 -2
  7. package/dist/cleanup/cleanup.service.js +435 -3
  8. package/dist/cleanup/cleanup.types.d.ts +10 -0
  9. package/dist/cleanup/index.d.ts +1 -1
  10. package/dist/cleanup/index.js +1 -1
  11. package/dist/cleanup/internals.d.ts +2 -2
  12. package/dist/cleanup/internals.js +1 -1
  13. package/dist/cli/cli.help.d.ts +2 -0
  14. package/dist/cli/cli.help.js +217 -0
  15. package/dist/cli/cli.service.d.ts +42 -2
  16. package/dist/cli/cli.service.js +357 -206
  17. package/dist/cli/local-config.d.ts +6 -0
  18. package/dist/cli/local-config.js +47 -3
  19. package/dist/runtime/attempt-node-runtime-launcher.d.ts +17 -0
  20. package/dist/runtime/attempt-node-runtime-launcher.js +101 -0
  21. package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
  22. package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
  23. package/dist/runtime/attempt-process-supervisor.d.ts +86 -0
  24. package/dist/runtime/attempt-process-supervisor.js +350 -0
  25. package/dist/runtime/generated-output-reset.d.ts +10 -0
  26. package/dist/runtime/generated-output-reset.js +103 -0
  27. package/dist/runtime/host-listener-inspector.d.ts +44 -0
  28. package/dist/runtime/host-listener-inspector.js +324 -0
  29. package/dist/runtime/host-port-broker.d.ts +76 -0
  30. package/dist/runtime/host-port-broker.js +333 -0
  31. package/dist/runtime/host-runtime-contract.d.ts +47 -0
  32. package/dist/runtime/host-runtime-contract.js +700 -0
  33. package/dist/runtime/host-runtime-lease.d.ts +133 -0
  34. package/dist/runtime/host-runtime-lease.js +1462 -0
  35. package/dist/runtime/host-runtime-promotion.d.ts +23 -0
  36. package/dist/runtime/host-runtime-promotion.js +159 -0
  37. package/dist/runtime/node-host-runtime-adapter.d.ts +51 -0
  38. package/dist/runtime/node-host-runtime-adapter.js +268 -0
  39. package/dist/runtime/playwright-host-runtime-adapter.d.ts +20 -0
  40. package/dist/runtime/playwright-host-runtime-adapter.js +220 -0
  41. package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
  42. package/dist/runtime/runtime-isolation-lease.js +158 -0
  43. package/dist/sandbox/command-runner.d.ts +4 -0
  44. package/dist/sandbox/command-runner.js +47 -0
  45. package/dist/sandbox/contract.d.ts +5 -0
  46. package/dist/sandbox/contract.js +276 -0
  47. package/dist/sandbox/event-budget.d.ts +11 -0
  48. package/dist/sandbox/event-budget.js +27 -0
  49. package/dist/sandbox/image-policy.d.ts +35 -0
  50. package/dist/sandbox/image-policy.js +247 -0
  51. package/dist/sandbox/index.d.ts +5 -0
  52. package/dist/sandbox/index.js +5 -0
  53. package/dist/sandbox/lease-watchdog.d.ts +18 -0
  54. package/dist/sandbox/lease-watchdog.js +67 -0
  55. package/dist/sandbox/preflight.d.ts +54 -0
  56. package/dist/sandbox/preflight.js +335 -0
  57. package/dist/sandbox/process.d.ts +20 -0
  58. package/dist/sandbox/process.js +209 -0
  59. package/dist/sandbox/provider-auth/codex.d.ts +26 -0
  60. package/dist/sandbox/provider-auth/codex.js +212 -0
  61. package/dist/sandbox/repository-lock.d.ts +5 -0
  62. package/dist/sandbox/repository-lock.js +119 -0
  63. package/dist/sandbox/runtime.d.ts +297 -0
  64. package/dist/sandbox/runtime.js +2338 -0
  65. package/dist/sandbox/sandbox.types.d.ts +86 -0
  66. package/dist/sandbox/sandbox.types.js +1 -0
  67. package/dist/sandbox/smoke.d.ts +76 -0
  68. package/dist/sandbox/smoke.js +380 -0
  69. package/dist/sandbox/worker-supervisor.d.ts +13 -0
  70. package/dist/sandbox/worker-supervisor.js +36 -0
  71. package/dist/sandbox/worker.d.ts +1 -0
  72. package/dist/sandbox/worker.js +225 -0
  73. package/dist/sandbox/workspace.service.d.ts +13 -0
  74. package/dist/sandbox/workspace.service.js +72 -0
  75. package/dist/session/managed-attempt-tools.d.ts +17 -0
  76. package/dist/session/managed-attempt-tools.js +712 -0
  77. package/dist/session/phase-completion.artifacts.d.ts +10 -0
  78. package/dist/session/phase-completion.artifacts.js +627 -0
  79. package/dist/session/phase-completion.d.ts +43 -46
  80. package/dist/session/phase-completion.helpers.d.ts +117 -0
  81. package/dist/session/phase-completion.helpers.js +701 -0
  82. package/dist/session/phase-completion.js +40 -3488
  83. package/dist/session/phase-completion.prompts.d.ts +14 -0
  84. package/dist/session/phase-completion.prompts.js +235 -0
  85. package/dist/session/phase-completion.runtime.d.ts +48 -0
  86. package/dist/session/phase-completion.runtime.js +786 -0
  87. package/dist/session/phase-completion.schemas.d.ts +3 -0
  88. package/dist/session/phase-completion.schemas.js +812 -0
  89. package/dist/session/phase-completion.validation.d.ts +37 -0
  90. package/dist/session/phase-completion.validation.js +850 -0
  91. package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
  92. package/dist/session/provider-cli/ci-repair-readiness.js +95 -0
  93. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +93 -0
  94. package/dist/session/provider-cli/provider-cli.artifacts.js +778 -0
  95. package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
  96. package/dist/session/provider-cli/provider-cli.deterministic-payloads.d.ts +12 -0
  97. package/dist/session/provider-cli/provider-cli.deterministic-payloads.js +279 -0
  98. package/dist/session/provider-cli/provider-cli.deterministic.d.ts +14 -0
  99. package/dist/session/provider-cli/provider-cli.deterministic.js +375 -0
  100. package/dist/session/provider-cli/provider-cli.process.d.ts +48 -0
  101. package/dist/session/provider-cli/provider-cli.process.js +474 -0
  102. package/dist/session/provider-cli/provider-cli.prompts.d.ts +47 -0
  103. package/dist/session/provider-cli/provider-cli.prompts.js +595 -0
  104. package/dist/session/provider-cli/provider-cli.qa-sidecars.d.ts +11 -0
  105. package/dist/session/provider-cli/provider-cli.qa-sidecars.js +66 -0
  106. package/dist/session/provider-cli/provider-cli.runtime.d.ts +9 -0
  107. package/dist/session/provider-cli/provider-cli.runtime.js +41 -0
  108. package/dist/session/provider-cli/provider-cli.service.d.ts +26 -53
  109. package/dist/session/provider-cli/provider-cli.service.js +524 -2571
  110. package/dist/session/provider-cli/provider-cli.types.d.ts +6 -0
  111. package/dist/session/provider-cli/provider-output-state.d.ts +22 -0
  112. package/dist/session/provider-cli/provider-output-state.js +473 -0
  113. package/dist/session/session-redaction.d.ts +3 -0
  114. package/dist/session/session-redaction.js +23 -0
  115. package/dist/session/session.base.d.ts +6 -1
  116. package/dist/session/session.service.d.ts +8 -2
  117. package/dist/session/session.service.js +480 -58
  118. package/dist/session/session.types.d.ts +161 -1
  119. package/dist/session/session.types.js +318 -3
  120. package/dist/shared/path/path.js +3 -1
  121. package/dist/shared/process/host-process-recovery.d.ts +12 -0
  122. package/dist/shared/process/host-process-recovery.js +78 -0
  123. package/dist/shared/process/host-process-supervisor.d.ts +57 -0
  124. package/dist/shared/process/host-process-supervisor.js +282 -0
  125. package/dist/shared/process/process.d.ts +1 -0
  126. package/dist/shared/process/process.js +1 -0
  127. package/dist/shared/process/windows-launch.d.ts +8 -0
  128. package/dist/shared/process/windows-launch.js +41 -0
  129. package/dist/shared/provider-capabilities.d.ts +1 -0
  130. package/dist/shared/provider-capabilities.js +3 -0
  131. package/dist/shared/redaction/redaction.js +10 -3
  132. package/dist/version.d.ts +1 -1
  133. package/dist/version.js +1 -1
  134. package/dist/worktree/artifact-injector.d.ts +3 -2
  135. package/dist/worktree/artifact-injector.js +82 -10
  136. package/dist/worktree/dependency-warmer.d.ts +21 -1
  137. package/dist/worktree/dependency-warmer.js +138 -5
  138. package/dist/worktree/index.d.ts +1 -0
  139. package/dist/worktree/index.js +1 -0
  140. package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
  141. package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
  142. package/dist/worktree/worktree-paths.d.ts +4 -0
  143. package/dist/worktree/worktree-paths.js +30 -0
  144. package/dist/worktree/worktree.base.d.ts +3 -0
  145. package/dist/worktree/worktree.service.d.ts +9 -3
  146. package/dist/worktree/worktree.service.js +100 -52
  147. package/dist/worktree/worktree.types.d.ts +3 -1
  148. package/package.json +6 -3
  149. package/runtime/windows-job-supervisor.ps1 +356 -0
  150. package/runtime/windows-listener-owner.ps1 +33 -0
@@ -0,0 +1,712 @@
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 { StreamingProviderProcessRunner } from "./provider-cli/provider-cli.process.js";
6
+ const managedCommandNames = new Set(["forge.run_managed_command", "run_managed_command"]);
7
+ const startManagedCommandNames = new Set(["forge.start_managed_command", "start_managed_command"]);
8
+ const getManagedCommandNames = new Set(["forge.get_managed_command", "get_managed_command"]);
9
+ const waitManagedCommandNames = new Set(["forge.wait_managed_command", "wait_managed_command"]);
10
+ const startManagedVerificationNames = new Set(["forge.start_managed_verification", "start_managed_verification"]);
11
+ const resetGeneratedOutputNames = new Set(["forge.reset_generated_outputs", "reset_generated_outputs"]);
12
+ const maximumCapturedCharacters = 200_000;
13
+ const maximumReturnedOutputCharacters = 4_000;
14
+ const maximumReportedFailedTests = 50;
15
+ const maximumSynchronousWaitMs = 240_000;
16
+ const managedCommandLaunchTimeoutMs = 60_000;
17
+ const managedCommandProgressPersistIntervalMs = 5_000;
18
+ const managedCommandReceiptPollIntervalMs = 1_000;
19
+ const managedCommandReceiptReplaceAttempts = 40;
20
+ const managedCommandReceiptReplaceRetryMs = 25;
21
+ const terminalManagedCommandStatuses = new Set([
22
+ "pass",
23
+ "fail",
24
+ "timed_out",
25
+ "timeout_termination_failed",
26
+ "runner_error",
27
+ ]);
28
+ const managedCommandTasks = new Map();
29
+ export const managedAttemptToolNames = [
30
+ "forge.run_managed_command",
31
+ "run_managed_command",
32
+ "forge.start_managed_command",
33
+ "start_managed_command",
34
+ "forge.get_managed_command",
35
+ "get_managed_command",
36
+ "forge.wait_managed_command",
37
+ "wait_managed_command",
38
+ "forge.start_managed_verification",
39
+ "start_managed_verification",
40
+ "forge.reset_generated_outputs",
41
+ "reset_generated_outputs",
42
+ ];
43
+ export function isManagedAttemptTool(name) {
44
+ return managedCommandNames.has(name)
45
+ || startManagedCommandNames.has(name)
46
+ || getManagedCommandNames.has(name)
47
+ || waitManagedCommandNames.has(name)
48
+ || startManagedVerificationNames.has(name)
49
+ || resetGeneratedOutputNames.has(name);
50
+ }
51
+ export function managedAttemptToolDefinition(name) {
52
+ if (startManagedVerificationNames.has(name)) {
53
+ return {
54
+ name,
55
+ 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 call forge.wait_managed_command once.",
56
+ inputSchema: {
57
+ type: "object",
58
+ additionalProperties: false,
59
+ required: ["profile"],
60
+ properties: {
61
+ profile: {
62
+ type: "string",
63
+ enum: ["dotnet_unit", "dotnet_contract", "dotnet_api_component", "dotnet_orchestration", "dotnet_infrastructure", "dotnet_solution"],
64
+ },
65
+ shard_count: { type: "integer", minimum: 1, maximum: 32, default: 1 },
66
+ shard_index: { type: "integer", minimum: 0, maximum: 31, default: 0 },
67
+ no_build: { type: "boolean", default: false },
68
+ timeout_ms: { type: "integer", minimum: 1000, maximum: 7200000 },
69
+ },
70
+ },
71
+ };
72
+ }
73
+ if (managedCommandNames.has(name) || startManagedCommandNames.has(name)) {
74
+ return {
75
+ name,
76
+ description: startManagedCommandNames.has(name)
77
+ ? "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."
78
+ : "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.",
79
+ inputSchema: {
80
+ type: "object",
81
+ additionalProperties: false,
82
+ required: ["command", "args"],
83
+ properties: {
84
+ command: { type: "string", minLength: 1 },
85
+ args: { type: "array", items: { type: "string" } },
86
+ cwd: { type: "string", description: "Worktree-relative directory; defaults to the worktree root." },
87
+ timeout_ms: { type: "integer", minimum: 1000, maximum: 7200000, default: 2700000 },
88
+ },
89
+ },
90
+ };
91
+ }
92
+ if (getManagedCommandNames.has(name)) {
93
+ return {
94
+ name,
95
+ description: "Read the durable status and final receipt for a command started by Forge's managed command runner.",
96
+ inputSchema: {
97
+ type: "object",
98
+ additionalProperties: false,
99
+ required: ["command_id"],
100
+ properties: {
101
+ command_id: { type: "string", minLength: 1 },
102
+ },
103
+ },
104
+ };
105
+ }
106
+ if (waitManagedCommandNames.has(name)) {
107
+ return {
108
+ name,
109
+ description: "Wait once for a command started by Forge's managed command runner to reach a terminal state. Live progress remains in the durable receipt/UI without repeated provider messages; this call returns the single terminal result.",
110
+ inputSchema: {
111
+ type: "object",
112
+ additionalProperties: false,
113
+ required: ["command_id"],
114
+ properties: {
115
+ command_id: { type: "string", minLength: 1 },
116
+ wait_timeout_ms: { type: "integer", minimum: 1000, maximum: 7200000, default: 7200000 },
117
+ },
118
+ },
119
+ };
120
+ }
121
+ return {
122
+ name,
123
+ description: "Quarantine known untracked generated test-output directories into this attempt's private output directory.",
124
+ inputSchema: { type: "object", additionalProperties: false, properties: {} },
125
+ };
126
+ }
127
+ export async function callManagedAttemptTool(name, input, worktreePath) {
128
+ if (resetGeneratedOutputNames.has(name)) {
129
+ const outputDirectory = requiredAttemptEnvironment("FORGE_TEST_OUTPUT_DIR");
130
+ const result = await resetGeneratedOutputs(worktreePath, outputDirectory);
131
+ return {
132
+ content: [{
133
+ type: "text",
134
+ text: result.quarantinedPaths.length === 0
135
+ ? "Forge found no stale generated output in the source worktree."
136
+ : `Forge quarantined ${result.quarantinedPaths.length} generated-output director${result.quarantinedPaths.length === 1 ? "y" : "ies"}: ${result.quarantinedPaths.join(", ")}.`,
137
+ }],
138
+ structuredContent: {
139
+ status: "pass",
140
+ quarantined_paths: result.quarantinedPaths,
141
+ quarantine_directory: result.quarantineDirectory,
142
+ },
143
+ };
144
+ }
145
+ if (getManagedCommandNames.has(name)) {
146
+ return await readManagedCommand(input);
147
+ }
148
+ if (waitManagedCommandNames.has(name)) {
149
+ return await waitManagedCommand(input);
150
+ }
151
+ if (startManagedVerificationNames.has(name)) {
152
+ return await startManagedVerification(input, worktreePath);
153
+ }
154
+ if (!managedCommandNames.has(name) && !startManagedCommandNames.has(name)) {
155
+ throw new Error(`Unknown managed attempt tool: ${name}`);
156
+ }
157
+ return await launchManagedCommand(asRecord(input), worktreePath, startManagedCommandNames.has(name));
158
+ }
159
+ async function launchManagedCommand(record, worktreePath, forceAsync, verificationProfile) {
160
+ const command = requiredInputString(record, "command");
161
+ const args = requiredStringArray(record, "args");
162
+ const timeoutMs = optionalTimeout(record.timeout_ms);
163
+ const cwd = await containedWorkingDirectory(worktreePath, record.cwd);
164
+ const logDirectory = requiredAttemptEnvironment("FORGE_LOG_DIR");
165
+ await fs.mkdir(path.join(logDirectory, "managed-commands"), { recursive: true, mode: 0o700 });
166
+ const commandId = randomUUID();
167
+ const metadataPath = path.join(logDirectory, "managed-commands", `${commandId}.json`);
168
+ const receiptPath = managedCommandReceiptPath(logDirectory, commandId);
169
+ const invocation = {
170
+ commandId,
171
+ command,
172
+ args,
173
+ cwd,
174
+ timeoutMs,
175
+ logDirectory,
176
+ metadataPath,
177
+ receiptPath,
178
+ startedAt: Date.now(),
179
+ verificationProfile,
180
+ };
181
+ if (forceAsync || timeoutMs > maximumSynchronousWaitMs) {
182
+ const started = managedCommandResult({
183
+ ...invocation,
184
+ timedOut: false,
185
+ status: "starting",
186
+ exitCode: null,
187
+ stdout: "",
188
+ stderr: "",
189
+ });
190
+ await persistManagedCommandReceipt(receiptPath, started.structuredContent);
191
+ const task = executeManagedCommand(invocation);
192
+ managedCommandTasks.set(commandId, task);
193
+ void task.finally(() => managedCommandTasks.delete(commandId)).catch(() => undefined);
194
+ return {
195
+ ...managedCommandPublicResult(started),
196
+ content: [{
197
+ type: "text",
198
+ 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.`,
199
+ }],
200
+ };
201
+ }
202
+ return await executeManagedCommand(invocation);
203
+ }
204
+ async function startManagedVerification(input, worktreePath) {
205
+ const record = asRecord(input);
206
+ const profile = requiredInputString(record, "profile");
207
+ const shardCount = optionalBoundedInteger(record.shard_count, "shard_count", 1, 32, 1);
208
+ const shardIndex = optionalBoundedInteger(record.shard_index, "shard_index", 0, 31, 0);
209
+ if (shardIndex >= shardCount) {
210
+ throw new Error("managed_verification_invalid: shard_index must be less than shard_count.");
211
+ }
212
+ const noBuild = optionalBoolean(record.no_build, "no_build", false);
213
+ const outputDirectory = requiredAttemptEnvironment("FORGE_TEST_OUTPUT_DIR");
214
+ const profileCommand = buildManagedVerificationCommand(profile, outputDirectory, shardCount, shardIndex, noBuild);
215
+ return await launchManagedCommand({
216
+ command: profileCommand.command,
217
+ args: profileCommand.args,
218
+ timeout_ms: record.timeout_ms ?? profileCommand.timeoutMs,
219
+ }, worktreePath, true, profile);
220
+ }
221
+ export function buildManagedVerificationCommand(profile, outputDirectory, shardCount, shardIndex, noBuild) {
222
+ if (profile === "dotnet_solution") {
223
+ if (shardCount !== 1 || shardIndex !== 0) {
224
+ throw new Error("managed_verification_invalid: dotnet_solution does not accept sharding; select a test tier instead.");
225
+ }
226
+ const args = [
227
+ "test", "Forge.sln", "--nologo", "-c", "Release",
228
+ "--results-directory", outputDirectory,
229
+ "--logger", "trx;LogFileName=forge-solution.trx",
230
+ "--blame-hang", "--blame-hang-timeout", "15m",
231
+ ];
232
+ if (noBuild)
233
+ args.push("--no-build");
234
+ return { command: "dotnet", args, timeoutMs: 2_700_000 };
235
+ }
236
+ const tiers = {
237
+ dotnet_unit: "Unit",
238
+ dotnet_contract: "Contract",
239
+ dotnet_api_component: "ApiComponent",
240
+ dotnet_orchestration: "Orchestration",
241
+ dotnet_infrastructure: "Infrastructure",
242
+ };
243
+ const tier = tiers[profile];
244
+ if (!tier)
245
+ throw new Error(`managed_verification_invalid: unsupported profile ${profile}.`);
246
+ const command = process.platform === "win32" ? "powershell.exe" : "pwsh";
247
+ const args = [
248
+ "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "scripts/run-dotnet-test-tier.ps1",
249
+ "-Tier", tier,
250
+ "-ResultsDirectory", outputDirectory,
251
+ "-ShardCount", String(shardCount),
252
+ "-ShardIndex", String(shardIndex),
253
+ ];
254
+ if (noBuild)
255
+ args.push("-NoBuild");
256
+ return { command, args, timeoutMs: tier === "Unit" || tier === "Contract" ? 900_000 : 2_700_000 };
257
+ }
258
+ async function executeManagedCommand(invocation) {
259
+ const environment = inheritedEnvironment({
260
+ FORGE_RUNTIME_MODE: "host",
261
+ FORGE_RESOURCE_LEASE_ID: requiredAttemptEnvironment("FORGE_RESOURCE_LEASE_ID"),
262
+ FORGE_DISPATCH_ATTEMPT_ID: requiredAttemptEnvironment("FORGE_DISPATCH_ATTEMPT_ID"),
263
+ // This nested command owns a distinct supervisor identity. A command-local
264
+ // fence avoids exposing the parent attempt's fencing capability to Codex's
265
+ // MCP subprocess configuration while preserving exact-tree cleanup proof.
266
+ FORGE_FENCING_TOKEN: randomUUID(),
267
+ FORGE_LOG_DIR: invocation.logDirectory,
268
+ FORGE_TEST_OUTPUT_DIR: requiredAttemptEnvironment("FORGE_TEST_OUTPUT_DIR"),
269
+ FORGE_PROCESS_SUPERVISOR_ID: `managed-${invocation.commandId.replaceAll("-", "")}`,
270
+ FORGE_PROCESS_SUPERVISOR_METADATA_PATH: invocation.metadataPath,
271
+ });
272
+ let stdout = "";
273
+ let stderr = "";
274
+ const controller = new AbortController();
275
+ let timedOut = false;
276
+ let timer = null;
277
+ let outputRevision = 0;
278
+ let lastOutputAt = null;
279
+ let lastOutputStream = null;
280
+ let lastProgressPersistedAt = 0;
281
+ let progressPersistence = Promise.resolve();
282
+ const launchTimer = setTimeout(() => {
283
+ timedOut = true;
284
+ controller.abort(new Error(`managed_command_launch_timeout: process supervisor was not ready within ${managedCommandLaunchTimeoutMs}ms.`));
285
+ }, managedCommandLaunchTimeoutMs);
286
+ try {
287
+ const result = await new StreamingProviderProcessRunner().run({
288
+ command: invocation.command,
289
+ args: invocation.args,
290
+ cwd: invocation.cwd,
291
+ env: environment,
292
+ captureOutput: false,
293
+ }, {
294
+ onStarted: async () => {
295
+ clearTimeout(launchTimer);
296
+ const running = managedCommandResult({
297
+ ...invocation,
298
+ timedOut: false,
299
+ status: "running",
300
+ exitCode: null,
301
+ stdout,
302
+ stderr,
303
+ outputRevision,
304
+ lastOutputAt,
305
+ lastOutputStream,
306
+ });
307
+ await persistManagedCommandReceipt(invocation.receiptPath, running.structuredContent);
308
+ timer = setTimeout(() => {
309
+ timedOut = true;
310
+ controller.abort(new Error(`managed_command_timeout: exceeded ${invocation.timeoutMs}ms.`));
311
+ }, invocation.timeoutMs);
312
+ },
313
+ onStdoutLine: (line) => {
314
+ stdout = appendBounded(stdout, line);
315
+ outputRevision += 1;
316
+ lastOutputAt = Date.now();
317
+ lastOutputStream = "stdout";
318
+ progressPersistence = persistManagedCommandProgressIfDue(invocation, () => ({ stdout, stderr, outputRevision, lastOutputAt, lastOutputStream }), progressPersistence, () => lastProgressPersistedAt, value => { lastProgressPersistedAt = value; });
319
+ return progressPersistence;
320
+ },
321
+ onStderrLine: (line) => {
322
+ stderr = appendBounded(stderr, line);
323
+ outputRevision += 1;
324
+ lastOutputAt = Date.now();
325
+ lastOutputStream = "stderr";
326
+ progressPersistence = persistManagedCommandProgressIfDue(invocation, () => ({ stdout, stderr, outputRevision, lastOutputAt, lastOutputStream }), progressPersistence, () => lastProgressPersistedAt, value => { lastProgressPersistedAt = value; });
327
+ return progressPersistence;
328
+ },
329
+ }, controller.signal);
330
+ await progressPersistence;
331
+ const status = timedOut ? "timed_out" : result.exitCode === 0 ? "pass" : "fail";
332
+ const receipt = managedCommandResult({
333
+ ...invocation,
334
+ timedOut,
335
+ status,
336
+ exitCode: result.exitCode,
337
+ stdout,
338
+ stderr,
339
+ outputRevision,
340
+ lastOutputAt,
341
+ lastOutputStream,
342
+ });
343
+ await persistManagedCommandReceipt(invocation.receiptPath, receipt.structuredContent);
344
+ return managedCommandPublicResult(receipt);
345
+ }
346
+ catch (error) {
347
+ const message = error instanceof Error ? error.message : String(error);
348
+ stderr = appendBounded(stderr, message);
349
+ outputRevision += 1;
350
+ lastOutputAt = Date.now();
351
+ lastOutputStream = "stderr";
352
+ const receipt = managedCommandResult({
353
+ ...invocation,
354
+ timedOut,
355
+ status: timedOut ? "timeout_termination_failed" : "runner_error",
356
+ exitCode: null,
357
+ stdout,
358
+ stderr,
359
+ outputRevision,
360
+ lastOutputAt,
361
+ lastOutputStream,
362
+ });
363
+ await persistManagedCommandReceipt(invocation.receiptPath, receipt.structuredContent);
364
+ return managedCommandPublicResult(receipt);
365
+ }
366
+ finally {
367
+ clearTimeout(launchTimer);
368
+ if (timer)
369
+ clearTimeout(timer);
370
+ }
371
+ }
372
+ async function readManagedCommand(input) {
373
+ const commandId = validatedCommandId(asRecord(input));
374
+ const receiptPath = managedCommandReceiptPath(requiredAttemptEnvironment("FORGE_LOG_DIR"), commandId);
375
+ let receipt;
376
+ try {
377
+ receipt = JSON.parse(await fs.readFile(receiptPath, "utf8"));
378
+ }
379
+ catch (error) {
380
+ if (error.code === "ENOENT") {
381
+ throw new Error(`managed_command_not_found: no durable receipt exists for ${commandId}.`, { cause: error });
382
+ }
383
+ throw error;
384
+ }
385
+ const status = typeof receipt.status === "string" ? receipt.status : "unknown";
386
+ const stdout = typeof receipt.stdout === "string" ? receipt.stdout : "";
387
+ const stderr = typeof receipt.stderr === "string" ? receipt.stderr : "";
388
+ return managedCommandPublicResult({
389
+ content: [{
390
+ type: "text",
391
+ text: terminalManagedCommandStatuses.has(status)
392
+ ? terminalManagedCommandText({
393
+ commandId,
394
+ status,
395
+ exitCode: typeof receipt.exit_code === "number" ? receipt.exit_code : null,
396
+ elapsedMs: typeof receipt.elapsed_ms === "number" ? receipt.elapsed_ms : null,
397
+ stdout,
398
+ stderr,
399
+ })
400
+ : `Forge managed command ${commandId} is ${status}. Continue independent work and use forge.wait_managed_command when ready for its terminal receipt.`,
401
+ }],
402
+ structuredContent: receipt,
403
+ isError: terminalManagedCommandStatuses.has(status) && status !== "pass",
404
+ });
405
+ }
406
+ async function waitManagedCommand(input) {
407
+ const record = asRecord(input);
408
+ const commandId = validatedCommandId(record);
409
+ const waitTimeoutMs = optionalWaitTimeout(record.wait_timeout_ms);
410
+ const deadline = Date.now() + waitTimeoutMs;
411
+ const receiptPath = managedCommandReceiptPath(requiredAttemptEnvironment("FORGE_LOG_DIR"), commandId);
412
+ const task = managedCommandTasks.get(commandId);
413
+ if (task) {
414
+ const result = await Promise.race([
415
+ task,
416
+ delay(waitTimeoutMs).then(() => null),
417
+ ]);
418
+ if (result)
419
+ return result;
420
+ }
421
+ while (Date.now() < deadline) {
422
+ const result = await readManagedCommand({ command_id: commandId });
423
+ if (terminalManagedCommandStatuses.has(String(result.structuredContent.status))) {
424
+ return result;
425
+ }
426
+ await delay(Math.min(managedCommandReceiptPollIntervalMs, Math.max(1, deadline - Date.now())));
427
+ }
428
+ const receipt = await readManagedCommand({ command_id: commandId });
429
+ return {
430
+ ...receipt,
431
+ content: [{
432
+ type: "text",
433
+ text: `Forge is still supervising managed command ${commandId} after waiting ${waitTimeoutMs}ms. Its durable receipt remains at ${receiptPath}.`,
434
+ }],
435
+ };
436
+ }
437
+ function managedCommandReceiptPath(logDirectory, commandId) {
438
+ return path.join(logDirectory, "managed-commands", `${commandId}.receipt.json`);
439
+ }
440
+ async function persistManagedCommandReceipt(receiptPath, receipt) {
441
+ const temporaryPath = `${receiptPath}.${randomUUID()}.tmp`;
442
+ await fs.writeFile(temporaryPath, `${JSON.stringify(receipt, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
443
+ try {
444
+ for (let attempt = 1;; attempt += 1) {
445
+ try {
446
+ await fs.rename(temporaryPath, receiptPath);
447
+ break;
448
+ }
449
+ catch (error) {
450
+ const code = error.code;
451
+ if (attempt >= managedCommandReceiptReplaceAttempts
452
+ || (code !== "EPERM" && code !== "EBUSY" && code !== "EACCES")) {
453
+ throw error;
454
+ }
455
+ await delay(managedCommandReceiptReplaceRetryMs);
456
+ }
457
+ }
458
+ }
459
+ finally {
460
+ await fs.rm(temporaryPath, { force: true }).catch(() => undefined);
461
+ }
462
+ }
463
+ function managedCommandResult(values) {
464
+ const elapsedMs = Date.now() - values.startedAt;
465
+ const isTerminal = terminalManagedCommandStatuses.has(values.status);
466
+ const diagnostics = isTerminal
467
+ ? managedCommandDiagnostics(values.stdout, values.stderr)
468
+ : undefined;
469
+ return {
470
+ content: [{
471
+ type: "text",
472
+ text: isTerminal
473
+ ? terminalManagedCommandText({
474
+ commandId: values.commandId,
475
+ status: values.status,
476
+ exitCode: values.exitCode,
477
+ elapsedMs,
478
+ stdout: values.stdout,
479
+ stderr: values.stderr,
480
+ }, diagnostics)
481
+ : `Forge managed command ${values.commandId} is ${values.status} after ${elapsedMs}ms.`,
482
+ }],
483
+ structuredContent: {
484
+ command_id: values.commandId,
485
+ status: values.status,
486
+ command: [values.command, ...values.args],
487
+ cwd: values.cwd,
488
+ timeout_ms: values.timeoutMs,
489
+ elapsed_ms: elapsedMs,
490
+ started_at: new Date(values.startedAt).toISOString(),
491
+ timed_out: values.timedOut,
492
+ exit_code: values.exitCode,
493
+ stdout: values.stdout,
494
+ stderr: values.stderr,
495
+ ...(diagnostics ? { diagnostics } : {}),
496
+ output_truncated: values.stdout.length >= maximumCapturedCharacters
497
+ || values.stderr.length >= maximumCapturedCharacters,
498
+ output_revision: values.outputRevision ?? 0,
499
+ last_output_at: values.lastOutputAt ? new Date(values.lastOutputAt).toISOString() : null,
500
+ last_output_stream: values.lastOutputStream ?? null,
501
+ supervisor_metadata_path: values.metadataPath,
502
+ receipt_path: values.receiptPath,
503
+ verification_profile: values.verificationProfile ?? null,
504
+ verification_profile_runner_authored: values.verificationProfile !== undefined,
505
+ runtime_configuration: managedRuntimeConfiguration(),
506
+ process_tree_terminated: values.status === "timed_out" ? true : values.timedOut ? false : undefined,
507
+ },
508
+ isError: isTerminal && values.status !== "pass",
509
+ };
510
+ }
511
+ function managedRuntimeConfiguration() {
512
+ const providerOnlySpecs = process.env.FORGE_CI_REPAIR_PROVIDER_ONLY_SPECS?.trim();
513
+ let providerOnlySpecCount = 0;
514
+ if (providerOnlySpecs) {
515
+ try {
516
+ const parsed = JSON.parse(providerOnlySpecs);
517
+ providerOnlySpecCount = Array.isArray(parsed)
518
+ ? parsed.filter(value => typeof value === "string" && value.length > 0).length
519
+ : 0;
520
+ }
521
+ catch {
522
+ providerOnlySpecCount = 0;
523
+ }
524
+ }
525
+ return {
526
+ attempt_database_schema: process.env.FORGE_ATTEMPT_DATABASE_SCHEMA ?? null,
527
+ attempt_database_readiness_path: process.env.FORGE_ATTEMPT_DATABASE_READINESS_PATH ?? null,
528
+ playwright_config: process.env.FORGE_PLAYWRIGHT_CONFIG ?? null,
529
+ provider_only_spec_count: providerOnlySpecCount,
530
+ provider_only_spec_fingerprint: providerOnlySpecs
531
+ ? `sha256:${createHash("sha256").update(providerOnlySpecs).digest("hex")}`
532
+ : null,
533
+ };
534
+ }
535
+ function managedCommandPublicResult(result) {
536
+ const stdout = typeof result.structuredContent.stdout === "string" ? result.structuredContent.stdout : "";
537
+ const stderr = typeof result.structuredContent.stderr === "string" ? result.structuredContent.stderr : "";
538
+ const { stdout: _stdout, stderr: _stderr, ...receipt } = result.structuredContent;
539
+ return {
540
+ ...result,
541
+ structuredContent: {
542
+ ...receipt,
543
+ stdout_tail: tail(stdout, maximumReturnedOutputCharacters),
544
+ stderr_tail: tail(stderr, maximumReturnedOutputCharacters),
545
+ full_output_in_receipt: true,
546
+ },
547
+ };
548
+ }
549
+ function persistManagedCommandProgressIfDue(invocation, snapshot, previousPersistence, lastPersistedAt, setLastPersistedAt) {
550
+ const now = Date.now();
551
+ if (now - lastPersistedAt() < managedCommandProgressPersistIntervalMs)
552
+ return previousPersistence;
553
+ setLastPersistedAt(now);
554
+ const current = snapshot();
555
+ return previousPersistence.then(async () => {
556
+ const receipt = managedCommandResult({
557
+ ...invocation,
558
+ timedOut: false,
559
+ status: "running",
560
+ exitCode: null,
561
+ ...current,
562
+ });
563
+ await persistManagedCommandReceipt(invocation.receiptPath, receipt.structuredContent);
564
+ });
565
+ }
566
+ async function containedWorkingDirectory(worktreePath, input) {
567
+ const root = await fs.realpath(path.resolve(worktreePath));
568
+ const relative = input === undefined ? "." : requiredStandaloneString(input, "cwd");
569
+ if (path.isAbsolute(relative))
570
+ throw new Error("managed_command_invalid: cwd must be worktree-relative.");
571
+ const target = await fs.realpath(path.resolve(root, relative));
572
+ const containment = path.relative(root, target);
573
+ if (containment.startsWith(`..${path.sep}`) || path.isAbsolute(containment)) {
574
+ throw new Error("managed_command_invalid: cwd resolves outside the source worktree.");
575
+ }
576
+ return target;
577
+ }
578
+ function inheritedEnvironment(overrides) {
579
+ const env = {};
580
+ for (const [key, value] of Object.entries(process.env)) {
581
+ if (typeof value === "string" && !looksSensitiveEnvironmentName(key))
582
+ env[key] = value;
583
+ }
584
+ return { ...env, ...overrides };
585
+ }
586
+ function looksSensitiveEnvironmentName(name) {
587
+ const normalized = name.trim().toUpperCase();
588
+ return normalized.includes("SECRET")
589
+ || normalized.includes("TOKEN")
590
+ || normalized.includes("PASSWORD")
591
+ || normalized.includes("CREDENTIAL")
592
+ || normalized.endsWith("_API_KEY")
593
+ || normalized === "OPENAI_API_KEY";
594
+ }
595
+ function requiredAttemptEnvironment(name) {
596
+ const value = process.env[name]?.trim();
597
+ if (!value)
598
+ throw new Error(`managed_attempt_tool_unavailable: ${name} is not configured.`);
599
+ return value;
600
+ }
601
+ function asRecord(value) {
602
+ if (!value || typeof value !== "object" || Array.isArray(value))
603
+ throw new Error("managed_command_invalid: input must be an object.");
604
+ return value;
605
+ }
606
+ function requiredInputString(record, name) {
607
+ return requiredStandaloneString(record[name], name);
608
+ }
609
+ function validatedCommandId(record) {
610
+ const commandId = requiredInputString(record, "command_id");
611
+ 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)) {
612
+ throw new Error("managed_command_invalid: command_id must be a UUID returned by Forge.");
613
+ }
614
+ return commandId;
615
+ }
616
+ function requiredStandaloneString(value, name) {
617
+ if (typeof value !== "string" || !value.trim())
618
+ throw new Error(`managed_command_invalid: ${name} is required.`);
619
+ return value.trim();
620
+ }
621
+ function requiredStringArray(record, name) {
622
+ const value = record[name];
623
+ if (!Array.isArray(value) || value.some(entry => typeof entry !== "string")) {
624
+ throw new Error(`managed_command_invalid: ${name} must be a string array.`);
625
+ }
626
+ return value;
627
+ }
628
+ function optionalTimeout(value) {
629
+ if (value === undefined)
630
+ return 2_700_000;
631
+ if (!Number.isSafeInteger(value) || value < 1_000 || value > 7_200_000) {
632
+ throw new Error("managed_command_invalid: timeout_ms must be an integer from 1000 through 7200000.");
633
+ }
634
+ return value;
635
+ }
636
+ function optionalWaitTimeout(value) {
637
+ if (value === undefined)
638
+ return 7_200_000;
639
+ if (!Number.isSafeInteger(value) || value < 1_000 || value > 7_200_000) {
640
+ throw new Error("managed_command_invalid: wait_timeout_ms must be an integer from 1000 through 7200000.");
641
+ }
642
+ return value;
643
+ }
644
+ function optionalBoundedInteger(value, name, minimum, maximum, fallback) {
645
+ if (value === undefined)
646
+ return fallback;
647
+ if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
648
+ throw new Error(`managed_verification_invalid: ${name} must be an integer from ${minimum} through ${maximum}.`);
649
+ }
650
+ return value;
651
+ }
652
+ function optionalBoolean(value, name, fallback) {
653
+ if (value === undefined)
654
+ return fallback;
655
+ if (typeof value !== "boolean")
656
+ throw new Error(`managed_verification_invalid: ${name} must be a boolean.`);
657
+ return value;
658
+ }
659
+ async function delay(ms) {
660
+ await new Promise(resolve => setTimeout(resolve, ms));
661
+ }
662
+ function appendBounded(current, line) {
663
+ const next = `${current}${line}\n`;
664
+ return next.length <= maximumCapturedCharacters
665
+ ? next
666
+ : next.slice(-maximumCapturedCharacters);
667
+ }
668
+ function managedCommandDiagnostics(stdout, stderr) {
669
+ const lines = `${stdout}\n${stderr}`
670
+ .split(/\r?\n/u)
671
+ .map(line => line.trim())
672
+ .filter(Boolean);
673
+ const failedTests = new Set();
674
+ for (const line of lines) {
675
+ const xunit = line.match(/^\[xUnit\.net[^\]]*\]\s+(.+?)\s+\[FAIL\]$/u);
676
+ if (xunit?.[1])
677
+ failedTests.add(xunit[1].trim());
678
+ const vstest = line.match(/^Failed\s+(.+?)(?:\s+\[(?:\d|<).*)?$/u);
679
+ if (vstest?.[1] && !vstest[1].startsWith("!"))
680
+ failedTests.add(vstest[1].trim());
681
+ }
682
+ const summaryLines = lines
683
+ .filter(line => /(?:Failed!|Passed!|Test Run|Tests?:\s|Test Files|\[FAIL\]|error\s+[A-Z]+\d+)/iu.test(line))
684
+ .slice(-20);
685
+ return {
686
+ failed_tests: [...failedTests].slice(0, maximumReportedFailedTests),
687
+ failed_test_count: failedTests.size,
688
+ summary_lines: summaryLines,
689
+ };
690
+ }
691
+ function terminalManagedCommandText(values, diagnostics = managedCommandDiagnostics(values.stdout, values.stderr)) {
692
+ const failedTests = diagnostics.failed_tests;
693
+ const summaryLines = diagnostics.summary_lines;
694
+ const duration = values.elapsedMs === null ? "an unknown duration" : `${values.elapsedMs}ms`;
695
+ const sections = [
696
+ `Forge managed command ${values.commandId} ${values.status} after ${duration} (exit ${values.exitCode ?? "unavailable"}).`,
697
+ ];
698
+ if (failedTests.length > 0) {
699
+ sections.push(`Failed tests (${diagnostics.failed_test_count}):\n${failedTests.map(test => `- ${test}`).join("\n")}`);
700
+ }
701
+ if (summaryLines.length > 0) {
702
+ sections.push(`Test summary:\n${summaryLines.join("\n")}`);
703
+ }
704
+ const stdoutTail = tail(values.stdout, maximumReturnedOutputCharacters);
705
+ const stderrTail = tail(values.stderr, maximumReturnedOutputCharacters);
706
+ sections.push(`stdout tail:\n${stdoutTail || "(empty)"}`);
707
+ sections.push(`stderr tail:\n${stderrTail || "(empty)"}`);
708
+ return sections.join("\n\n");
709
+ }
710
+ function tail(value, maximumCharacters) {
711
+ return value.length <= maximumCharacters ? value : value.slice(-maximumCharacters);
712
+ }