@loucompanion/forge-bridge 0.1.3-dev.f66395c3ac21 → 0.1.4-dev.04a1e5b9e484
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 +165 -1
- 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 +9 -1
- package/dist/bridge/bridge.service.d.ts +129 -3
- package/dist/bridge/bridge.service.js +2427 -70
- package/dist/bridge/bridge.types.d.ts +218 -2
- 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 +14 -2
- package/dist/cleanup/cleanup.service.js +496 -3
- package/dist/cleanup/cleanup.types.d.ts +10 -0
- package/dist/cleanup/index.d.ts +1 -1
- package/dist/cleanup/index.js +1 -1
- package/dist/cleanup/internals.d.ts +2 -2
- package/dist/cleanup/internals.js +1 -1
- package/dist/cli/cli.help.d.ts +2 -0
- package/dist/cli/cli.help.js +217 -0
- package/dist/cli/cli.service.d.ts +55 -7
- package/dist/cli/cli.service.js +429 -219
- package/dist/cli/local-config.d.ts +7 -0
- package/dist/cli/local-config.js +51 -3
- 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 +18 -0
- package/dist/runtime/attempt-node-runtime-launcher.js +140 -0
- 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-registry.d.ts +20 -0
- package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
- package/dist/runtime/attempt-process-supervisor.d.ts +102 -0
- package/dist/runtime/attempt-process-supervisor.js +421 -0
- 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 +20 -0
- package/dist/runtime/generated-output-reset.js +157 -0
- 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 +51 -0
- package/dist/runtime/host-listener-inspector.js +385 -0
- package/dist/runtime/host-port-broker.d.ts +76 -0
- package/dist/runtime/host-port-broker.js +333 -0
- package/dist/runtime/host-runtime-contract.d.ts +58 -0
- package/dist/runtime/host-runtime-contract.js +813 -0
- package/dist/runtime/host-runtime-lease.d.ts +217 -0
- package/dist/runtime/host-runtime-lease.js +2487 -0
- package/dist/runtime/host-runtime-promotion.d.ts +32 -0
- package/dist/runtime/host-runtime-promotion.js +206 -0
- 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 +54 -0
- package/dist/runtime/node-host-runtime-adapter.js +286 -0
- 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 +21 -0
- package/dist/runtime/playwright-host-runtime-adapter.js +268 -0
- 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.d.ts +67 -0
- package/dist/runtime/runtime-isolation-lease.js +161 -0
- package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
- package/dist/runtime/workspace-resource-estimator.js +174 -0
- package/dist/sandbox/command-runner.d.ts +4 -0
- package/dist/sandbox/command-runner.js +47 -0
- package/dist/sandbox/contract.d.ts +5 -0
- package/dist/sandbox/contract.js +276 -0
- package/dist/sandbox/event-budget.d.ts +11 -0
- package/dist/sandbox/event-budget.js +27 -0
- package/dist/sandbox/image-policy.d.ts +35 -0
- package/dist/sandbox/image-policy.js +247 -0
- package/dist/sandbox/index.d.ts +5 -0
- package/dist/sandbox/index.js +5 -0
- package/dist/sandbox/lease-watchdog.d.ts +18 -0
- package/dist/sandbox/lease-watchdog.js +67 -0
- package/dist/sandbox/preflight.d.ts +84 -0
- package/dist/sandbox/preflight.js +521 -0
- package/dist/sandbox/process.d.ts +20 -0
- package/dist/sandbox/process.js +209 -0
- package/dist/sandbox/provider-auth/codex.d.ts +26 -0
- package/dist/sandbox/provider-auth/codex.js +212 -0
- package/dist/sandbox/repository-lock.d.ts +6 -0
- package/dist/sandbox/repository-lock.js +129 -0
- package/dist/sandbox/runtime.d.ts +302 -0
- package/dist/sandbox/runtime.js +2368 -0
- package/dist/sandbox/sandbox.types.d.ts +93 -0
- package/dist/sandbox/sandbox.types.js +1 -0
- package/dist/sandbox/smoke.d.ts +76 -0
- package/dist/sandbox/smoke.js +380 -0
- package/dist/sandbox/worker-supervisor.d.ts +13 -0
- package/dist/sandbox/worker-supervisor.js +36 -0
- package/dist/sandbox/worker.d.ts +1 -0
- package/dist/sandbox/worker.js +225 -0
- package/dist/sandbox/workspace.service.d.ts +13 -0
- package/dist/sandbox/workspace.service.js +74 -0
- package/dist/session/managed-attempt-tools.d.ts +28 -0
- package/dist/session/managed-attempt-tools.js +1016 -0
- 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 +27 -0
- package/dist/session/phase-completion.artifacts.js +829 -0
- package/dist/session/phase-completion.d.ts +53 -42
- package/dist/session/phase-completion.helpers.d.ts +117 -0
- package/dist/session/phase-completion.helpers.js +707 -0
- package/dist/session/phase-completion.js +125 -3007
- package/dist/session/phase-completion.prompts.d.ts +14 -0
- package/dist/session/phase-completion.prompts.js +233 -0
- package/dist/session/phase-completion.runtime.d.ts +49 -0
- package/dist/session/phase-completion.runtime.js +840 -0
- package/dist/session/phase-completion.schemas.d.ts +3 -0
- package/dist/session/phase-completion.schemas.js +859 -0
- package/dist/session/phase-completion.validation.d.ts +38 -0
- package/dist/session/phase-completion.validation.js +918 -0
- 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 +16 -0
- package/dist/session/provider-cli/ci-repair-readiness.js +110 -0
- package/dist/session/provider-cli/provider-cli.artifacts.d.ts +95 -0
- package/dist/session/provider-cli/provider-cli.artifacts.js +812 -0
- package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
- package/dist/session/provider-cli/provider-cli.deterministic-payloads.d.ts +12 -0
- package/dist/session/provider-cli/provider-cli.deterministic-payloads.js +279 -0
- package/dist/session/provider-cli/provider-cli.deterministic.d.ts +14 -0
- package/dist/session/provider-cli/provider-cli.deterministic.js +407 -0
- package/dist/session/provider-cli/provider-cli.process.d.ts +99 -0
- package/dist/session/provider-cli/provider-cli.process.js +874 -0
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +52 -0
- package/dist/session/provider-cli/provider-cli.prompts.js +715 -0
- package/dist/session/provider-cli/provider-cli.qa-sidecars.d.ts +11 -0
- package/dist/session/provider-cli/provider-cli.qa-sidecars.js +66 -0
- 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 +22 -0
- package/dist/session/provider-cli/provider-cli.runtime.js +92 -0
- package/dist/session/provider-cli/provider-cli.service.d.ts +47 -53
- package/dist/session/provider-cli/provider-cli.service.js +1258 -2497
- package/dist/session/provider-cli/provider-cli.types.d.ts +8 -0
- package/dist/session/provider-cli/provider-output-state.d.ts +32 -0
- package/dist/session/provider-cli/provider-output-state.js +817 -0
- 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.d.ts +3 -0
- package/dist/session/session-redaction.js +448 -0
- package/dist/session/session.base.d.ts +6 -1
- package/dist/session/session.service.d.ts +8 -2
- package/dist/session/session.service.js +586 -59
- package/dist/session/session.types.d.ts +324 -3
- package/dist/session/session.types.js +939 -10
- 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 +21 -1
- 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.d.ts +12 -0
- package/dist/shared/process/host-process-recovery.js +79 -0
- package/dist/shared/process/host-process-supervisor.d.ts +59 -0
- package/dist/shared/process/host-process-supervisor.js +331 -0
- package/dist/shared/process/process.d.ts +3 -0
- package/dist/shared/process/process.js +13 -7
- package/dist/shared/process/windows-launch.d.ts +8 -0
- package/dist/shared/process/windows-launch.js +41 -0
- package/dist/shared/provider-capabilities.d.ts +4 -0
- package/dist/shared/provider-capabilities.js +88 -0
- package/dist/shared/redaction/redaction.d.ts +1 -0
- package/dist/shared/redaction/redaction.js +47 -4
- 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/artifact-injector.d.ts +3 -2
- package/dist/worktree/artifact-injector.js +82 -10
- package/dist/worktree/dependency-warmer.d.ts +53 -1
- package/dist/worktree/dependency-warmer.js +619 -11
- package/dist/worktree/index.d.ts +2 -1
- package/dist/worktree/index.js +1 -0
- package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
- package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
- 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-paths.d.ts +4 -0
- package/dist/worktree/worktree-paths.js +30 -0
- package/dist/worktree/worktree.base.d.ts +3 -0
- package/dist/worktree/worktree.service.d.ts +16 -3
- package/dist/worktree/worktree.service.js +334 -54
- package/dist/worktree/worktree.types.d.ts +9 -1
- package/package.json +12 -7
- package/runtime/windows-job-supervisor.ps1 +722 -0
- package/runtime/windows-listener-owner.ps1 +33 -0
|
@@ -0,0 +1,840 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { attemptDatabaseCapabilityEnvironment, attemptDatabaseCapabilitySecrets, } from "../runtime/attempt-database-broker.js";
|
|
5
|
+
import { completionPhaseFromDispatch, completionPhaseOrder, completionRedactionSecretsEnv, completionToolNamesForDebugGate, completionToolNamesForDecomposeGate, completionToolNamesForDraftGate, completionToolNamesForPhase, completionToolNamesForQaGate, debugCompletionPhaseOrder, decomposeCompletionGateOrder, draftCompletionGateOrder, qaCompletionGateOrder, toolServerName, usesArchitectCycleCompletion, usesDebugCycleCompletion, usesDevelopCycleCompletion, usesQaCycleCompletion } from "./phase-completion.js";
|
|
6
|
+
import { CompletionPayloadValidationError, asDebugCompletionPhase, asJsonRpcMessage, asMutableRecord, asRecord, artifactOwnerRelativeRoot, decomposeGateFromToolName, draftGateFromToolName, errorMessage, forgeBridgeCliBinPath, isDebugCompletionPhase, isDecomposeDispatch, isDevelopCompletionPhase, normalizedOwnerType, parseCompletionRedactionSecrets, parseOptionalJsonObject, parseStrictCompletionPhase, phaseFromToolName, qaGateFromToolName, requiredString, rpcError, tomlString, tomlStringArray, writeFileAtomic, writeJsonRpc } from "./phase-completion.validation.js";
|
|
7
|
+
import { completionPromptForDebugCycle, completionPromptForDecomposeCycle, completionPromptForDevelopCycle, completionPromptForDraftCycle, completionPromptForPhase, completionPromptForQaCycle, completionToolNextInstruction, completionToolNextInstructionForDebugGate, completionToolNextInstructionForDecomposeGate, completionToolNextInstructionForDraftGate, completionToolNextInstructionForQaGate, nextDebugPhaseAfter, nextPhaseAfter } from "./phase-completion.prompts.js";
|
|
8
|
+
import { configuredNativeTestPostgresFallbackPath, nativeTestPostgresFallbackConfigEnvironment, } from "../runtime/native-test-postgres-fallback.js";
|
|
9
|
+
import { toolDescription, toolInputSchema } from "./phase-completion.schemas.js";
|
|
10
|
+
import { writeCompletionArtifact, writeDecomposeGateCompletionArtifact, writeDraftGateCompletionArtifact } from "./phase-completion.artifacts.js";
|
|
11
|
+
import { callManagedAttemptTool, isManagedAttemptTool, managedAttemptToolDefinition, managedAttemptToolNames, } from "./managed-attempt-tools.js";
|
|
12
|
+
export async function prepareCompletionToolRuntime(options) {
|
|
13
|
+
const phase = options.phase ?? completionPhaseFromDispatch(options.dispatch);
|
|
14
|
+
const worktreePath = path.resolve(options.worktree.worktreePath);
|
|
15
|
+
const workItemId = options.dispatch.workItem.id;
|
|
16
|
+
const ownerType = normalizedOwnerType(options.dispatch.cycleRun.ownerType) ?? "work_item";
|
|
17
|
+
const ownerId = options.dispatch.cycleRun.ownerId ?? workItemId;
|
|
18
|
+
const mcpArgs = buildCompletionToolServerArgs({
|
|
19
|
+
worktreePath,
|
|
20
|
+
workItemId,
|
|
21
|
+
ownerType,
|
|
22
|
+
ownerId,
|
|
23
|
+
phase,
|
|
24
|
+
producedBy: `${options.dispatch.execution.provider}-cli`,
|
|
25
|
+
developCycle: usesDevelopCycleCompletion(options.dispatch),
|
|
26
|
+
qaCycle: usesQaCycleCompletion(options.dispatch),
|
|
27
|
+
debugCycle: usesDebugCycleCompletion(options.dispatch),
|
|
28
|
+
architectCycle: usesArchitectCycleCompletion(options.dispatch),
|
|
29
|
+
draftCycle: (phase === "draft"
|
|
30
|
+
|| options.dispatch.execution.lane.trim().toLowerCase() === "draft"
|
|
31
|
+
|| options.dispatch.cycleRun.kind.trim().toLowerCase() === "draft")
|
|
32
|
+
&& !isDecomposeDispatch(options.dispatch),
|
|
33
|
+
decomposeCycle: isDecomposeDispatch(options.dispatch)
|
|
34
|
+
});
|
|
35
|
+
const runtime = {
|
|
36
|
+
serverName: toolServerName,
|
|
37
|
+
command: process.execPath,
|
|
38
|
+
args: [forgeBridgeCliBinPath(), ...mcpArgs],
|
|
39
|
+
env: completionToolRuntimeEnvironment(options.runtimeEnvironment, configuredNativeTestPostgresFallbackPath()),
|
|
40
|
+
cleanup: async () => undefined
|
|
41
|
+
};
|
|
42
|
+
const provider = options.dispatch.execution.provider.trim().toLowerCase();
|
|
43
|
+
if (provider === "cursor") {
|
|
44
|
+
return await prepareCursorCompletionToolRuntime(worktreePath, runtime);
|
|
45
|
+
}
|
|
46
|
+
if (provider === "claude") {
|
|
47
|
+
return await prepareClaudeCompletionToolRuntime(runtime);
|
|
48
|
+
}
|
|
49
|
+
return runtime;
|
|
50
|
+
}
|
|
51
|
+
export function buildCompletionToolServerArgs(config) {
|
|
52
|
+
const args = [
|
|
53
|
+
"mcp",
|
|
54
|
+
"--worktree",
|
|
55
|
+
path.resolve(config.worktreePath),
|
|
56
|
+
"--work-item-id",
|
|
57
|
+
config.workItemId,
|
|
58
|
+
"--phase",
|
|
59
|
+
config.phase,
|
|
60
|
+
"--produced-by",
|
|
61
|
+
config.producedBy ?? "forge-completion-tool"
|
|
62
|
+
];
|
|
63
|
+
if (config.ownerType || config.ownerId) {
|
|
64
|
+
args.push("--owner-type", normalizedOwnerType(config.ownerType) ?? "work_item");
|
|
65
|
+
args.push("--owner-id", config.ownerId ?? config.workItemId);
|
|
66
|
+
}
|
|
67
|
+
if (config.developCycle) {
|
|
68
|
+
args.push("--develop-cycle");
|
|
69
|
+
}
|
|
70
|
+
if (config.qaCycle) {
|
|
71
|
+
args.push("--qa-cycle");
|
|
72
|
+
}
|
|
73
|
+
if (config.debugCycle) {
|
|
74
|
+
args.push("--debug-cycle");
|
|
75
|
+
}
|
|
76
|
+
if (config.architectCycle) {
|
|
77
|
+
args.push("--architect-cycle");
|
|
78
|
+
}
|
|
79
|
+
if (config.draftCycle) {
|
|
80
|
+
args.push("--draft-cycle");
|
|
81
|
+
}
|
|
82
|
+
if (config.decomposeCycle) {
|
|
83
|
+
args.push("--decompose-cycle");
|
|
84
|
+
}
|
|
85
|
+
return args;
|
|
86
|
+
}
|
|
87
|
+
export function codexMcpConfigArgs(runtime) {
|
|
88
|
+
const prefix = `mcp_servers.${runtime.serverName}`;
|
|
89
|
+
const args = [
|
|
90
|
+
"-c",
|
|
91
|
+
`${prefix}.command=${tomlString(runtime.command)}`,
|
|
92
|
+
"-c",
|
|
93
|
+
`${prefix}.args=${tomlStringArray(runtime.args)}`,
|
|
94
|
+
"-c",
|
|
95
|
+
`${prefix}.default_tools_approval_mode=${tomlString("approve")}`,
|
|
96
|
+
"-c",
|
|
97
|
+
`${prefix}.tool_timeout_sec=7200`
|
|
98
|
+
];
|
|
99
|
+
for (const [key, value] of Object.entries(runtime.env)) {
|
|
100
|
+
args.push("-c", `${prefix}.env.${key}=${tomlString(value)}`);
|
|
101
|
+
}
|
|
102
|
+
return args;
|
|
103
|
+
}
|
|
104
|
+
export function cursorMcpServerConfig(runtime) {
|
|
105
|
+
return {
|
|
106
|
+
command: runtime.command,
|
|
107
|
+
args: [...runtime.args],
|
|
108
|
+
env: runtime.env
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
export function claudeMcpServerConfig(runtime) {
|
|
112
|
+
return {
|
|
113
|
+
type: "stdio",
|
|
114
|
+
command: runtime.command,
|
|
115
|
+
args: [...runtime.args],
|
|
116
|
+
env: runtime.env
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
export function completionToolNamesForServer(config) {
|
|
120
|
+
return config.decomposeCycle
|
|
121
|
+
? decomposeCompletionGateOrder.flatMap((gate) => completionToolNamesForDecomposeGate(gate))
|
|
122
|
+
: config.draftCycle
|
|
123
|
+
? draftCompletionGateOrder.flatMap((gate) => completionToolNamesForDraftGate(gate))
|
|
124
|
+
: config.debugCycle
|
|
125
|
+
? debugCompletionPhaseOrder.flatMap((gate) => completionToolNamesForDebugGate(gate))
|
|
126
|
+
: config.qaCycle
|
|
127
|
+
? qaCompletionGateOrder.flatMap((gate) => completionToolNamesForQaGate(gate))
|
|
128
|
+
: config.developCycle
|
|
129
|
+
? completionPhaseOrder.flatMap((phase) => completionToolNamesForPhase(phase))
|
|
130
|
+
: config.architectCycle
|
|
131
|
+
? completionToolNamesForPhase("architect")
|
|
132
|
+
: completionToolNamesForPhase(config.phase);
|
|
133
|
+
}
|
|
134
|
+
export class CompletionToolServerSession {
|
|
135
|
+
config;
|
|
136
|
+
currentPhase;
|
|
137
|
+
currentQaGate = "qa_plan";
|
|
138
|
+
currentDraftGate = "draft_intent";
|
|
139
|
+
currentDecomposeGate = "decompose_subject_intake";
|
|
140
|
+
managedCommandReceipts = [];
|
|
141
|
+
completed = false;
|
|
142
|
+
constructor(config) {
|
|
143
|
+
this.config = config;
|
|
144
|
+
this.currentPhase = config.phase;
|
|
145
|
+
}
|
|
146
|
+
get instructions() {
|
|
147
|
+
return this.config.decomposeCycle
|
|
148
|
+
? completionPromptForDecomposeCycle(this.currentDecomposeGate)
|
|
149
|
+
: this.config.draftCycle
|
|
150
|
+
? completionPromptForDraftCycle(this.currentDraftGate)
|
|
151
|
+
: this.config.debugCycle
|
|
152
|
+
? completionPromptForDebugCycle(asDebugCompletionPhase(this.currentPhase))
|
|
153
|
+
: this.config.qaCycle
|
|
154
|
+
? completionPromptForQaCycle(this.currentQaGate, artifactOwnerRelativeRoot(this.config.ownerType, this.config.ownerId, this.config.workItemId))
|
|
155
|
+
: this.config.developCycle
|
|
156
|
+
? completionPromptForDevelopCycle(this.currentPhase)
|
|
157
|
+
: completionPromptForPhase(this.currentPhase, artifactOwnerRelativeRoot(this.config.ownerType, this.config.ownerId, this.config.workItemId));
|
|
158
|
+
}
|
|
159
|
+
toolDefinitions() {
|
|
160
|
+
const completionTools = completionToolNamesForServer(this.config).map((name) => toolDefinition(name, phaseFromToolName(name), qaGateFromToolName(name), decomposeGateFromToolName(name)));
|
|
161
|
+
return managedAttemptToolsAvailable()
|
|
162
|
+
? [...completionTools, ...managedAttemptToolNames.map(managedAttemptToolDefinition)]
|
|
163
|
+
: completionTools;
|
|
164
|
+
}
|
|
165
|
+
async callTool(toolName, input) {
|
|
166
|
+
if (isManagedAttemptTool(toolName)) {
|
|
167
|
+
if (!managedAttemptToolsAvailable()) {
|
|
168
|
+
throw new Error("Forge managed-attempt tools require an active managed host runtime.");
|
|
169
|
+
}
|
|
170
|
+
const result = await callManagedAttemptTool(toolName, input, this.config.worktreePath);
|
|
171
|
+
if (Array.isArray(result.structuredContent.command)) {
|
|
172
|
+
this.managedCommandReceipts.push(result.structuredContent);
|
|
173
|
+
}
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
const requestedPhase = phaseFromToolName(toolName);
|
|
177
|
+
if (this.completed) {
|
|
178
|
+
throw new Error("Forge completion cycle is already complete; no more completion calls are accepted.");
|
|
179
|
+
}
|
|
180
|
+
if (this.config.decomposeCycle) {
|
|
181
|
+
return await this.callDecomposeGateTool(toolName, requestedPhase, input);
|
|
182
|
+
}
|
|
183
|
+
if (this.config.draftCycle) {
|
|
184
|
+
return await this.callDraftGateTool(toolName, requestedPhase, input);
|
|
185
|
+
}
|
|
186
|
+
if (this.config.debugCycle) {
|
|
187
|
+
return await this.callDebugGateTool(toolName, requestedPhase, input);
|
|
188
|
+
}
|
|
189
|
+
if (this.config.qaCycle) {
|
|
190
|
+
return await this.callQaGateTool(toolName, requestedPhase, input);
|
|
191
|
+
}
|
|
192
|
+
if (requestedPhase !== this.currentPhase) {
|
|
193
|
+
throw new Error(`Tool ${toolName} is out of order. Current Forge phase is ${this.currentPhase}.`);
|
|
194
|
+
}
|
|
195
|
+
const completedPhase = this.currentPhase;
|
|
196
|
+
const reference = await writeCompletionArtifact({ ...this.config, phase: completedPhase }, toolName, input);
|
|
197
|
+
const status = reference.status.trim().toLowerCase();
|
|
198
|
+
const pass = status === "pass";
|
|
199
|
+
const nextPhase = pass
|
|
200
|
+
? this.advanceAfterPass(completedPhase)
|
|
201
|
+
: completedPhase;
|
|
202
|
+
const nextInstruction = completionToolNextInstruction(completedPhase, status, nextPhase);
|
|
203
|
+
const reason = completedPhase === "qa"
|
|
204
|
+
? `QA artifact recorded locally with status ${status}; API ingestion and acceptance are pending.`
|
|
205
|
+
: pass
|
|
206
|
+
? `${completedPhase} completion artifact accepted.`
|
|
207
|
+
: `${completedPhase} completion artifact recorded with status ${status}; rework is required before phase progression.`;
|
|
208
|
+
return {
|
|
209
|
+
content: [
|
|
210
|
+
{
|
|
211
|
+
type: "text",
|
|
212
|
+
text: `Forge recorded ${reference.phase} completion artifact ${path.relative(reference.repoPath, reference.path)} (${reference.status}, ${reference.contentHash}). ${nextInstruction}`
|
|
213
|
+
}
|
|
214
|
+
],
|
|
215
|
+
structuredContent: {
|
|
216
|
+
...reference,
|
|
217
|
+
...acceptedArtifactReceipt(reference),
|
|
218
|
+
gate: {
|
|
219
|
+
phase: completedPhase,
|
|
220
|
+
status,
|
|
221
|
+
allowed: pass,
|
|
222
|
+
reason
|
|
223
|
+
},
|
|
224
|
+
nextPhase,
|
|
225
|
+
nextInstruction
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
async callDecomposeGateTool(toolName, requestedPhase, input) {
|
|
230
|
+
const requestedGate = decomposeGateFromToolName(toolName);
|
|
231
|
+
if (requestedPhase !== "decompose" || !requestedGate) {
|
|
232
|
+
throw new Error(`Tool ${toolName} is not available in Decompose-cycle mode. Current Forge Decompose gate is ${this.currentDecomposeGate}.`);
|
|
233
|
+
}
|
|
234
|
+
if (requestedGate !== this.currentDecomposeGate) {
|
|
235
|
+
throw new Error(`Tool ${toolName} is out of order. Current Forge Decompose gate is ${this.currentDecomposeGate}.`);
|
|
236
|
+
}
|
|
237
|
+
const completedGate = this.currentDecomposeGate;
|
|
238
|
+
const reference = await writeDecomposeGateCompletionArtifact(this.config, completedGate, toolName, input);
|
|
239
|
+
const status = reference.status.trim().toLowerCase();
|
|
240
|
+
const pass = status === "pass";
|
|
241
|
+
const nextGate = pass ? this.advanceAfterDecomposeGate(completedGate) : completedGate;
|
|
242
|
+
const nextPhase = pass
|
|
243
|
+
? (nextGate ? "decompose" : "sign_off_ready")
|
|
244
|
+
: "decompose";
|
|
245
|
+
const nextInstruction = completionToolNextInstructionForDecomposeGate(completedGate, status, nextGate);
|
|
246
|
+
const reason = pass
|
|
247
|
+
? nextGate
|
|
248
|
+
? `${completedGate} artifact accepted; continue to ${nextGate}.`
|
|
249
|
+
: `Decompose proposal artifact accepted with status ${status}.`
|
|
250
|
+
: `${completedGate} artifact recorded with status ${status}; rework is required before continuing.`;
|
|
251
|
+
return {
|
|
252
|
+
content: [{ type: "text", text: `Forge recorded ${completedGate} artifact ${path.relative(reference.repoPath, reference.path)} (${reference.status}, ${reference.contentHash}). ${nextInstruction}` }],
|
|
253
|
+
structuredContent: {
|
|
254
|
+
...reference,
|
|
255
|
+
...acceptedArtifactReceipt(reference),
|
|
256
|
+
gate: { phase: "decompose", decomposeGate: completedGate, status, allowed: pass, reason },
|
|
257
|
+
nextPhase,
|
|
258
|
+
nextInstruction
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
async callDraftGateTool(toolName, requestedPhase, input) {
|
|
263
|
+
const requestedGate = draftGateFromToolName(toolName);
|
|
264
|
+
if (requestedPhase !== "draft" || !requestedGate) {
|
|
265
|
+
throw new Error(`Tool ${toolName} is not available in Draft-cycle mode. Current Forge Draft gate is ${this.currentDraftGate}.`);
|
|
266
|
+
}
|
|
267
|
+
if (requestedGate !== this.currentDraftGate) {
|
|
268
|
+
throw new Error(`Tool ${toolName} is out of order. Current Forge Draft gate is ${this.currentDraftGate}.`);
|
|
269
|
+
}
|
|
270
|
+
const completedGate = this.currentDraftGate;
|
|
271
|
+
const reference = await writeDraftGateCompletionArtifact(this.config, completedGate, toolName, input);
|
|
272
|
+
const status = reference.status.trim().toLowerCase();
|
|
273
|
+
const nextGate = this.advanceAfterDraftGate(completedGate);
|
|
274
|
+
const nextPhase = nextGate ? "draft" : "sign_off_ready";
|
|
275
|
+
const nextInstruction = completionToolNextInstructionForDraftGate(completedGate, status, nextGate);
|
|
276
|
+
return {
|
|
277
|
+
content: [{ type: "text", text: `Forge recorded ${completedGate} artifact ${path.relative(reference.repoPath, reference.path)} (${reference.status}, ${reference.contentHash}). ${nextInstruction}` }],
|
|
278
|
+
structuredContent: {
|
|
279
|
+
...reference,
|
|
280
|
+
...acceptedArtifactReceipt(reference),
|
|
281
|
+
gate: { phase: "draft", draftGate: completedGate, status, allowed: status !== "blocked", reason: nextGate ? `${completedGate} artifact accepted; continue to ${nextGate}.` : `Draft proposal artifact accepted with status ${status}.` },
|
|
282
|
+
nextPhase,
|
|
283
|
+
nextInstruction
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
async callDebugGateTool(toolName, requestedPhase, input) {
|
|
288
|
+
const currentDebugGate = asDebugCompletionPhase(this.currentPhase);
|
|
289
|
+
if (!isDebugCompletionPhase(requestedPhase)) {
|
|
290
|
+
throw new Error(`Tool ${toolName} is not available in Debug-cycle mode. Current Forge Debug gate is ${currentDebugGate}.`);
|
|
291
|
+
}
|
|
292
|
+
if (requestedPhase !== currentDebugGate) {
|
|
293
|
+
throw new Error(`Tool ${toolName} is out of order. Current Forge Debug gate is ${currentDebugGate}.`);
|
|
294
|
+
}
|
|
295
|
+
const completedGate = currentDebugGate;
|
|
296
|
+
const reference = await writeCompletionArtifact({ ...this.config, phase: completedGate }, toolName, input);
|
|
297
|
+
const status = reference.status.trim().toLowerCase();
|
|
298
|
+
const pass = status === "pass";
|
|
299
|
+
const nextGate = pass ? this.advanceAfterDebugGate(completedGate) : completedGate;
|
|
300
|
+
const nextPhase = nextGate ?? "qa_ready";
|
|
301
|
+
const nextInstruction = completionToolNextInstructionForDebugGate(completedGate, status, nextGate);
|
|
302
|
+
const reason = pass
|
|
303
|
+
? `${completedGate} artifact accepted${nextGate ? `; continue to ${nextGate}.` : "; Debug regression is QA-ready."}`
|
|
304
|
+
: `${completedGate} artifact recorded with status ${status}; rework is required before Debug progression.`;
|
|
305
|
+
return {
|
|
306
|
+
content: [
|
|
307
|
+
{
|
|
308
|
+
type: "text",
|
|
309
|
+
text: `Forge recorded ${completedGate} artifact ${path.relative(reference.repoPath, reference.path)} (${reference.status}, ${reference.contentHash}). ${nextInstruction}`
|
|
310
|
+
}
|
|
311
|
+
],
|
|
312
|
+
structuredContent: {
|
|
313
|
+
...reference,
|
|
314
|
+
...acceptedArtifactReceipt(reference),
|
|
315
|
+
gate: {
|
|
316
|
+
phase: completedGate,
|
|
317
|
+
status,
|
|
318
|
+
allowed: pass,
|
|
319
|
+
reason
|
|
320
|
+
},
|
|
321
|
+
nextPhase,
|
|
322
|
+
nextInstruction
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
async callQaGateTool(toolName, requestedPhase, input) {
|
|
327
|
+
const requestedGate = qaGateFromToolName(toolName);
|
|
328
|
+
if (requestedPhase !== "qa" || !requestedGate) {
|
|
329
|
+
throw new Error(`Tool ${toolName} is not available in QA-cycle mode. Current Forge QA gate is ${this.currentQaGate}.`);
|
|
330
|
+
}
|
|
331
|
+
if (requestedGate !== this.currentQaGate) {
|
|
332
|
+
throw new Error(`Tool ${toolName} is out of order. Current Forge QA gate is ${this.currentQaGate}.`);
|
|
333
|
+
}
|
|
334
|
+
const completedGate = this.currentQaGate;
|
|
335
|
+
if (completedGate === "qa_regression" && managedAttemptToolsAvailable()) {
|
|
336
|
+
assertManagedQaRegressionEvidence(input, this.managedCommandReceipts);
|
|
337
|
+
}
|
|
338
|
+
const reference = await writeCompletionArtifact({ ...this.config, phase: "qa" }, toolName, input);
|
|
339
|
+
const status = reference.status.trim().toLowerCase();
|
|
340
|
+
const nextGate = this.advanceAfterQaGate(completedGate);
|
|
341
|
+
const nextPhase = nextGate ? "qa" : "sign_off_ready";
|
|
342
|
+
const nextInstruction = completionToolNextInstructionForQaGate(completedGate, status, nextGate);
|
|
343
|
+
const allowed = status !== "blocked";
|
|
344
|
+
const reason = nextGate
|
|
345
|
+
? `${completedGate} artifact accepted locally; continue to ${nextGate}.`
|
|
346
|
+
: `QA verdict artifact recorded locally with status ${status}; API ingestion and acceptance are pending.`;
|
|
347
|
+
return {
|
|
348
|
+
content: [
|
|
349
|
+
{
|
|
350
|
+
type: "text",
|
|
351
|
+
text: `Forge recorded ${completedGate} artifact ${path.relative(reference.repoPath, reference.path)} (${reference.status}, ${reference.contentHash}). ${nextInstruction}`
|
|
352
|
+
}
|
|
353
|
+
],
|
|
354
|
+
structuredContent: {
|
|
355
|
+
...reference,
|
|
356
|
+
...acceptedArtifactReceipt(reference),
|
|
357
|
+
gate: {
|
|
358
|
+
phase: "qa",
|
|
359
|
+
qaGate: completedGate,
|
|
360
|
+
status,
|
|
361
|
+
allowed,
|
|
362
|
+
reason
|
|
363
|
+
},
|
|
364
|
+
nextPhase,
|
|
365
|
+
nextInstruction
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
advanceAfterPass(phase) {
|
|
370
|
+
if (phase === "qa") {
|
|
371
|
+
this.completed = true;
|
|
372
|
+
return "sign_off_ready";
|
|
373
|
+
}
|
|
374
|
+
if (phase === "ci_repair") {
|
|
375
|
+
this.completed = true;
|
|
376
|
+
return "provider_checks_ready";
|
|
377
|
+
}
|
|
378
|
+
if (isDebugCompletionPhase(phase)) {
|
|
379
|
+
return nextDebugPhaseAfter(phase) ?? "qa_ready";
|
|
380
|
+
}
|
|
381
|
+
const next = nextPhaseAfter(phase);
|
|
382
|
+
if (!this.config.developCycle) {
|
|
383
|
+
return next ?? "dev_test_ready";
|
|
384
|
+
}
|
|
385
|
+
if (next) {
|
|
386
|
+
this.currentPhase = next;
|
|
387
|
+
return next;
|
|
388
|
+
}
|
|
389
|
+
this.completed = true;
|
|
390
|
+
return "dev_test_ready";
|
|
391
|
+
}
|
|
392
|
+
advanceAfterDecomposeGate(gate) {
|
|
393
|
+
const index = decomposeCompletionGateOrder.indexOf(gate);
|
|
394
|
+
const next = index >= 0 ? decomposeCompletionGateOrder[index + 1] ?? null : null;
|
|
395
|
+
if (next) {
|
|
396
|
+
this.currentDecomposeGate = next;
|
|
397
|
+
return next;
|
|
398
|
+
}
|
|
399
|
+
this.completed = true;
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
advanceAfterDraftGate(gate) {
|
|
403
|
+
const index = draftCompletionGateOrder.indexOf(gate);
|
|
404
|
+
const next = index >= 0 ? draftCompletionGateOrder[index + 1] ?? null : null;
|
|
405
|
+
if (next) {
|
|
406
|
+
this.currentDraftGate = next;
|
|
407
|
+
return next;
|
|
408
|
+
}
|
|
409
|
+
this.completed = true;
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
advanceAfterQaGate(gate) {
|
|
413
|
+
const index = qaCompletionGateOrder.indexOf(gate);
|
|
414
|
+
const next = index >= 0 ? qaCompletionGateOrder[index + 1] ?? null : null;
|
|
415
|
+
if (next) {
|
|
416
|
+
this.currentQaGate = next;
|
|
417
|
+
return next;
|
|
418
|
+
}
|
|
419
|
+
this.completed = true;
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
advanceAfterDebugGate(gate) {
|
|
423
|
+
const index = debugCompletionPhaseOrder.indexOf(gate);
|
|
424
|
+
const next = index >= 0 ? debugCompletionPhaseOrder[index + 1] ?? null : null;
|
|
425
|
+
if (next) {
|
|
426
|
+
this.currentPhase = next;
|
|
427
|
+
return next;
|
|
428
|
+
}
|
|
429
|
+
this.completed = true;
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
const completionToolRuntimeEnvironmentNames = [
|
|
434
|
+
"FORGE_RUNTIME_MODE",
|
|
435
|
+
"FORGE_RESOURCE_LEASE_ID",
|
|
436
|
+
"FORGE_DISPATCH_ATTEMPT_ID",
|
|
437
|
+
"FORGE_E2E_OWNERSHIP_MODE",
|
|
438
|
+
"FORGE_E2E_OWNERSHIP_TOKEN",
|
|
439
|
+
"FORGE_WORKTREE_PATH",
|
|
440
|
+
"FORGE_LOG_DIR",
|
|
441
|
+
"FORGE_CACHE_DIR",
|
|
442
|
+
"FORGE_TEST_OUTPUT_DIR",
|
|
443
|
+
"FORGE_ATTEMPT_DATABASE_SCHEMA",
|
|
444
|
+
"FORGE_ATTEMPT_DATABASE_READINESS_PATH",
|
|
445
|
+
"FORGE_ATTEMPT_DATABASE_CAPABILITY",
|
|
446
|
+
"FORGE_CI_REPAIR_READINESS_PATH",
|
|
447
|
+
"FORGE_CI_REPAIR_HANDOFF_PATH",
|
|
448
|
+
"FORGE_CI_REPAIR_PROVIDER_ONLY_SPECS",
|
|
449
|
+
"FORGE_PLAYWRIGHT_CONFIG",
|
|
450
|
+
"FORGE_API_URL",
|
|
451
|
+
"FORGE_WEB_URL",
|
|
452
|
+
"FORGE_E2E_API_URL",
|
|
453
|
+
"FORGE_E2E_WEB_URL",
|
|
454
|
+
"PLAYWRIGHT_BROWSERS_PATH",
|
|
455
|
+
"FORGE_TMPDIR",
|
|
456
|
+
"NPM_CONFIG_CACHE",
|
|
457
|
+
"TMPDIR",
|
|
458
|
+
"TMP",
|
|
459
|
+
"TEMP",
|
|
460
|
+
];
|
|
461
|
+
export function completionToolRuntimeEnvironment(runtimeEnvironment, nativeTestPostgresFallbackPath) {
|
|
462
|
+
if (!runtimeEnvironment)
|
|
463
|
+
return {};
|
|
464
|
+
const env = {};
|
|
465
|
+
for (const name of completionToolRuntimeEnvironmentNames) {
|
|
466
|
+
const value = runtimeEnvironment[name]?.trim();
|
|
467
|
+
if (value)
|
|
468
|
+
env[name] = value;
|
|
469
|
+
}
|
|
470
|
+
// Managed verification reads the operator's native test cluster only through
|
|
471
|
+
// this explicit path, and only while Docker is unavailable.
|
|
472
|
+
if (nativeTestPostgresFallbackPath) {
|
|
473
|
+
env[nativeTestPostgresFallbackConfigEnvironment] = nativeTestPostgresFallbackPath;
|
|
474
|
+
}
|
|
475
|
+
return env;
|
|
476
|
+
}
|
|
477
|
+
const minimumManagedSolutionTestTimeoutMs = 45 * 60 * 1000;
|
|
478
|
+
/**
|
|
479
|
+
* A solution-wide Forge test is long enough that a generic provider shell can
|
|
480
|
+
* expire while VSTest is still healthy. On managed host attempts, only the
|
|
481
|
+
* attempt-owned command runner can prove the exact process outcome and clean
|
|
482
|
+
* up its process tree. Keep this check at the completion gate so prompt drift
|
|
483
|
+
* cannot turn an arbitrary shell timeout into a QA blocker.
|
|
484
|
+
*/
|
|
485
|
+
export function assertManagedQaRegressionEvidence(input, receipts) {
|
|
486
|
+
const payload = asRecord(input, "QA regression completion payload");
|
|
487
|
+
const nestedEvidence = asMutableRecord(payload.evidence);
|
|
488
|
+
const commands = payload.commands ?? nestedEvidence.commands;
|
|
489
|
+
if (!Array.isArray(commands))
|
|
490
|
+
return;
|
|
491
|
+
for (const [index, entry] of commands.entries()) {
|
|
492
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
493
|
+
continue;
|
|
494
|
+
const command = entry.command;
|
|
495
|
+
if (typeof command !== "string" || !isForgeSolutionTestCommand(command))
|
|
496
|
+
continue;
|
|
497
|
+
const managedCommandId = entry.managed_command_id;
|
|
498
|
+
if (typeof managedCommandId !== "string" || !managedCommandId.trim()) {
|
|
499
|
+
throw new Error(`qa_regression_managed_command_id_required: commands[${index}] runs the solution-wide Forge test. `
|
|
500
|
+
+ "Run it with forge.start_managed_command, continue independent work, then call forge.wait_managed_command for a transport-safe receipt. "
|
|
501
|
+
+ "If command_continues=true, continue useful work or wait again after retry_after_ms. Include the terminal managed_command_id in this command evidence.");
|
|
502
|
+
}
|
|
503
|
+
const matchingReceipts = receipts.filter((receipt) => {
|
|
504
|
+
const tokens = receipt.command;
|
|
505
|
+
return receipt.command_id === managedCommandId
|
|
506
|
+
&& Array.isArray(tokens)
|
|
507
|
+
&& tokens.every((token) => typeof token === "string")
|
|
508
|
+
&& isForgeSolutionTestCommand(tokens.join(" "));
|
|
509
|
+
});
|
|
510
|
+
if (matchingReceipts.length === 0) {
|
|
511
|
+
throw new Error(`qa_regression_managed_command_required: commands[${index}] runs the solution-wide Forge test. `
|
|
512
|
+
+ "Run it with forge.start_managed_command, continue independent work, then call forge.wait_managed_command for a transport-safe receipt. "
|
|
513
|
+
+ "If command_continues=true, continue useful work or wait again after retry_after_ms. Include its matching terminal managed_command_id; generic-shell timeout evidence is not accepted on a managed host.");
|
|
514
|
+
}
|
|
515
|
+
const terminalReceipts = matchingReceipts.filter((receipt) => typeof receipt.status === "string" && [
|
|
516
|
+
"pass",
|
|
517
|
+
"fail",
|
|
518
|
+
"timed_out",
|
|
519
|
+
"timeout_termination_failed",
|
|
520
|
+
"runner_error",
|
|
521
|
+
].includes(receipt.status));
|
|
522
|
+
if (terminalReceipts.length === 0) {
|
|
523
|
+
throw new Error(`qa_regression_managed_command_in_progress: commands[${index}] has not produced a terminal managed receipt. `
|
|
524
|
+
+ "Call forge.wait_managed_command. If command_continues=true, continue useful work or wait again after retry_after_ms; resubmit this gate only after the receipt is terminal.");
|
|
525
|
+
}
|
|
526
|
+
const adequateReceipt = terminalReceipts.some((receipt) => {
|
|
527
|
+
if (receipt.timed_out !== true)
|
|
528
|
+
return true;
|
|
529
|
+
return typeof receipt.timeout_ms === "number"
|
|
530
|
+
&& receipt.timeout_ms >= minimumManagedSolutionTestTimeoutMs
|
|
531
|
+
&& receipt.process_tree_terminated === true;
|
|
532
|
+
});
|
|
533
|
+
if (!adequateReceipt) {
|
|
534
|
+
throw new Error(`qa_regression_managed_timeout_too_short: commands[${index}] timed out before the minimum `
|
|
535
|
+
+ `${minimumManagedSolutionTestTimeoutMs}ms solution-test budget. Rerun it with forge.start_managed_command `
|
|
536
|
+
+ "using at least a 45-minute timeout and resubmit this gate.");
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
function isForgeSolutionTestCommand(command) {
|
|
541
|
+
const normalized = command.trim().toLowerCase().replaceAll("\\", "/");
|
|
542
|
+
return normalized.includes("dotnet")
|
|
543
|
+
&& /(?:^|\s)test(?:\s|$)/u.test(normalized)
|
|
544
|
+
&& /(?:^|[\s/"'])forge\.sln(?:[\s"']|$)/u.test(normalized);
|
|
545
|
+
}
|
|
546
|
+
function managedAttemptToolsAvailable() {
|
|
547
|
+
return process.env.FORGE_RUNTIME_MODE === "host"
|
|
548
|
+
&& Boolean(process.env.FORGE_RESOURCE_LEASE_ID?.trim())
|
|
549
|
+
&& Boolean(process.env.FORGE_DISPATCH_ATTEMPT_ID?.trim())
|
|
550
|
+
&& Boolean(process.env.FORGE_LOG_DIR?.trim())
|
|
551
|
+
&& Boolean(process.env.FORGE_TEST_OUTPUT_DIR?.trim());
|
|
552
|
+
}
|
|
553
|
+
export async function runCompletionMcpServer(config) {
|
|
554
|
+
const session = new CompletionToolServerSession(config);
|
|
555
|
+
process.stdin.setEncoding("utf8");
|
|
556
|
+
let buffer = "";
|
|
557
|
+
for await (const chunk of process.stdin) {
|
|
558
|
+
buffer += chunk;
|
|
559
|
+
let newlineIndex = buffer.indexOf("\n");
|
|
560
|
+
while (newlineIndex >= 0) {
|
|
561
|
+
const rawLine = buffer.slice(0, newlineIndex).trim();
|
|
562
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
563
|
+
if (rawLine.length > 0) {
|
|
564
|
+
await handleJsonRpcLine(session, rawLine);
|
|
565
|
+
}
|
|
566
|
+
newlineIndex = buffer.indexOf("\n");
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
const trailing = buffer.trim();
|
|
570
|
+
if (trailing.length > 0) {
|
|
571
|
+
await handleJsonRpcLine(session, trailing);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
export function parseCompletionMcpArgs(argv) {
|
|
575
|
+
let worktreePath = null;
|
|
576
|
+
let workItemId = null;
|
|
577
|
+
let phase = "implementation";
|
|
578
|
+
let ownerType;
|
|
579
|
+
let ownerId;
|
|
580
|
+
let producedBy;
|
|
581
|
+
let developCycle = false;
|
|
582
|
+
let qaCycle = false;
|
|
583
|
+
let debugCycle = false;
|
|
584
|
+
let architectCycle = false;
|
|
585
|
+
let draftCycle = false;
|
|
586
|
+
let decomposeCycle = false;
|
|
587
|
+
for (let index = 1; index < argv.length; index += 1) {
|
|
588
|
+
const arg = argv[index];
|
|
589
|
+
if (arg === "--worktree") {
|
|
590
|
+
worktreePath = argv[++index] ?? null;
|
|
591
|
+
}
|
|
592
|
+
else if (arg === "--work-item-id") {
|
|
593
|
+
workItemId = argv[++index] ?? null;
|
|
594
|
+
}
|
|
595
|
+
else if (arg === "--owner-type") {
|
|
596
|
+
ownerType = normalizedOwnerType(argv[++index]);
|
|
597
|
+
if (!ownerType) {
|
|
598
|
+
return { ok: false, error: "mcp --owner-type must be work_item or story." };
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
else if (arg === "--owner-id") {
|
|
602
|
+
ownerId = argv[++index] ?? undefined;
|
|
603
|
+
}
|
|
604
|
+
else if (arg === "--phase") {
|
|
605
|
+
const rawPhase = argv[++index];
|
|
606
|
+
const parsedPhase = parseStrictCompletionPhase(rawPhase);
|
|
607
|
+
if (!parsedPhase) {
|
|
608
|
+
return { ok: false, error: "mcp --phase must be one of: implementation, self-review, validation, qa, architect, draft, decompose, ci-repair, comment-repair, debug-repro, debug-root-cause, debug-fix, debug-regression." };
|
|
609
|
+
}
|
|
610
|
+
phase = parsedPhase;
|
|
611
|
+
}
|
|
612
|
+
else if (arg === "--produced-by") {
|
|
613
|
+
producedBy = argv[++index] ?? undefined;
|
|
614
|
+
}
|
|
615
|
+
else if (arg === "--develop-cycle") {
|
|
616
|
+
developCycle = true;
|
|
617
|
+
}
|
|
618
|
+
else if (arg === "--qa-cycle") {
|
|
619
|
+
qaCycle = true;
|
|
620
|
+
}
|
|
621
|
+
else if (arg === "--debug-cycle") {
|
|
622
|
+
debugCycle = true;
|
|
623
|
+
}
|
|
624
|
+
else if (arg === "--architect-cycle") {
|
|
625
|
+
architectCycle = true;
|
|
626
|
+
}
|
|
627
|
+
else if (arg === "--draft-cycle") {
|
|
628
|
+
draftCycle = true;
|
|
629
|
+
}
|
|
630
|
+
else if (arg === "--decompose-cycle") {
|
|
631
|
+
decomposeCycle = true;
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
return { ok: false, error: `Unknown mcp argument: ${arg}` };
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
if (!worktreePath?.trim() || !workItemId?.trim()) {
|
|
638
|
+
return { ok: false, error: "mcp requires --worktree and --work-item-id." };
|
|
639
|
+
}
|
|
640
|
+
if ([developCycle, qaCycle, debugCycle, architectCycle, draftCycle, decomposeCycle].filter(Boolean).length > 1) {
|
|
641
|
+
return { ok: false, error: "mcp accepts only one cycle mode." };
|
|
642
|
+
}
|
|
643
|
+
if (developCycle && !isDevelopCompletionPhase(phase)) {
|
|
644
|
+
return { ok: false, error: "mcp --develop-cycle requires --phase implementation, self-review, or validation." };
|
|
645
|
+
}
|
|
646
|
+
if (qaCycle && phase !== "qa") {
|
|
647
|
+
return { ok: false, error: "mcp --qa-cycle requires --phase qa." };
|
|
648
|
+
}
|
|
649
|
+
if (debugCycle && !isDebugCompletionPhase(phase)) {
|
|
650
|
+
return { ok: false, error: "mcp --debug-cycle requires --phase debug-repro, debug-root-cause, debug-fix, or debug-regression." };
|
|
651
|
+
}
|
|
652
|
+
if (architectCycle && phase !== "architect") {
|
|
653
|
+
return { ok: false, error: "mcp --architect-cycle requires --phase architect." };
|
|
654
|
+
}
|
|
655
|
+
if (draftCycle && phase !== "draft") {
|
|
656
|
+
return { ok: false, error: "mcp --draft-cycle requires --phase draft." };
|
|
657
|
+
}
|
|
658
|
+
if (decomposeCycle && phase !== "decompose") {
|
|
659
|
+
return { ok: false, error: "mcp --decompose-cycle requires --phase decompose." };
|
|
660
|
+
}
|
|
661
|
+
return {
|
|
662
|
+
ok: true,
|
|
663
|
+
config: {
|
|
664
|
+
worktreePath: path.resolve(worktreePath),
|
|
665
|
+
workItemId,
|
|
666
|
+
ownerType,
|
|
667
|
+
ownerId,
|
|
668
|
+
phase,
|
|
669
|
+
producedBy,
|
|
670
|
+
redactionSecrets: [
|
|
671
|
+
...parseCompletionRedactionSecrets(process.env[completionRedactionSecretsEnv]),
|
|
672
|
+
...attemptDatabaseCapabilitySecrets(process.env[attemptDatabaseCapabilityEnvironment]),
|
|
673
|
+
],
|
|
674
|
+
developCycle,
|
|
675
|
+
qaCycle,
|
|
676
|
+
debugCycle,
|
|
677
|
+
architectCycle,
|
|
678
|
+
draftCycle,
|
|
679
|
+
decomposeCycle
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
export function completionRedactionEnv(secrets) {
|
|
684
|
+
const filtered = secrets.filter((secret) => secret.length > 0);
|
|
685
|
+
return filtered.length > 0
|
|
686
|
+
? { [completionRedactionSecretsEnv]: JSON.stringify(filtered) }
|
|
687
|
+
: {};
|
|
688
|
+
}
|
|
689
|
+
async function prepareCursorCompletionToolRuntime(worktreePath, runtime) {
|
|
690
|
+
const cursorDir = path.join(worktreePath, ".cursor");
|
|
691
|
+
const configPath = path.join(cursorDir, "mcp.json");
|
|
692
|
+
const original = await fs.readFile(configPath, "utf8").then((content) => ({ existed: true, content }), () => ({ existed: false, content: "" }));
|
|
693
|
+
const existing = original.existed ? parseOptionalJsonObject(original.content, configPath) : {};
|
|
694
|
+
const servers = asMutableRecord(existing.mcpServers);
|
|
695
|
+
servers[runtime.serverName] = cursorMcpServerConfig(runtime);
|
|
696
|
+
const next = {
|
|
697
|
+
...existing,
|
|
698
|
+
mcpServers: servers
|
|
699
|
+
};
|
|
700
|
+
await fs.mkdir(cursorDir, { recursive: true });
|
|
701
|
+
await writeFileAtomic(configPath, `${JSON.stringify(next, null, 2)}\n`);
|
|
702
|
+
return {
|
|
703
|
+
...runtime,
|
|
704
|
+
cursorMcpConfigPath: configPath,
|
|
705
|
+
cleanup: async () => {
|
|
706
|
+
if (original.existed) {
|
|
707
|
+
await writeFileAtomic(configPath, original.content);
|
|
708
|
+
}
|
|
709
|
+
else {
|
|
710
|
+
await fs.rm(configPath, { force: true });
|
|
711
|
+
await fs.rmdir(cursorDir).catch(() => undefined);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Claude Code takes MCP servers from `--mcp-config <file>`. The file lives in an attempt-owned
|
|
718
|
+
* temp directory (never the worktree) because its env block can carry redaction secrets, and
|
|
719
|
+
* passing the JSON inline would not survive Windows shim quoting.
|
|
720
|
+
*/
|
|
721
|
+
async function prepareClaudeCompletionToolRuntime(runtime) {
|
|
722
|
+
const configDir = await fs.mkdtemp(path.join(os.tmpdir(), "forge-claude-mcp-"));
|
|
723
|
+
const configPath = path.join(configDir, "mcp.json");
|
|
724
|
+
const config = {
|
|
725
|
+
mcpServers: {
|
|
726
|
+
[runtime.serverName]: claudeMcpServerConfig(runtime)
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
730
|
+
return {
|
|
731
|
+
...runtime,
|
|
732
|
+
claudeMcpConfigPath: configPath,
|
|
733
|
+
cleanup: async () => {
|
|
734
|
+
await fs.rm(configDir, { recursive: true, force: true });
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
async function handleJsonRpcLine(session, rawLine) {
|
|
739
|
+
let message;
|
|
740
|
+
try {
|
|
741
|
+
message = JSON.parse(rawLine);
|
|
742
|
+
}
|
|
743
|
+
catch (error) {
|
|
744
|
+
writeJsonRpc({ jsonrpc: "2.0", id: null, error: rpcError(-32700, errorMessage(error)) });
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
const request = asJsonRpcMessage(message);
|
|
748
|
+
if (!request) {
|
|
749
|
+
writeJsonRpc({ jsonrpc: "2.0", id: null, error: rpcError(-32600, "Invalid JSON-RPC message.") });
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
if (request.id === undefined) {
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
try {
|
|
756
|
+
const result = await handleJsonRpcRequest(session, request.method, request.params);
|
|
757
|
+
writeJsonRpc({ jsonrpc: "2.0", id: request.id, result });
|
|
758
|
+
}
|
|
759
|
+
catch (error) {
|
|
760
|
+
writeJsonRpc({ jsonrpc: "2.0", id: request.id, error: rpcError(-32000, errorMessage(error)) });
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
async function handleJsonRpcRequest(session, method, params) {
|
|
764
|
+
if (method === "initialize") {
|
|
765
|
+
const initializeParams = params && typeof params === "object" && !Array.isArray(params)
|
|
766
|
+
? params
|
|
767
|
+
: {};
|
|
768
|
+
return {
|
|
769
|
+
protocolVersion: typeof initializeParams.protocolVersion === "string" ? initializeParams.protocolVersion : "2025-06-18",
|
|
770
|
+
capabilities: {
|
|
771
|
+
tools: {}
|
|
772
|
+
},
|
|
773
|
+
serverInfo: {
|
|
774
|
+
name: "forge-completion-tools",
|
|
775
|
+
version: "0.1.0"
|
|
776
|
+
},
|
|
777
|
+
instructions: session.instructions
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
if (method === "ping") {
|
|
781
|
+
return {};
|
|
782
|
+
}
|
|
783
|
+
if (method === "tools/list") {
|
|
784
|
+
return {
|
|
785
|
+
tools: session.toolDefinitions()
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
if (method === "tools/call") {
|
|
789
|
+
const call = asRecord(params, "tools/call params");
|
|
790
|
+
const name = requiredString(call, "name");
|
|
791
|
+
const args = call.arguments ?? {};
|
|
792
|
+
try {
|
|
793
|
+
return await session.callTool(name, args);
|
|
794
|
+
}
|
|
795
|
+
catch (error) {
|
|
796
|
+
if (isManagedAttemptTool(name))
|
|
797
|
+
throw error;
|
|
798
|
+
return completionToolRejectedResult(name, error);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
throw new Error(`Unsupported MCP method: ${method}`);
|
|
802
|
+
}
|
|
803
|
+
export function completionToolRejectedResult(toolName, error) {
|
|
804
|
+
const issues = error instanceof CompletionPayloadValidationError
|
|
805
|
+
? [...error.issues]
|
|
806
|
+
: [errorMessage(error)];
|
|
807
|
+
const repairInstruction = "Repair every listed issue and resubmit this same gate once. Do not inspect the artifact path: rejected calls never write or replace completion artifacts.";
|
|
808
|
+
return {
|
|
809
|
+
content: [{
|
|
810
|
+
type: "text",
|
|
811
|
+
text: `Forge rejected ${toolName}. No artifact was written.\n${issues.map(issue => `- ${issue}`).join("\n")}\n${repairInstruction}`,
|
|
812
|
+
}],
|
|
813
|
+
structuredContent: {
|
|
814
|
+
status: "rejected",
|
|
815
|
+
tool_name: toolName,
|
|
816
|
+
artifact_written: false,
|
|
817
|
+
issues,
|
|
818
|
+
repair_instruction: repairInstruction,
|
|
819
|
+
},
|
|
820
|
+
isError: true,
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
function acceptedArtifactReceipt(reference) {
|
|
824
|
+
return {
|
|
825
|
+
artifact_written: true,
|
|
826
|
+
artifact_receipt: {
|
|
827
|
+
schema_version: 1,
|
|
828
|
+
path: reference.path,
|
|
829
|
+
report_path: reference.reportPath,
|
|
830
|
+
content_hash: reference.contentHash,
|
|
831
|
+
},
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
function toolDefinition(name, phase, qaGate = null, decomposeGate = null) {
|
|
835
|
+
return {
|
|
836
|
+
name,
|
|
837
|
+
description: toolDescription(phase, qaGate, decomposeGate),
|
|
838
|
+
inputSchema: toolInputSchema(phase, qaGate, decomposeGate)
|
|
839
|
+
};
|
|
840
|
+
}
|