@loucompanion/forge-bridge 0.1.3-dev.d46a8e73d03d → 0.1.3-dev.e7b62d8fdd33

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 (145) hide show
  1. package/README.md +83 -0
  2. package/dist/bridge/bridge.base.d.ts +4 -1
  3. package/dist/bridge/bridge.service.d.ts +55 -3
  4. package/dist/bridge/bridge.service.js +834 -87
  5. package/dist/bridge/bridge.types.d.ts +89 -1
  6. package/dist/bridge/runner-host-capabilities.d.ts +15 -0
  7. package/dist/bridge/runner-host-capabilities.js +93 -0
  8. package/dist/cleanup/cleanup.service.d.ts +14 -2
  9. package/dist/cleanup/cleanup.service.js +496 -3
  10. package/dist/cleanup/cleanup.types.d.ts +10 -0
  11. package/dist/cleanup/index.d.ts +1 -1
  12. package/dist/cleanup/index.js +1 -1
  13. package/dist/cleanup/internals.d.ts +2 -2
  14. package/dist/cleanup/internals.js +1 -1
  15. package/dist/cli/cli.help.js +37 -3
  16. package/dist/cli/cli.service.d.ts +33 -2
  17. package/dist/cli/cli.service.js +209 -27
  18. package/dist/cli/local-config.d.ts +5 -0
  19. package/dist/cli/local-config.js +45 -1
  20. package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
  21. package/dist/runtime/aspnet-runtime-prebuild.js +340 -0
  22. package/dist/runtime/attempt-node-runtime-launcher.d.ts +17 -0
  23. package/dist/runtime/attempt-node-runtime-launcher.js +105 -0
  24. package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
  25. package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
  26. package/dist/runtime/attempt-process-supervisor.d.ts +97 -0
  27. package/dist/runtime/attempt-process-supervisor.js +386 -0
  28. package/dist/runtime/docker-engine-probe.d.ts +83 -0
  29. package/dist/runtime/docker-engine-probe.js +300 -0
  30. package/dist/runtime/generated-output-reset.d.ts +10 -0
  31. package/dist/runtime/generated-output-reset.js +103 -0
  32. package/dist/runtime/host-listener-inspector.d.ts +46 -0
  33. package/dist/runtime/host-listener-inspector.js +349 -0
  34. package/dist/runtime/host-port-broker.d.ts +76 -0
  35. package/dist/runtime/host-port-broker.js +333 -0
  36. package/dist/runtime/host-runtime-contract.d.ts +47 -0
  37. package/dist/runtime/host-runtime-contract.js +700 -0
  38. package/dist/runtime/host-runtime-lease.d.ts +197 -0
  39. package/dist/runtime/host-runtime-lease.js +2203 -0
  40. package/dist/runtime/host-runtime-promotion.d.ts +23 -0
  41. package/dist/runtime/host-runtime-promotion.js +159 -0
  42. package/dist/runtime/memory-capacity.d.ts +68 -0
  43. package/dist/runtime/memory-capacity.js +121 -0
  44. package/dist/runtime/node-host-runtime-adapter.d.ts +54 -0
  45. package/dist/runtime/node-host-runtime-adapter.js +284 -0
  46. package/dist/runtime/playwright-host-runtime-adapter.d.ts +21 -0
  47. package/dist/runtime/playwright-host-runtime-adapter.js +231 -0
  48. package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
  49. package/dist/runtime/runtime-isolation-lease.js +158 -0
  50. package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
  51. package/dist/runtime/workspace-resource-estimator.js +174 -0
  52. package/dist/sandbox/preflight.d.ts +45 -1
  53. package/dist/sandbox/preflight.js +299 -34
  54. package/dist/sandbox/process.js +125 -40
  55. package/dist/sandbox/repository-lock.js +41 -3
  56. package/dist/sandbox/runtime.d.ts +66 -3
  57. package/dist/sandbox/runtime.js +670 -105
  58. package/dist/sandbox/sandbox.types.d.ts +10 -0
  59. package/dist/sandbox/smoke.d.ts +76 -0
  60. package/dist/sandbox/smoke.js +380 -0
  61. package/dist/sandbox/worker-supervisor.js +1 -1
  62. package/dist/sandbox/worker.js +2 -5
  63. package/dist/sandbox/workspace.service.d.ts +1 -0
  64. package/dist/sandbox/workspace.service.js +28 -5
  65. package/dist/session/managed-attempt-tools.d.ts +21 -0
  66. package/dist/session/managed-attempt-tools.js +912 -0
  67. package/dist/session/managed-build-coordinator.d.ts +5 -0
  68. package/dist/session/managed-build-coordinator.js +105 -0
  69. package/dist/session/managed-build-receipt.d.ts +21 -0
  70. package/dist/session/managed-build-receipt.js +335 -0
  71. package/dist/session/phase-completion.artifacts.d.ts +17 -0
  72. package/dist/session/phase-completion.artifacts.js +233 -24
  73. package/dist/session/phase-completion.d.ts +12 -2
  74. package/dist/session/phase-completion.helpers.d.ts +5 -0
  75. package/dist/session/phase-completion.helpers.js +51 -10
  76. package/dist/session/phase-completion.js +14 -3
  77. package/dist/session/phase-completion.prompts.d.ts +3 -3
  78. package/dist/session/phase-completion.prompts.js +24 -8
  79. package/dist/session/phase-completion.runtime.d.ts +13 -1
  80. package/dist/session/phase-completion.runtime.js +189 -8
  81. package/dist/session/phase-completion.schemas.js +117 -4
  82. package/dist/session/phase-completion.validation.d.ts +4 -1
  83. package/dist/session/phase-completion.validation.js +153 -22
  84. package/dist/session/pre-publication-validation.d.ts +46 -0
  85. package/dist/session/pre-publication-validation.js +208 -0
  86. package/dist/session/provider-cli/ci-repair-readiness.d.ts +16 -0
  87. package/dist/session/provider-cli/ci-repair-readiness.js +110 -0
  88. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +3 -2
  89. package/dist/session/provider-cli/provider-cli.artifacts.js +65 -7
  90. package/dist/session/provider-cli/provider-cli.deterministic.js +49 -1
  91. package/dist/session/provider-cli/provider-cli.process.d.ts +25 -0
  92. package/dist/session/provider-cli/provider-cli.process.js +463 -34
  93. package/dist/session/provider-cli/provider-cli.prompts.d.ts +4 -1
  94. package/dist/session/provider-cli/provider-cli.prompts.js +108 -25
  95. package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
  96. package/dist/session/provider-cli/provider-cli.repair-epoch.d.ts +6 -0
  97. package/dist/session/provider-cli/provider-cli.repair-epoch.js +11 -0
  98. package/dist/session/provider-cli/provider-cli.runtime.d.ts +1 -1
  99. package/dist/session/provider-cli/provider-cli.runtime.js +6 -3
  100. package/dist/session/provider-cli/provider-cli.service.d.ts +22 -2
  101. package/dist/session/provider-cli/provider-cli.service.js +524 -100
  102. package/dist/session/provider-cli/provider-cli.types.d.ts +5 -0
  103. package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
  104. package/dist/session/provider-cli/provider-output-state.js +91 -16
  105. package/dist/session/provider-cli/provider-usage.d.ts +7 -0
  106. package/dist/session/provider-cli/provider-usage.js +28 -5
  107. package/dist/session/session-redaction.d.ts +3 -0
  108. package/dist/session/session-redaction.js +38 -0
  109. package/dist/session/session.base.d.ts +6 -1
  110. package/dist/session/session.service.d.ts +8 -2
  111. package/dist/session/session.service.js +522 -41
  112. package/dist/session/session.types.d.ts +72 -0
  113. package/dist/session/session.types.js +74 -3
  114. package/dist/shared/format/format-bytes.d.ts +1 -0
  115. package/dist/shared/format/format-bytes.js +9 -0
  116. package/dist/shared/path/path.d.ts +1 -0
  117. package/dist/shared/path/path.js +21 -1
  118. package/dist/shared/process/dotnet-toolchain.d.ts +14 -0
  119. package/dist/shared/process/dotnet-toolchain.js +179 -0
  120. package/dist/shared/process/host-process-recovery.d.ts +12 -0
  121. package/dist/shared/process/host-process-recovery.js +78 -0
  122. package/dist/shared/process/host-process-supervisor.d.ts +59 -0
  123. package/dist/shared/process/host-process-supervisor.js +330 -0
  124. package/dist/shared/process/process.d.ts +1 -0
  125. package/dist/shared/process/process.js +1 -0
  126. package/dist/shared/redaction/redaction.js +12 -2
  127. package/dist/version.d.ts +1 -1
  128. package/dist/version.js +1 -1
  129. package/dist/worktree/artifact-injector.d.ts +3 -2
  130. package/dist/worktree/artifact-injector.js +43 -4
  131. package/dist/worktree/dependency-warmer.d.ts +28 -1
  132. package/dist/worktree/dependency-warmer.js +308 -9
  133. package/dist/worktree/index.d.ts +2 -1
  134. package/dist/worktree/index.js +1 -0
  135. package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
  136. package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
  137. package/dist/worktree/worktree-paths.d.ts +4 -0
  138. package/dist/worktree/worktree-paths.js +30 -0
  139. package/dist/worktree/worktree.base.d.ts +3 -0
  140. package/dist/worktree/worktree.service.d.ts +13 -2
  141. package/dist/worktree/worktree.service.js +238 -47
  142. package/dist/worktree/worktree.types.d.ts +5 -1
  143. package/package.json +5 -3
  144. package/runtime/windows-job-supervisor.ps1 +444 -0
  145. package/runtime/windows-listener-owner.ps1 +33 -0
@@ -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,
@@ -237,12 +255,29 @@ export async function writeQaGateCompletionArtifact(config, gate, input) {
237
255
  const status = normalizeQaGateStatus(requiredString(payload, "status"));
238
256
  validateQaGatePayloadShape(gate, payload, status);
239
257
  validateOptionalWorkItemId(payload, config.workItemId);
240
- const redactedPayload = redactSecrets({ ...payload, status }, config.redactionSecrets ?? []);
258
+ const ownerFields = artifactOwnerFields(config);
259
+ const validatedSidecars = gate === "qa_manual_web"
260
+ ? await validateQaSidecarReferences(config.worktreePath, config.workItemId, qaArray(payload, "sidecars", qaEvidenceSource(payload)), ownerFields.owner_type, ownerFields.owner_id)
261
+ : null;
262
+ const redactedPayload = redactSecrets({
263
+ ...payload,
264
+ status,
265
+ ...(validatedSidecars ? { sidecars: validatedSidecars } : {})
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
+ }
241
277
  const producedBy = optionalString(redactedPayload, "produced_by")
242
278
  ?? optionalString(redactedPayload, "producedBy")
243
279
  ?? config.producedBy
244
280
  ?? "forge-completion-tool";
245
- const ownerFields = artifactOwnerFields(config);
246
281
  const artifactRoot = ownerFields.owner_type && ownerFields.owner_id
247
282
  ? artifactRootForOwner(config.worktreePath, ownerFields.owner_type, ownerFields.owner_id)
248
283
  : artifactRootFor(config.worktreePath, config.workItemId);
@@ -262,7 +297,6 @@ export async function writeQaGateCompletionArtifact(config, gate, input) {
262
297
  ...artifactWithoutHash,
263
298
  content_hash: contentHash
264
299
  };
265
- await fs.mkdir(artifactRoot, { recursive: true });
266
300
  await writeFileAtomic(artifactPath, jsonText(artifact));
267
301
  return {
268
302
  phase: "qa",
@@ -371,12 +405,13 @@ export async function writeQaCompletionArtifact(config, payload, status, produce
371
405
  content_hash: contentHash
372
406
  });
373
407
  const finalReport = reportMarkdown(payload, "qa", status, producedBy, contentHash);
374
- await fs.mkdir(artifactRoot, { recursive: true });
375
- await writeFileAtomic(planPath, planJson);
376
- await writeFileAtomic(evidencePath, evidenceJson);
377
- await writeFileAtomic(findingsPath, findingsJson);
378
- await writeFileAtomic(verdictPath, verdictJson);
379
- 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));
380
415
  return {
381
416
  phase: "qa",
382
417
  workItemId: config.workItemId,
@@ -546,6 +581,7 @@ async function validateQaSidecarReferences(worktreePath, workItemId, sidecars, o
546
581
  : artifactRootFor(worktreePath, workItemId);
547
582
  const qaSidecarRoot = path.resolve(artifactRoot, "qa");
548
583
  const validated = [];
584
+ const validatedByPath = new Map();
549
585
  for (const [index, sidecar] of sidecars.entries()) {
550
586
  if (!sidecar || typeof sidecar !== "object" || Array.isArray(sidecar)) {
551
587
  throw new Error(`sidecars[${index}] must be an object.`);
@@ -581,9 +617,19 @@ async function validateQaSidecarReferences(worktreePath, workItemId, sidecars, o
581
617
  if (actualHash !== normalizeSha256ContentHash(contentHash)) {
582
618
  throw new Error(`sidecars[${index}].content_hash does not match the referenced file.`);
583
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 });
584
630
  validated.push({
585
631
  ...record,
586
- path: path.relative(worktreePath, fullPath).split(path.sep).join("/"),
632
+ path: canonicalPath,
587
633
  content_hash: actualHash,
588
634
  bytes: stat.size
589
635
  });
@@ -618,3 +664,166 @@ export async function readCompletionArtifact(worktreePath, workItemId, phase, ow
618
664
  phase
619
665
  };
620
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
+ }
@@ -9,7 +9,8 @@ export type DebugCompletionPhase = "debug_repro" | "debug_root_cause" | "debug_f
9
9
  export type ArchitectCompletionPhase = "architect";
10
10
  export type DraftCompletionPhase = "draft";
11
11
  export type DecomposeCompletionPhase = "decompose";
12
- export type CompletionPhase = DevelopCompletionPhase | "qa" | DebugCompletionPhase | ArchitectCompletionPhase | DraftCompletionPhase | DecomposeCompletionPhase;
12
+ export type CiRepairCompletionPhase = "ci_repair";
13
+ export type CompletionPhase = DevelopCompletionPhase | "qa" | DebugCompletionPhase | ArchitectCompletionPhase | DraftCompletionPhase | DecomposeCompletionPhase | CiRepairCompletionPhase;
13
14
  export type QaCompletionGate = "qa_plan" | "qa_regression" | "qa_api" | "qa_manual_web" | "qa_bug_repro" | "qa_findings" | "qa_verdict";
14
15
  export type DraftCompletionGate = "draft_intent" | "draft_repo_context" | "draft_graph_context" | "draft_product_scope" | "draft_work_order_plan" | "draft_critic" | "draft_proposal";
15
16
  export type DecomposeCompletionGate = "decompose_subject_intake" | "decompose_context_review" | "decompose_breakdown_strategy" | "decompose_child_items" | "decompose_dependency_graph" | "decompose_critic" | "decompose_proposal";
@@ -25,6 +26,7 @@ export interface CompletionToolRuntimeOptions {
25
26
  readonly dispatch: RunOnceDispatchPacket;
26
27
  readonly worktree: WorktreeHandle;
27
28
  readonly phase?: CompletionPhase;
29
+ readonly runtimeEnvironment?: Readonly<Record<string, string>>;
28
30
  }
29
31
  export interface CompletionToolServerConfig {
30
32
  readonly worktreePath: string;
@@ -47,6 +49,13 @@ export interface CompletionToolCallResult {
47
49
  readonly text: string;
48
50
  }[];
49
51
  readonly structuredContent: CompletionArtifactReference & {
52
+ readonly artifact_written: true;
53
+ readonly artifact_receipt: {
54
+ readonly schema_version: 1;
55
+ readonly path: string;
56
+ readonly report_path: string;
57
+ readonly content_hash: string;
58
+ };
50
59
  readonly gate: {
51
60
  readonly phase: CompletionPhase;
52
61
  readonly qaGate?: QaCompletionGate;
@@ -56,7 +65,7 @@ export interface CompletionToolCallResult {
56
65
  readonly allowed: boolean;
57
66
  readonly reason: string;
58
67
  };
59
- readonly nextPhase: CompletionPhase | "dev_test_ready" | "qa_ready" | "sign_off_ready";
68
+ readonly nextPhase: CompletionPhase | "dev_test_ready" | "qa_ready" | "sign_off_ready" | "provider_checks_ready";
60
69
  readonly nextInstruction: string;
61
70
  };
62
71
  }
@@ -98,6 +107,7 @@ export declare const qaFindingScopes: Set<string>;
98
107
  export declare const qaCandidateBugActions: Set<string>;
99
108
  export declare const qaScopeSurfaces: Set<string>;
100
109
  export declare const qaScopeEvidence: Set<string>;
110
+ export declare const qaScopeCapabilities: Set<string>;
101
111
  export declare const qaScopeSources: Set<string>;
102
112
  export declare const qaScopeAuthorities: Set<string>;
103
113
  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;
@@ -85,6 +85,9 @@ export function parseStrictCompletionPhase(value) {
85
85
  if (normalized === "decompose" || normalized === "decomposition" || normalized === "work_order_decomposition" || normalized === "feature_decomposition") {
86
86
  return "decompose";
87
87
  }
88
+ if (normalized === "ci_repair" || normalized === "pr_checks" || normalized === "repair_pr_checks") {
89
+ return "ci_repair";
90
+ }
88
91
  return null;
89
92
  }
90
93
  export function normalizeDebugCompletionPhase(value) {
@@ -156,6 +159,9 @@ export function phaseFromToolName(toolName) {
156
159
  if (normalized === "forge.complete_architect_plan" || normalized === "complete_architect_plan") {
157
160
  return "architect";
158
161
  }
162
+ if (normalized === "forge.complete_ci_repair" || normalized === "complete_ci_repair") {
163
+ return "ci_repair";
164
+ }
159
165
  if (draftGateFromToolName(toolName)) {
160
166
  return "draft";
161
167
  }
@@ -256,28 +262,45 @@ export function requirePassingCommands(payload) {
256
262
  if (commands.length === 0) {
257
263
  throw new Error("commands must include at least one deterministic validation command.");
258
264
  }
265
+ const issues = [];
259
266
  for (const [index, command] of commands.entries()) {
260
267
  if (!command || typeof command !== "object" || Array.isArray(command)) {
261
- throw new Error(`commands[${index}] must be an object.`);
268
+ issues.push(`commands[${index}] must be an object.`);
269
+ continue;
262
270
  }
263
271
  const record = command;
264
272
  if (typeof record.command !== "string" || record.command.trim().length === 0) {
265
- throw new Error(`commands[${index}].command must be a non-empty string.`);
273
+ issues.push(`commands[${index}].command must be a non-empty string.`);
266
274
  }
267
275
  if (typeof record.summary !== "string" || record.summary.trim().length === 0) {
268
- throw new Error(`commands[${index}].summary must be a non-empty string.`);
276
+ issues.push(`commands[${index}].summary must be a non-empty string.`);
269
277
  }
270
278
  if (typeof record.exit_code !== "number" || !Number.isFinite(record.exit_code)) {
271
- throw new Error(`commands[${index}].exit_code must be a finite number.`);
279
+ issues.push(`commands[${index}].exit_code must be a finite number.`);
272
280
  }
273
- if (record.exit_code !== 0) {
274
- throw new Error(`commands[${index}].exit_code must be 0 when status is pass.`);
281
+ else if (record.exit_code !== 0) {
282
+ issues.push(`commands[${index}].exit_code must be 0 when status is pass.`);
275
283
  }
276
284
  const status = typeof record.status === "string" ? record.status.trim().toLowerCase() : "";
277
285
  if (status !== "pass") {
278
- throw new Error(`commands[${index}].status must be pass.`);
286
+ issues.push(`commands[${index}].status must be pass.`);
279
287
  }
280
288
  }
289
+ throwCompletionPayloadIssues(issues);
290
+ }
291
+ export class CompletionPayloadValidationError extends Error {
292
+ issues;
293
+ constructor(issues) {
294
+ super(`completion_payload_invalid: ${issues.length} correction${issues.length === 1 ? "" : "s"} required; no artifact was written.\n`
295
+ + issues.map(issue => `- ${issue}`).join("\n")
296
+ + "\nRepair every listed field and resubmit this gate once.");
297
+ this.issues = issues;
298
+ this.name = "CompletionPayloadValidationError";
299
+ }
300
+ }
301
+ export function throwCompletionPayloadIssues(issues) {
302
+ if (issues.length > 0)
303
+ throw new CompletionPayloadValidationError(issues);
281
304
  }
282
305
  export function requireArray(payload, key) {
283
306
  if (!Array.isArray(payload[key])) {
@@ -600,9 +623,22 @@ export async function readOptionalFile(filePath) {
600
623
  }
601
624
  export async function writeFileAtomic(filePath, content) {
602
625
  await fs.mkdir(path.dirname(filePath), { recursive: true });
603
- const tempPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${process.pid}.${Date.now()}.tmp`);
626
+ const tempPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${process.pid}.${crypto.randomUUID()}.tmp`);
604
627
  await fs.writeFile(tempPath, content, "utf8");
605
- await fs.rename(tempPath, filePath);
628
+ try {
629
+ await fs.rename(tempPath, filePath);
630
+ }
631
+ catch (error) {
632
+ if (process.platform !== "win32" || !isNodeError(error) || (error.code !== "EPERM" && error.code !== "EEXIST")) {
633
+ await fs.rm(tempPath, { force: true });
634
+ throw error;
635
+ }
636
+ // Windows rename does not reliably replace an existing destination. QA-cycle gates
637
+ // intentionally refresh canonical bundle files, so remove only the exact destination
638
+ // after the replacement temp file is durable, then complete the rename.
639
+ await fs.rm(filePath, { force: true });
640
+ await fs.rename(tempPath, filePath);
641
+ }
606
642
  }
607
643
  export function stableStringify(value) {
608
644
  return JSON.stringify(sortJson(value));
@@ -626,7 +662,12 @@ export function forgeBridgeCliBinPath() {
626
662
  return fileURLToPath(new URL("../cli/bin.js", import.meta.url));
627
663
  }
628
664
  export function tomlString(value) {
629
- return JSON.stringify(value);
665
+ if (value.includes("'''")) {
666
+ throw new Error("Codex MCP configuration values cannot contain three consecutive apostrophes.");
667
+ }
668
+ // Codex config overrides pass through npm's PowerShell shim on Windows.
669
+ // Literal TOML strings survive that boundary; basic-string double quotes do not.
670
+ return `'''${value}'''`;
630
671
  }
631
672
  export function tomlStringArray(values) {
632
673
  return `[${values.map(tomlString).join(", ")}]`;
@@ -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",
@@ -195,6 +197,10 @@ export const phaseFiles = {
195
197
  decompose: {
196
198
  jsonFileName: "decompose.proposal.json",
197
199
  reportFileName: "decompose.report.md"
200
+ },
201
+ ci_repair: {
202
+ jsonFileName: "ci-repair.handoff.json",
203
+ reportFileName: "ci-repair.report.md"
198
204
  }
199
205
  };
200
206
  export function completionPhaseFromDispatch(dispatch) {
@@ -224,6 +230,9 @@ export function normalizeCompletionPhase(value) {
224
230
  if (normalized === "decompose" || normalized === "decomposition" || normalized === "work_order_decomposition" || normalized === "feature_decomposition") {
225
231
  return "decompose";
226
232
  }
233
+ if (normalized === "ci_repair" || normalized === "pr_checks" || normalized === "repair_pr_checks") {
234
+ return "ci_repair";
235
+ }
227
236
  return "implementation";
228
237
  }
229
238
  export function usesDevelopCycleCompletion(dispatch) {
@@ -275,9 +284,11 @@ export function completionToolNamesForPhase(phase) {
275
284
  ? "complete_self_review"
276
285
  : phase === "validation"
277
286
  ? "complete_validation"
278
- : phase === "architect"
279
- ? "complete_architect_plan"
280
- : "complete_qa";
287
+ : phase === "ci_repair"
288
+ ? "complete_ci_repair"
289
+ : phase === "architect"
290
+ ? "complete_architect_plan"
291
+ : "complete_qa";
281
292
  return [`forge.${base}`, base];
282
293
  }
283
294
  export function completionToolNamesForQaGate(gate) {
@@ -1,13 +1,13 @@
1
1
  import { type CompletionPhase, type DebugCompletionPhase, type DecomposeCompletionGate, type DevelopCompletionPhase, type DraftCompletionGate, type QaCompletionGate } from "./phase-completion.js";
2
- export declare function completionPromptForPhase(phase: CompletionPhase): string;
2
+ export declare function completionPromptForPhase(phase: CompletionPhase, artifactRoot?: string): string;
3
3
  export declare function completionPromptForDevelopCycle(phase: CompletionPhase): string;
4
- export declare function completionPromptForQaCycle(currentGate?: QaCompletionGate): string;
4
+ export declare function completionPromptForQaCycle(currentGate?: QaCompletionGate, artifactRoot?: string): string;
5
5
  export declare function completionPromptForDraftCycle(currentGate?: DraftCompletionGate): string;
6
6
  export declare function completionPromptForDecomposeCycle(currentGate?: DecomposeCompletionGate): string;
7
7
  export declare function completionPromptForDebugCycle(currentGate?: DebugCompletionPhase): string;
8
8
  export declare function nextPhaseAfter(phase: CompletionPhase): DevelopCompletionPhase | null;
9
9
  export declare function nextDebugPhaseAfter(phase: DebugCompletionPhase): DebugCompletionPhase | null;
10
- export declare function completionToolNextInstruction(completedPhase: CompletionPhase, status: string, nextPhase: CompletionPhase | "dev_test_ready" | "qa_ready" | "sign_off_ready"): string;
10
+ export declare function completionToolNextInstruction(completedPhase: CompletionPhase, status: string, nextPhase: CompletionPhase | "dev_test_ready" | "qa_ready" | "sign_off_ready" | "provider_checks_ready"): string;
11
11
  export declare function completionToolNextInstructionForDraftGate(completedGate: DraftCompletionGate, status: string, nextGate: DraftCompletionGate | null): string;
12
12
  export declare function completionToolNextInstructionForQaGate(completedGate: QaCompletionGate, status: string, nextGate: QaCompletionGate | null): string;
13
13
  export declare function completionToolNextInstructionForDecomposeGate(completedGate: DecomposeCompletionGate, status: string, nextGate: DecomposeCompletionGate | null): string;