@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.
Files changed (165) hide show
  1. package/README.md +88 -2
  2. package/dist/bridge/bridge-chat.types.d.ts +111 -0
  3. package/dist/bridge/bridge-chat.types.js +401 -0
  4. package/dist/bridge/bridge.base.d.ts +4 -1
  5. package/dist/bridge/bridge.service.d.ts +72 -3
  6. package/dist/bridge/bridge.service.js +1563 -152
  7. package/dist/bridge/bridge.types.d.ts +46 -1
  8. package/dist/bridge/dispatch-result-outbox.d.ts +25 -0
  9. package/dist/bridge/dispatch-result-outbox.js +62 -0
  10. package/dist/bridge/dispatch-result-worker.d.ts +2 -0
  11. package/dist/bridge/dispatch-result-worker.js +122 -0
  12. package/dist/bridge/internals.d.ts +2 -0
  13. package/dist/bridge/internals.js +1 -0
  14. package/dist/bridge/readiness-result-outbox.d.ts +21 -0
  15. package/dist/bridge/readiness-result-outbox.js +54 -0
  16. package/dist/bridge/readiness-result-worker.d.ts +2 -0
  17. package/dist/bridge/readiness-result-worker.js +121 -0
  18. package/dist/bridge/runner-host-capabilities.d.ts +15 -0
  19. package/dist/bridge/runner-host-capabilities.js +93 -0
  20. package/dist/cleanup/cleanup.service.d.ts +1 -0
  21. package/dist/cleanup/cleanup.service.js +70 -9
  22. package/dist/cli/cli.help.js +2 -2
  23. package/dist/cli/cli.service.d.ts +14 -6
  24. package/dist/cli/cli.service.js +75 -16
  25. package/dist/cli/local-config.d.ts +1 -0
  26. package/dist/cli/local-config.js +4 -0
  27. package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
  28. package/dist/runtime/aspnet-runtime-prebuild.js +341 -0
  29. package/dist/runtime/attempt-database-broker.d.ts +89 -0
  30. package/dist/runtime/attempt-database-broker.js +390 -0
  31. package/dist/runtime/attempt-node-runtime-launcher.d.ts +2 -1
  32. package/dist/runtime/attempt-node-runtime-launcher.js +57 -18
  33. package/dist/runtime/attempt-path-operation.d.ts +21 -0
  34. package/dist/runtime/attempt-path-operation.js +94 -0
  35. package/dist/runtime/attempt-private-directory.d.ts +6 -0
  36. package/dist/runtime/attempt-private-directory.js +54 -0
  37. package/dist/runtime/attempt-process-supervisor.d.ts +11 -0
  38. package/dist/runtime/attempt-process-supervisor.js +70 -22
  39. package/dist/runtime/canonical-source-file.d.ts +3 -0
  40. package/dist/runtime/canonical-source-file.js +54 -0
  41. package/dist/runtime/docker-engine-probe.d.ts +90 -0
  42. package/dist/runtime/docker-engine-probe.js +333 -0
  43. package/dist/runtime/generated-output-reset.d.ts +10 -0
  44. package/dist/runtime/generated-output-reset.js +62 -8
  45. package/dist/runtime/host-database-capability-boundary.d.ts +16 -0
  46. package/dist/runtime/host-database-capability-boundary.js +62 -0
  47. package/dist/runtime/host-diagnostics.d.ts +11 -0
  48. package/dist/runtime/host-diagnostics.js +192 -0
  49. package/dist/runtime/host-execution-root.d.ts +4 -0
  50. package/dist/runtime/host-execution-root.js +39 -0
  51. package/dist/runtime/host-listener-inspector.d.ts +8 -1
  52. package/dist/runtime/host-listener-inspector.js +121 -60
  53. package/dist/runtime/host-runtime-contract.d.ts +12 -1
  54. package/dist/runtime/host-runtime-contract.js +168 -55
  55. package/dist/runtime/host-runtime-lease.d.ts +92 -8
  56. package/dist/runtime/host-runtime-lease.js +1141 -114
  57. package/dist/runtime/host-runtime-promotion.d.ts +12 -3
  58. package/dist/runtime/host-runtime-promotion.js +59 -12
  59. package/dist/runtime/memory-capacity.d.ts +70 -0
  60. package/dist/runtime/memory-capacity.js +123 -0
  61. package/dist/runtime/native-test-postgres-fallback.d.ts +25 -0
  62. package/dist/runtime/native-test-postgres-fallback.js +91 -0
  63. package/dist/runtime/node-host-runtime-adapter.d.ts +3 -0
  64. package/dist/runtime/node-host-runtime-adapter.js +29 -11
  65. package/dist/runtime/pinned-directory-process.d.ts +9 -0
  66. package/dist/runtime/pinned-directory-process.js +61 -0
  67. package/dist/runtime/pinned-source-directory.d.ts +17 -0
  68. package/dist/runtime/pinned-source-directory.js +94 -0
  69. package/dist/runtime/playwright-host-runtime-adapter.d.ts +1 -0
  70. package/dist/runtime/playwright-host-runtime-adapter.js +53 -5
  71. package/dist/runtime/prepared-runtime-identity.d.ts +6 -0
  72. package/dist/runtime/prepared-runtime-identity.js +218 -0
  73. package/dist/runtime/prepared-runtime-readiness.d.ts +30 -0
  74. package/dist/runtime/prepared-runtime-readiness.js +285 -0
  75. package/dist/runtime/runtime-isolation-lease.js +4 -1
  76. package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
  77. package/dist/runtime/workspace-resource-estimator.js +174 -0
  78. package/dist/sandbox/preflight.d.ts +31 -1
  79. package/dist/sandbox/preflight.js +237 -51
  80. package/dist/sandbox/repository-lock.d.ts +2 -1
  81. package/dist/sandbox/repository-lock.js +19 -9
  82. package/dist/sandbox/runtime.d.ts +6 -1
  83. package/dist/sandbox/runtime.js +137 -107
  84. package/dist/sandbox/sandbox.types.d.ts +7 -0
  85. package/dist/sandbox/workspace.service.js +2 -0
  86. package/dist/session/managed-attempt-tools.d.ts +17 -1
  87. package/dist/session/managed-attempt-tools.js +489 -34
  88. package/dist/session/managed-build-coordinator.d.ts +5 -0
  89. package/dist/session/managed-build-coordinator.js +106 -0
  90. package/dist/session/managed-build-receipt.d.ts +21 -0
  91. package/dist/session/managed-build-receipt.js +337 -0
  92. package/dist/session/managed-verification-timeouts.d.ts +8 -0
  93. package/dist/session/managed-verification-timeouts.js +17 -0
  94. package/dist/session/phase-completion.artifacts.d.ts +17 -0
  95. package/dist/session/phase-completion.artifacts.js +224 -22
  96. package/dist/session/phase-completion.d.ts +11 -1
  97. package/dist/session/phase-completion.helpers.d.ts +5 -0
  98. package/dist/session/phase-completion.helpers.js +30 -7
  99. package/dist/session/phase-completion.js +14 -3
  100. package/dist/session/phase-completion.prompts.js +17 -18
  101. package/dist/session/phase-completion.runtime.d.ts +3 -1
  102. package/dist/session/phase-completion.runtime.js +126 -17
  103. package/dist/session/phase-completion.schemas.js +49 -2
  104. package/dist/session/phase-completion.validation.d.ts +1 -0
  105. package/dist/session/phase-completion.validation.js +111 -21
  106. package/dist/session/pre-publication-validation.d.ts +75 -0
  107. package/dist/session/pre-publication-validation.js +661 -0
  108. package/dist/session/provider-cli/bridge-cli.conformance-fixtures.d.ts +72 -0
  109. package/dist/session/provider-cli/bridge-cli.conformance-fixtures.js +462 -0
  110. package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
  111. package/dist/session/provider-cli/ci-repair-readiness.js +23 -3
  112. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +4 -2
  113. package/dist/session/provider-cli/provider-cli.artifacts.js +47 -13
  114. package/dist/session/provider-cli/provider-cli.deterministic.js +34 -2
  115. package/dist/session/provider-cli/provider-cli.process.d.ts +51 -0
  116. package/dist/session/provider-cli/provider-cli.process.js +422 -22
  117. package/dist/session/provider-cli/provider-cli.prompts.d.ts +5 -0
  118. package/dist/session/provider-cli/provider-cli.prompts.js +136 -16
  119. package/dist/session/provider-cli/provider-cli.repair-epoch.d.ts +6 -0
  120. package/dist/session/provider-cli/provider-cli.repair-epoch.js +11 -0
  121. package/dist/session/provider-cli/provider-cli.runtime.d.ts +14 -1
  122. package/dist/session/provider-cli/provider-cli.runtime.js +54 -3
  123. package/dist/session/provider-cli/provider-cli.service.d.ts +30 -5
  124. package/dist/session/provider-cli/provider-cli.service.js +773 -69
  125. package/dist/session/provider-cli/provider-cli.types.d.ts +2 -0
  126. package/dist/session/provider-cli/provider-output-state.d.ts +10 -0
  127. package/dist/session/provider-cli/provider-output-state.js +361 -17
  128. package/dist/session/provider-cli/provider-usage.d.ts +7 -0
  129. package/dist/session/provider-cli/provider-usage.js +28 -5
  130. package/dist/session/session-redaction.js +434 -9
  131. package/dist/session/session.service.js +145 -23
  132. package/dist/session/session.types.d.ts +147 -2
  133. package/dist/session/session.types.js +587 -7
  134. package/dist/session/unreferenced-exports.d.ts +12 -0
  135. package/dist/session/unreferenced-exports.js +59 -0
  136. package/dist/shared/format/format-bytes.d.ts +1 -0
  137. package/dist/shared/format/format-bytes.js +9 -0
  138. package/dist/shared/git/git.js +2 -1
  139. package/dist/shared/path/path.d.ts +1 -0
  140. package/dist/shared/path/path.js +18 -0
  141. package/dist/shared/process/dotnet-toolchain.d.ts +17 -0
  142. package/dist/shared/process/dotnet-toolchain.js +307 -0
  143. package/dist/shared/process/host-process-recovery.js +3 -2
  144. package/dist/shared/process/host-process-supervisor.d.ts +2 -0
  145. package/dist/shared/process/host-process-supervisor.js +60 -11
  146. package/dist/shared/process/process.d.ts +2 -0
  147. package/dist/shared/process/process.js +12 -7
  148. package/dist/shared/provider-capabilities.d.ts +3 -0
  149. package/dist/shared/provider-capabilities.js +85 -0
  150. package/dist/shared/redaction/redaction.d.ts +1 -0
  151. package/dist/shared/redaction/redaction.js +39 -3
  152. package/dist/shared/time/monotonic.d.ts +1 -0
  153. package/dist/shared/time/monotonic.js +8 -0
  154. package/dist/version.d.ts +1 -1
  155. package/dist/version.js +1 -1
  156. package/dist/worktree/dependency-warmer.d.ts +41 -9
  157. package/dist/worktree/dependency-warmer.js +497 -22
  158. package/dist/worktree/index.d.ts +1 -1
  159. package/dist/worktree/story-qa-reference-hydrator.d.ts +4 -0
  160. package/dist/worktree/story-qa-reference-hydrator.js +253 -0
  161. package/dist/worktree/worktree.service.d.ts +7 -0
  162. package/dist/worktree/worktree.service.js +253 -21
  163. package/dist/worktree/worktree.types.d.ts +7 -1
  164. package/package.json +7 -5
  165. 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
- if (options.dispatch.execution.provider.trim().toLowerCase() !== "cursor") {
40
- return runtime;
42
+ const provider = options.dispatch.execution.provider.trim().toLowerCase();
43
+ if (provider === "cursor") {
44
+ return await prepareCursorCompletionToolRuntime(worktreePath, runtime);
41
45
  }
42
- return await prepareCursorCompletionToolRuntime(worktreePath, runtime);
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 = pass
189
- ? `${completedPhase} completion artifact accepted.`
190
- : `${completedPhase} completion artifact recorded with status ${status}; rework is required before phase progression.`;
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 accepted with status ${status}.`;
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 once for the terminal receipt, and include the returned managed_command_id in this command evidence.");
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 once for the terminal receipt, and include its matching managed_command_id; generic-shell timeout evidence is not accepted on a managed host.");
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 once for its terminal receipt, then resubmit this gate.");
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: parseCompletionRedactionSecrets(process.env[completionRedactionSecretsEnv]),
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
- return await session.callTool(name, args);
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: reviewSectionSchema(),
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 and completed through one forge.wait_managed_command call for managed-host evidence." },
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
- requireEvidenceRecord(command, `commands[${index}]`);
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
- requireNonEmptyStringRecord(record, "command", `commands[${index}].`);
259
- requireNonEmptyStringRecord(record, "status", `commands[${index}].`);
260
- requireNonEmptyStringRecord(record, "summary", `commands[${index}].`);
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
- throw new Error(`commands[${index}].exit_code is required.`);
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
- validateClassifiedNonBlockingQaFailure(record, index);
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
- throw new Error(`commands[${index}].exit_code must be 0 when QA gate status is pass.`);
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
- throw new Error(`commands[${index}].status must be pass when QA gate status is pass.`);
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
- for (const task of implementationPlan.tasks) {
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
- if (taskIds.has(taskId)) {
329
- throw new Error(`implementation_plan.tasks: duplicate task id '${taskId}'.`);
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
- requireEvidenceRecord(command, `commands[${index}]`);
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
- requireNonEmptyStringRecord(record, "command", `commands[${index}].`);
722
- requireNonEmptyStringRecord(record, "status", `commands[${index}].`);
723
- requireNonEmptyStringRecord(record, "summary", `commands[${index}].`);
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
- throw new Error(`commands[${index}].exit_code is required.`);
814
+ commandIssues.push(`commands[${index}].exit_code is required.`);
726
815
  }
727
- if (status === "pass" && record.exit_code !== 0) {
728
- throw new Error(`commands[${index}].exit_code must be 0 when QA status is pass.`);
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
- throw new Error(`commands[${index}].status must be pass when QA status is pass.`);
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";