@loucompanion/forge-bridge 0.1.3-dev.ba7d36726eda → 0.1.3-dev.bca7d0ccee53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bridge/bridge.base.d.ts +2 -1
- package/dist/bridge/bridge.service.d.ts +8 -0
- package/dist/bridge/bridge.service.js +184 -12
- package/dist/bridge/bridge.types.d.ts +37 -0
- package/dist/cli/cli.help.js +1 -1
- package/dist/cli/cli.service.js +1 -1
- package/dist/cli/local-config.js +2 -1
- package/dist/runtime/attempt-node-runtime-launcher.d.ts +3 -1
- package/dist/runtime/attempt-node-runtime-launcher.js +62 -11
- package/dist/runtime/attempt-process-supervisor-registry.js +1 -1
- package/dist/runtime/attempt-process-supervisor.d.ts +12 -0
- package/dist/runtime/attempt-process-supervisor.js +75 -15
- package/dist/runtime/generated-output-reset.d.ts +10 -0
- package/dist/runtime/generated-output-reset.js +103 -0
- package/dist/runtime/host-listener-inspector.d.ts +16 -0
- package/dist/runtime/host-listener-inspector.js +143 -4
- package/dist/runtime/host-runtime-contract.d.ts +9 -1
- package/dist/runtime/host-runtime-contract.js +411 -6
- package/dist/runtime/host-runtime-lease.d.ts +12 -1
- package/dist/runtime/host-runtime-lease.js +271 -50
- package/dist/runtime/host-runtime-promotion.d.ts +2 -0
- package/dist/runtime/host-runtime-promotion.js +35 -4
- package/dist/runtime/node-host-runtime-adapter.d.ts +7 -0
- package/dist/runtime/node-host-runtime-adapter.js +71 -8
- package/dist/runtime/playwright-host-runtime-adapter.js +84 -0
- package/dist/sandbox/preflight.d.ts +9 -0
- package/dist/sandbox/preflight.js +39 -4
- package/dist/sandbox/workspace.service.js +7 -2
- package/dist/session/managed-attempt-tools.d.ts +17 -0
- package/dist/session/managed-attempt-tools.js +712 -0
- package/dist/session/phase-completion.artifacts.js +9 -2
- package/dist/session/phase-completion.d.ts +11 -2
- package/dist/session/phase-completion.helpers.d.ts +5 -0
- package/dist/session/phase-completion.helpers.js +45 -9
- package/dist/session/phase-completion.js +12 -3
- package/dist/session/phase-completion.prompts.d.ts +3 -3
- package/dist/session/phase-completion.prompts.js +21 -6
- package/dist/session/phase-completion.runtime.d.ts +13 -1
- package/dist/session/phase-completion.runtime.js +183 -8
- package/dist/session/phase-completion.schemas.js +108 -3
- package/dist/session/phase-completion.validation.d.ts +4 -1
- package/dist/session/phase-completion.validation.js +144 -18
- package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
- package/dist/session/provider-cli/ci-repair-readiness.js +95 -0
- package/dist/session/provider-cli/provider-cli.deterministic.js +47 -1
- package/dist/session/provider-cli/provider-cli.process.d.ts +14 -0
- package/dist/session/provider-cli/provider-cli.process.js +176 -18
- package/dist/session/provider-cli/provider-cli.prompts.js +30 -8
- package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
- package/dist/session/provider-cli/provider-cli.service.d.ts +19 -2
- package/dist/session/provider-cli/provider-cli.service.js +390 -83
- package/dist/session/provider-cli/provider-cli.types.d.ts +3 -0
- package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
- package/dist/session/provider-cli/provider-output-state.js +30 -2
- package/dist/session/session.base.d.ts +2 -1
- package/dist/session/session.service.d.ts +2 -1
- package/dist/session/session.service.js +303 -30
- package/dist/session/session.types.d.ts +38 -0
- package/dist/session/session.types.js +17 -0
- package/dist/shared/process/host-process-supervisor.d.ts +18 -1
- package/dist/shared/process/host-process-supervisor.js +94 -7
- package/dist/shared/process/process.d.ts +1 -0
- package/dist/shared/process/process.js +1 -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 +43 -4
- package/dist/worktree/dependency-warmer.d.ts +10 -1
- package/dist/worktree/dependency-warmer.js +94 -5
- package/dist/worktree/worktree.service.d.ts +1 -0
- package/dist/worktree/worktree.service.js +12 -1
- package/package.json +1 -1
- package/runtime/windows-job-supervisor.ps1 +89 -20
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { completionPhaseFromDispatch, completionPhaseOrder, completionRedactionSecretsEnv, completionToolNamesForDebugGate, completionToolNamesForDecomposeGate, completionToolNamesForDraftGate, completionToolNamesForPhase, completionToolNamesForQaGate, debugCompletionPhaseOrder, decomposeCompletionGateOrder, draftCompletionGateOrder, qaCompletionGateOrder, toolServerName, usesArchitectCycleCompletion, usesDebugCycleCompletion, usesDevelopCycleCompletion, usesQaCycleCompletion } from "./phase-completion.js";
|
|
4
|
-
import { asDebugCompletionPhase, asJsonRpcMessage, asMutableRecord, asRecord, decomposeGateFromToolName, draftGateFromToolName, errorMessage, forgeBridgeCliBinPath, isDebugCompletionPhase, isDecomposeDispatch, isDevelopCompletionPhase, normalizedOwnerType, parseCompletionRedactionSecrets, parseOptionalJsonObject, parseStrictCompletionPhase, phaseFromToolName, qaGateFromToolName, requiredString, rpcError, tomlString, tomlStringArray, writeFileAtomic, writeJsonRpc } from "./phase-completion.validation.js";
|
|
4
|
+
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";
|
|
5
5
|
import { completionPromptForDebugCycle, completionPromptForDecomposeCycle, completionPromptForDevelopCycle, completionPromptForDraftCycle, completionPromptForPhase, completionPromptForQaCycle, completionToolNextInstruction, completionToolNextInstructionForDebugGate, completionToolNextInstructionForDecomposeGate, completionToolNextInstructionForDraftGate, completionToolNextInstructionForQaGate, nextDebugPhaseAfter, nextPhaseAfter } from "./phase-completion.prompts.js";
|
|
6
6
|
import { toolDescription, toolInputSchema } from "./phase-completion.schemas.js";
|
|
7
7
|
import { writeCompletionArtifact, writeDecomposeGateCompletionArtifact, writeDraftGateCompletionArtifact } from "./phase-completion.artifacts.js";
|
|
8
|
+
import { callManagedAttemptTool, isManagedAttemptTool, managedAttemptToolDefinition, managedAttemptToolNames, } from "./managed-attempt-tools.js";
|
|
8
9
|
export async function prepareCompletionToolRuntime(options) {
|
|
9
10
|
const phase = options.phase ?? completionPhaseFromDispatch(options.dispatch);
|
|
10
11
|
const worktreePath = path.resolve(options.worktree.worktreePath);
|
|
@@ -32,7 +33,7 @@ export async function prepareCompletionToolRuntime(options) {
|
|
|
32
33
|
serverName: toolServerName,
|
|
33
34
|
command: process.execPath,
|
|
34
35
|
args: [forgeBridgeCliBinPath(), ...mcpArgs],
|
|
35
|
-
env:
|
|
36
|
+
env: completionToolRuntimeEnvironment(options.runtimeEnvironment),
|
|
36
37
|
cleanup: async () => undefined
|
|
37
38
|
};
|
|
38
39
|
if (options.dispatch.execution.provider.trim().toLowerCase() !== "cursor") {
|
|
@@ -84,7 +85,9 @@ export function codexMcpConfigArgs(runtime) {
|
|
|
84
85
|
"-c",
|
|
85
86
|
`${prefix}.args=${tomlStringArray(runtime.args)}`,
|
|
86
87
|
"-c",
|
|
87
|
-
`${prefix}.default_tools_approval_mode=${tomlString("approve")}
|
|
88
|
+
`${prefix}.default_tools_approval_mode=${tomlString("approve")}`,
|
|
89
|
+
"-c",
|
|
90
|
+
`${prefix}.tool_timeout_sec=7200`
|
|
88
91
|
];
|
|
89
92
|
for (const [key, value] of Object.entries(runtime.env)) {
|
|
90
93
|
args.push("-c", `${prefix}.env.${key}=${tomlString(value)}`);
|
|
@@ -119,6 +122,7 @@ export class CompletionToolServerSession {
|
|
|
119
122
|
currentQaGate = "qa_plan";
|
|
120
123
|
currentDraftGate = "draft_intent";
|
|
121
124
|
currentDecomposeGate = "decompose_subject_intake";
|
|
125
|
+
managedCommandReceipts = [];
|
|
122
126
|
completed = false;
|
|
123
127
|
constructor(config) {
|
|
124
128
|
this.config = config;
|
|
@@ -132,15 +136,28 @@ export class CompletionToolServerSession {
|
|
|
132
136
|
: this.config.debugCycle
|
|
133
137
|
? completionPromptForDebugCycle(asDebugCompletionPhase(this.currentPhase))
|
|
134
138
|
: this.config.qaCycle
|
|
135
|
-
? completionPromptForQaCycle(this.currentQaGate)
|
|
139
|
+
? completionPromptForQaCycle(this.currentQaGate, artifactOwnerRelativeRoot(this.config.ownerType, this.config.ownerId, this.config.workItemId))
|
|
136
140
|
: this.config.developCycle
|
|
137
141
|
? completionPromptForDevelopCycle(this.currentPhase)
|
|
138
|
-
: completionPromptForPhase(this.currentPhase);
|
|
142
|
+
: completionPromptForPhase(this.currentPhase, artifactOwnerRelativeRoot(this.config.ownerType, this.config.ownerId, this.config.workItemId));
|
|
139
143
|
}
|
|
140
144
|
toolDefinitions() {
|
|
141
|
-
|
|
145
|
+
const completionTools = completionToolNamesForServer(this.config).map((name) => toolDefinition(name, phaseFromToolName(name), qaGateFromToolName(name), decomposeGateFromToolName(name)));
|
|
146
|
+
return managedAttemptToolsAvailable()
|
|
147
|
+
? [...completionTools, ...managedAttemptToolNames.map(managedAttemptToolDefinition)]
|
|
148
|
+
: completionTools;
|
|
142
149
|
}
|
|
143
150
|
async callTool(toolName, input) {
|
|
151
|
+
if (isManagedAttemptTool(toolName)) {
|
|
152
|
+
if (!managedAttemptToolsAvailable()) {
|
|
153
|
+
throw new Error("Forge managed-attempt tools require an active managed host runtime.");
|
|
154
|
+
}
|
|
155
|
+
const result = await callManagedAttemptTool(toolName, input, this.config.worktreePath);
|
|
156
|
+
if (Array.isArray(result.structuredContent.command)) {
|
|
157
|
+
this.managedCommandReceipts.push(result.structuredContent);
|
|
158
|
+
}
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
144
161
|
const requestedPhase = phaseFromToolName(toolName);
|
|
145
162
|
if (this.completed) {
|
|
146
163
|
throw new Error("Forge completion cycle is already complete; no more completion calls are accepted.");
|
|
@@ -180,6 +197,7 @@ export class CompletionToolServerSession {
|
|
|
180
197
|
],
|
|
181
198
|
structuredContent: {
|
|
182
199
|
...reference,
|
|
200
|
+
...acceptedArtifactReceipt(reference),
|
|
183
201
|
gate: {
|
|
184
202
|
phase: completedPhase,
|
|
185
203
|
status,
|
|
@@ -217,6 +235,7 @@ export class CompletionToolServerSession {
|
|
|
217
235
|
content: [{ type: "text", text: `Forge recorded ${completedGate} artifact ${path.relative(reference.repoPath, reference.path)} (${reference.status}, ${reference.contentHash}). ${nextInstruction}` }],
|
|
218
236
|
structuredContent: {
|
|
219
237
|
...reference,
|
|
238
|
+
...acceptedArtifactReceipt(reference),
|
|
220
239
|
gate: { phase: "decompose", decomposeGate: completedGate, status, allowed: pass, reason },
|
|
221
240
|
nextPhase,
|
|
222
241
|
nextInstruction
|
|
@@ -241,6 +260,7 @@ export class CompletionToolServerSession {
|
|
|
241
260
|
content: [{ type: "text", text: `Forge recorded ${completedGate} artifact ${path.relative(reference.repoPath, reference.path)} (${reference.status}, ${reference.contentHash}). ${nextInstruction}` }],
|
|
242
261
|
structuredContent: {
|
|
243
262
|
...reference,
|
|
263
|
+
...acceptedArtifactReceipt(reference),
|
|
244
264
|
gate: { phase: "draft", draftGate: completedGate, status, allowed: status !== "blocked", reason: nextGate ? `${completedGate} artifact accepted; continue to ${nextGate}.` : `Draft proposal artifact accepted with status ${status}.` },
|
|
245
265
|
nextPhase,
|
|
246
266
|
nextInstruction
|
|
@@ -274,6 +294,7 @@ export class CompletionToolServerSession {
|
|
|
274
294
|
],
|
|
275
295
|
structuredContent: {
|
|
276
296
|
...reference,
|
|
297
|
+
...acceptedArtifactReceipt(reference),
|
|
277
298
|
gate: {
|
|
278
299
|
phase: completedGate,
|
|
279
300
|
status,
|
|
@@ -294,6 +315,9 @@ export class CompletionToolServerSession {
|
|
|
294
315
|
throw new Error(`Tool ${toolName} is out of order. Current Forge QA gate is ${this.currentQaGate}.`);
|
|
295
316
|
}
|
|
296
317
|
const completedGate = this.currentQaGate;
|
|
318
|
+
if (completedGate === "qa_regression" && managedAttemptToolsAvailable()) {
|
|
319
|
+
assertManagedQaRegressionEvidence(input, this.managedCommandReceipts);
|
|
320
|
+
}
|
|
297
321
|
const reference = await writeCompletionArtifact({ ...this.config, phase: "qa" }, toolName, input);
|
|
298
322
|
const status = reference.status.trim().toLowerCase();
|
|
299
323
|
const nextGate = this.advanceAfterQaGate(completedGate);
|
|
@@ -312,6 +336,7 @@ export class CompletionToolServerSession {
|
|
|
312
336
|
],
|
|
313
337
|
structuredContent: {
|
|
314
338
|
...reference,
|
|
339
|
+
...acceptedArtifactReceipt(reference),
|
|
315
340
|
gate: {
|
|
316
341
|
phase: "qa",
|
|
317
342
|
qaGate: completedGate,
|
|
@@ -329,6 +354,10 @@ export class CompletionToolServerSession {
|
|
|
329
354
|
this.completed = true;
|
|
330
355
|
return "sign_off_ready";
|
|
331
356
|
}
|
|
357
|
+
if (phase === "ci_repair") {
|
|
358
|
+
this.completed = true;
|
|
359
|
+
return "provider_checks_ready";
|
|
360
|
+
}
|
|
332
361
|
if (isDebugCompletionPhase(phase)) {
|
|
333
362
|
return nextDebugPhaseAfter(phase) ?? "qa_ready";
|
|
334
363
|
}
|
|
@@ -384,6 +413,114 @@ export class CompletionToolServerSession {
|
|
|
384
413
|
return null;
|
|
385
414
|
}
|
|
386
415
|
}
|
|
416
|
+
const completionToolRuntimeEnvironmentNames = [
|
|
417
|
+
"FORGE_RUNTIME_MODE",
|
|
418
|
+
"FORGE_RESOURCE_LEASE_ID",
|
|
419
|
+
"FORGE_DISPATCH_ATTEMPT_ID",
|
|
420
|
+
"FORGE_WORKTREE_PATH",
|
|
421
|
+
"FORGE_LOG_DIR",
|
|
422
|
+
"FORGE_CACHE_DIR",
|
|
423
|
+
"FORGE_TEST_OUTPUT_DIR",
|
|
424
|
+
"FORGE_ATTEMPT_DATABASE_SCHEMA",
|
|
425
|
+
"FORGE_ATTEMPT_DATABASE_READINESS_PATH",
|
|
426
|
+
"FORGE_CI_REPAIR_READINESS_PATH",
|
|
427
|
+
"FORGE_CI_REPAIR_HANDOFF_PATH",
|
|
428
|
+
"FORGE_CI_REPAIR_PROVIDER_ONLY_SPECS",
|
|
429
|
+
"FORGE_PLAYWRIGHT_CONFIG",
|
|
430
|
+
"FORGE_E2E_API_URL",
|
|
431
|
+
"FORGE_E2E_WEB_URL",
|
|
432
|
+
"PLAYWRIGHT_BROWSERS_PATH",
|
|
433
|
+
"FORGE_TMPDIR",
|
|
434
|
+
"NPM_CONFIG_CACHE",
|
|
435
|
+
"TMPDIR",
|
|
436
|
+
"TMP",
|
|
437
|
+
"TEMP",
|
|
438
|
+
];
|
|
439
|
+
export function completionToolRuntimeEnvironment(runtimeEnvironment) {
|
|
440
|
+
if (!runtimeEnvironment)
|
|
441
|
+
return {};
|
|
442
|
+
const env = {};
|
|
443
|
+
for (const name of completionToolRuntimeEnvironmentNames) {
|
|
444
|
+
const value = runtimeEnvironment[name]?.trim();
|
|
445
|
+
if (value)
|
|
446
|
+
env[name] = value;
|
|
447
|
+
}
|
|
448
|
+
return env;
|
|
449
|
+
}
|
|
450
|
+
const minimumManagedSolutionTestTimeoutMs = 45 * 60 * 1000;
|
|
451
|
+
/**
|
|
452
|
+
* A solution-wide Forge test is long enough that a generic provider shell can
|
|
453
|
+
* expire while VSTest is still healthy. On managed host attempts, only the
|
|
454
|
+
* attempt-owned command runner can prove the exact process outcome and clean
|
|
455
|
+
* up its process tree. Keep this check at the completion gate so prompt drift
|
|
456
|
+
* cannot turn an arbitrary shell timeout into a QA blocker.
|
|
457
|
+
*/
|
|
458
|
+
export function assertManagedQaRegressionEvidence(input, receipts) {
|
|
459
|
+
const payload = asRecord(input, "QA regression completion payload");
|
|
460
|
+
const nestedEvidence = asMutableRecord(payload.evidence);
|
|
461
|
+
const commands = payload.commands ?? nestedEvidence.commands;
|
|
462
|
+
if (!Array.isArray(commands))
|
|
463
|
+
return;
|
|
464
|
+
for (const [index, entry] of commands.entries()) {
|
|
465
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
466
|
+
continue;
|
|
467
|
+
const command = entry.command;
|
|
468
|
+
if (typeof command !== "string" || !isForgeSolutionTestCommand(command))
|
|
469
|
+
continue;
|
|
470
|
+
const managedCommandId = entry.managed_command_id;
|
|
471
|
+
if (typeof managedCommandId !== "string" || !managedCommandId.trim()) {
|
|
472
|
+
throw new Error(`qa_regression_managed_command_id_required: commands[${index}] runs the solution-wide Forge test. `
|
|
473
|
+
+ "Run it with forge.start_managed_command, continue independent work, call forge.wait_managed_command once for the terminal receipt, and include the returned managed_command_id in this command evidence.");
|
|
474
|
+
}
|
|
475
|
+
const matchingReceipts = receipts.filter((receipt) => {
|
|
476
|
+
const tokens = receipt.command;
|
|
477
|
+
return receipt.command_id === managedCommandId
|
|
478
|
+
&& Array.isArray(tokens)
|
|
479
|
+
&& tokens.every((token) => typeof token === "string")
|
|
480
|
+
&& isForgeSolutionTestCommand(tokens.join(" "));
|
|
481
|
+
});
|
|
482
|
+
if (matchingReceipts.length === 0) {
|
|
483
|
+
throw new Error(`qa_regression_managed_command_required: commands[${index}] runs the solution-wide Forge test. `
|
|
484
|
+
+ "Run it with forge.start_managed_command, continue independent work, call forge.wait_managed_command once for the terminal receipt, and include its matching managed_command_id; generic-shell timeout evidence is not accepted on a managed host.");
|
|
485
|
+
}
|
|
486
|
+
const terminalReceipts = matchingReceipts.filter((receipt) => typeof receipt.status === "string" && [
|
|
487
|
+
"pass",
|
|
488
|
+
"fail",
|
|
489
|
+
"timed_out",
|
|
490
|
+
"timeout_termination_failed",
|
|
491
|
+
"runner_error",
|
|
492
|
+
].includes(receipt.status));
|
|
493
|
+
if (terminalReceipts.length === 0) {
|
|
494
|
+
throw new Error(`qa_regression_managed_command_in_progress: commands[${index}] has not produced a terminal managed receipt. `
|
|
495
|
+
+ "Call forge.wait_managed_command once for its terminal receipt, then resubmit this gate.");
|
|
496
|
+
}
|
|
497
|
+
const adequateReceipt = terminalReceipts.some((receipt) => {
|
|
498
|
+
if (receipt.timed_out !== true)
|
|
499
|
+
return true;
|
|
500
|
+
return typeof receipt.timeout_ms === "number"
|
|
501
|
+
&& receipt.timeout_ms >= minimumManagedSolutionTestTimeoutMs
|
|
502
|
+
&& receipt.process_tree_terminated === true;
|
|
503
|
+
});
|
|
504
|
+
if (!adequateReceipt) {
|
|
505
|
+
throw new Error(`qa_regression_managed_timeout_too_short: commands[${index}] timed out before the minimum `
|
|
506
|
+
+ `${minimumManagedSolutionTestTimeoutMs}ms solution-test budget. Rerun it with forge.start_managed_command `
|
|
507
|
+
+ "using at least a 45-minute timeout and resubmit this gate.");
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
function isForgeSolutionTestCommand(command) {
|
|
512
|
+
const normalized = command.trim().toLowerCase().replaceAll("\\", "/");
|
|
513
|
+
return normalized.includes("dotnet")
|
|
514
|
+
&& /(?:^|\s)test(?:\s|$)/u.test(normalized)
|
|
515
|
+
&& /(?:^|[\s/"'])forge\.sln(?:[\s"']|$)/u.test(normalized);
|
|
516
|
+
}
|
|
517
|
+
function managedAttemptToolsAvailable() {
|
|
518
|
+
return process.env.FORGE_RUNTIME_MODE === "host"
|
|
519
|
+
&& Boolean(process.env.FORGE_RESOURCE_LEASE_ID?.trim())
|
|
520
|
+
&& Boolean(process.env.FORGE_DISPATCH_ATTEMPT_ID?.trim())
|
|
521
|
+
&& Boolean(process.env.FORGE_LOG_DIR?.trim())
|
|
522
|
+
&& Boolean(process.env.FORGE_TEST_OUTPUT_DIR?.trim());
|
|
523
|
+
}
|
|
387
524
|
export async function runCompletionMcpServer(config) {
|
|
388
525
|
const session = new CompletionToolServerSession(config);
|
|
389
526
|
process.stdin.setEncoding("utf8");
|
|
@@ -439,7 +576,7 @@ export function parseCompletionMcpArgs(argv) {
|
|
|
439
576
|
const rawPhase = argv[++index];
|
|
440
577
|
const parsedPhase = parseStrictCompletionPhase(rawPhase);
|
|
441
578
|
if (!parsedPhase) {
|
|
442
|
-
return { ok: false, error: "mcp --phase must be one of: implementation, self-review, validation, qa, architect, draft, debug-repro, debug-root-cause, debug-fix, debug-regression." };
|
|
579
|
+
return { ok: false, error: "mcp --phase must be one of: implementation, self-review, validation, qa, architect, draft, decompose, ci-repair, debug-repro, debug-root-cause, debug-fix, debug-regression." };
|
|
443
580
|
}
|
|
444
581
|
phase = parsedPhase;
|
|
445
582
|
}
|
|
@@ -598,10 +735,48 @@ async function handleJsonRpcRequest(session, method, params) {
|
|
|
598
735
|
const call = asRecord(params, "tools/call params");
|
|
599
736
|
const name = requiredString(call, "name");
|
|
600
737
|
const args = call.arguments ?? {};
|
|
601
|
-
|
|
738
|
+
try {
|
|
739
|
+
return await session.callTool(name, args);
|
|
740
|
+
}
|
|
741
|
+
catch (error) {
|
|
742
|
+
if (isManagedAttemptTool(name))
|
|
743
|
+
throw error;
|
|
744
|
+
return completionToolRejectedResult(name, error);
|
|
745
|
+
}
|
|
602
746
|
}
|
|
603
747
|
throw new Error(`Unsupported MCP method: ${method}`);
|
|
604
748
|
}
|
|
749
|
+
export function completionToolRejectedResult(toolName, error) {
|
|
750
|
+
const issues = error instanceof CompletionPayloadValidationError
|
|
751
|
+
? [...error.issues]
|
|
752
|
+
: [errorMessage(error)];
|
|
753
|
+
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.";
|
|
754
|
+
return {
|
|
755
|
+
content: [{
|
|
756
|
+
type: "text",
|
|
757
|
+
text: `Forge rejected ${toolName}. No artifact was written.\n${issues.map(issue => `- ${issue}`).join("\n")}\n${repairInstruction}`,
|
|
758
|
+
}],
|
|
759
|
+
structuredContent: {
|
|
760
|
+
status: "rejected",
|
|
761
|
+
tool_name: toolName,
|
|
762
|
+
artifact_written: false,
|
|
763
|
+
issues,
|
|
764
|
+
repair_instruction: repairInstruction,
|
|
765
|
+
},
|
|
766
|
+
isError: true,
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
function acceptedArtifactReceipt(reference) {
|
|
770
|
+
return {
|
|
771
|
+
artifact_written: true,
|
|
772
|
+
artifact_receipt: {
|
|
773
|
+
schema_version: 1,
|
|
774
|
+
path: reference.path,
|
|
775
|
+
report_path: reference.reportPath,
|
|
776
|
+
content_hash: reference.contentHash,
|
|
777
|
+
},
|
|
778
|
+
};
|
|
779
|
+
}
|
|
605
780
|
function toolDefinition(name, phase, qaGate = null, decomposeGate = null) {
|
|
606
781
|
return {
|
|
607
782
|
name,
|
|
@@ -16,6 +16,9 @@ export function toolDescription(phase, qaGate = null, decomposeGate = null) {
|
|
|
16
16
|
if (phase === "implementation") {
|
|
17
17
|
return "Complete Forge implementation by writing implementation.completeness.json with scoped changed-file, verification, acceptance-criteria, gap, and next-phase evidence.";
|
|
18
18
|
}
|
|
19
|
+
if (phase === "ci_repair") {
|
|
20
|
+
return "Complete Forge CI repair by writing ci-repair.handoff.json with failed-check, root-cause, scoped-change, workflow-equivalence, validation, provider-state, and push-readiness evidence.";
|
|
21
|
+
}
|
|
19
22
|
if (phase === "self_review") {
|
|
20
23
|
return "Complete Forge self-review by writing self-review.report.json with diff review, findings, security, edge-case, design, deterministic-comparison, fixes, and remaining-risk evidence.";
|
|
21
24
|
}
|
|
@@ -55,6 +58,94 @@ export function toolInputSchema(phase, qaGate = null, decomposeGate = null) {
|
|
|
55
58
|
if (isDebugCompletionPhase(phase)) {
|
|
56
59
|
return debugToolInputSchema(phase, baseProperties);
|
|
57
60
|
}
|
|
61
|
+
if (phase === "ci_repair") {
|
|
62
|
+
return objectToolSchema({
|
|
63
|
+
...baseProperties,
|
|
64
|
+
failed_checks: {
|
|
65
|
+
type: "array",
|
|
66
|
+
description: "Required and non-empty for pass. Provider check/job/step evidence inspected before repair.",
|
|
67
|
+
items: evidenceObjectSchema(["name", "provider_state"], {
|
|
68
|
+
name: { type: "string" },
|
|
69
|
+
provider_state: { type: "string" },
|
|
70
|
+
workflow: { type: "string" },
|
|
71
|
+
job: { type: "string" },
|
|
72
|
+
step: { type: "string" },
|
|
73
|
+
details_url: { type: "string" },
|
|
74
|
+
evidence: { type: "string" }
|
|
75
|
+
})
|
|
76
|
+
},
|
|
77
|
+
root_cause: {
|
|
78
|
+
type: "object",
|
|
79
|
+
description: "Required for pass. Include classification, summary, and concrete evidence.",
|
|
80
|
+
additionalProperties: true,
|
|
81
|
+
properties: {
|
|
82
|
+
classification: { type: "string" },
|
|
83
|
+
summary: { type: "string" },
|
|
84
|
+
evidence: { type: "array", items: { type: "string" } }
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
changed_files: {
|
|
88
|
+
type: "array",
|
|
89
|
+
description: "Files changed by the repair. May be empty only when no_code_action explains the provider-only action or blocker.",
|
|
90
|
+
items: evidenceObjectSchema(["path", "summary"], {
|
|
91
|
+
path: { type: "string" },
|
|
92
|
+
summary: { type: "string" }
|
|
93
|
+
})
|
|
94
|
+
},
|
|
95
|
+
no_code_action: {
|
|
96
|
+
type: "object",
|
|
97
|
+
description: "Required when changed_files is empty. Describe the evidence-backed provider action or blocker.",
|
|
98
|
+
additionalProperties: true,
|
|
99
|
+
properties: {
|
|
100
|
+
action: { type: "string" },
|
|
101
|
+
reason: { type: "string" },
|
|
102
|
+
evidence: { type: "string" }
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
narrow_validation: {
|
|
106
|
+
type: "array",
|
|
107
|
+
description: "Required and non-empty for pass. The focused failing command or evidence-backed substitute.",
|
|
108
|
+
items: commandEvidenceSchema()
|
|
109
|
+
},
|
|
110
|
+
workflow_validation: {
|
|
111
|
+
type: "array",
|
|
112
|
+
description: "Required and non-empty for pass. Each failed-job guard/test/drift step with local equivalence supported, substitute, or provider_only.",
|
|
113
|
+
items: evidenceObjectSchema(["step", "equivalence", "status"], {
|
|
114
|
+
step: { type: "string" },
|
|
115
|
+
equivalence: { type: "string", enum: ["supported", "substitute", "provider_only"] },
|
|
116
|
+
command: { type: "string" },
|
|
117
|
+
exit_code: { type: "number" },
|
|
118
|
+
status: { type: "string" },
|
|
119
|
+
summary: { type: "string" },
|
|
120
|
+
reason: { type: "string" },
|
|
121
|
+
substitute: { type: "string" }
|
|
122
|
+
})
|
|
123
|
+
},
|
|
124
|
+
broader_validation: {
|
|
125
|
+
type: "array",
|
|
126
|
+
description: "Required and non-empty for pass. Normal validation covering every touched compiled/tested surface.",
|
|
127
|
+
items: commandEvidenceSchema()
|
|
128
|
+
},
|
|
129
|
+
remaining_risks: {
|
|
130
|
+
type: "array",
|
|
131
|
+
description: "Required for pass, even when empty.",
|
|
132
|
+
items: evidenceObjectSchema(["summary"], {
|
|
133
|
+
summary: { type: "string" },
|
|
134
|
+
mitigation: { type: "string" },
|
|
135
|
+
owner: { type: "string" }
|
|
136
|
+
})
|
|
137
|
+
},
|
|
138
|
+
provider_state: {
|
|
139
|
+
type: "string",
|
|
140
|
+
enum: ["ready_for_rerun", "rerun_requested", "checks_pending", "still_failing", "blocked_by_provider"],
|
|
141
|
+
description: "Provider state after local validation and before Forge-owned publication."
|
|
142
|
+
},
|
|
143
|
+
ready_for_push: {
|
|
144
|
+
type: "boolean",
|
|
145
|
+
description: "Required for pass and must be true. Forge Bridge performs the commit/push only after this gate passes."
|
|
146
|
+
}
|
|
147
|
+
}, ["status", "summary"], "For status pass, include failed_checks, root_cause, changed_files or no_code_action, narrow_validation, workflow_validation, broader_validation, remaining_risks, provider_state, and ready_for_push: true.");
|
|
148
|
+
}
|
|
58
149
|
if (phase === "implementation") {
|
|
59
150
|
return objectToolSchema({
|
|
60
151
|
...baseProperties,
|
|
@@ -198,7 +289,7 @@ export function toolInputSchema(phase, qaGate = null, decomposeGate = null) {
|
|
|
198
289
|
},
|
|
199
290
|
sidecars: {
|
|
200
291
|
type: "array",
|
|
201
|
-
description: "Optional QA sidecar files already written under
|
|
292
|
+
description: "Optional QA sidecar files already written under the dispatch-provided QA artifacts path with path/content_hash/bytes metadata.",
|
|
202
293
|
items: evidenceObjectSchema(["path", "content_hash", "bytes"], {
|
|
203
294
|
path: { type: "string" },
|
|
204
295
|
content_hash: { type: "string" },
|
|
@@ -652,9 +743,23 @@ function qaScopeSchema(description) {
|
|
|
652
743
|
function commandEvidenceSchema() {
|
|
653
744
|
return evidenceObjectSchema(["command", "exit_code", "status", "summary"], {
|
|
654
745
|
command: { type: "string" },
|
|
655
|
-
|
|
746
|
+
managed_command_id: { type: "string", description: "Receipt ID returned by forge.start_managed_command and completed through one forge.wait_managed_command call for managed-host evidence." },
|
|
747
|
+
exit_code: { type: ["number", "null"], description: "Process exit code, or null when the managed runner timed out or could not launch the process." },
|
|
656
748
|
status: { type: "string" },
|
|
657
|
-
summary: { type: "string" }
|
|
749
|
+
summary: { type: "string" },
|
|
750
|
+
classification: {
|
|
751
|
+
type: "string",
|
|
752
|
+
enum: ["out_of_scope_failure", "baseline_failure"],
|
|
753
|
+
description: "Only for a verified non-blocking failure in a passing QA regression gate. Never use for an in-scope or unexplained failure."
|
|
754
|
+
},
|
|
755
|
+
candidate_bug_id: { type: "string", description: "Required for a classified non-blocking failure; links the preserved failure to candidate_bugs." },
|
|
756
|
+
scope_evidence: { type: "string", description: "Required proof that the failing surface is outside the current acceptance criteria and changed-file scope." },
|
|
757
|
+
acceptance_criteria_independent: { type: "boolean", description: "Required and true only when the current item can be judged independently of this failure." },
|
|
758
|
+
repro_command: { type: "string", description: "Required for out_of_scope_failure; focused command that reproduced the unrelated failure." },
|
|
759
|
+
repro_exit_code: { type: "number", description: "Required non-zero focused repro exit code for out_of_scope_failure." },
|
|
760
|
+
baseline_ref: { type: "string", description: "Required for baseline_failure; immutable ref where the same failure was observed." },
|
|
761
|
+
baseline_exit_code: { type: "number", description: "Required non-zero baseline command exit code for baseline_failure." },
|
|
762
|
+
failure_signature_match: { type: "boolean", description: "Required and true for baseline_failure after matching failing test/error signatures." }
|
|
658
763
|
});
|
|
659
764
|
}
|
|
660
765
|
function resolvedFindingSchema() {
|
|
@@ -2,9 +2,12 @@ import { type CompletionPhase, type DebugCompletionPhase, type QaCompletionGate
|
|
|
2
2
|
export declare function normalizeCompletionStatus(value: string, phase?: CompletionPhase): string;
|
|
3
3
|
export declare function normalizeQaGateStatus(value: string): string;
|
|
4
4
|
export declare function validatePayloadShape(phase: CompletionPhase, payload: Record<string, unknown>): void;
|
|
5
|
+
export declare function validateCiRepairPayloadShape(payload: Record<string, unknown>): void;
|
|
5
6
|
export declare function validateDebugPayloadShape(phase: DebugCompletionPhase, payload: Record<string, unknown>): void;
|
|
6
7
|
export declare function validateQaGatePayloadShape(gate: QaCompletionGate, payload: Record<string, unknown>, status: string): void;
|
|
7
|
-
export declare function validateQaGatePassingCommands(commands: readonly unknown[]
|
|
8
|
+
export declare function validateQaGatePassingCommands(commands: readonly unknown[], options?: {
|
|
9
|
+
readonly allowClassifiedNonBlockingFailures?: boolean;
|
|
10
|
+
}): void;
|
|
8
11
|
export declare function validateArchitectPayloadShape(payload: Record<string, unknown>, status: string): void;
|
|
9
12
|
export declare function validateArchitectTaskDevelopScope(task: Record<string, unknown>, taskId: string): void;
|
|
10
13
|
export declare function validateArchitectVerificationScope(task: Record<string, unknown>, taskId: string): void;
|