@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,3 +1,4 @@
1
+ import crypto from "node:crypto";
1
2
  import fs from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import { redactSecrets } from "../shared/redaction/redaction.js";
@@ -12,8 +13,11 @@ export async function writeDecomposeGateCompletionArtifact(config, gate, toolNam
12
13
  validateDecomposeProposalJson(proposalJson);
13
14
  }
14
15
  const redactedPayload = redactSecrets({ ...payload, status, phase: "decompose", gate, tool_name: toolName }, config.redactionSecrets ?? []);
16
+ assertStructuralFieldsUnchanged(payload, redactedPayload);
17
+ if (gate === "decompose_proposal") {
18
+ validatePostRedaction(() => validateDecomposeProposalJson(asRecord(redactedPayload["proposal_json"] ?? redactedPayload["decomposition_json"], "proposal_json")));
19
+ }
15
20
  const root = artifactRootFor(config.worktreePath, config.workItemId);
16
- await fs.mkdir(root, { recursive: true });
17
21
  const artifactPath = path.join(root, decomposeGateFileNames[gate]);
18
22
  const reportPath = gate === "decompose_proposal" ? path.join(root, phaseFiles.decompose.reportFileName) : path.join(root, `${gate}.md`);
19
23
  const verdictJson = jsonText(redactedPayload);
@@ -21,8 +25,10 @@ export async function writeDecomposeGateCompletionArtifact(config, gate, toolNam
21
25
 
22
26
  ${optionalString(redactedPayload, "summary") ?? "Decompose gate recorded."}
23
27
  `;
24
- await writeFileAtomic(artifactPath, verdictJson);
25
- await writeFileAtomic(reportPath, report);
28
+ await publishArtifactBundleAtomic(root, [
29
+ { fileName: path.basename(reportPath), content: report },
30
+ { fileName: path.basename(artifactPath), content: verdictJson }
31
+ ], path.basename(artifactPath));
26
32
  return {
27
33
  phase: "decompose",
28
34
  workItemId: config.workItemId,
@@ -44,14 +50,19 @@ export async function writeDraftGateCompletionArtifact(config, gate, toolName, i
44
50
  validateDraftProposalJson(draftJson);
45
51
  }
46
52
  const redactedPayload = redactSecrets({ ...payload, status, phase: "draft", gate, tool_name: toolName }, config.redactionSecrets ?? []);
53
+ assertStructuralFieldsUnchanged(payload, redactedPayload);
54
+ if (gate === "draft_proposal") {
55
+ validatePostRedaction(() => validateDraftProposalJson(asRecord(redactedPayload["draft_json"], "draft_json")));
56
+ }
47
57
  const root = artifactRootFor(config.worktreePath, config.workItemId);
48
- await fs.mkdir(root, { recursive: true });
49
58
  const artifactPath = path.join(root, draftGateFileNames[gate]);
50
59
  const reportPath = gate === "draft_proposal" ? path.join(root, phaseFiles.draft.reportFileName) : path.join(root, `${gate}.md`);
51
60
  const verdictJson = jsonText(redactedPayload);
52
61
  const report = optionalString(redactedPayload, "report_markdown") ?? `# ${gate.replace(/_/g, " ")}\n\n${optionalString(redactedPayload, "summary") ?? "Draft gate recorded."}\n`;
53
- await writeFileAtomic(artifactPath, verdictJson);
54
- await writeFileAtomic(reportPath, report);
62
+ await publishArtifactBundleAtomic(root, [
63
+ { fileName: path.basename(reportPath), content: report },
64
+ { fileName: path.basename(artifactPath), content: verdictJson }
65
+ ], path.basename(artifactPath));
55
66
  return {
56
67
  phase: "draft",
57
68
  workItemId: config.workItemId,
@@ -148,6 +159,11 @@ export async function writeCompletionArtifact(config, toolName, input) {
148
159
  validatePayloadShape(phase, validationPayload);
149
160
  validateOptionalWorkItemId(validationPayload, config.workItemId);
150
161
  const redactedPayload = redactSecrets(validationPayload, config.redactionSecrets ?? []);
162
+ assertStructuralFieldsUnchanged(validationPayload, redactedPayload);
163
+ validatePostRedaction(() => {
164
+ validatePayloadShape(phase, redactedPayload);
165
+ validateOptionalWorkItemId(redactedPayload, config.workItemId);
166
+ });
151
167
  const producedBy = optionalString(redactedPayload, "produced_by")
152
168
  ?? optionalString(redactedPayload, "producedBy")
153
169
  ?? config.producedBy
@@ -175,9 +191,10 @@ export async function writeCompletionArtifact(config, toolName, input) {
175
191
  const report = reportMarkdown(redactedPayload, phase, status, producedBy, contentHash);
176
192
  const artifactPath = completionArtifactJsonPath(config.worktreePath, config.workItemId, phase);
177
193
  const reportPath = completionArtifactReportPath(config.worktreePath, config.workItemId, phase);
178
- await fs.mkdir(path.dirname(artifactPath), { recursive: true });
179
- await writeFileAtomic(artifactPath, artifactJson);
180
- await writeFileAtomic(reportPath, report);
194
+ await publishArtifactBundleAtomic(path.dirname(artifactPath), [
195
+ { fileName: path.basename(reportPath), content: report },
196
+ { fileName: path.basename(artifactPath), content: artifactJson }
197
+ ], path.basename(artifactPath));
181
198
  return {
182
199
  phase,
183
200
  workItemId: config.workItemId,
@@ -215,11 +232,12 @@ export async function writeArchitectCompletionArtifact(config, payload, status,
215
232
  ...artifactWithoutHash,
216
233
  content_hash: contentHash
217
234
  };
218
- await fs.mkdir(artifactRoot, { recursive: true });
219
- await writeFileAtomic(implementationPlanPath, jsonText(implementationPlan));
220
- await writeFileAtomic(qaPlanPath, jsonText(qaPlan));
221
- await writeFileAtomic(verdictPath, jsonText(verdict));
222
- await writeFileAtomic(reportPath, `${report.trim()}\n`);
235
+ await publishArtifactBundleAtomic(artifactRoot, [
236
+ { fileName: path.basename(implementationPlanPath), content: jsonText(implementationPlan) },
237
+ { fileName: path.basename(qaPlanPath), content: jsonText(qaPlan) },
238
+ { fileName: path.basename(reportPath), content: `${report.trim()}\n` },
239
+ { fileName: path.basename(verdictPath), content: jsonText(verdict) }
240
+ ], path.basename(verdictPath));
223
241
  return {
224
242
  phase: "architect",
225
243
  workItemId: config.workItemId,
@@ -246,6 +264,16 @@ export async function writeQaGateCompletionArtifact(config, gate, input) {
246
264
  status,
247
265
  ...(validatedSidecars ? { sidecars: validatedSidecars } : {})
248
266
  }, config.redactionSecrets ?? []);
267
+ assertStructuralFieldsUnchanged(payload, redactedPayload);
268
+ validatePostRedaction(() => {
269
+ validateQaGatePayloadShape(gate, redactedPayload, status);
270
+ validateOptionalWorkItemId(redactedPayload, config.workItemId);
271
+ });
272
+ if (gate === "qa_manual_web") {
273
+ await validateQaSidecarReferences(config.worktreePath, config.workItemId, qaArray(redactedPayload, "sidecars", qaEvidenceSource(redactedPayload)), ownerFields.owner_type, ownerFields.owner_id).catch((error) => {
274
+ throw postRedactionValidationError(error);
275
+ });
276
+ }
249
277
  const producedBy = optionalString(redactedPayload, "produced_by")
250
278
  ?? optionalString(redactedPayload, "producedBy")
251
279
  ?? config.producedBy
@@ -269,7 +297,6 @@ export async function writeQaGateCompletionArtifact(config, gate, input) {
269
297
  ...artifactWithoutHash,
270
298
  content_hash: contentHash
271
299
  };
272
- await fs.mkdir(artifactRoot, { recursive: true });
273
300
  await writeFileAtomic(artifactPath, jsonText(artifact));
274
301
  return {
275
302
  phase: "qa",
@@ -378,12 +405,13 @@ export async function writeQaCompletionArtifact(config, payload, status, produce
378
405
  content_hash: contentHash
379
406
  });
380
407
  const finalReport = reportMarkdown(payload, "qa", status, producedBy, contentHash);
381
- await fs.mkdir(artifactRoot, { recursive: true });
382
- await writeFileAtomic(planPath, planJson);
383
- await writeFileAtomic(evidencePath, evidenceJson);
384
- await writeFileAtomic(findingsPath, findingsJson);
385
- await writeFileAtomic(verdictPath, verdictJson);
386
- await writeFileAtomic(reportPath, finalReport);
408
+ await publishArtifactBundleAtomic(artifactRoot, [
409
+ { fileName: path.basename(planPath), content: planJson },
410
+ { fileName: path.basename(evidencePath), content: evidenceJson },
411
+ { fileName: path.basename(findingsPath), content: findingsJson },
412
+ { fileName: path.basename(reportPath), content: finalReport },
413
+ { fileName: path.basename(verdictPath), content: verdictJson }
414
+ ], path.basename(verdictPath));
387
415
  return {
388
416
  phase: "qa",
389
417
  workItemId: config.workItemId,
@@ -553,6 +581,7 @@ async function validateQaSidecarReferences(worktreePath, workItemId, sidecars, o
553
581
  : artifactRootFor(worktreePath, workItemId);
554
582
  const qaSidecarRoot = path.resolve(artifactRoot, "qa");
555
583
  const validated = [];
584
+ const validatedByPath = new Map();
556
585
  for (const [index, sidecar] of sidecars.entries()) {
557
586
  if (!sidecar || typeof sidecar !== "object" || Array.isArray(sidecar)) {
558
587
  throw new Error(`sidecars[${index}] must be an object.`);
@@ -588,9 +617,19 @@ async function validateQaSidecarReferences(worktreePath, workItemId, sidecars, o
588
617
  if (actualHash !== normalizeSha256ContentHash(contentHash)) {
589
618
  throw new Error(`sidecars[${index}].content_hash does not match the referenced file.`);
590
619
  }
620
+ const canonicalPath = path.relative(worktreePath, fullPath).split(path.sep).join("/");
621
+ const duplicateKey = canonicalPath.toLowerCase();
622
+ const existing = validatedByPath.get(duplicateKey);
623
+ if (existing) {
624
+ if (existing.contentHash !== actualHash || existing.bytes !== stat.size) {
625
+ throw new Error(`sidecars[${index}] conflicts with another payload for ${canonicalPath}.`);
626
+ }
627
+ continue;
628
+ }
629
+ validatedByPath.set(duplicateKey, { contentHash: actualHash, bytes: stat.size });
591
630
  validated.push({
592
631
  ...record,
593
- path: path.relative(worktreePath, fullPath).split(path.sep).join("/"),
632
+ path: canonicalPath,
594
633
  content_hash: actualHash,
595
634
  bytes: stat.size
596
635
  });
@@ -625,3 +664,166 @@ export async function readCompletionArtifact(worktreePath, workItemId, phase, ow
625
664
  phase
626
665
  };
627
666
  }
667
+ /**
668
+ * Stages a complete phase bundle outside the live artifact directory, then
669
+ * publishes its canonical verdict/marker last. Consumers treat the marker as
670
+ * the bundle's commit point, so they can observe either the old complete bundle
671
+ * or the new complete bundle, never a partially written set. A failed or
672
+ * cancelled publication rolls every touched file back before rejecting.
673
+ */
674
+ export async function publishArtifactBundleAtomic(artifactRoot, files, commitFileName, options = {}) {
675
+ validateArtifactBundleFiles(files, commitFileName);
676
+ throwIfPublicationCancelled(options.signal);
677
+ const parent = path.dirname(artifactRoot);
678
+ const transactionId = `${process.pid}.${crypto.randomUUID()}`;
679
+ const stagingRoot = path.join(parent, `.${path.basename(artifactRoot)}.${transactionId}.publish`);
680
+ const backupRoot = path.join(stagingRoot, ".previous");
681
+ const orderedFiles = [
682
+ ...files.filter((file) => file.fileName !== commitFileName),
683
+ files.find((file) => file.fileName === commitFileName)
684
+ ];
685
+ const backedUp = new Set();
686
+ const published = new Set();
687
+ try {
688
+ await fs.mkdir(parent, { recursive: true });
689
+ await fs.mkdir(stagingRoot, { recursive: false });
690
+ for (const file of orderedFiles) {
691
+ throwIfPublicationCancelled(options.signal);
692
+ const handle = await fs.open(path.join(stagingRoot, file.fileName), "wx");
693
+ try {
694
+ await handle.writeFile(file.content, { encoding: "utf8", signal: options.signal });
695
+ await handle.sync();
696
+ }
697
+ finally {
698
+ await handle.close();
699
+ }
700
+ }
701
+ throwIfPublicationCancelled(options.signal);
702
+ await fs.mkdir(artifactRoot, { recursive: true });
703
+ await fs.mkdir(backupRoot, { recursive: true });
704
+ // Hide the previous commit marker before changing any supporting file.
705
+ for (const fileName of [commitFileName, ...orderedFiles.map((file) => file.fileName).filter((name) => name !== commitFileName)]) {
706
+ const livePath = path.join(artifactRoot, fileName);
707
+ try {
708
+ await fs.rename(livePath, path.join(backupRoot, fileName));
709
+ backedUp.add(fileName);
710
+ }
711
+ catch (error) {
712
+ if (!isNodeError(error) || error.code !== "ENOENT") {
713
+ throw error;
714
+ }
715
+ }
716
+ }
717
+ for (const [index, file] of orderedFiles.entries()) {
718
+ throwIfPublicationCancelled(options.signal);
719
+ await options.beforePublishFile?.(file.fileName, index);
720
+ await fs.rename(path.join(stagingRoot, file.fileName), path.join(artifactRoot, file.fileName));
721
+ published.add(file.fileName);
722
+ }
723
+ }
724
+ catch (error) {
725
+ const rollbackErrors = [];
726
+ for (const file of [...orderedFiles].reverse()) {
727
+ if (!published.has(file.fileName))
728
+ continue;
729
+ await fs.rm(path.join(artifactRoot, file.fileName), { force: true }).catch(() => {
730
+ rollbackErrors.push(file.fileName);
731
+ });
732
+ }
733
+ // Restore the previous marker last for the same observable commit boundary.
734
+ const restoreOrder = [
735
+ ...orderedFiles.filter((file) => file.fileName !== commitFileName),
736
+ orderedFiles.find((file) => file.fileName === commitFileName)
737
+ ];
738
+ for (const file of restoreOrder) {
739
+ if (!backedUp.has(file.fileName))
740
+ continue;
741
+ await fs.rename(path.join(backupRoot, file.fileName), path.join(artifactRoot, file.fileName)).catch(() => {
742
+ rollbackErrors.push(file.fileName);
743
+ });
744
+ }
745
+ if (rollbackErrors.length === 0
746
+ && error instanceof Error
747
+ && error.message.startsWith("artifact_atomic_publish_cancelled:")) {
748
+ throw error;
749
+ }
750
+ const suffix = rollbackErrors.length > 0 ? " Rollback did not restore every bundle file." : "";
751
+ throw new Error(`artifact_atomic_publish_failure: completion artifact bundle was not published.${suffix}`, { cause: error });
752
+ }
753
+ finally {
754
+ await fs.rm(stagingRoot, { recursive: true, force: true }).catch(() => undefined);
755
+ }
756
+ }
757
+ function validateArtifactBundleFiles(files, commitFileName) {
758
+ if (files.length === 0 || !files.some((file) => file.fileName === commitFileName)) {
759
+ throw new Error("artifact_atomic_publish_failure: bundle commit file is missing.");
760
+ }
761
+ const names = new Set();
762
+ for (const file of files) {
763
+ if (!file.fileName || path.basename(file.fileName) !== file.fileName || names.has(file.fileName)) {
764
+ throw new Error("artifact_atomic_publish_failure: bundle file names must be unique safe base names.");
765
+ }
766
+ names.add(file.fileName);
767
+ }
768
+ }
769
+ function throwIfPublicationCancelled(signal) {
770
+ if (signal?.aborted) {
771
+ throw new Error("artifact_atomic_publish_cancelled: completion artifact bundle publication was cancelled.");
772
+ }
773
+ }
774
+ function validatePostRedaction(validate) {
775
+ try {
776
+ validate();
777
+ }
778
+ catch (error) {
779
+ throw postRedactionValidationError(error);
780
+ }
781
+ }
782
+ function postRedactionValidationError(error) {
783
+ const message = error instanceof Error ? error.message : String(error);
784
+ return new Error(`artifact_post_redaction_validation_failed: ${message}`, { cause: error });
785
+ }
786
+ function assertStructuralFieldsUnchanged(original, redacted) {
787
+ if (structuralFieldFingerprint(original) !== structuralFieldFingerprint(redacted)) {
788
+ // This stable code is intentionally safe for telemetry: it does not include
789
+ // the structural value or any candidate secret.
790
+ throw new Error("artifact_redaction_changed_structural_field: redaction changed a structural identifier; the bundle was not published.");
791
+ }
792
+ }
793
+ function structuralFieldFingerprint(value, key = "", result = []) {
794
+ if (Array.isArray(value)) {
795
+ if (isStructuralFieldKey(key) && value.every((item) => item === null || typeof item !== "object")) {
796
+ result.push(stableStringify(value));
797
+ }
798
+ else {
799
+ for (const item of value)
800
+ structuralFieldFingerprint(item, key, result);
801
+ }
802
+ return result.join("\n");
803
+ }
804
+ if (!value || typeof value !== "object") {
805
+ if (isStructuralFieldKey(key))
806
+ result.push(stableStringify(value));
807
+ return result.join("\n");
808
+ }
809
+ for (const [childKey, child] of Object.entries(value)) {
810
+ if (isStructuralFieldKey(childKey)) {
811
+ result.push(`${childKey}:${stableStringify(child)}`);
812
+ }
813
+ else {
814
+ structuralFieldFingerprint(child, childKey, result);
815
+ }
816
+ }
817
+ return result.join("\n");
818
+ }
819
+ function isStructuralFieldKey(key) {
820
+ return key === "id"
821
+ || key === "before"
822
+ || key === "after"
823
+ || key === "from"
824
+ || key === "to"
825
+ || key === "tasks"
826
+ || key === "phases"
827
+ || /(?:^|_)(?:id|ids)$/i.test(key)
828
+ || /(?:Id|Ids)$/.test(key);
829
+ }
@@ -10,7 +10,8 @@ export type ArchitectCompletionPhase = "architect";
10
10
  export type DraftCompletionPhase = "draft";
11
11
  export type DecomposeCompletionPhase = "decompose";
12
12
  export type CiRepairCompletionPhase = "ci_repair";
13
- export type CompletionPhase = DevelopCompletionPhase | "qa" | DebugCompletionPhase | ArchitectCompletionPhase | DraftCompletionPhase | DecomposeCompletionPhase | CiRepairCompletionPhase;
13
+ export type CommentRepairCompletionPhase = "comment_repair";
14
+ export type CompletionPhase = DevelopCompletionPhase | "qa" | DebugCompletionPhase | ArchitectCompletionPhase | DraftCompletionPhase | DecomposeCompletionPhase | CiRepairCompletionPhase | CommentRepairCompletionPhase;
14
15
  export type QaCompletionGate = "qa_plan" | "qa_regression" | "qa_api" | "qa_manual_web" | "qa_bug_repro" | "qa_findings" | "qa_verdict";
15
16
  export type DraftCompletionGate = "draft_intent" | "draft_repo_context" | "draft_graph_context" | "draft_product_scope" | "draft_work_order_plan" | "draft_critic" | "draft_proposal";
16
17
  export type DecomposeCompletionGate = "decompose_subject_intake" | "decompose_context_review" | "decompose_breakdown_strategy" | "decompose_child_items" | "decompose_dependency_graph" | "decompose_critic" | "decompose_proposal";
@@ -20,6 +21,7 @@ export interface CompletionToolRuntime {
20
21
  readonly args: readonly string[];
21
22
  readonly env: Readonly<Record<string, string>>;
22
23
  readonly cursorMcpConfigPath?: string | null;
24
+ readonly claudeMcpConfigPath?: string | null;
23
25
  cleanup(): Promise<void>;
24
26
  }
25
27
  export interface CompletionToolRuntimeOptions {
@@ -49,6 +51,13 @@ export interface CompletionToolCallResult {
49
51
  readonly text: string;
50
52
  }[];
51
53
  readonly structuredContent: CompletionArtifactReference & {
54
+ readonly artifact_written: true;
55
+ readonly artifact_receipt: {
56
+ readonly schema_version: 1;
57
+ readonly path: string;
58
+ readonly report_path: string;
59
+ readonly content_hash: string;
60
+ };
52
61
  readonly gate: {
53
62
  readonly phase: CompletionPhase;
54
63
  readonly qaGate?: QaCompletionGate;
@@ -100,6 +109,7 @@ export declare const qaFindingScopes: Set<string>;
100
109
  export declare const qaCandidateBugActions: Set<string>;
101
110
  export declare const qaScopeSurfaces: Set<string>;
102
111
  export declare const qaScopeEvidence: Set<string>;
112
+ export declare const qaScopeCapabilities: Set<string>;
103
113
  export declare const qaScopeSources: Set<string>;
104
114
  export declare const qaScopeAuthorities: Set<string>;
105
115
  export declare const qaScopeConfidence: Set<string>;
@@ -33,6 +33,11 @@ export declare function requireField(payload: Record<string, unknown>, key: stri
33
33
  export declare function requireNonPassEvidence(payload: Record<string, unknown>): void;
34
34
  export declare function requireImplementationEvidence(payload: Record<string, unknown>): void;
35
35
  export declare function requirePassingCommands(payload: Record<string, unknown>): void;
36
+ export declare class CompletionPayloadValidationError extends Error {
37
+ readonly issues: readonly string[];
38
+ constructor(issues: readonly string[]);
39
+ }
40
+ export declare function throwCompletionPayloadIssues(issues: readonly string[]): void;
36
41
  export declare function requireArray(payload: Record<string, unknown>, key: string): void;
37
42
  export declare function requireNonEmptyArray(payload: Record<string, unknown>, key: string): void;
38
43
  export declare function arrayLength(payload: Record<string, unknown>, key: string): number;
@@ -88,6 +88,9 @@ export function parseStrictCompletionPhase(value) {
88
88
  if (normalized === "ci_repair" || normalized === "pr_checks" || normalized === "repair_pr_checks") {
89
89
  return "ci_repair";
90
90
  }
91
+ if (normalized === "comment_repair" || normalized === "pr_comments" || normalized === "repair_pr_comments") {
92
+ return "comment_repair";
93
+ }
91
94
  return null;
92
95
  }
93
96
  export function normalizeDebugCompletionPhase(value) {
@@ -162,6 +165,9 @@ export function phaseFromToolName(toolName) {
162
165
  if (normalized === "forge.complete_ci_repair" || normalized === "complete_ci_repair") {
163
166
  return "ci_repair";
164
167
  }
168
+ if (normalized === "forge.complete_comment_repair" || normalized === "complete_comment_repair") {
169
+ return "comment_repair";
170
+ }
165
171
  if (draftGateFromToolName(toolName)) {
166
172
  return "draft";
167
173
  }
@@ -262,28 +268,45 @@ export function requirePassingCommands(payload) {
262
268
  if (commands.length === 0) {
263
269
  throw new Error("commands must include at least one deterministic validation command.");
264
270
  }
271
+ const issues = [];
265
272
  for (const [index, command] of commands.entries()) {
266
273
  if (!command || typeof command !== "object" || Array.isArray(command)) {
267
- throw new Error(`commands[${index}] must be an object.`);
274
+ issues.push(`commands[${index}] must be an object.`);
275
+ continue;
268
276
  }
269
277
  const record = command;
270
278
  if (typeof record.command !== "string" || record.command.trim().length === 0) {
271
- throw new Error(`commands[${index}].command must be a non-empty string.`);
279
+ issues.push(`commands[${index}].command must be a non-empty string.`);
272
280
  }
273
281
  if (typeof record.summary !== "string" || record.summary.trim().length === 0) {
274
- throw new Error(`commands[${index}].summary must be a non-empty string.`);
282
+ issues.push(`commands[${index}].summary must be a non-empty string.`);
275
283
  }
276
284
  if (typeof record.exit_code !== "number" || !Number.isFinite(record.exit_code)) {
277
- throw new Error(`commands[${index}].exit_code must be a finite number.`);
285
+ issues.push(`commands[${index}].exit_code must be a finite number.`);
278
286
  }
279
- if (record.exit_code !== 0) {
280
- throw new Error(`commands[${index}].exit_code must be 0 when status is pass.`);
287
+ else if (record.exit_code !== 0) {
288
+ issues.push(`commands[${index}].exit_code must be 0 when status is pass.`);
281
289
  }
282
290
  const status = typeof record.status === "string" ? record.status.trim().toLowerCase() : "";
283
291
  if (status !== "pass") {
284
- throw new Error(`commands[${index}].status must be pass.`);
292
+ issues.push(`commands[${index}].status must be pass.`);
285
293
  }
286
294
  }
295
+ throwCompletionPayloadIssues(issues);
296
+ }
297
+ export class CompletionPayloadValidationError extends Error {
298
+ issues;
299
+ constructor(issues) {
300
+ super(`completion_payload_invalid: ${issues.length} correction${issues.length === 1 ? "" : "s"} required; no artifact was written.\n`
301
+ + issues.map(issue => `- ${issue}`).join("\n")
302
+ + "\nRepair every listed field and resubmit this gate once.");
303
+ this.issues = issues;
304
+ this.name = "CompletionPayloadValidationError";
305
+ }
306
+ }
307
+ export function throwCompletionPayloadIssues(issues) {
308
+ if (issues.length > 0)
309
+ throw new CompletionPayloadValidationError(issues);
287
310
  }
288
311
  export function requireArray(payload, key) {
289
312
  if (!Array.isArray(payload[key])) {
@@ -133,6 +133,7 @@ export const qaScopeEvidence = new Set([
133
133
  "migration_check",
134
134
  "config_check"
135
135
  ]);
136
+ export const qaScopeCapabilities = new Set(["docker_engine"]);
136
137
  export const qaScopeSources = new Set(["user", "draft", "decompose", "architect", "heuristic", "system"]);
137
138
  export const qaScopeAuthorities = new Set(["user", "decompose", "architect", "system"]);
138
139
  export const qaScopeConfidence = new Set(["explicit", "inferred"]);
@@ -140,6 +141,7 @@ export const qaScopeFields = new Set([
140
141
  "schema_version",
141
142
  "surface",
142
143
  "required_evidence",
144
+ "required_capabilities",
143
145
  "not_applicable",
144
146
  "browser_required",
145
147
  "api_required",
@@ -199,6 +201,10 @@ export const phaseFiles = {
199
201
  ci_repair: {
200
202
  jsonFileName: "ci-repair.handoff.json",
201
203
  reportFileName: "ci-repair.report.md"
204
+ },
205
+ comment_repair: {
206
+ jsonFileName: "comment-repair.handoff.json",
207
+ reportFileName: "comment-repair.report.md"
202
208
  }
203
209
  };
204
210
  export function completionPhaseFromDispatch(dispatch) {
@@ -231,6 +237,9 @@ export function normalizeCompletionPhase(value) {
231
237
  if (normalized === "ci_repair" || normalized === "pr_checks" || normalized === "repair_pr_checks") {
232
238
  return "ci_repair";
233
239
  }
240
+ if (normalized === "comment_repair" || normalized === "pr_comments" || normalized === "repair_pr_comments") {
241
+ return "comment_repair";
242
+ }
234
243
  return "implementation";
235
244
  }
236
245
  export function usesDevelopCycleCompletion(dispatch) {
@@ -284,9 +293,11 @@ export function completionToolNamesForPhase(phase) {
284
293
  ? "complete_validation"
285
294
  : phase === "ci_repair"
286
295
  ? "complete_ci_repair"
287
- : phase === "architect"
288
- ? "complete_architect_plan"
289
- : "complete_qa";
296
+ : phase === "comment_repair"
297
+ ? "complete_comment_repair"
298
+ : phase === "architect"
299
+ ? "complete_architect_plan"
300
+ : "complete_qa";
290
301
  return [`forge.${base}`, base];
291
302
  }
292
303
  export function completionToolNamesForQaGate(gate) {
@@ -17,8 +17,9 @@ Forge will mirror only the tool-written QA artifacts; reporting QA completion in
17
17
  Do not write Architect artifacts by hand. When the plan is complete, call \`${toolName}(...)\`${alias ? ` (or \`${alias}\` if your client shows unqualified MCP tool names)` : ""} with the full structured Architect planning payload.
18
18
  That tool writes \`.forge/work-items/{workItemId}/artifacts/implementation.plan.json\`, \`qa.plan.json\`, \`${artifactName}\`, and \`architect.report.md\`.
19
19
  The payload must put \`phases\`, \`tasks\`, \`dependencies\`, and \`phase_gates\` as top-level fields inside \`implementation_plan\`, for example:
20
- \`{ "status": "pass", "summary": "...", "implementation_plan": { "schema_version": 1, "plan_id": "plan-...", "phases": [{ "id": "phase-1", "tasks": ["task-1"], "gate": "gate-1" }], "tasks": [{ "id": "task-1", "verification_scope": { "schema_version": 1, "surface": ["domain_contract"], "required_evidence": ["unit_tests"], "not_applicable": ["browser_manual", "api_http"], "browser_required": false, "api_required": false, "rationale": "Backend-only child task." }, "tests": [{ "command": "npm test" }] }], "dependencies": [], "phase_gates": [{ "id": "gate-1", "phase": "phase-1" }] }, "qa_plan": { "schema_version": 1, "final_qa": [{ "id": "qa-1", "coverage": "..." }] }, "report_markdown": "# Architect Report" }\`.
20
+ \`{ "status": "pass", "summary": "...", "implementation_plan": { "schema_version": 1, "plan_id": "plan-...", "phases": [{ "id": "phase-1", "tasks": ["task-1"], "gate": "gate-1" }], "tasks": [{ "id": "task-1", "verification_scope": { "schema_version": 1, "surface": ["domain_contract"], "required_evidence": ["unit_tests"], "required_capabilities": [], "not_applicable": ["browser_manual", "api_http"], "browser_required": false, "api_required": false, "rationale": "Backend-only child task." }, "tests": [{ "command": "npm test" }] }], "dependencies": [], "phase_gates": [{ "id": "gate-1", "phase": "phase-1" }] }, "qa_plan": { "schema_version": 1, "final_qa": [{ "id": "qa-1", "coverage": "..." }] }, "report_markdown": "# Architect Report" }\`.
21
21
  \`implementation_plan.tasks\` are Develop child tasks only: each task should include \`verification_scope\` describing task-level QA evidence, and must require a focused repo/product implementation change. Put parent-story QA, sign-off, child-count/materialization inventory, parent plan artifact checks, and final integrated acceptance proof in \`qa_plan.final_qa\`, not as child Develop tasks.
22
+ When a task's implementation or validation starts containers or otherwise needs the host Docker daemon, include \`"required_capabilities": ["docker_engine"]\` in its verification scope. Do not encode a database, image, or Docker version as a runner capability; the repository owns those versions.
22
23
  Dependency entries use canonical \`before\` and \`after\` task ids, for example \`{ "before": "task-1", "after": "task-2" }\`. Forge accepts \`from\`/\`to\` only as aliases and normalizes them before writing artifacts.
23
24
  Use \`status: "pass"\` for a valid pending-approval plan, \`needs_revision\` when story/design input must change, or \`blocked\` with a concrete blocker.
24
25
  Forge will mirror only the tool-written Architect artifacts; reporting the plan in prose is not enough.`;
@@ -58,7 +59,8 @@ Complete the QA cycle in order inside this same provider session. Start long, in
58
59
  6. Follow the returned findings instruction, then call \`forge.complete_qa_findings(...)\`.
59
60
  7. Follow the returned verdict instruction, then call \`forge.complete_qa_verdict(...)\`.
60
61
  Use \`status: "skipped"\` with a reason for gates that are not applicable. If a gate is blocked, keep moving through later gates as skipped due to the blocker and finish with a blocked verdict.
61
- On a managed host, start solution-wide \`dotnet test Forge.sln\` evidence through \`forge.start_managed_command\` with at least a 45-minute timeout. Do not poll it. Continue independent QA work, then call \`forge.wait_managed_command\` once; Forge keeps the session healthy and streams durable progress while that call waits. Include the returned \`managed_command_id\` in command evidence. The regression gate rejects generic-shell timeouts and non-terminal receipts for that command.
62
+ On a managed host, use focused checks plus the relevant runner-authored \`forge.start_managed_verification\` profiles for normal Story QA; shard slow tiers when broader evidence is justified. Reserve the \`dotnet_solution\` profile for shared test/runtime infrastructure, Feature/PR integration, or an explicit QA-plan requirement. Use \`forge.start_managed_command\` only for other commands expected to exceed two minutes. Continue independent QA work, then call \`forge.wait_managed_command\` for the durable terminal receipt; if it returns \`command_continues=true\`, continue useful work or wait again after \`retry_after_ms\`. Do not poll \`forge.get_managed_command\`. Include the terminal \`managed_command_id\` in command evidence. Forge records live progress outside the provider transcript.
63
+ For a negative assertion, make the command itself exit 0 when the forbidden condition is absent. In particular, raw \`rg\` exit 1 means no matches but cannot be submitted as passing evidence; wrap it so no match exits 0, a match fails, and tool errors remain failures. Before every completion call, ensure passing command records have \`exit_code: 0\`, \`status: "pass"\`, and a summary. A rejected call writes no artifact; repair every issue returned by Forge and resubmit the same gate once without probing the filesystem.
62
64
  The final verdict tool writes the canonical QA bundle under \`${artifactRoot}/\`: \`qa.plan.json\`, \`qa.evidence.json\`, \`qa.findings.json\`, \`qa.verdict.json\`, and \`qa.report.md\`.
63
65
  Include \`policy_warnings\` in the final verdict for non-blocking Forge policy or scope mismatches that should remain visible after QA.
64
66
  The legacy \`forge.complete_qa\` one-shot tool is not available in normal QA-cycle mode. Reporting QA completion in prose is not enough.`;
@@ -90,7 +92,7 @@ Required order:
90
92
  5. Follow the returned dependency-graph instruction, validate keys/dependencies/order, then call \`forge.complete_decompose_dependency_graph(...)\`.
91
93
  6. Follow the returned critic instruction, critique scope, DAG safety, manual reconciliation risk, and missing acceptance detail, then call \`forge.complete_decompose_critic(...)\`.
92
94
  7. Follow the returned proposal instruction, then call \`forge.complete_decompose_proposal(...)\` with final JSON in \`proposal_json\`.
93
- The final proposal tool writes \`decompose.proposal.json\` under \`.forge/work-items/{workItemId}/artifacts/\`. The proposal_json object must contain summary, optional blockedReason, and candidates. Work Order-to-Feature candidates may include \`estimated_qa_scope\`; Feature-to-Story candidates should include \`qa_scope\` with schema_version 1 plus scheduleIndex, waveIndex, expectedOutput, handoffSummary, and dependencyReason for dependency handoff.`;
95
+ The final proposal tool writes \`decompose.proposal.json\` under \`.forge/work-items/{workItemId}/artifacts/\`. The proposal_json object must contain summary, optional blockedReason, and candidates. Work Order-to-Feature candidates may include \`estimated_qa_scope\`; Feature-to-Story candidates should include \`qa_scope\` with schema_version 1 plus scheduleIndex, waveIndex, expectedOutput, handoffSummary, and dependencyReason for dependency handoff. Set \`qa_scope.required_capabilities\` to \`["docker_engine"]\` when implementation or validation starts containers or needs a Docker daemon; repository-specific service and image versions do not belong in the runner capability name.`;
94
96
  }
95
97
  export function completionPromptForDebugCycle(currentGate = "debug_repro") {
96
98
  return `Forge completion tools are available through the MCP server named \`${toolServerName}\`.
@@ -116,13 +118,7 @@ export function nextDebugPhaseAfter(phase) {
116
118
  }
117
119
  export function completionToolNextInstruction(completedPhase, status, nextPhase) {
118
120
  if (completedPhase === "qa") {
119
- if (status === "blocked") {
120
- return "QA blocked. Forge recorded the blocker reason and will surface the run for human attention.";
121
- }
122
- if (status === "needs_rework") {
123
- return "QA findings recorded. Forge has the structured evidence needed to route this work back for implementation rework.";
124
- }
125
- return "QA accepted. Forge has the structured evidence needed to move this work item to Sign-Off.";
121
+ return qaLocalCompletionInstruction(status);
126
122
  }
127
123
  if (completedPhase === "ci_repair") {
128
124
  return status === "pass"
@@ -164,13 +160,7 @@ export function completionToolNextInstructionForDraftGate(completedGate, status,
164
160
  }
165
161
  export function completionToolNextInstructionForQaGate(completedGate, status, nextGate) {
166
162
  if (!nextGate) {
167
- if (status === "blocked") {
168
- return "QA blocked. Forge recorded the blocker reason and will surface the run for human attention.";
169
- }
170
- if (status === "needs_rework") {
171
- return "QA findings recorded. Forge has the structured evidence needed to route this work back for implementation rework.";
172
- }
173
- return "QA accepted. Forge has the structured evidence needed to move this work item to Sign-Off.";
163
+ return qaLocalCompletionInstruction(status);
174
164
  }
175
165
  const skipped = status === "skipped";
176
166
  const blocked = status === "blocked";
@@ -181,6 +171,15 @@ export function completionToolNextInstructionForQaGate(completedGate, status, ne
181
171
  : `${completedGate} accepted. Continue to ${nextGate}.`;
182
172
  return `${prefix} ${qaGateInstruction(nextGate)}`;
183
173
  }
174
+ function qaLocalCompletionInstruction(status) {
175
+ if (status === "blocked") {
176
+ return "QA blocker recorded locally. API ingestion and validation are still pending before Forge can surface the run for human attention.";
177
+ }
178
+ if (status === "needs_rework") {
179
+ return "QA findings recorded locally. API ingestion and validation are still pending before Forge can route implementation rework.";
180
+ }
181
+ return "QA bundle recorded locally. API ingestion and validation are still pending; this receipt does not authorize Sign-Off.";
182
+ }
184
183
  export function completionToolNextInstructionForDecomposeGate(completedGate, status, nextGate) {
185
184
  if (status !== "pass") {
186
185
  return `Stay in ${completedGate}, address the recorded blockers/rework, then call ${expectedDecomposeCompletionToolName(completedGate)} again.`;
@@ -217,7 +216,7 @@ export function completionToolNextInstructionForDebugGate(completedGate, status,
217
216
  function qaGateInstruction(gate) {
218
217
  switch (gate) {
219
218
  case "qa_regression":
220
- return "Run or explicitly skip applicable regression/build/lint/typecheck/e2e checks, then call forge.complete_qa_regression. On a managed host, start solution-wide dotnet test Forge.sln early with forge.start_managed_command using at least a 45-minute timeout. Do not poll it: continue independent QA checks, then call forge.wait_managed_command once and include its terminal managed_command_id in command evidence. Forge keeps the session healthy and streams durable progress while that wait call is active. A reproduced failure outside the current diff and acceptance criteria may remain non-blocking only when the current item has independent passing evidence: preserve it as an out_of_scope_failure with candidate_bug_id, scope_evidence, acceptance_criteria_independent: true, and a failing focused repro command/exit code. Never classify an unexplained, in-scope, or infrastructure failure this way.";
219
+ return "Run or explicitly skip applicable regression/build/lint/typecheck/e2e checks, then call forge.complete_qa_regression. For normal Story QA, prefer focused checks and runner-authored forge.start_managed_verification tier profiles; reserve dotnet_solution for shared infrastructure, Feature/PR integration, or an explicit plan requirement. Use forge.start_managed_command only for other commands expected to exceed two minutes, continue independent QA, then call forge.wait_managed_command and include its terminal managed_command_id in command evidence. If the wait returns command_continues=true, continue useful work or wait again after retry_after_ms; do not poll get_managed_command. Normalize negative assertions so an expected no-match exits 0. A reproduced failure outside the current diff and acceptance criteria may remain non-blocking only when the current item has independent passing evidence: preserve it as an out_of_scope_failure with candidate_bug_id, scope_evidence, acceptance_criteria_independent: true, and a failing focused repro command/exit code. Never classify an unexplained, in-scope, or infrastructure failure this way.";
221
220
  case "qa_api":
222
221
  return "Run or explicitly skip applicable API/backend contract checks, then call forge.complete_qa_api.";
223
222
  case "qa_manual_web":
@@ -4,6 +4,7 @@ export declare function prepareCompletionToolRuntime(options: CompletionToolRunt
4
4
  export declare function buildCompletionToolServerArgs(config: CompletionToolServerConfig): readonly string[];
5
5
  export declare function codexMcpConfigArgs(runtime: CompletionToolRuntime): readonly string[];
6
6
  export declare function cursorMcpServerConfig(runtime: CompletionToolRuntime): unknown;
7
+ export declare function claudeMcpServerConfig(runtime: CompletionToolRuntime): unknown;
7
8
  export declare function completionToolNamesForServer(config: CompletionToolServerConfig): readonly string[];
8
9
  export declare class CompletionToolServerSession {
9
10
  private readonly config;
@@ -27,7 +28,7 @@ export declare class CompletionToolServerSession {
27
28
  private advanceAfterQaGate;
28
29
  private advanceAfterDebugGate;
29
30
  }
30
- export declare function completionToolRuntimeEnvironment(runtimeEnvironment?: Readonly<Record<string, string>>): Readonly<Record<string, string>>;
31
+ export declare function completionToolRuntimeEnvironment(runtimeEnvironment?: Readonly<Record<string, string>>, nativeTestPostgresFallbackPath?: string | null): Readonly<Record<string, string>>;
31
32
  /**
32
33
  * A solution-wide Forge test is long enough that a generic provider shell can
33
34
  * expire while VSTest is still healthy. On managed host attempts, only the
@@ -45,3 +46,4 @@ export declare function parseCompletionMcpArgs(argv: readonly string[]): {
45
46
  readonly error: string;
46
47
  };
47
48
  export declare function completionRedactionEnv(secrets: readonly string[]): Readonly<Record<string, string>>;
49
+ export declare function completionToolRejectedResult(toolName: string, error: unknown): Readonly<Record<string, unknown>>;