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