@loucompanion/forge-bridge 0.1.3-dev.fbe15023cdfe → 0.1.4-dev.bfa11ebe98c6
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 +1562 -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-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 +67 -20
- 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 +191 -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 +107 -53
- package/dist/runtime/host-runtime-lease.d.ts +90 -8
- package/dist/runtime/host-runtime-lease.js +1097 -108
- 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 +28 -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 +17 -8
- package/dist/sandbox/runtime.d.ts +6 -1
- package/dist/sandbox/runtime.js +132 -103
- 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 +481 -31
- package/dist/session/managed-build-coordinator.d.ts +5 -0
- package/dist/session/managed-build-coordinator.js +105 -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 +118 -16
- 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 +657 -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 +8 -1
- package/dist/session/provider-cli/provider-cli.runtime.js +40 -3
- package/dist/session/provider-cli/provider-cli.service.d.ts +30 -5
- package/dist/session/provider-cli/provider-cli.service.js +771 -68
- 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-supervisor.d.ts +2 -0
- package/dist/shared/process/host-process-supervisor.js +57 -9
- 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/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,8 +1,10 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
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, 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
|
+
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
6
|
import { completionPromptForDebugCycle, completionPromptForDecomposeCycle, completionPromptForDevelopCycle, completionPromptForDraftCycle, completionPromptForPhase, completionPromptForQaCycle, completionToolNextInstruction, completionToolNextInstructionForDebugGate, completionToolNextInstructionForDecomposeGate, completionToolNextInstructionForDraftGate, completionToolNextInstructionForQaGate, nextDebugPhaseAfter, nextPhaseAfter } from "./phase-completion.prompts.js";
|
|
7
|
+
import { configuredNativeTestPostgresFallbackPath, nativeTestPostgresFallbackConfigEnvironment, } from "../runtime/native-test-postgres-fallback.js";
|
|
6
8
|
import { toolDescription, toolInputSchema } from "./phase-completion.schemas.js";
|
|
7
9
|
import { writeCompletionArtifact, writeDecomposeGateCompletionArtifact, writeDraftGateCompletionArtifact } from "./phase-completion.artifacts.js";
|
|
8
10
|
import { callManagedAttemptTool, isManagedAttemptTool, managedAttemptToolDefinition, managedAttemptToolNames, } from "./managed-attempt-tools.js";
|
|
@@ -33,13 +35,17 @@ export async function prepareCompletionToolRuntime(options) {
|
|
|
33
35
|
serverName: toolServerName,
|
|
34
36
|
command: process.execPath,
|
|
35
37
|
args: [forgeBridgeCliBinPath(), ...mcpArgs],
|
|
36
|
-
env: completionToolRuntimeEnvironment(options.runtimeEnvironment),
|
|
38
|
+
env: completionToolRuntimeEnvironment(options.runtimeEnvironment, configuredNativeTestPostgresFallbackPath()),
|
|
37
39
|
cleanup: async () => undefined
|
|
38
40
|
};
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
const provider = options.dispatch.execution.provider.trim().toLowerCase();
|
|
42
|
+
if (provider === "cursor") {
|
|
43
|
+
return await prepareCursorCompletionToolRuntime(worktreePath, runtime);
|
|
41
44
|
}
|
|
42
|
-
|
|
45
|
+
if (provider === "claude") {
|
|
46
|
+
return await prepareClaudeCompletionToolRuntime(runtime);
|
|
47
|
+
}
|
|
48
|
+
return runtime;
|
|
43
49
|
}
|
|
44
50
|
export function buildCompletionToolServerArgs(config) {
|
|
45
51
|
const args = [
|
|
@@ -101,6 +107,14 @@ export function cursorMcpServerConfig(runtime) {
|
|
|
101
107
|
env: runtime.env
|
|
102
108
|
};
|
|
103
109
|
}
|
|
110
|
+
export function claudeMcpServerConfig(runtime) {
|
|
111
|
+
return {
|
|
112
|
+
type: "stdio",
|
|
113
|
+
command: runtime.command,
|
|
114
|
+
args: [...runtime.args],
|
|
115
|
+
env: runtime.env
|
|
116
|
+
};
|
|
117
|
+
}
|
|
104
118
|
export function completionToolNamesForServer(config) {
|
|
105
119
|
return config.decomposeCycle
|
|
106
120
|
? decomposeCompletionGateOrder.flatMap((gate) => completionToolNamesForDecomposeGate(gate))
|
|
@@ -185,9 +199,11 @@ export class CompletionToolServerSession {
|
|
|
185
199
|
? this.advanceAfterPass(completedPhase)
|
|
186
200
|
: completedPhase;
|
|
187
201
|
const nextInstruction = completionToolNextInstruction(completedPhase, status, nextPhase);
|
|
188
|
-
const reason =
|
|
189
|
-
?
|
|
190
|
-
:
|
|
202
|
+
const reason = completedPhase === "qa"
|
|
203
|
+
? `QA artifact recorded locally with status ${status}; API ingestion and acceptance are pending.`
|
|
204
|
+
: pass
|
|
205
|
+
? `${completedPhase} completion artifact accepted.`
|
|
206
|
+
: `${completedPhase} completion artifact recorded with status ${status}; rework is required before phase progression.`;
|
|
191
207
|
return {
|
|
192
208
|
content: [
|
|
193
209
|
{
|
|
@@ -197,6 +213,7 @@ export class CompletionToolServerSession {
|
|
|
197
213
|
],
|
|
198
214
|
structuredContent: {
|
|
199
215
|
...reference,
|
|
216
|
+
...acceptedArtifactReceipt(reference),
|
|
200
217
|
gate: {
|
|
201
218
|
phase: completedPhase,
|
|
202
219
|
status,
|
|
@@ -234,6 +251,7 @@ export class CompletionToolServerSession {
|
|
|
234
251
|
content: [{ type: "text", text: `Forge recorded ${completedGate} artifact ${path.relative(reference.repoPath, reference.path)} (${reference.status}, ${reference.contentHash}). ${nextInstruction}` }],
|
|
235
252
|
structuredContent: {
|
|
236
253
|
...reference,
|
|
254
|
+
...acceptedArtifactReceipt(reference),
|
|
237
255
|
gate: { phase: "decompose", decomposeGate: completedGate, status, allowed: pass, reason },
|
|
238
256
|
nextPhase,
|
|
239
257
|
nextInstruction
|
|
@@ -258,6 +276,7 @@ export class CompletionToolServerSession {
|
|
|
258
276
|
content: [{ type: "text", text: `Forge recorded ${completedGate} artifact ${path.relative(reference.repoPath, reference.path)} (${reference.status}, ${reference.contentHash}). ${nextInstruction}` }],
|
|
259
277
|
structuredContent: {
|
|
260
278
|
...reference,
|
|
279
|
+
...acceptedArtifactReceipt(reference),
|
|
261
280
|
gate: { phase: "draft", draftGate: completedGate, status, allowed: status !== "blocked", reason: nextGate ? `${completedGate} artifact accepted; continue to ${nextGate}.` : `Draft proposal artifact accepted with status ${status}.` },
|
|
262
281
|
nextPhase,
|
|
263
282
|
nextInstruction
|
|
@@ -291,6 +310,7 @@ export class CompletionToolServerSession {
|
|
|
291
310
|
],
|
|
292
311
|
structuredContent: {
|
|
293
312
|
...reference,
|
|
313
|
+
...acceptedArtifactReceipt(reference),
|
|
294
314
|
gate: {
|
|
295
315
|
phase: completedGate,
|
|
296
316
|
status,
|
|
@@ -321,8 +341,8 @@ export class CompletionToolServerSession {
|
|
|
321
341
|
const nextInstruction = completionToolNextInstructionForQaGate(completedGate, status, nextGate);
|
|
322
342
|
const allowed = status !== "blocked";
|
|
323
343
|
const reason = nextGate
|
|
324
|
-
? `${completedGate} artifact accepted; continue to ${nextGate}.`
|
|
325
|
-
: `QA verdict artifact
|
|
344
|
+
? `${completedGate} artifact accepted locally; continue to ${nextGate}.`
|
|
345
|
+
: `QA verdict artifact recorded locally with status ${status}; API ingestion and acceptance are pending.`;
|
|
326
346
|
return {
|
|
327
347
|
content: [
|
|
328
348
|
{
|
|
@@ -332,6 +352,7 @@ export class CompletionToolServerSession {
|
|
|
332
352
|
],
|
|
333
353
|
structuredContent: {
|
|
334
354
|
...reference,
|
|
355
|
+
...acceptedArtifactReceipt(reference),
|
|
335
356
|
gate: {
|
|
336
357
|
phase: "qa",
|
|
337
358
|
qaGate: completedGate,
|
|
@@ -412,14 +433,28 @@ const completionToolRuntimeEnvironmentNames = [
|
|
|
412
433
|
"FORGE_RUNTIME_MODE",
|
|
413
434
|
"FORGE_RESOURCE_LEASE_ID",
|
|
414
435
|
"FORGE_DISPATCH_ATTEMPT_ID",
|
|
436
|
+
"FORGE_E2E_OWNERSHIP_MODE",
|
|
437
|
+
"FORGE_E2E_OWNERSHIP_TOKEN",
|
|
438
|
+
"FORGE_WORKTREE_PATH",
|
|
415
439
|
"FORGE_LOG_DIR",
|
|
440
|
+
"FORGE_CACHE_DIR",
|
|
416
441
|
"FORGE_TEST_OUTPUT_DIR",
|
|
442
|
+
"FORGE_ATTEMPT_DATABASE_SCHEMA",
|
|
443
|
+
"FORGE_ATTEMPT_DATABASE_READINESS_PATH",
|
|
444
|
+
"FORGE_CI_REPAIR_READINESS_PATH",
|
|
445
|
+
"FORGE_CI_REPAIR_HANDOFF_PATH",
|
|
446
|
+
"FORGE_CI_REPAIR_PROVIDER_ONLY_SPECS",
|
|
447
|
+
"FORGE_PLAYWRIGHT_CONFIG",
|
|
448
|
+
"FORGE_E2E_API_URL",
|
|
449
|
+
"FORGE_E2E_WEB_URL",
|
|
450
|
+
"PLAYWRIGHT_BROWSERS_PATH",
|
|
417
451
|
"FORGE_TMPDIR",
|
|
452
|
+
"NPM_CONFIG_CACHE",
|
|
418
453
|
"TMPDIR",
|
|
419
454
|
"TMP",
|
|
420
455
|
"TEMP",
|
|
421
456
|
];
|
|
422
|
-
export function completionToolRuntimeEnvironment(runtimeEnvironment) {
|
|
457
|
+
export function completionToolRuntimeEnvironment(runtimeEnvironment, nativeTestPostgresFallbackPath) {
|
|
423
458
|
if (!runtimeEnvironment)
|
|
424
459
|
return {};
|
|
425
460
|
const env = {};
|
|
@@ -428,6 +463,11 @@ export function completionToolRuntimeEnvironment(runtimeEnvironment) {
|
|
|
428
463
|
if (value)
|
|
429
464
|
env[name] = value;
|
|
430
465
|
}
|
|
466
|
+
// Managed verification reads the operator's native test cluster only through
|
|
467
|
+
// this explicit path, and only while Docker is unavailable.
|
|
468
|
+
if (nativeTestPostgresFallbackPath) {
|
|
469
|
+
env[nativeTestPostgresFallbackConfigEnvironment] = nativeTestPostgresFallbackPath;
|
|
470
|
+
}
|
|
431
471
|
return env;
|
|
432
472
|
}
|
|
433
473
|
const minimumManagedSolutionTestTimeoutMs = 45 * 60 * 1000;
|
|
@@ -453,7 +493,8 @@ export function assertManagedQaRegressionEvidence(input, receipts) {
|
|
|
453
493
|
const managedCommandId = entry.managed_command_id;
|
|
454
494
|
if (typeof managedCommandId !== "string" || !managedCommandId.trim()) {
|
|
455
495
|
throw new Error(`qa_regression_managed_command_id_required: commands[${index}] runs the solution-wide Forge test. `
|
|
456
|
-
+ "Run it with forge.start_managed_command, continue independent work, call forge.wait_managed_command
|
|
496
|
+
+ "Run it with forge.start_managed_command, continue independent work, then call forge.wait_managed_command for a transport-safe receipt. "
|
|
497
|
+
+ "If command_continues=true, continue useful work or wait again after retry_after_ms. Include the terminal managed_command_id in this command evidence.");
|
|
457
498
|
}
|
|
458
499
|
const matchingReceipts = receipts.filter((receipt) => {
|
|
459
500
|
const tokens = receipt.command;
|
|
@@ -464,7 +505,8 @@ export function assertManagedQaRegressionEvidence(input, receipts) {
|
|
|
464
505
|
});
|
|
465
506
|
if (matchingReceipts.length === 0) {
|
|
466
507
|
throw new Error(`qa_regression_managed_command_required: commands[${index}] runs the solution-wide Forge test. `
|
|
467
|
-
+ "Run it with forge.start_managed_command, continue independent work, call forge.wait_managed_command
|
|
508
|
+
+ "Run it with forge.start_managed_command, continue independent work, then call forge.wait_managed_command for a transport-safe receipt. "
|
|
509
|
+
+ "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.");
|
|
468
510
|
}
|
|
469
511
|
const terminalReceipts = matchingReceipts.filter((receipt) => typeof receipt.status === "string" && [
|
|
470
512
|
"pass",
|
|
@@ -475,7 +517,7 @@ export function assertManagedQaRegressionEvidence(input, receipts) {
|
|
|
475
517
|
].includes(receipt.status));
|
|
476
518
|
if (terminalReceipts.length === 0) {
|
|
477
519
|
throw new Error(`qa_regression_managed_command_in_progress: commands[${index}] has not produced a terminal managed receipt. `
|
|
478
|
-
+ "Call forge.wait_managed_command
|
|
520
|
+
+ "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.");
|
|
479
521
|
}
|
|
480
522
|
const adequateReceipt = terminalReceipts.some((receipt) => {
|
|
481
523
|
if (receipt.timed_out !== true)
|
|
@@ -559,7 +601,7 @@ export function parseCompletionMcpArgs(argv) {
|
|
|
559
601
|
const rawPhase = argv[++index];
|
|
560
602
|
const parsedPhase = parseStrictCompletionPhase(rawPhase);
|
|
561
603
|
if (!parsedPhase) {
|
|
562
|
-
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." };
|
|
604
|
+
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." };
|
|
563
605
|
}
|
|
564
606
|
phase = parsedPhase;
|
|
565
607
|
}
|
|
@@ -664,6 +706,28 @@ async function prepareCursorCompletionToolRuntime(worktreePath, runtime) {
|
|
|
664
706
|
}
|
|
665
707
|
};
|
|
666
708
|
}
|
|
709
|
+
/**
|
|
710
|
+
* Claude Code takes MCP servers from `--mcp-config <file>`. The file lives in an attempt-owned
|
|
711
|
+
* temp directory (never the worktree) because its env block can carry redaction secrets, and
|
|
712
|
+
* passing the JSON inline would not survive Windows shim quoting.
|
|
713
|
+
*/
|
|
714
|
+
async function prepareClaudeCompletionToolRuntime(runtime) {
|
|
715
|
+
const configDir = await fs.mkdtemp(path.join(os.tmpdir(), "forge-claude-mcp-"));
|
|
716
|
+
const configPath = path.join(configDir, "mcp.json");
|
|
717
|
+
const config = {
|
|
718
|
+
mcpServers: {
|
|
719
|
+
[runtime.serverName]: claudeMcpServerConfig(runtime)
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
723
|
+
return {
|
|
724
|
+
...runtime,
|
|
725
|
+
claudeMcpConfigPath: configPath,
|
|
726
|
+
cleanup: async () => {
|
|
727
|
+
await fs.rm(configDir, { recursive: true, force: true });
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
}
|
|
667
731
|
async function handleJsonRpcLine(session, rawLine) {
|
|
668
732
|
let message;
|
|
669
733
|
try {
|
|
@@ -718,10 +782,48 @@ async function handleJsonRpcRequest(session, method, params) {
|
|
|
718
782
|
const call = asRecord(params, "tools/call params");
|
|
719
783
|
const name = requiredString(call, "name");
|
|
720
784
|
const args = call.arguments ?? {};
|
|
721
|
-
|
|
785
|
+
try {
|
|
786
|
+
return await session.callTool(name, args);
|
|
787
|
+
}
|
|
788
|
+
catch (error) {
|
|
789
|
+
if (isManagedAttemptTool(name))
|
|
790
|
+
throw error;
|
|
791
|
+
return completionToolRejectedResult(name, error);
|
|
792
|
+
}
|
|
722
793
|
}
|
|
723
794
|
throw new Error(`Unsupported MCP method: ${method}`);
|
|
724
795
|
}
|
|
796
|
+
export function completionToolRejectedResult(toolName, error) {
|
|
797
|
+
const issues = error instanceof CompletionPayloadValidationError
|
|
798
|
+
? [...error.issues]
|
|
799
|
+
: [errorMessage(error)];
|
|
800
|
+
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.";
|
|
801
|
+
return {
|
|
802
|
+
content: [{
|
|
803
|
+
type: "text",
|
|
804
|
+
text: `Forge rejected ${toolName}. No artifact was written.\n${issues.map(issue => `- ${issue}`).join("\n")}\n${repairInstruction}`,
|
|
805
|
+
}],
|
|
806
|
+
structuredContent: {
|
|
807
|
+
status: "rejected",
|
|
808
|
+
tool_name: toolName,
|
|
809
|
+
artifact_written: false,
|
|
810
|
+
issues,
|
|
811
|
+
repair_instruction: repairInstruction,
|
|
812
|
+
},
|
|
813
|
+
isError: true,
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
function acceptedArtifactReceipt(reference) {
|
|
817
|
+
return {
|
|
818
|
+
artifact_written: true,
|
|
819
|
+
artifact_receipt: {
|
|
820
|
+
schema_version: 1,
|
|
821
|
+
path: reference.path,
|
|
822
|
+
report_path: reference.reportPath,
|
|
823
|
+
content_hash: reference.contentHash,
|
|
824
|
+
},
|
|
825
|
+
};
|
|
826
|
+
}
|
|
725
827
|
function toolDefinition(name, phase, qaGate = null, decomposeGate = null) {
|
|
726
828
|
return {
|
|
727
829
|
name,
|
|
@@ -19,6 +19,9 @@ export function toolDescription(phase, qaGate = null, decomposeGate = null) {
|
|
|
19
19
|
if (phase === "ci_repair") {
|
|
20
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
21
|
}
|
|
22
|
+
if (phase === "comment_repair") {
|
|
23
|
+
return "Complete Forge pull-request comment repair by recording one evidence-backed disposition for every actionable finding in the dispatch snapshot.";
|
|
24
|
+
}
|
|
22
25
|
if (phase === "self_review") {
|
|
23
26
|
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.";
|
|
24
27
|
}
|
|
@@ -146,6 +149,42 @@ export function toolInputSchema(phase, qaGate = null, decomposeGate = null) {
|
|
|
146
149
|
}
|
|
147
150
|
}, ["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
151
|
}
|
|
152
|
+
if (phase === "comment_repair") {
|
|
153
|
+
return objectToolSchema({
|
|
154
|
+
...baseProperties,
|
|
155
|
+
repair_head_sha: { type: "string", description: "Exact pull-request head from the dispatch snapshot." },
|
|
156
|
+
repair_issue_key: { type: "string", description: "Exact review issue key from the dispatch snapshot." },
|
|
157
|
+
dispositions: {
|
|
158
|
+
type: "array",
|
|
159
|
+
description: "Exactly one outcome for every actionable dispatch finding; conversation-only comments are omitted.",
|
|
160
|
+
items: evidenceObjectSchema(["claim_id", "kind", "body_sha256", "disposition", "evidence"], {
|
|
161
|
+
claim_id: { type: "string" },
|
|
162
|
+
kind: { type: "string", enum: ["review_thread", "review_body", "issue_comment"] },
|
|
163
|
+
body_sha256: { type: "string" },
|
|
164
|
+
disposition: { type: "string", enum: ["fixed", "rejected", "already_addressed", "needs_decision", "accepted_transfer", "owned_elsewhere"] },
|
|
165
|
+
accepted_transfer: { type: "object", properties: {
|
|
166
|
+
amendment_id: { type: "string" }, version: { type: "integer", minimum: 1 }, content_hash: { type: "string" },
|
|
167
|
+
target_work_item_id: { type: "string" }, obligation_reference: { type: "string" },
|
|
168
|
+
}, required: ["amendment_id", "version", "content_hash", "target_work_item_id", "obligation_reference"], additionalProperties: false },
|
|
169
|
+
owned_elsewhere: { type: "object", description: "Another open Story in this Work Order whose accepted acceptance criteria already require the requested change.", properties: {
|
|
170
|
+
target_work_item_id: { type: "string" },
|
|
171
|
+
quoted_obligation: { type: "string", description: "Verbatim passage (at least 40 characters) from that Story's acceptance criteria that covers the finding." },
|
|
172
|
+
}, required: ["target_work_item_id", "quoted_obligation"], additionalProperties: false },
|
|
173
|
+
evidence: { type: "string" }
|
|
174
|
+
})
|
|
175
|
+
},
|
|
176
|
+
changed_files: {
|
|
177
|
+
type: "array",
|
|
178
|
+
description: "Files changed for fixed findings. May be empty when every finding is rejected, already addressed, or needs a decision.",
|
|
179
|
+
items: evidenceObjectSchema(["path", "summary"], { path: { type: "string" }, summary: { type: "string" } })
|
|
180
|
+
},
|
|
181
|
+
validation: {
|
|
182
|
+
type: "array",
|
|
183
|
+
description: "Scoped verification evidence. May be empty only when no source change was made.",
|
|
184
|
+
items: commandEvidenceSchema()
|
|
185
|
+
}
|
|
186
|
+
}, ["status", "summary", "repair_head_sha", "repair_issue_key", "dispositions", "changed_files", "validation"], "Use status pass after every actionable finding has an evidence-backed disposition. Rejected and needs_decision are legitimate outcomes and must not be disguised as code changes.");
|
|
187
|
+
}
|
|
149
188
|
if (phase === "implementation") {
|
|
150
189
|
return objectToolSchema({
|
|
151
190
|
...baseProperties,
|
|
@@ -221,7 +260,10 @@ export function toolInputSchema(phase, qaGate = null, decomposeGate = null) {
|
|
|
221
260
|
file: { type: "string" }
|
|
222
261
|
})
|
|
223
262
|
},
|
|
224
|
-
security_review:
|
|
263
|
+
security_review: {
|
|
264
|
+
...reviewSectionSchema(),
|
|
265
|
+
description: "Required for pass. Use pass after review, or not_applicable only with notes explaining why the change has no security-relevant surface."
|
|
266
|
+
},
|
|
225
267
|
edge_case_review: reviewSectionSchema(),
|
|
226
268
|
design_review: {
|
|
227
269
|
...reviewSectionSchema(),
|
|
@@ -719,6 +761,11 @@ function qaScopeSchema(description) {
|
|
|
719
761
|
schema_version: { type: "number", enum: [1] },
|
|
720
762
|
surface: { type: "array", items: { type: "string", enum: [...qaScopeSurfaces] } },
|
|
721
763
|
required_evidence: { type: "array", items: { type: "string", enum: [...qaScopeEvidence] } },
|
|
764
|
+
required_capabilities: {
|
|
765
|
+
type: "array",
|
|
766
|
+
description: "Runner facilities required by implementation and validation. Use docker_engine when tests or project services require access to a Docker daemon, even when the agent itself runs in a managed host worktree.",
|
|
767
|
+
items: { type: "string", enum: ["docker_engine"] }
|
|
768
|
+
},
|
|
722
769
|
not_applicable: { type: "array", items: { type: "string", enum: [...qaScopeEvidence] } },
|
|
723
770
|
browser_required: { type: "boolean" },
|
|
724
771
|
api_required: { type: "boolean" },
|
|
@@ -743,7 +790,7 @@ function qaScopeSchema(description) {
|
|
|
743
790
|
function commandEvidenceSchema() {
|
|
744
791
|
return evidenceObjectSchema(["command", "exit_code", "status", "summary"], {
|
|
745
792
|
command: { type: "string" },
|
|
746
|
-
managed_command_id: { type: "string", description: "Receipt ID returned by forge.start_managed_command
|
|
793
|
+
managed_command_id: { type: "string", description: "Receipt ID returned by forge.start_managed_command after forge.wait_managed_command reports a terminal receipt. A transport-safe wait can return command_continues=true; wait again after retry_after_ms before submitting the gate." },
|
|
747
794
|
exit_code: { type: ["number", "null"], description: "Process exit code, or null when the managed runner timed out or could not launch the process." },
|
|
748
795
|
status: { type: "string" },
|
|
749
796
|
summary: { type: "string" },
|
|
@@ -2,6 +2,7 @@ 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 validateCommentRepairPayloadShape(payload: Record<string, unknown>): void;
|
|
5
6
|
export declare function validateCiRepairPayloadShape(payload: Record<string, unknown>): void;
|
|
6
7
|
export declare function validateDebugPayloadShape(phase: DebugCompletionPhase, payload: Record<string, unknown>): void;
|
|
7
8
|
export declare function validateQaGatePayloadShape(gate: QaCompletionGate, payload: Record<string, unknown>, status: string): void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { qaBlockerReasons, qaCandidateBugActions, qaFindingScopes, qaFindingSeverities, qaGateToolNames, qaScopeAuthorities, qaScopeConfidence, qaScopeEvidence, qaScopeFields, qaScopeSources, qaScopeSurfaces } from "./phase-completion.js";
|
|
1
|
+
import { qaBlockerReasons, qaCandidateBugActions, qaFindingScopes, qaFindingSeverities, qaGateToolNames, qaScopeAuthorities, qaScopeCapabilities, qaScopeConfidence, qaScopeEvidence, qaScopeFields, qaScopeSources, qaScopeSurfaces } from "./phase-completion.js";
|
|
2
2
|
export function normalizeCompletionStatus(value, phase) {
|
|
3
3
|
const normalized = value.trim().toLowerCase().replace(/-/g, "_");
|
|
4
4
|
if (phase === "qa") {
|
|
@@ -55,6 +55,10 @@ export function validatePayloadShape(phase, payload) {
|
|
|
55
55
|
validateCiRepairPayloadShape(payload);
|
|
56
56
|
return;
|
|
57
57
|
}
|
|
58
|
+
if (phase === "comment_repair") {
|
|
59
|
+
validateCommentRepairPayloadShape(payload);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
58
62
|
if (isDebugCompletionPhase(phase)) {
|
|
59
63
|
validateDebugPayloadShape(phase, payload);
|
|
60
64
|
return;
|
|
@@ -101,6 +105,65 @@ export function validatePayloadShape(phase, payload) {
|
|
|
101
105
|
requirePassingCommands(payload);
|
|
102
106
|
requireSatisfiedAcceptanceCriteria(payload);
|
|
103
107
|
}
|
|
108
|
+
export function validateCommentRepairPayloadShape(payload) {
|
|
109
|
+
requireNonEmptyStringRecord(payload, "repair_head_sha", "");
|
|
110
|
+
requireNonEmptyStringRecord(payload, "repair_issue_key", "");
|
|
111
|
+
requireArray(payload, "dispositions");
|
|
112
|
+
requireNonEmptyArray(payload, "dispositions");
|
|
113
|
+
const seen = new Set();
|
|
114
|
+
let hasFixed = false;
|
|
115
|
+
for (const [index, entry] of payload.dispositions.entries()) {
|
|
116
|
+
requireEvidenceRecord(entry, `dispositions[${index}]`);
|
|
117
|
+
const record = entry;
|
|
118
|
+
for (const key of ["claim_id", "kind", "body_sha256", "disposition", "evidence"]) {
|
|
119
|
+
requireNonEmptyStringRecord(record, key, `dispositions[${index}].`);
|
|
120
|
+
}
|
|
121
|
+
requireKnownStringRecord(record, "kind", new Set(["review_thread", "review_body", "issue_comment"]), `dispositions[${index}].`);
|
|
122
|
+
requireKnownStringRecord(record, "disposition", new Set(["fixed", "rejected", "already_addressed", "needs_decision", "accepted_transfer", "owned_elsewhere"]), `dispositions[${index}].`);
|
|
123
|
+
if (record.disposition === "owned_elsewhere") {
|
|
124
|
+
const receipt = record.owned_elsewhere;
|
|
125
|
+
if (!receipt || typeof receipt !== "object" || Array.isArray(receipt))
|
|
126
|
+
throw new Error("owned_elsewhere requires the owning Story and its quoted obligation; server verification remains mandatory.");
|
|
127
|
+
const owner = receipt;
|
|
128
|
+
if (typeof owner.target_work_item_id !== "string" || !owner.target_work_item_id.trim())
|
|
129
|
+
throw new Error("owned_elsewhere.target_work_item_id is required.");
|
|
130
|
+
if (typeof owner.quoted_obligation !== "string" || owner.quoted_obligation.trim().length < 40)
|
|
131
|
+
throw new Error("owned_elsewhere.quoted_obligation must quote at least 40 characters of the owning Story's acceptance criteria.");
|
|
132
|
+
}
|
|
133
|
+
if (record.disposition === "accepted_transfer") {
|
|
134
|
+
const receipt = record.accepted_transfer;
|
|
135
|
+
if (!receipt || typeof receipt !== "object" || Array.isArray(receipt))
|
|
136
|
+
throw new Error("accepted_transfer requires an approved amendment receipt; server verification remains mandatory.");
|
|
137
|
+
const transfer = receipt;
|
|
138
|
+
for (const key of ["amendment_id", "content_hash", "target_work_item_id", "obligation_reference"]) {
|
|
139
|
+
if (typeof transfer[key] !== "string" || !transfer[key].trim())
|
|
140
|
+
throw new Error(`accepted_transfer.${key} is required.`);
|
|
141
|
+
}
|
|
142
|
+
if (!Number.isInteger(transfer.version) || Number(transfer.version) < 1)
|
|
143
|
+
throw new Error("accepted_transfer.version must be positive.");
|
|
144
|
+
}
|
|
145
|
+
const identity = `${normalizedString(optionalString(record, "kind"))}:${optionalString(record, "claim_id")}:${normalizedString(optionalString(record, "body_sha256"))}`;
|
|
146
|
+
if (seen.has(identity))
|
|
147
|
+
throw new Error(`dispositions[${index}] duplicates finding ${identity}.`);
|
|
148
|
+
seen.add(identity);
|
|
149
|
+
hasFixed ||= normalizedString(optionalString(record, "disposition")) === "fixed";
|
|
150
|
+
}
|
|
151
|
+
requireArray(payload, "changed_files");
|
|
152
|
+
requireArray(payload, "validation");
|
|
153
|
+
for (const [index, changedFile] of payload.changed_files.entries()) {
|
|
154
|
+
requireEvidenceRecord(changedFile, `changed_files[${index}]`);
|
|
155
|
+
const record = changedFile;
|
|
156
|
+
requireNonEmptyStringRecord(record, "path", `changed_files[${index}].`);
|
|
157
|
+
requireNonEmptyStringRecord(record, "summary", `changed_files[${index}].`);
|
|
158
|
+
}
|
|
159
|
+
if (hasFixed && arrayLength(payload, "changed_files") === 0) {
|
|
160
|
+
throw new Error("fixed comment dispositions require changed_files evidence.");
|
|
161
|
+
}
|
|
162
|
+
if (arrayLength(payload, "changed_files") > 0) {
|
|
163
|
+
requireNonEmptyArray(payload, "validation");
|
|
164
|
+
validateQaGatePassingCommands(payload.validation);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
104
167
|
export function validateCiRepairPayloadShape(payload) {
|
|
105
168
|
requireArray(payload, "failed_checks");
|
|
106
169
|
requireNonEmptyArray(payload, "failed_checks");
|
|
@@ -252,29 +315,43 @@ export function validateQaGatePayloadShape(gate, payload, status) {
|
|
|
252
315
|
}
|
|
253
316
|
}
|
|
254
317
|
export function validateQaGatePassingCommands(commands, options = {}) {
|
|
318
|
+
const issues = [];
|
|
255
319
|
for (const [index, command] of commands.entries()) {
|
|
256
|
-
|
|
320
|
+
if (!command || typeof command !== "object" || Array.isArray(command)) {
|
|
321
|
+
issues.push(`commands[${index}] must be an object.`);
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
257
324
|
const record = command;
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
325
|
+
if (!optionalString(record, "command"))
|
|
326
|
+
issues.push(`commands[${index}].command is required.`);
|
|
327
|
+
if (!optionalString(record, "status"))
|
|
328
|
+
issues.push(`commands[${index}].status is required.`);
|
|
329
|
+
if (!optionalString(record, "summary"))
|
|
330
|
+
issues.push(`commands[${index}].summary is required.`);
|
|
261
331
|
if (typeof record.exit_code !== "number" || !Number.isFinite(record.exit_code)) {
|
|
262
|
-
|
|
332
|
+
issues.push(`commands[${index}].exit_code is required.`);
|
|
333
|
+
continue;
|
|
263
334
|
}
|
|
264
335
|
const passed = record.exit_code === 0 && normalizedString(record.status) === "pass";
|
|
265
336
|
if (!passed
|
|
266
337
|
&& options.allowClassifiedNonBlockingFailures
|
|
267
338
|
&& optionalString(record, "classification")) {
|
|
268
|
-
|
|
339
|
+
try {
|
|
340
|
+
validateClassifiedNonBlockingQaFailure(record, index);
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
issues.push(error instanceof Error ? error.message : String(error));
|
|
344
|
+
}
|
|
269
345
|
continue;
|
|
270
346
|
}
|
|
271
347
|
if (record.exit_code !== 0) {
|
|
272
|
-
|
|
348
|
+
issues.push(`commands[${index}].exit_code must be 0 when QA gate status is pass.`);
|
|
273
349
|
}
|
|
274
350
|
if (normalizedString(record.status) !== "pass") {
|
|
275
|
-
|
|
351
|
+
issues.push(`commands[${index}].status must be pass when QA gate status is pass.`);
|
|
276
352
|
}
|
|
277
353
|
}
|
|
354
|
+
throwCompletionPayloadIssues(issues);
|
|
278
355
|
}
|
|
279
356
|
function validateClassifiedNonBlockingQaFailure(record, index) {
|
|
280
357
|
const classification = normalizedString(optionalString(record, "classification"));
|
|
@@ -318,18 +395,22 @@ export function validateArchitectPayloadShape(payload, status) {
|
|
|
318
395
|
throw new Error("implementation_plan.tasks: expected non-empty array; correction: include at least one task with id and tests.");
|
|
319
396
|
}
|
|
320
397
|
const taskIds = new Set();
|
|
321
|
-
|
|
398
|
+
const taskIndexById = new Map();
|
|
399
|
+
for (const [taskIndex, task] of implementationPlan.tasks.entries()) {
|
|
322
400
|
if (!task || typeof task !== "object" || Array.isArray(task)) {
|
|
323
401
|
throw new Error("implementation_plan.tasks[]: expected object entries; correction: each task must be an object with id and tests.");
|
|
324
402
|
}
|
|
325
403
|
const taskRecord = task;
|
|
326
404
|
requireNonEmptyStringRecord(taskRecord, "id", "implementation_plan.tasks[].");
|
|
327
405
|
const taskId = optionalString(taskRecord, "id") ?? "";
|
|
328
|
-
|
|
329
|
-
|
|
406
|
+
const firstTaskIndex = taskIndexById.get(taskId);
|
|
407
|
+
if (firstTaskIndex !== undefined) {
|
|
408
|
+
throw new Error(`implementation_plan.tasks[${taskIndex}].id duplicates implementation_plan.tasks[${firstTaskIndex}].id '${taskId}'; correction: assign the duplicate a unique stable task id, update every phase/dependency reference to that task, and resubmit forge.complete_architect_plan.`);
|
|
330
409
|
}
|
|
331
410
|
taskIds.add(taskId);
|
|
411
|
+
taskIndexById.set(taskId, taskIndex);
|
|
332
412
|
validateArchitectTaskDevelopScope(taskRecord, taskId);
|
|
413
|
+
requireNonEmptyStringArrayRecord(taskRecord, "acceptance_criteria", `implementation_plan.tasks[${taskId}].`);
|
|
333
414
|
validateArchitectVerificationScope(taskRecord, taskId);
|
|
334
415
|
requireArchitectArray(taskRecord, "tests", payload, { path: `implementation_plan.tasks[${optionalString(taskRecord, "id") ?? "?"}].tests` });
|
|
335
416
|
if (arrayLength(taskRecord, "tests") === 0) {
|
|
@@ -373,6 +454,7 @@ export function validateQaScopeRecord(scope, pathLabel) {
|
|
|
373
454
|
}
|
|
374
455
|
const surface = qaScopeStringArray(scope, "surface", pathLabel, qaScopeSurfaces);
|
|
375
456
|
const requiredEvidence = qaScopeStringArray(scope, "required_evidence", pathLabel, qaScopeEvidence);
|
|
457
|
+
qaScopeStringArray(scope, "required_capabilities", pathLabel, qaScopeCapabilities);
|
|
376
458
|
const notApplicable = qaScopeStringArray(scope, "not_applicable", pathLabel, qaScopeEvidence);
|
|
377
459
|
const requiredSet = new Set(requiredEvidence);
|
|
378
460
|
for (const evidence of notApplicable) {
|
|
@@ -715,22 +797,30 @@ export function validateQaPayloadShape(payload, status) {
|
|
|
715
797
|
throw new Error("QA pass/needs_rework requires at least one command, browser check, or evidence note.");
|
|
716
798
|
}
|
|
717
799
|
}
|
|
800
|
+
const commandIssues = [];
|
|
718
801
|
for (const [index, command] of commands.entries()) {
|
|
719
|
-
|
|
802
|
+
if (!command || typeof command !== "object" || Array.isArray(command)) {
|
|
803
|
+
commandIssues.push(`commands[${index}] must be an object.`);
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
720
806
|
const record = command;
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
807
|
+
if (!optionalString(record, "command"))
|
|
808
|
+
commandIssues.push(`commands[${index}].command is required.`);
|
|
809
|
+
if (!optionalString(record, "status"))
|
|
810
|
+
commandIssues.push(`commands[${index}].status is required.`);
|
|
811
|
+
if (!optionalString(record, "summary"))
|
|
812
|
+
commandIssues.push(`commands[${index}].summary is required.`);
|
|
724
813
|
if (typeof record.exit_code !== "number" || !Number.isFinite(record.exit_code)) {
|
|
725
|
-
|
|
814
|
+
commandIssues.push(`commands[${index}].exit_code is required.`);
|
|
726
815
|
}
|
|
727
|
-
if (status === "pass" && record.exit_code !== 0) {
|
|
728
|
-
|
|
816
|
+
else if (status === "pass" && record.exit_code !== 0) {
|
|
817
|
+
commandIssues.push(`commands[${index}].exit_code must be 0 when QA status is pass.`);
|
|
729
818
|
}
|
|
730
819
|
if (status === "pass" && normalizedString(record.status) !== "pass") {
|
|
731
|
-
|
|
820
|
+
commandIssues.push(`commands[${index}].status must be pass when QA status is pass.`);
|
|
732
821
|
}
|
|
733
822
|
}
|
|
823
|
+
throwCompletionPayloadIssues(commandIssues);
|
|
734
824
|
for (const [index, browserCheck] of browserChecks.entries()) {
|
|
735
825
|
requireEvidenceRecord(browserCheck, `browser_checks[${index}]`);
|
|
736
826
|
const record = browserCheck;
|
|
@@ -824,5 +914,5 @@ export function validateOptionalWorkItemId(payload, expectedWorkItemId) {
|
|
|
824
914
|
throw new Error(`completion payload work item ${actual} does not match dispatch work item ${expectedWorkItemId}.`);
|
|
825
915
|
}
|
|
826
916
|
}
|
|
827
|
-
import { requireField, requireNonPassEvidence, requireImplementationEvidence, requirePassingCommands, requireArray, requireNonEmptyArray, arrayLength, requireAllowedStatus, requireObject, requiredRecord, requireBoolean, requireNumericOne, requireSatisfiedAcceptanceCriteria, validateResolvedFindings, isDebugCompletionPhase, qaEvidenceSource, qaArray, qaObjectArray, qaPolicyWarnings, qaStringArray, requireEvidenceRecord, requireNonEmptyStringRecord, requireKnownStringRecord, requireNonEmptyStringArrayRecord, requireSubstantiveEvidenceRecord, normalizedString, requiredString, optionalString, stripUndefined, } from "./phase-completion.helpers.js";
|
|
917
|
+
import { requireField, requireNonPassEvidence, requireImplementationEvidence, requirePassingCommands, requireArray, requireNonEmptyArray, arrayLength, requireAllowedStatus, requireObject, requiredRecord, requireBoolean, requireNumericOne, requireSatisfiedAcceptanceCriteria, validateResolvedFindings, isDebugCompletionPhase, qaEvidenceSource, qaArray, qaObjectArray, qaPolicyWarnings, qaStringArray, requireEvidenceRecord, requireNonEmptyStringRecord, requireKnownStringRecord, requireNonEmptyStringArrayRecord, requireSubstantiveEvidenceRecord, normalizedString, requiredString, optionalString, stripUndefined, throwCompletionPayloadIssues, } from "./phase-completion.helpers.js";
|
|
828
918
|
export * from "./phase-completion.helpers.js";
|