@loucompanion/forge-bridge 0.1.3-dev.fbe15023cdfe → 0.1.4-dev.41da3780c451
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -2
- package/dist/bridge/bridge-chat.types.d.ts +111 -0
- package/dist/bridge/bridge-chat.types.js +401 -0
- package/dist/bridge/bridge.base.d.ts +4 -1
- package/dist/bridge/bridge.service.d.ts +72 -3
- package/dist/bridge/bridge.service.js +1563 -152
- package/dist/bridge/bridge.types.d.ts +46 -1
- package/dist/bridge/dispatch-result-outbox.d.ts +25 -0
- package/dist/bridge/dispatch-result-outbox.js +62 -0
- package/dist/bridge/dispatch-result-worker.d.ts +2 -0
- package/dist/bridge/dispatch-result-worker.js +122 -0
- package/dist/bridge/internals.d.ts +2 -0
- package/dist/bridge/internals.js +1 -0
- package/dist/bridge/readiness-result-outbox.d.ts +21 -0
- package/dist/bridge/readiness-result-outbox.js +54 -0
- package/dist/bridge/readiness-result-worker.d.ts +2 -0
- package/dist/bridge/readiness-result-worker.js +121 -0
- package/dist/bridge/runner-host-capabilities.d.ts +15 -0
- package/dist/bridge/runner-host-capabilities.js +93 -0
- package/dist/cleanup/cleanup.service.d.ts +1 -0
- package/dist/cleanup/cleanup.service.js +70 -9
- package/dist/cli/cli.help.js +2 -2
- package/dist/cli/cli.service.d.ts +14 -6
- package/dist/cli/cli.service.js +75 -16
- package/dist/cli/local-config.d.ts +1 -0
- package/dist/cli/local-config.js +4 -0
- package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
- package/dist/runtime/aspnet-runtime-prebuild.js +341 -0
- package/dist/runtime/attempt-database-broker.d.ts +89 -0
- package/dist/runtime/attempt-database-broker.js +390 -0
- package/dist/runtime/attempt-node-runtime-launcher.d.ts +2 -1
- package/dist/runtime/attempt-node-runtime-launcher.js +57 -18
- package/dist/runtime/attempt-path-operation.d.ts +21 -0
- package/dist/runtime/attempt-path-operation.js +94 -0
- package/dist/runtime/attempt-private-directory.d.ts +6 -0
- package/dist/runtime/attempt-private-directory.js +54 -0
- package/dist/runtime/attempt-process-supervisor.d.ts +11 -0
- package/dist/runtime/attempt-process-supervisor.js +70 -22
- package/dist/runtime/canonical-source-file.d.ts +3 -0
- package/dist/runtime/canonical-source-file.js +54 -0
- package/dist/runtime/docker-engine-probe.d.ts +90 -0
- package/dist/runtime/docker-engine-probe.js +333 -0
- package/dist/runtime/generated-output-reset.d.ts +10 -0
- package/dist/runtime/generated-output-reset.js +62 -8
- package/dist/runtime/host-database-capability-boundary.d.ts +16 -0
- package/dist/runtime/host-database-capability-boundary.js +62 -0
- package/dist/runtime/host-diagnostics.d.ts +11 -0
- package/dist/runtime/host-diagnostics.js +192 -0
- package/dist/runtime/host-execution-root.d.ts +4 -0
- package/dist/runtime/host-execution-root.js +39 -0
- package/dist/runtime/host-listener-inspector.d.ts +8 -1
- package/dist/runtime/host-listener-inspector.js +121 -60
- package/dist/runtime/host-runtime-contract.d.ts +12 -1
- package/dist/runtime/host-runtime-contract.js +168 -55
- package/dist/runtime/host-runtime-lease.d.ts +92 -8
- package/dist/runtime/host-runtime-lease.js +1141 -114
- package/dist/runtime/host-runtime-promotion.d.ts +12 -3
- package/dist/runtime/host-runtime-promotion.js +59 -12
- package/dist/runtime/memory-capacity.d.ts +70 -0
- package/dist/runtime/memory-capacity.js +123 -0
- package/dist/runtime/native-test-postgres-fallback.d.ts +25 -0
- package/dist/runtime/native-test-postgres-fallback.js +91 -0
- package/dist/runtime/node-host-runtime-adapter.d.ts +3 -0
- package/dist/runtime/node-host-runtime-adapter.js +29 -11
- package/dist/runtime/pinned-directory-process.d.ts +9 -0
- package/dist/runtime/pinned-directory-process.js +61 -0
- package/dist/runtime/pinned-source-directory.d.ts +17 -0
- package/dist/runtime/pinned-source-directory.js +94 -0
- package/dist/runtime/playwright-host-runtime-adapter.d.ts +1 -0
- package/dist/runtime/playwright-host-runtime-adapter.js +53 -5
- package/dist/runtime/prepared-runtime-identity.d.ts +6 -0
- package/dist/runtime/prepared-runtime-identity.js +218 -0
- package/dist/runtime/prepared-runtime-readiness.d.ts +30 -0
- package/dist/runtime/prepared-runtime-readiness.js +285 -0
- package/dist/runtime/runtime-isolation-lease.js +4 -1
- package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
- package/dist/runtime/workspace-resource-estimator.js +174 -0
- package/dist/sandbox/preflight.d.ts +31 -1
- package/dist/sandbox/preflight.js +237 -51
- package/dist/sandbox/repository-lock.d.ts +2 -1
- package/dist/sandbox/repository-lock.js +19 -9
- package/dist/sandbox/runtime.d.ts +6 -1
- package/dist/sandbox/runtime.js +137 -107
- package/dist/sandbox/sandbox.types.d.ts +7 -0
- package/dist/sandbox/workspace.service.js +2 -0
- package/dist/session/managed-attempt-tools.d.ts +17 -1
- package/dist/session/managed-attempt-tools.js +489 -34
- package/dist/session/managed-build-coordinator.d.ts +5 -0
- package/dist/session/managed-build-coordinator.js +106 -0
- package/dist/session/managed-build-receipt.d.ts +21 -0
- package/dist/session/managed-build-receipt.js +337 -0
- package/dist/session/managed-verification-timeouts.d.ts +8 -0
- package/dist/session/managed-verification-timeouts.js +17 -0
- package/dist/session/phase-completion.artifacts.d.ts +17 -0
- package/dist/session/phase-completion.artifacts.js +224 -22
- package/dist/session/phase-completion.d.ts +11 -1
- package/dist/session/phase-completion.helpers.d.ts +5 -0
- package/dist/session/phase-completion.helpers.js +30 -7
- package/dist/session/phase-completion.js +14 -3
- package/dist/session/phase-completion.prompts.js +17 -18
- package/dist/session/phase-completion.runtime.d.ts +3 -1
- package/dist/session/phase-completion.runtime.js +126 -17
- package/dist/session/phase-completion.schemas.js +49 -2
- package/dist/session/phase-completion.validation.d.ts +1 -0
- package/dist/session/phase-completion.validation.js +111 -21
- package/dist/session/pre-publication-validation.d.ts +75 -0
- package/dist/session/pre-publication-validation.js +661 -0
- package/dist/session/provider-cli/bridge-cli.conformance-fixtures.d.ts +72 -0
- package/dist/session/provider-cli/bridge-cli.conformance-fixtures.js +462 -0
- package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
- package/dist/session/provider-cli/ci-repair-readiness.js +23 -3
- package/dist/session/provider-cli/provider-cli.artifacts.d.ts +4 -2
- package/dist/session/provider-cli/provider-cli.artifacts.js +47 -13
- package/dist/session/provider-cli/provider-cli.deterministic.js +34 -2
- package/dist/session/provider-cli/provider-cli.process.d.ts +51 -0
- package/dist/session/provider-cli/provider-cli.process.js +422 -22
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +5 -0
- package/dist/session/provider-cli/provider-cli.prompts.js +136 -16
- package/dist/session/provider-cli/provider-cli.repair-epoch.d.ts +6 -0
- package/dist/session/provider-cli/provider-cli.repair-epoch.js +11 -0
- package/dist/session/provider-cli/provider-cli.runtime.d.ts +14 -1
- package/dist/session/provider-cli/provider-cli.runtime.js +54 -3
- package/dist/session/provider-cli/provider-cli.service.d.ts +30 -5
- package/dist/session/provider-cli/provider-cli.service.js +773 -69
- package/dist/session/provider-cli/provider-cli.types.d.ts +2 -0
- package/dist/session/provider-cli/provider-output-state.d.ts +10 -0
- package/dist/session/provider-cli/provider-output-state.js +361 -17
- package/dist/session/provider-cli/provider-usage.d.ts +7 -0
- package/dist/session/provider-cli/provider-usage.js +28 -5
- package/dist/session/session-redaction.js +434 -9
- package/dist/session/session.service.js +145 -23
- package/dist/session/session.types.d.ts +147 -2
- package/dist/session/session.types.js +587 -7
- package/dist/session/unreferenced-exports.d.ts +12 -0
- package/dist/session/unreferenced-exports.js +59 -0
- package/dist/shared/format/format-bytes.d.ts +1 -0
- package/dist/shared/format/format-bytes.js +9 -0
- package/dist/shared/git/git.js +2 -1
- package/dist/shared/path/path.d.ts +1 -0
- package/dist/shared/path/path.js +18 -0
- package/dist/shared/process/dotnet-toolchain.d.ts +17 -0
- package/dist/shared/process/dotnet-toolchain.js +307 -0
- package/dist/shared/process/host-process-recovery.js +3 -2
- package/dist/shared/process/host-process-supervisor.d.ts +2 -0
- package/dist/shared/process/host-process-supervisor.js +60 -11
- package/dist/shared/process/process.d.ts +2 -0
- package/dist/shared/process/process.js +12 -7
- package/dist/shared/provider-capabilities.d.ts +3 -0
- package/dist/shared/provider-capabilities.js +85 -0
- package/dist/shared/redaction/redaction.d.ts +1 -0
- package/dist/shared/redaction/redaction.js +39 -3
- package/dist/shared/time/monotonic.d.ts +1 -0
- package/dist/shared/time/monotonic.js +8 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/dependency-warmer.d.ts +41 -9
- package/dist/worktree/dependency-warmer.js +497 -22
- package/dist/worktree/index.d.ts +1 -1
- package/dist/worktree/story-qa-reference-hydrator.d.ts +4 -0
- package/dist/worktree/story-qa-reference-hydrator.js +253 -0
- package/dist/worktree/worktree.service.d.ts +7 -0
- package/dist/worktree/worktree.service.js +253 -21
- package/dist/worktree/worktree.types.d.ts +7 -1
- package/package.json +7 -5
- package/runtime/windows-job-supervisor.ps1 +380 -14
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
2
3
|
import path from "node:path";
|
|
4
|
+
import { attemptDatabaseCapabilityEnvironment, attemptDatabaseCapabilitySecrets, } from "../runtime/attempt-database-broker.js";
|
|
3
5
|
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";
|
|
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";
|
|
5
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";
|
|
6
9
|
import { toolDescription, toolInputSchema } from "./phase-completion.schemas.js";
|
|
7
10
|
import { writeCompletionArtifact, writeDecomposeGateCompletionArtifact, writeDraftGateCompletionArtifact } from "./phase-completion.artifacts.js";
|
|
8
11
|
import { callManagedAttemptTool, isManagedAttemptTool, managedAttemptToolDefinition, managedAttemptToolNames, } from "./managed-attempt-tools.js";
|
|
@@ -33,13 +36,17 @@ export async function prepareCompletionToolRuntime(options) {
|
|
|
33
36
|
serverName: toolServerName,
|
|
34
37
|
command: process.execPath,
|
|
35
38
|
args: [forgeBridgeCliBinPath(), ...mcpArgs],
|
|
36
|
-
env: completionToolRuntimeEnvironment(options.runtimeEnvironment),
|
|
39
|
+
env: completionToolRuntimeEnvironment(options.runtimeEnvironment, configuredNativeTestPostgresFallbackPath()),
|
|
37
40
|
cleanup: async () => undefined
|
|
38
41
|
};
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
const provider = options.dispatch.execution.provider.trim().toLowerCase();
|
|
43
|
+
if (provider === "cursor") {
|
|
44
|
+
return await prepareCursorCompletionToolRuntime(worktreePath, runtime);
|
|
41
45
|
}
|
|
42
|
-
|
|
46
|
+
if (provider === "claude") {
|
|
47
|
+
return await prepareClaudeCompletionToolRuntime(runtime);
|
|
48
|
+
}
|
|
49
|
+
return runtime;
|
|
43
50
|
}
|
|
44
51
|
export function buildCompletionToolServerArgs(config) {
|
|
45
52
|
const args = [
|
|
@@ -101,6 +108,14 @@ export function cursorMcpServerConfig(runtime) {
|
|
|
101
108
|
env: runtime.env
|
|
102
109
|
};
|
|
103
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
|
+
}
|
|
104
119
|
export function completionToolNamesForServer(config) {
|
|
105
120
|
return config.decomposeCycle
|
|
106
121
|
? decomposeCompletionGateOrder.flatMap((gate) => completionToolNamesForDecomposeGate(gate))
|
|
@@ -185,9 +200,11 @@ export class CompletionToolServerSession {
|
|
|
185
200
|
? this.advanceAfterPass(completedPhase)
|
|
186
201
|
: completedPhase;
|
|
187
202
|
const nextInstruction = completionToolNextInstruction(completedPhase, status, nextPhase);
|
|
188
|
-
const reason =
|
|
189
|
-
?
|
|
190
|
-
:
|
|
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.`;
|
|
191
208
|
return {
|
|
192
209
|
content: [
|
|
193
210
|
{
|
|
@@ -197,6 +214,7 @@ export class CompletionToolServerSession {
|
|
|
197
214
|
],
|
|
198
215
|
structuredContent: {
|
|
199
216
|
...reference,
|
|
217
|
+
...acceptedArtifactReceipt(reference),
|
|
200
218
|
gate: {
|
|
201
219
|
phase: completedPhase,
|
|
202
220
|
status,
|
|
@@ -234,6 +252,7 @@ export class CompletionToolServerSession {
|
|
|
234
252
|
content: [{ type: "text", text: `Forge recorded ${completedGate} artifact ${path.relative(reference.repoPath, reference.path)} (${reference.status}, ${reference.contentHash}). ${nextInstruction}` }],
|
|
235
253
|
structuredContent: {
|
|
236
254
|
...reference,
|
|
255
|
+
...acceptedArtifactReceipt(reference),
|
|
237
256
|
gate: { phase: "decompose", decomposeGate: completedGate, status, allowed: pass, reason },
|
|
238
257
|
nextPhase,
|
|
239
258
|
nextInstruction
|
|
@@ -258,6 +277,7 @@ export class CompletionToolServerSession {
|
|
|
258
277
|
content: [{ type: "text", text: `Forge recorded ${completedGate} artifact ${path.relative(reference.repoPath, reference.path)} (${reference.status}, ${reference.contentHash}). ${nextInstruction}` }],
|
|
259
278
|
structuredContent: {
|
|
260
279
|
...reference,
|
|
280
|
+
...acceptedArtifactReceipt(reference),
|
|
261
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}.` },
|
|
262
282
|
nextPhase,
|
|
263
283
|
nextInstruction
|
|
@@ -291,6 +311,7 @@ export class CompletionToolServerSession {
|
|
|
291
311
|
],
|
|
292
312
|
structuredContent: {
|
|
293
313
|
...reference,
|
|
314
|
+
...acceptedArtifactReceipt(reference),
|
|
294
315
|
gate: {
|
|
295
316
|
phase: completedGate,
|
|
296
317
|
status,
|
|
@@ -321,8 +342,8 @@ export class CompletionToolServerSession {
|
|
|
321
342
|
const nextInstruction = completionToolNextInstructionForQaGate(completedGate, status, nextGate);
|
|
322
343
|
const allowed = status !== "blocked";
|
|
323
344
|
const reason = nextGate
|
|
324
|
-
? `${completedGate} artifact accepted; continue to ${nextGate}.`
|
|
325
|
-
: `QA verdict artifact
|
|
345
|
+
? `${completedGate} artifact accepted locally; continue to ${nextGate}.`
|
|
346
|
+
: `QA verdict artifact recorded locally with status ${status}; API ingestion and acceptance are pending.`;
|
|
326
347
|
return {
|
|
327
348
|
content: [
|
|
328
349
|
{
|
|
@@ -332,6 +353,7 @@ export class CompletionToolServerSession {
|
|
|
332
353
|
],
|
|
333
354
|
structuredContent: {
|
|
334
355
|
...reference,
|
|
356
|
+
...acceptedArtifactReceipt(reference),
|
|
335
357
|
gate: {
|
|
336
358
|
phase: "qa",
|
|
337
359
|
qaGate: completedGate,
|
|
@@ -412,14 +434,31 @@ const completionToolRuntimeEnvironmentNames = [
|
|
|
412
434
|
"FORGE_RUNTIME_MODE",
|
|
413
435
|
"FORGE_RESOURCE_LEASE_ID",
|
|
414
436
|
"FORGE_DISPATCH_ATTEMPT_ID",
|
|
437
|
+
"FORGE_E2E_OWNERSHIP_MODE",
|
|
438
|
+
"FORGE_E2E_OWNERSHIP_TOKEN",
|
|
439
|
+
"FORGE_WORKTREE_PATH",
|
|
415
440
|
"FORGE_LOG_DIR",
|
|
441
|
+
"FORGE_CACHE_DIR",
|
|
416
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",
|
|
417
455
|
"FORGE_TMPDIR",
|
|
456
|
+
"NPM_CONFIG_CACHE",
|
|
418
457
|
"TMPDIR",
|
|
419
458
|
"TMP",
|
|
420
459
|
"TEMP",
|
|
421
460
|
];
|
|
422
|
-
export function completionToolRuntimeEnvironment(runtimeEnvironment) {
|
|
461
|
+
export function completionToolRuntimeEnvironment(runtimeEnvironment, nativeTestPostgresFallbackPath) {
|
|
423
462
|
if (!runtimeEnvironment)
|
|
424
463
|
return {};
|
|
425
464
|
const env = {};
|
|
@@ -428,6 +467,11 @@ export function completionToolRuntimeEnvironment(runtimeEnvironment) {
|
|
|
428
467
|
if (value)
|
|
429
468
|
env[name] = value;
|
|
430
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
|
+
}
|
|
431
475
|
return env;
|
|
432
476
|
}
|
|
433
477
|
const minimumManagedSolutionTestTimeoutMs = 45 * 60 * 1000;
|
|
@@ -453,7 +497,8 @@ export function assertManagedQaRegressionEvidence(input, receipts) {
|
|
|
453
497
|
const managedCommandId = entry.managed_command_id;
|
|
454
498
|
if (typeof managedCommandId !== "string" || !managedCommandId.trim()) {
|
|
455
499
|
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
|
|
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.");
|
|
457
502
|
}
|
|
458
503
|
const matchingReceipts = receipts.filter((receipt) => {
|
|
459
504
|
const tokens = receipt.command;
|
|
@@ -464,7 +509,8 @@ export function assertManagedQaRegressionEvidence(input, receipts) {
|
|
|
464
509
|
});
|
|
465
510
|
if (matchingReceipts.length === 0) {
|
|
466
511
|
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
|
|
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.");
|
|
468
514
|
}
|
|
469
515
|
const terminalReceipts = matchingReceipts.filter((receipt) => typeof receipt.status === "string" && [
|
|
470
516
|
"pass",
|
|
@@ -475,7 +521,7 @@ export function assertManagedQaRegressionEvidence(input, receipts) {
|
|
|
475
521
|
].includes(receipt.status));
|
|
476
522
|
if (terminalReceipts.length === 0) {
|
|
477
523
|
throw new Error(`qa_regression_managed_command_in_progress: commands[${index}] has not produced a terminal managed receipt. `
|
|
478
|
-
+ "Call forge.wait_managed_command
|
|
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.");
|
|
479
525
|
}
|
|
480
526
|
const adequateReceipt = terminalReceipts.some((receipt) => {
|
|
481
527
|
if (receipt.timed_out !== true)
|
|
@@ -559,7 +605,7 @@ export function parseCompletionMcpArgs(argv) {
|
|
|
559
605
|
const rawPhase = argv[++index];
|
|
560
606
|
const parsedPhase = parseStrictCompletionPhase(rawPhase);
|
|
561
607
|
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." };
|
|
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." };
|
|
563
609
|
}
|
|
564
610
|
phase = parsedPhase;
|
|
565
611
|
}
|
|
@@ -621,7 +667,10 @@ export function parseCompletionMcpArgs(argv) {
|
|
|
621
667
|
ownerId,
|
|
622
668
|
phase,
|
|
623
669
|
producedBy,
|
|
624
|
-
redactionSecrets:
|
|
670
|
+
redactionSecrets: [
|
|
671
|
+
...parseCompletionRedactionSecrets(process.env[completionRedactionSecretsEnv]),
|
|
672
|
+
...attemptDatabaseCapabilitySecrets(process.env[attemptDatabaseCapabilityEnvironment]),
|
|
673
|
+
],
|
|
625
674
|
developCycle,
|
|
626
675
|
qaCycle,
|
|
627
676
|
debugCycle,
|
|
@@ -664,6 +713,28 @@ async function prepareCursorCompletionToolRuntime(worktreePath, runtime) {
|
|
|
664
713
|
}
|
|
665
714
|
};
|
|
666
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
|
+
}
|
|
667
738
|
async function handleJsonRpcLine(session, rawLine) {
|
|
668
739
|
let message;
|
|
669
740
|
try {
|
|
@@ -718,10 +789,48 @@ async function handleJsonRpcRequest(session, method, params) {
|
|
|
718
789
|
const call = asRecord(params, "tools/call params");
|
|
719
790
|
const name = requiredString(call, "name");
|
|
720
791
|
const args = call.arguments ?? {};
|
|
721
|
-
|
|
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
|
+
}
|
|
722
800
|
}
|
|
723
801
|
throw new Error(`Unsupported MCP method: ${method}`);
|
|
724
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
|
+
}
|
|
725
834
|
function toolDefinition(name, phase, qaGate = null, decomposeGate = null) {
|
|
726
835
|
return {
|
|
727
836
|
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";
|