@loucompanion/forge-bridge 0.1.3-dev.f2b8822ce953 → 0.1.3-dev.fbe15023cdfe

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 (143) hide show
  1. package/README.md +78 -0
  2. package/dist/bridge/bridge.base.d.ts +6 -1
  3. package/dist/bridge/bridge.service.d.ts +60 -3
  4. package/dist/bridge/bridge.service.js +970 -24
  5. package/dist/bridge/bridge.types.d.ts +172 -1
  6. package/dist/cleanup/cleanup.service.d.ts +13 -2
  7. package/dist/cleanup/cleanup.service.js +435 -3
  8. package/dist/cleanup/cleanup.types.d.ts +10 -0
  9. package/dist/cleanup/index.d.ts +1 -1
  10. package/dist/cleanup/index.js +1 -1
  11. package/dist/cleanup/internals.d.ts +2 -2
  12. package/dist/cleanup/internals.js +1 -1
  13. package/dist/cli/cli.help.js +47 -1
  14. package/dist/cli/cli.service.d.ts +42 -2
  15. package/dist/cli/cli.service.js +351 -22
  16. package/dist/cli/local-config.d.ts +6 -0
  17. package/dist/cli/local-config.js +47 -3
  18. package/dist/runtime/attempt-node-runtime-launcher.d.ts +17 -0
  19. package/dist/runtime/attempt-node-runtime-launcher.js +101 -0
  20. package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
  21. package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
  22. package/dist/runtime/attempt-process-supervisor.d.ts +86 -0
  23. package/dist/runtime/attempt-process-supervisor.js +350 -0
  24. package/dist/runtime/generated-output-reset.d.ts +10 -0
  25. package/dist/runtime/generated-output-reset.js +103 -0
  26. package/dist/runtime/host-listener-inspector.d.ts +44 -0
  27. package/dist/runtime/host-listener-inspector.js +324 -0
  28. package/dist/runtime/host-port-broker.d.ts +76 -0
  29. package/dist/runtime/host-port-broker.js +333 -0
  30. package/dist/runtime/host-runtime-contract.d.ts +47 -0
  31. package/dist/runtime/host-runtime-contract.js +700 -0
  32. package/dist/runtime/host-runtime-lease.d.ts +133 -0
  33. package/dist/runtime/host-runtime-lease.js +1460 -0
  34. package/dist/runtime/host-runtime-promotion.d.ts +23 -0
  35. package/dist/runtime/host-runtime-promotion.js +159 -0
  36. package/dist/runtime/node-host-runtime-adapter.d.ts +51 -0
  37. package/dist/runtime/node-host-runtime-adapter.js +268 -0
  38. package/dist/runtime/playwright-host-runtime-adapter.d.ts +20 -0
  39. package/dist/runtime/playwright-host-runtime-adapter.js +220 -0
  40. package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
  41. package/dist/runtime/runtime-isolation-lease.js +158 -0
  42. package/dist/sandbox/command-runner.d.ts +4 -0
  43. package/dist/sandbox/command-runner.js +47 -0
  44. package/dist/sandbox/contract.d.ts +5 -0
  45. package/dist/sandbox/contract.js +276 -0
  46. package/dist/sandbox/event-budget.d.ts +11 -0
  47. package/dist/sandbox/event-budget.js +27 -0
  48. package/dist/sandbox/image-policy.d.ts +35 -0
  49. package/dist/sandbox/image-policy.js +247 -0
  50. package/dist/sandbox/index.d.ts +5 -0
  51. package/dist/sandbox/index.js +5 -0
  52. package/dist/sandbox/lease-watchdog.d.ts +18 -0
  53. package/dist/sandbox/lease-watchdog.js +67 -0
  54. package/dist/sandbox/preflight.d.ts +54 -0
  55. package/dist/sandbox/preflight.js +335 -0
  56. package/dist/sandbox/process.d.ts +20 -0
  57. package/dist/sandbox/process.js +209 -0
  58. package/dist/sandbox/provider-auth/codex.d.ts +26 -0
  59. package/dist/sandbox/provider-auth/codex.js +212 -0
  60. package/dist/sandbox/repository-lock.d.ts +5 -0
  61. package/dist/sandbox/repository-lock.js +119 -0
  62. package/dist/sandbox/runtime.d.ts +297 -0
  63. package/dist/sandbox/runtime.js +2338 -0
  64. package/dist/sandbox/sandbox.types.d.ts +86 -0
  65. package/dist/sandbox/sandbox.types.js +1 -0
  66. package/dist/sandbox/smoke.d.ts +76 -0
  67. package/dist/sandbox/smoke.js +380 -0
  68. package/dist/sandbox/worker-supervisor.d.ts +13 -0
  69. package/dist/sandbox/worker-supervisor.js +36 -0
  70. package/dist/sandbox/worker.d.ts +1 -0
  71. package/dist/sandbox/worker.js +225 -0
  72. package/dist/sandbox/workspace.service.d.ts +13 -0
  73. package/dist/sandbox/workspace.service.js +72 -0
  74. package/dist/session/managed-attempt-tools.d.ts +12 -0
  75. package/dist/session/managed-attempt-tools.js +561 -0
  76. package/dist/session/phase-completion.artifacts.js +32 -4
  77. package/dist/session/phase-completion.d.ts +4 -2
  78. package/dist/session/phase-completion.helpers.js +27 -3
  79. package/dist/session/phase-completion.js +12 -3
  80. package/dist/session/phase-completion.prompts.d.ts +3 -3
  81. package/dist/session/phase-completion.prompts.js +20 -6
  82. package/dist/session/phase-completion.runtime.d.ts +12 -1
  83. package/dist/session/phase-completion.runtime.js +129 -7
  84. package/dist/session/phase-completion.schemas.js +132 -5
  85. package/dist/session/phase-completion.validation.d.ts +4 -1
  86. package/dist/session/phase-completion.validation.js +106 -2
  87. package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
  88. package/dist/session/provider-cli/ci-repair-readiness.js +90 -0
  89. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +1 -1
  90. package/dist/session/provider-cli/provider-cli.artifacts.js +28 -4
  91. package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
  92. package/dist/session/provider-cli/provider-cli.deterministic.d.ts +2 -0
  93. package/dist/session/provider-cli/provider-cli.deterministic.js +66 -9
  94. package/dist/session/provider-cli/provider-cli.process.d.ts +17 -0
  95. package/dist/session/provider-cli/provider-cli.process.js +223 -14
  96. package/dist/session/provider-cli/provider-cli.prompts.d.ts +17 -2
  97. package/dist/session/provider-cli/provider-cli.prompts.js +226 -18
  98. package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
  99. package/dist/session/provider-cli/provider-cli.runtime.d.ts +1 -1
  100. package/dist/session/provider-cli/provider-cli.runtime.js +3 -2
  101. package/dist/session/provider-cli/provider-cli.service.d.ts +19 -3
  102. package/dist/session/provider-cli/provider-cli.service.js +503 -98
  103. package/dist/session/provider-cli/provider-cli.types.d.ts +6 -0
  104. package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
  105. package/dist/session/provider-cli/provider-output-state.js +30 -2
  106. package/dist/session/session-redaction.d.ts +3 -0
  107. package/dist/session/session-redaction.js +23 -0
  108. package/dist/session/session.base.d.ts +6 -1
  109. package/dist/session/session.service.d.ts +8 -2
  110. package/dist/session/session.service.js +463 -58
  111. package/dist/session/session.types.d.ts +144 -1
  112. package/dist/session/session.types.js +279 -3
  113. package/dist/shared/path/path.js +3 -1
  114. package/dist/shared/process/host-process-recovery.d.ts +12 -0
  115. package/dist/shared/process/host-process-recovery.js +78 -0
  116. package/dist/shared/process/host-process-supervisor.d.ts +57 -0
  117. package/dist/shared/process/host-process-supervisor.js +282 -0
  118. package/dist/shared/process/process.d.ts +1 -0
  119. package/dist/shared/process/process.js +1 -0
  120. package/dist/shared/process/windows-launch.d.ts +1 -1
  121. package/dist/shared/process/windows-launch.js +6 -3
  122. package/dist/shared/provider-capabilities.d.ts +1 -0
  123. package/dist/shared/provider-capabilities.js +3 -0
  124. package/dist/shared/redaction/redaction.js +10 -3
  125. package/dist/version.d.ts +1 -1
  126. package/dist/version.js +1 -1
  127. package/dist/worktree/artifact-injector.d.ts +3 -2
  128. package/dist/worktree/artifact-injector.js +43 -4
  129. package/dist/worktree/dependency-warmer.d.ts +21 -1
  130. package/dist/worktree/dependency-warmer.js +138 -5
  131. package/dist/worktree/index.d.ts +1 -0
  132. package/dist/worktree/index.js +1 -0
  133. package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
  134. package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
  135. package/dist/worktree/worktree-paths.d.ts +4 -0
  136. package/dist/worktree/worktree-paths.js +30 -0
  137. package/dist/worktree/worktree.base.d.ts +3 -0
  138. package/dist/worktree/worktree.service.d.ts +7 -2
  139. package/dist/worktree/worktree.service.js +72 -44
  140. package/dist/worktree/worktree.types.d.ts +3 -1
  141. package/package.json +6 -3
  142. package/runtime/windows-job-supervisor.ps1 +356 -0
  143. package/runtime/windows-listener-owner.ps1 +33 -0
@@ -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
  }
@@ -600,9 +606,22 @@ export async function readOptionalFile(filePath) {
600
606
  }
601
607
  export async function writeFileAtomic(filePath, content) {
602
608
  await fs.mkdir(path.dirname(filePath), { recursive: true });
603
- const tempPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${process.pid}.${Date.now()}.tmp`);
609
+ const tempPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${process.pid}.${crypto.randomUUID()}.tmp`);
604
610
  await fs.writeFile(tempPath, content, "utf8");
605
- await fs.rename(tempPath, filePath);
611
+ try {
612
+ await fs.rename(tempPath, filePath);
613
+ }
614
+ catch (error) {
615
+ if (process.platform !== "win32" || !isNodeError(error) || (error.code !== "EPERM" && error.code !== "EEXIST")) {
616
+ await fs.rm(tempPath, { force: true });
617
+ throw error;
618
+ }
619
+ // Windows rename does not reliably replace an existing destination. QA-cycle gates
620
+ // intentionally refresh canonical bundle files, so remove only the exact destination
621
+ // after the replacement temp file is durable, then complete the rename.
622
+ await fs.rm(filePath, { force: true });
623
+ await fs.rename(tempPath, filePath);
624
+ }
606
625
  }
607
626
  export function stableStringify(value) {
608
627
  return JSON.stringify(sortJson(value));
@@ -626,7 +645,12 @@ export function forgeBridgeCliBinPath() {
626
645
  return fileURLToPath(new URL("../cli/bin.js", import.meta.url));
627
646
  }
628
647
  export function tomlString(value) {
629
- return JSON.stringify(value);
648
+ if (value.includes("'''")) {
649
+ throw new Error("Codex MCP configuration values cannot contain three consecutive apostrophes.");
650
+ }
651
+ // Codex config overrides pass through npm's PowerShell shim on Windows.
652
+ // Literal TOML strings survive that boundary; basic-string double quotes do not.
653
+ return `'''${value}'''`;
630
654
  }
631
655
  export function tomlStringArray(values) {
632
656
  return `[${values.map(tomlString).join(", ")}]`;
@@ -195,6 +195,10 @@ export const phaseFiles = {
195
195
  decompose: {
196
196
  jsonFileName: "decompose.proposal.json",
197
197
  reportFileName: "decompose.report.md"
198
+ },
199
+ ci_repair: {
200
+ jsonFileName: "ci-repair.handoff.json",
201
+ reportFileName: "ci-repair.report.md"
198
202
  }
199
203
  };
200
204
  export function completionPhaseFromDispatch(dispatch) {
@@ -224,6 +228,9 @@ export function normalizeCompletionPhase(value) {
224
228
  if (normalized === "decompose" || normalized === "decomposition" || normalized === "work_order_decomposition" || normalized === "feature_decomposition") {
225
229
  return "decompose";
226
230
  }
231
+ if (normalized === "ci_repair" || normalized === "pr_checks" || normalized === "repair_pr_checks") {
232
+ return "ci_repair";
233
+ }
227
234
  return "implementation";
228
235
  }
229
236
  export function usesDevelopCycleCompletion(dispatch) {
@@ -275,9 +282,11 @@ export function completionToolNamesForPhase(phase) {
275
282
  ? "complete_self_review"
276
283
  : phase === "validation"
277
284
  ? "complete_validation"
278
- : phase === "architect"
279
- ? "complete_architect_plan"
280
- : "complete_qa";
285
+ : phase === "ci_repair"
286
+ ? "complete_ci_repair"
287
+ : phase === "architect"
288
+ ? "complete_architect_plan"
289
+ : "complete_qa";
281
290
  return [`forge.${base}`, base];
282
291
  }
283
292
  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;
@@ -1,13 +1,13 @@
1
1
  import { completionPhaseOrder, completionToolNamesForPhase, debugCompletionPhaseOrder, expectedCompletionToolName, expectedDebugCompletionToolName, expectedDecomposeCompletionToolName, phaseFiles, toolServerName } from "./phase-completion.js";
2
2
  import { isDebugCompletionPhase, isDevelopCompletionPhase } from "./phase-completion.validation.js";
3
- export function completionPromptForPhase(phase) {
3
+ export function completionPromptForPhase(phase, artifactRoot = ".forge/work-items/{workItemId}/artifacts") {
4
4
  const toolName = expectedCompletionToolName(phase);
5
5
  const alias = completionToolNamesForPhase(phase).find((name) => name !== toolName);
6
6
  const artifactName = phaseFiles[phase].jsonFileName;
7
7
  if (phase === "qa") {
8
8
  return `Forge completion tools are available through the MCP server named \`${toolServerName}\`.
9
9
  Do not write QA artifacts by hand. When QA is complete, call \`${toolName}(...)\`${alias ? ` (or \`${alias}\` if your client shows unqualified MCP tool names)` : ""} with the required structured QA payload.
10
- That tool writes the durable QA bundle under \`.forge/work-items/{workItemId}/artifacts/\`: \`qa.plan.json\`, \`qa.evidence.json\`, \`qa.findings.json\`, \`${artifactName}\`, and \`qa.report.md\`.
10
+ That tool writes the durable QA bundle under \`${artifactRoot}/\`: \`qa.plan.json\`, \`qa.evidence.json\`, \`qa.findings.json\`, \`${artifactName}\`, and \`qa.report.md\`.
11
11
  Use \`status: "pass"\` only when the validated changes satisfy acceptance criteria with concrete evidence. Use \`status: "needs_rework"\` with actionable in-scope findings, or \`status: "blocked"\` with a concrete blocker_reason.
12
12
  Include \`policy_warnings\` for non-blocking Forge policy or scope mismatches that should remain visible after QA.
13
13
  Forge will mirror only the tool-written QA artifacts; reporting QA completion in prose is not enough.`;
@@ -22,6 +22,14 @@ The payload must put \`phases\`, \`tasks\`, \`dependencies\`, and \`phase_gates\
22
22
  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
23
  Use \`status: "pass"\` for a valid pending-approval plan, \`needs_revision\` when story/design input must change, or \`blocked\` with a concrete blocker.
24
24
  Forge will mirror only the tool-written Architect artifacts; reporting the plan in prose is not enough.`;
25
+ }
26
+ if (phase === "ci_repair") {
27
+ return `Forge completion tools are available through the MCP server named \`${toolServerName}\`.
28
+ Read the generated \`.forge/work-items/{workItemId}/ci-repair.readiness.json\` before running validation.
29
+ Do not write the CI-repair handoff by hand. After the repair and locally equivalent validation are complete, call \`${toolName}(...)\`${alias ? ` (or \`${alias}\` if your client shows unqualified MCP tool names)` : ""}.
30
+ That tool validates failed-check inspection, root cause, scoped changes, narrow proof, every failed-job guard/test/drift step, provider-only classifications, broader validation, remaining risk, provider state, and push readiness.
31
+ It writes \`.forge/work-items/{workItemId}/artifacts/${artifactName}\`. Forge Bridge will not commit, push, or report CI repair success until this gate passes.
32
+ For a step classified \`provider_only\`, include the exact reason and provider check that will supply the missing proof. Do not weaken or delete the provider check.`;
25
33
  }
26
34
  return `Forge completion tools are available through the MCP server named \`${toolServerName}\`.
27
35
  Do not write phase artifacts by hand. When the phase is complete, call \`${toolName}(...)\`${alias ? ` (or \`${alias}\` if your client shows unqualified MCP tool names)` : ""} with the required evidence payload.
@@ -38,10 +46,10 @@ Complete the dev cycle in order inside this same provider session:
38
46
  Each tool writes the canonical artifact under \`.forge/work-items/{workItemId}/artifacts/\` and returns the next-phase instruction Forge expects you to follow.
39
47
  If the current phase cannot pass, call the current phase tool with a non-pass status and explicit blockers instead of reporting completion in prose only.`;
40
48
  }
41
- export function completionPromptForQaCycle(currentGate = "qa_plan") {
49
+ export function completionPromptForQaCycle(currentGate = "qa_plan", artifactRoot = ".forge/work-items/{workItemId}/artifacts") {
42
50
  return `Forge completion tools are available through the MCP server named \`${toolServerName}\`.
43
51
  Do not write QA artifacts by hand. The tool server enforces QA gate order and starts this run at \`${currentGate}\`.
44
- Complete the QA cycle in order inside this same provider session:
52
+ Complete the QA cycle in order inside this same provider session. Start long, independent regression verification as early as practical, continue API/source/manual review while it runs, and wait for its receipt only when those independent checks are complete:
45
53
  1. Call \`forge.complete_qa_plan(...)\` after the QA test plan/checklist is ready.
46
54
  2. Follow the returned regression instruction, then call \`forge.complete_qa_regression(...)\`.
47
55
  3. Follow the returned API/backend instruction, then call \`forge.complete_qa_api(...)\`.
@@ -50,7 +58,8 @@ Complete the QA cycle in order inside this same provider session:
50
58
  6. Follow the returned findings instruction, then call \`forge.complete_qa_findings(...)\`.
51
59
  7. Follow the returned verdict instruction, then call \`forge.complete_qa_verdict(...)\`.
52
60
  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.
53
- The final verdict tool writes the canonical QA bundle under \`.forge/work-items/{workItemId}/artifacts/\`: \`qa.plan.json\`, \`qa.evidence.json\`, \`qa.findings.json\`, \`qa.verdict.json\`, and \`qa.report.md\`.
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
+ 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\`.
54
63
  Include \`policy_warnings\` in the final verdict for non-blocking Forge policy or scope mismatches that should remain visible after QA.
55
64
  The legacy \`forge.complete_qa\` one-shot tool is not available in normal QA-cycle mode. Reporting QA completion in prose is not enough.`;
56
65
  }
@@ -115,6 +124,11 @@ export function completionToolNextInstruction(completedPhase, status, nextPhase)
115
124
  }
116
125
  return "QA accepted. Forge has the structured evidence needed to move this work item to Sign-Off.";
117
126
  }
127
+ if (completedPhase === "ci_repair") {
128
+ return status === "pass"
129
+ ? "CI repair handoff accepted. Forge Bridge may now commit, push, and return the PR to provider checks."
130
+ : "Stay in CI repair, address the exact missing evidence returned by this gate, then call forge.complete_ci_repair again.";
131
+ }
118
132
  if (status !== "pass") {
119
133
  return `Stay in ${completedPhase}, address the recorded blockers, then call ${expectedCompletionToolName(completedPhase)} again.`;
120
134
  }
@@ -203,7 +217,7 @@ export function completionToolNextInstructionForDebugGate(completedGate, status,
203
217
  function qaGateInstruction(gate) {
204
218
  switch (gate) {
205
219
  case "qa_regression":
206
- return "Run or explicitly skip applicable regression/build/lint/typecheck/e2e checks, then call forge.complete_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.";
207
221
  case "qa_api":
208
222
  return "Run or explicitly skip applicable API/backend contract checks, then call forge.complete_qa_api.";
209
223
  case "qa_manual_web":
@@ -1,4 +1,5 @@
1
1
  import { type CompletionToolCallResult, type CompletionToolRuntime, type CompletionToolRuntimeOptions, type CompletionToolServerConfig } from "./phase-completion.js";
2
+ import { type ManagedAttemptToolResult } from "./managed-attempt-tools.js";
2
3
  export declare function prepareCompletionToolRuntime(options: CompletionToolRuntimeOptions): Promise<CompletionToolRuntime>;
3
4
  export declare function buildCompletionToolServerArgs(config: CompletionToolServerConfig): readonly string[];
4
5
  export declare function codexMcpConfigArgs(runtime: CompletionToolRuntime): readonly string[];
@@ -10,11 +11,12 @@ export declare class CompletionToolServerSession {
10
11
  private currentQaGate;
11
12
  private currentDraftGate;
12
13
  private currentDecomposeGate;
14
+ private readonly managedCommandReceipts;
13
15
  private completed;
14
16
  constructor(config: CompletionToolServerConfig);
15
17
  get instructions(): string;
16
18
  toolDefinitions(): readonly unknown[];
17
- callTool(toolName: string, input: unknown): Promise<CompletionToolCallResult>;
19
+ callTool(toolName: string, input: unknown): Promise<CompletionToolCallResult | ManagedAttemptToolResult>;
18
20
  private callDecomposeGateTool;
19
21
  private callDraftGateTool;
20
22
  private callDebugGateTool;
@@ -25,6 +27,15 @@ export declare class CompletionToolServerSession {
25
27
  private advanceAfterQaGate;
26
28
  private advanceAfterDebugGate;
27
29
  }
30
+ export declare function completionToolRuntimeEnvironment(runtimeEnvironment?: Readonly<Record<string, string>>): Readonly<Record<string, string>>;
31
+ /**
32
+ * A solution-wide Forge test is long enough that a generic provider shell can
33
+ * expire while VSTest is still healthy. On managed host attempts, only the
34
+ * attempt-owned command runner can prove the exact process outcome and clean
35
+ * up its process tree. Keep this check at the completion gate so prompt drift
36
+ * cannot turn an arbitrary shell timeout into a QA blocker.
37
+ */
38
+ export declare function assertManagedQaRegressionEvidence(input: unknown, receipts: readonly Readonly<Record<string, unknown>>[]): void;
28
39
  export declare function runCompletionMcpServer(config: CompletionToolServerConfig): Promise<void>;
29
40
  export declare function parseCompletionMcpArgs(argv: readonly string[]): {
30
41
  readonly ok: true;
@@ -1,10 +1,11 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { completionPhaseFromDispatch, completionPhaseOrder, completionRedactionSecretsEnv, completionToolNamesForDebugGate, completionToolNamesForDecomposeGate, completionToolNamesForDraftGate, completionToolNamesForPhase, completionToolNamesForQaGate, debugCompletionPhaseOrder, decomposeCompletionGateOrder, draftCompletionGateOrder, qaCompletionGateOrder, toolServerName, usesArchitectCycleCompletion, usesDebugCycleCompletion, usesDevelopCycleCompletion, usesQaCycleCompletion } from "./phase-completion.js";
4
- import { asDebugCompletionPhase, asJsonRpcMessage, asMutableRecord, asRecord, decomposeGateFromToolName, draftGateFromToolName, errorMessage, forgeBridgeCliBinPath, isDebugCompletionPhase, isDecomposeDispatch, isDevelopCompletionPhase, normalizedOwnerType, parseCompletionRedactionSecrets, parseOptionalJsonObject, parseStrictCompletionPhase, phaseFromToolName, qaGateFromToolName, requiredString, rpcError, tomlString, tomlStringArray, writeFileAtomic, writeJsonRpc } from "./phase-completion.validation.js";
4
+ import { asDebugCompletionPhase, asJsonRpcMessage, asMutableRecord, asRecord, artifactOwnerRelativeRoot, decomposeGateFromToolName, draftGateFromToolName, errorMessage, forgeBridgeCliBinPath, isDebugCompletionPhase, isDecomposeDispatch, isDevelopCompletionPhase, normalizedOwnerType, parseCompletionRedactionSecrets, parseOptionalJsonObject, parseStrictCompletionPhase, phaseFromToolName, qaGateFromToolName, requiredString, rpcError, tomlString, tomlStringArray, writeFileAtomic, writeJsonRpc } from "./phase-completion.validation.js";
5
5
  import { completionPromptForDebugCycle, completionPromptForDecomposeCycle, completionPromptForDevelopCycle, completionPromptForDraftCycle, completionPromptForPhase, completionPromptForQaCycle, completionToolNextInstruction, completionToolNextInstructionForDebugGate, completionToolNextInstructionForDecomposeGate, completionToolNextInstructionForDraftGate, completionToolNextInstructionForQaGate, nextDebugPhaseAfter, nextPhaseAfter } from "./phase-completion.prompts.js";
6
6
  import { toolDescription, toolInputSchema } from "./phase-completion.schemas.js";
7
7
  import { writeCompletionArtifact, writeDecomposeGateCompletionArtifact, writeDraftGateCompletionArtifact } from "./phase-completion.artifacts.js";
8
+ import { callManagedAttemptTool, isManagedAttemptTool, managedAttemptToolDefinition, managedAttemptToolNames, } from "./managed-attempt-tools.js";
8
9
  export async function prepareCompletionToolRuntime(options) {
9
10
  const phase = options.phase ?? completionPhaseFromDispatch(options.dispatch);
10
11
  const worktreePath = path.resolve(options.worktree.worktreePath);
@@ -32,7 +33,7 @@ export async function prepareCompletionToolRuntime(options) {
32
33
  serverName: toolServerName,
33
34
  command: process.execPath,
34
35
  args: [forgeBridgeCliBinPath(), ...mcpArgs],
35
- env: {},
36
+ env: completionToolRuntimeEnvironment(options.runtimeEnvironment),
36
37
  cleanup: async () => undefined
37
38
  };
38
39
  if (options.dispatch.execution.provider.trim().toLowerCase() !== "cursor") {
@@ -82,7 +83,11 @@ export function codexMcpConfigArgs(runtime) {
82
83
  "-c",
83
84
  `${prefix}.command=${tomlString(runtime.command)}`,
84
85
  "-c",
85
- `${prefix}.args=${tomlStringArray(runtime.args)}`
86
+ `${prefix}.args=${tomlStringArray(runtime.args)}`,
87
+ "-c",
88
+ `${prefix}.default_tools_approval_mode=${tomlString("approve")}`,
89
+ "-c",
90
+ `${prefix}.tool_timeout_sec=7200`
86
91
  ];
87
92
  for (const [key, value] of Object.entries(runtime.env)) {
88
93
  args.push("-c", `${prefix}.env.${key}=${tomlString(value)}`);
@@ -117,6 +122,7 @@ export class CompletionToolServerSession {
117
122
  currentQaGate = "qa_plan";
118
123
  currentDraftGate = "draft_intent";
119
124
  currentDecomposeGate = "decompose_subject_intake";
125
+ managedCommandReceipts = [];
120
126
  completed = false;
121
127
  constructor(config) {
122
128
  this.config = config;
@@ -130,15 +136,28 @@ export class CompletionToolServerSession {
130
136
  : this.config.debugCycle
131
137
  ? completionPromptForDebugCycle(asDebugCompletionPhase(this.currentPhase))
132
138
  : this.config.qaCycle
133
- ? completionPromptForQaCycle(this.currentQaGate)
139
+ ? completionPromptForQaCycle(this.currentQaGate, artifactOwnerRelativeRoot(this.config.ownerType, this.config.ownerId, this.config.workItemId))
134
140
  : this.config.developCycle
135
141
  ? completionPromptForDevelopCycle(this.currentPhase)
136
- : completionPromptForPhase(this.currentPhase);
142
+ : completionPromptForPhase(this.currentPhase, artifactOwnerRelativeRoot(this.config.ownerType, this.config.ownerId, this.config.workItemId));
137
143
  }
138
144
  toolDefinitions() {
139
- return completionToolNamesForServer(this.config).map((name) => toolDefinition(name, phaseFromToolName(name), qaGateFromToolName(name), decomposeGateFromToolName(name)));
145
+ const completionTools = completionToolNamesForServer(this.config).map((name) => toolDefinition(name, phaseFromToolName(name), qaGateFromToolName(name), decomposeGateFromToolName(name)));
146
+ return managedAttemptToolsAvailable()
147
+ ? [...completionTools, ...managedAttemptToolNames.map(managedAttemptToolDefinition)]
148
+ : completionTools;
140
149
  }
141
150
  async callTool(toolName, input) {
151
+ if (isManagedAttemptTool(toolName)) {
152
+ if (!managedAttemptToolsAvailable()) {
153
+ throw new Error("Forge managed-attempt tools require an active managed host runtime.");
154
+ }
155
+ const result = await callManagedAttemptTool(toolName, input, this.config.worktreePath);
156
+ if (Array.isArray(result.structuredContent.command)) {
157
+ this.managedCommandReceipts.push(result.structuredContent);
158
+ }
159
+ return result;
160
+ }
142
161
  const requestedPhase = phaseFromToolName(toolName);
143
162
  if (this.completed) {
144
163
  throw new Error("Forge completion cycle is already complete; no more completion calls are accepted.");
@@ -292,6 +311,9 @@ export class CompletionToolServerSession {
292
311
  throw new Error(`Tool ${toolName} is out of order. Current Forge QA gate is ${this.currentQaGate}.`);
293
312
  }
294
313
  const completedGate = this.currentQaGate;
314
+ if (completedGate === "qa_regression" && managedAttemptToolsAvailable()) {
315
+ assertManagedQaRegressionEvidence(input, this.managedCommandReceipts);
316
+ }
295
317
  const reference = await writeCompletionArtifact({ ...this.config, phase: "qa" }, toolName, input);
296
318
  const status = reference.status.trim().toLowerCase();
297
319
  const nextGate = this.advanceAfterQaGate(completedGate);
@@ -327,6 +349,10 @@ export class CompletionToolServerSession {
327
349
  this.completed = true;
328
350
  return "sign_off_ready";
329
351
  }
352
+ if (phase === "ci_repair") {
353
+ this.completed = true;
354
+ return "provider_checks_ready";
355
+ }
330
356
  if (isDebugCompletionPhase(phase)) {
331
357
  return nextDebugPhaseAfter(phase) ?? "qa_ready";
332
358
  }
@@ -382,6 +408,102 @@ export class CompletionToolServerSession {
382
408
  return null;
383
409
  }
384
410
  }
411
+ const completionToolRuntimeEnvironmentNames = [
412
+ "FORGE_RUNTIME_MODE",
413
+ "FORGE_RESOURCE_LEASE_ID",
414
+ "FORGE_DISPATCH_ATTEMPT_ID",
415
+ "FORGE_LOG_DIR",
416
+ "FORGE_TEST_OUTPUT_DIR",
417
+ "FORGE_TMPDIR",
418
+ "TMPDIR",
419
+ "TMP",
420
+ "TEMP",
421
+ ];
422
+ export function completionToolRuntimeEnvironment(runtimeEnvironment) {
423
+ if (!runtimeEnvironment)
424
+ return {};
425
+ const env = {};
426
+ for (const name of completionToolRuntimeEnvironmentNames) {
427
+ const value = runtimeEnvironment[name]?.trim();
428
+ if (value)
429
+ env[name] = value;
430
+ }
431
+ return env;
432
+ }
433
+ const minimumManagedSolutionTestTimeoutMs = 45 * 60 * 1000;
434
+ /**
435
+ * A solution-wide Forge test is long enough that a generic provider shell can
436
+ * expire while VSTest is still healthy. On managed host attempts, only the
437
+ * attempt-owned command runner can prove the exact process outcome and clean
438
+ * up its process tree. Keep this check at the completion gate so prompt drift
439
+ * cannot turn an arbitrary shell timeout into a QA blocker.
440
+ */
441
+ export function assertManagedQaRegressionEvidence(input, receipts) {
442
+ const payload = asRecord(input, "QA regression completion payload");
443
+ const nestedEvidence = asMutableRecord(payload.evidence);
444
+ const commands = payload.commands ?? nestedEvidence.commands;
445
+ if (!Array.isArray(commands))
446
+ return;
447
+ for (const [index, entry] of commands.entries()) {
448
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
449
+ continue;
450
+ const command = entry.command;
451
+ if (typeof command !== "string" || !isForgeSolutionTestCommand(command))
452
+ continue;
453
+ const managedCommandId = entry.managed_command_id;
454
+ if (typeof managedCommandId !== "string" || !managedCommandId.trim()) {
455
+ throw new Error(`qa_regression_managed_command_id_required: commands[${index}] runs the solution-wide Forge test. `
456
+ + "Run it with forge.start_managed_command, continue independent work, call forge.wait_managed_command once for the terminal receipt, and include the returned managed_command_id in this command evidence.");
457
+ }
458
+ const matchingReceipts = receipts.filter((receipt) => {
459
+ const tokens = receipt.command;
460
+ return receipt.command_id === managedCommandId
461
+ && Array.isArray(tokens)
462
+ && tokens.every((token) => typeof token === "string")
463
+ && isForgeSolutionTestCommand(tokens.join(" "));
464
+ });
465
+ if (matchingReceipts.length === 0) {
466
+ throw new Error(`qa_regression_managed_command_required: commands[${index}] runs the solution-wide Forge test. `
467
+ + "Run it with forge.start_managed_command, continue independent work, call forge.wait_managed_command once for the terminal receipt, and include its matching managed_command_id; generic-shell timeout evidence is not accepted on a managed host.");
468
+ }
469
+ const terminalReceipts = matchingReceipts.filter((receipt) => typeof receipt.status === "string" && [
470
+ "pass",
471
+ "fail",
472
+ "timed_out",
473
+ "timeout_termination_failed",
474
+ "runner_error",
475
+ ].includes(receipt.status));
476
+ if (terminalReceipts.length === 0) {
477
+ throw new Error(`qa_regression_managed_command_in_progress: commands[${index}] has not produced a terminal managed receipt. `
478
+ + "Call forge.wait_managed_command once for its terminal receipt, then resubmit this gate.");
479
+ }
480
+ const adequateReceipt = terminalReceipts.some((receipt) => {
481
+ if (receipt.timed_out !== true)
482
+ return true;
483
+ return typeof receipt.timeout_ms === "number"
484
+ && receipt.timeout_ms >= minimumManagedSolutionTestTimeoutMs
485
+ && receipt.process_tree_terminated === true;
486
+ });
487
+ if (!adequateReceipt) {
488
+ throw new Error(`qa_regression_managed_timeout_too_short: commands[${index}] timed out before the minimum `
489
+ + `${minimumManagedSolutionTestTimeoutMs}ms solution-test budget. Rerun it with forge.start_managed_command `
490
+ + "using at least a 45-minute timeout and resubmit this gate.");
491
+ }
492
+ }
493
+ }
494
+ function isForgeSolutionTestCommand(command) {
495
+ const normalized = command.trim().toLowerCase().replaceAll("\\", "/");
496
+ return normalized.includes("dotnet")
497
+ && /(?:^|\s)test(?:\s|$)/u.test(normalized)
498
+ && /(?:^|[\s/"'])forge\.sln(?:[\s"']|$)/u.test(normalized);
499
+ }
500
+ function managedAttemptToolsAvailable() {
501
+ return process.env.FORGE_RUNTIME_MODE === "host"
502
+ && Boolean(process.env.FORGE_RESOURCE_LEASE_ID?.trim())
503
+ && Boolean(process.env.FORGE_DISPATCH_ATTEMPT_ID?.trim())
504
+ && Boolean(process.env.FORGE_LOG_DIR?.trim())
505
+ && Boolean(process.env.FORGE_TEST_OUTPUT_DIR?.trim());
506
+ }
385
507
  export async function runCompletionMcpServer(config) {
386
508
  const session = new CompletionToolServerSession(config);
387
509
  process.stdin.setEncoding("utf8");
@@ -437,7 +559,7 @@ export function parseCompletionMcpArgs(argv) {
437
559
  const rawPhase = argv[++index];
438
560
  const parsedPhase = parseStrictCompletionPhase(rawPhase);
439
561
  if (!parsedPhase) {
440
- return { ok: false, error: "mcp --phase must be one of: implementation, self-review, validation, qa, architect, draft, debug-repro, debug-root-cause, debug-fix, debug-regression." };
562
+ return { ok: false, error: "mcp --phase must be one of: implementation, self-review, validation, qa, architect, draft, decompose, ci-repair, debug-repro, debug-root-cause, debug-fix, debug-regression." };
441
563
  }
442
564
  phase = parsedPhase;
443
565
  }
@@ -16,6 +16,9 @@ export function toolDescription(phase, qaGate = null, decomposeGate = null) {
16
16
  if (phase === "implementation") {
17
17
  return "Complete Forge implementation by writing implementation.completeness.json with scoped changed-file, verification, acceptance-criteria, gap, and next-phase evidence.";
18
18
  }
19
+ if (phase === "ci_repair") {
20
+ return "Complete Forge CI repair by writing ci-repair.handoff.json with failed-check, root-cause, scoped-change, workflow-equivalence, validation, provider-state, and push-readiness evidence.";
21
+ }
19
22
  if (phase === "self_review") {
20
23
  return "Complete Forge self-review by writing self-review.report.json with diff review, findings, security, edge-case, design, deterministic-comparison, fixes, and remaining-risk evidence.";
21
24
  }
@@ -55,6 +58,94 @@ export function toolInputSchema(phase, qaGate = null, decomposeGate = null) {
55
58
  if (isDebugCompletionPhase(phase)) {
56
59
  return debugToolInputSchema(phase, baseProperties);
57
60
  }
61
+ if (phase === "ci_repair") {
62
+ return objectToolSchema({
63
+ ...baseProperties,
64
+ failed_checks: {
65
+ type: "array",
66
+ description: "Required and non-empty for pass. Provider check/job/step evidence inspected before repair.",
67
+ items: evidenceObjectSchema(["name", "provider_state"], {
68
+ name: { type: "string" },
69
+ provider_state: { type: "string" },
70
+ workflow: { type: "string" },
71
+ job: { type: "string" },
72
+ step: { type: "string" },
73
+ details_url: { type: "string" },
74
+ evidence: { type: "string" }
75
+ })
76
+ },
77
+ root_cause: {
78
+ type: "object",
79
+ description: "Required for pass. Include classification, summary, and concrete evidence.",
80
+ additionalProperties: true,
81
+ properties: {
82
+ classification: { type: "string" },
83
+ summary: { type: "string" },
84
+ evidence: { type: "array", items: { type: "string" } }
85
+ }
86
+ },
87
+ changed_files: {
88
+ type: "array",
89
+ description: "Files changed by the repair. May be empty only when no_code_action explains the provider-only action or blocker.",
90
+ items: evidenceObjectSchema(["path", "summary"], {
91
+ path: { type: "string" },
92
+ summary: { type: "string" }
93
+ })
94
+ },
95
+ no_code_action: {
96
+ type: "object",
97
+ description: "Required when changed_files is empty. Describe the evidence-backed provider action or blocker.",
98
+ additionalProperties: true,
99
+ properties: {
100
+ action: { type: "string" },
101
+ reason: { type: "string" },
102
+ evidence: { type: "string" }
103
+ }
104
+ },
105
+ narrow_validation: {
106
+ type: "array",
107
+ description: "Required and non-empty for pass. The focused failing command or evidence-backed substitute.",
108
+ items: commandEvidenceSchema()
109
+ },
110
+ workflow_validation: {
111
+ type: "array",
112
+ description: "Required and non-empty for pass. Each failed-job guard/test/drift step with local equivalence supported, substitute, or provider_only.",
113
+ items: evidenceObjectSchema(["step", "equivalence", "status"], {
114
+ step: { type: "string" },
115
+ equivalence: { type: "string", enum: ["supported", "substitute", "provider_only"] },
116
+ command: { type: "string" },
117
+ exit_code: { type: "number" },
118
+ status: { type: "string" },
119
+ summary: { type: "string" },
120
+ reason: { type: "string" },
121
+ substitute: { type: "string" }
122
+ })
123
+ },
124
+ broader_validation: {
125
+ type: "array",
126
+ description: "Required and non-empty for pass. Normal validation covering every touched compiled/tested surface.",
127
+ items: commandEvidenceSchema()
128
+ },
129
+ remaining_risks: {
130
+ type: "array",
131
+ description: "Required for pass, even when empty.",
132
+ items: evidenceObjectSchema(["summary"], {
133
+ summary: { type: "string" },
134
+ mitigation: { type: "string" },
135
+ owner: { type: "string" }
136
+ })
137
+ },
138
+ provider_state: {
139
+ type: "string",
140
+ enum: ["ready_for_rerun", "rerun_requested", "checks_pending", "still_failing", "blocked_by_provider"],
141
+ description: "Provider state after local validation and before Forge-owned publication."
142
+ },
143
+ ready_for_push: {
144
+ type: "boolean",
145
+ description: "Required for pass and must be true. Forge Bridge performs the commit/push only after this gate passes."
146
+ }
147
+ }, ["status", "summary"], "For status pass, include failed_checks, root_cause, changed_files or no_code_action, narrow_validation, workflow_validation, broader_validation, remaining_risks, provider_state, and ready_for_push: true.");
148
+ }
58
149
  if (phase === "implementation") {
59
150
  return objectToolSchema({
60
151
  ...baseProperties,
@@ -198,7 +289,7 @@ export function toolInputSchema(phase, qaGate = null, decomposeGate = null) {
198
289
  },
199
290
  sidecars: {
200
291
  type: "array",
201
- description: "Optional QA sidecar files already written under .forge/work-items/<id>/artifacts/qa/ with path/content_hash/bytes metadata.",
292
+ description: "Optional QA sidecar files already written under the dispatch-provided QA artifacts path with path/content_hash/bytes metadata.",
202
293
  items: evidenceObjectSchema(["path", "content_hash", "bytes"], {
203
294
  path: { type: "string" },
204
295
  content_hash: { type: "string" },
@@ -411,7 +502,9 @@ function decomposeGateToolInputSchema(gate, baseProperties) {
411
502
  estimatedQaScope: qaScopeSchema("Work Order-to-Feature planning QA estimate. This is not dispatch-authoritative QA scope."),
412
503
  estimated_qa_scope: qaScopeSchema("Work Order-to-Feature planning QA estimate. Snake_case alias for estimatedQaScope."),
413
504
  qaScope: qaScopeSchema("Feature-to-Story QA scope that becomes Story-level QA scope after proposal approval."),
414
- qa_scope: qaScopeSchema("Feature-to-Story QA scope. Snake_case alias for qaScope.")
505
+ qa_scope: qaScopeSchema("Feature-to-Story QA scope. Snake_case alias for qaScope."),
506
+ designerRefCandidates: designerRefCandidatesSchema("Designer packet refs that justify this candidate. Omit to inherit all parent refs unchanged."),
507
+ designer_ref_candidates: designerRefCandidatesSchema("Snake_case alias for designerRefCandidates.")
415
508
  }
416
509
  };
417
510
  const proposalSchema = {
@@ -440,7 +533,27 @@ function decomposeGateToolInputSchema(gate, baseProperties) {
440
533
  ...proposalSchema,
441
534
  description: "Alias for proposal_json when a client cannot send proposal_json."
442
535
  }
443
- }, ["status", "summary"], "For the final Decompose proposal, include proposal_json with a candidates array. Work Order-to-Feature candidates may include estimatedQaScope/estimated_qa_scope; Feature-to-Story candidates may include qaScope/qa_scope.");
536
+ }, ["status", "summary"], "For the final Decompose proposal, include proposal_json with a candidates array. Work Order-to-Feature candidates may include estimatedQaScope/estimated_qa_scope; Feature-to-Story candidates may include qaScope/qa_scope. Both levels may include designerRefCandidates/designer_ref_candidates.");
537
+ }
538
+ function designerRefCandidatesSchema(description) {
539
+ return {
540
+ type: "array",
541
+ description,
542
+ items: {
543
+ type: "object",
544
+ additionalProperties: false,
545
+ properties: {
546
+ designerRefId: { type: "string" },
547
+ designer_ref_id: { type: "string" },
548
+ refKey: { type: "string" },
549
+ ref_key: { type: "string" },
550
+ path: { type: "string" },
551
+ anchor: { type: "string" },
552
+ reason: { type: "string" },
553
+ required: { type: "boolean" }
554
+ }
555
+ }
556
+ };
444
557
  }
445
558
  function debugToolInputSchema(phase, baseProperties) {
446
559
  if (phase === "debug_repro") {
@@ -630,9 +743,23 @@ function qaScopeSchema(description) {
630
743
  function commandEvidenceSchema() {
631
744
  return evidenceObjectSchema(["command", "exit_code", "status", "summary"], {
632
745
  command: { type: "string" },
633
- exit_code: { type: "number" },
746
+ managed_command_id: { type: "string", description: "Receipt ID returned by forge.start_managed_command and completed through one forge.wait_managed_command call for managed-host evidence." },
747
+ exit_code: { type: ["number", "null"], description: "Process exit code, or null when the managed runner timed out or could not launch the process." },
634
748
  status: { type: "string" },
635
- summary: { type: "string" }
749
+ summary: { type: "string" },
750
+ classification: {
751
+ type: "string",
752
+ enum: ["out_of_scope_failure", "baseline_failure"],
753
+ description: "Only for a verified non-blocking failure in a passing QA regression gate. Never use for an in-scope or unexplained failure."
754
+ },
755
+ candidate_bug_id: { type: "string", description: "Required for a classified non-blocking failure; links the preserved failure to candidate_bugs." },
756
+ scope_evidence: { type: "string", description: "Required proof that the failing surface is outside the current acceptance criteria and changed-file scope." },
757
+ acceptance_criteria_independent: { type: "boolean", description: "Required and true only when the current item can be judged independently of this failure." },
758
+ repro_command: { type: "string", description: "Required for out_of_scope_failure; focused command that reproduced the unrelated failure." },
759
+ repro_exit_code: { type: "number", description: "Required non-zero focused repro exit code for out_of_scope_failure." },
760
+ baseline_ref: { type: "string", description: "Required for baseline_failure; immutable ref where the same failure was observed." },
761
+ baseline_exit_code: { type: "number", description: "Required non-zero baseline command exit code for baseline_failure." },
762
+ failure_signature_match: { type: "boolean", description: "Required and true for baseline_failure after matching failing test/error signatures." }
636
763
  });
637
764
  }
638
765
  function resolvedFindingSchema() {
@@ -2,9 +2,12 @@ import { type CompletionPhase, type DebugCompletionPhase, type QaCompletionGate
2
2
  export declare function normalizeCompletionStatus(value: string, phase?: CompletionPhase): string;
3
3
  export declare function normalizeQaGateStatus(value: string): string;
4
4
  export declare function validatePayloadShape(phase: CompletionPhase, payload: Record<string, unknown>): void;
5
+ export declare function validateCiRepairPayloadShape(payload: Record<string, unknown>): void;
5
6
  export declare function validateDebugPayloadShape(phase: DebugCompletionPhase, payload: Record<string, unknown>): void;
6
7
  export declare function validateQaGatePayloadShape(gate: QaCompletionGate, payload: Record<string, unknown>, status: string): void;
7
- export declare function validateQaGatePassingCommands(commands: readonly unknown[]): void;
8
+ export declare function validateQaGatePassingCommands(commands: readonly unknown[], options?: {
9
+ readonly allowClassifiedNonBlockingFailures?: boolean;
10
+ }): void;
8
11
  export declare function validateArchitectPayloadShape(payload: Record<string, unknown>, status: string): void;
9
12
  export declare function validateArchitectTaskDevelopScope(task: Record<string, unknown>, taskId: string): void;
10
13
  export declare function validateArchitectVerificationScope(task: Record<string, unknown>, taskId: string): void;