@loucompanion/forge-bridge 0.1.3-dev.f66395c3ac21 → 0.1.4-dev.bfa11ebe98c6

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 (219) hide show
  1. package/README.md +165 -1
  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 +9 -1
  5. package/dist/bridge/bridge.service.d.ts +129 -3
  6. package/dist/bridge/bridge.service.js +2426 -70
  7. package/dist/bridge/bridge.types.d.ts +218 -2
  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 +14 -2
  21. package/dist/cleanup/cleanup.service.js +496 -3
  22. package/dist/cleanup/cleanup.types.d.ts +10 -0
  23. package/dist/cleanup/index.d.ts +1 -1
  24. package/dist/cleanup/index.js +1 -1
  25. package/dist/cleanup/internals.d.ts +2 -2
  26. package/dist/cleanup/internals.js +1 -1
  27. package/dist/cli/cli.help.d.ts +2 -0
  28. package/dist/cli/cli.help.js +217 -0
  29. package/dist/cli/cli.service.d.ts +55 -7
  30. package/dist/cli/cli.service.js +429 -219
  31. package/dist/cli/local-config.d.ts +7 -0
  32. package/dist/cli/local-config.js +51 -3
  33. package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
  34. package/dist/runtime/aspnet-runtime-prebuild.js +341 -0
  35. package/dist/runtime/attempt-node-runtime-launcher.d.ts +18 -0
  36. package/dist/runtime/attempt-node-runtime-launcher.js +140 -0
  37. package/dist/runtime/attempt-path-operation.d.ts +21 -0
  38. package/dist/runtime/attempt-path-operation.js +94 -0
  39. package/dist/runtime/attempt-private-directory.d.ts +6 -0
  40. package/dist/runtime/attempt-private-directory.js +54 -0
  41. package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
  42. package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
  43. package/dist/runtime/attempt-process-supervisor.d.ts +97 -0
  44. package/dist/runtime/attempt-process-supervisor.js +397 -0
  45. package/dist/runtime/canonical-source-file.d.ts +3 -0
  46. package/dist/runtime/canonical-source-file.js +54 -0
  47. package/dist/runtime/docker-engine-probe.d.ts +90 -0
  48. package/dist/runtime/docker-engine-probe.js +333 -0
  49. package/dist/runtime/generated-output-reset.d.ts +20 -0
  50. package/dist/runtime/generated-output-reset.js +157 -0
  51. package/dist/runtime/host-database-capability-boundary.d.ts +16 -0
  52. package/dist/runtime/host-database-capability-boundary.js +62 -0
  53. package/dist/runtime/host-diagnostics.d.ts +11 -0
  54. package/dist/runtime/host-diagnostics.js +191 -0
  55. package/dist/runtime/host-execution-root.d.ts +4 -0
  56. package/dist/runtime/host-execution-root.js +39 -0
  57. package/dist/runtime/host-listener-inspector.d.ts +51 -0
  58. package/dist/runtime/host-listener-inspector.js +385 -0
  59. package/dist/runtime/host-port-broker.d.ts +76 -0
  60. package/dist/runtime/host-port-broker.js +333 -0
  61. package/dist/runtime/host-runtime-contract.d.ts +58 -0
  62. package/dist/runtime/host-runtime-contract.js +754 -0
  63. package/dist/runtime/host-runtime-lease.d.ts +215 -0
  64. package/dist/runtime/host-runtime-lease.js +2449 -0
  65. package/dist/runtime/host-runtime-promotion.d.ts +32 -0
  66. package/dist/runtime/host-runtime-promotion.js +206 -0
  67. package/dist/runtime/memory-capacity.d.ts +70 -0
  68. package/dist/runtime/memory-capacity.js +123 -0
  69. package/dist/runtime/native-test-postgres-fallback.d.ts +25 -0
  70. package/dist/runtime/native-test-postgres-fallback.js +91 -0
  71. package/dist/runtime/node-host-runtime-adapter.d.ts +54 -0
  72. package/dist/runtime/node-host-runtime-adapter.js +285 -0
  73. package/dist/runtime/pinned-directory-process.d.ts +9 -0
  74. package/dist/runtime/pinned-directory-process.js +61 -0
  75. package/dist/runtime/pinned-source-directory.d.ts +17 -0
  76. package/dist/runtime/pinned-source-directory.js +94 -0
  77. package/dist/runtime/playwright-host-runtime-adapter.d.ts +21 -0
  78. package/dist/runtime/playwright-host-runtime-adapter.js +268 -0
  79. package/dist/runtime/prepared-runtime-identity.d.ts +6 -0
  80. package/dist/runtime/prepared-runtime-identity.js +218 -0
  81. package/dist/runtime/prepared-runtime-readiness.d.ts +30 -0
  82. package/dist/runtime/prepared-runtime-readiness.js +285 -0
  83. package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
  84. package/dist/runtime/runtime-isolation-lease.js +161 -0
  85. package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
  86. package/dist/runtime/workspace-resource-estimator.js +174 -0
  87. package/dist/sandbox/command-runner.d.ts +4 -0
  88. package/dist/sandbox/command-runner.js +47 -0
  89. package/dist/sandbox/contract.d.ts +5 -0
  90. package/dist/sandbox/contract.js +276 -0
  91. package/dist/sandbox/event-budget.d.ts +11 -0
  92. package/dist/sandbox/event-budget.js +27 -0
  93. package/dist/sandbox/image-policy.d.ts +35 -0
  94. package/dist/sandbox/image-policy.js +247 -0
  95. package/dist/sandbox/index.d.ts +5 -0
  96. package/dist/sandbox/index.js +5 -0
  97. package/dist/sandbox/lease-watchdog.d.ts +18 -0
  98. package/dist/sandbox/lease-watchdog.js +67 -0
  99. package/dist/sandbox/preflight.d.ts +84 -0
  100. package/dist/sandbox/preflight.js +521 -0
  101. package/dist/sandbox/process.d.ts +20 -0
  102. package/dist/sandbox/process.js +209 -0
  103. package/dist/sandbox/provider-auth/codex.d.ts +26 -0
  104. package/dist/sandbox/provider-auth/codex.js +212 -0
  105. package/dist/sandbox/repository-lock.d.ts +6 -0
  106. package/dist/sandbox/repository-lock.js +128 -0
  107. package/dist/sandbox/runtime.d.ts +302 -0
  108. package/dist/sandbox/runtime.js +2367 -0
  109. package/dist/sandbox/sandbox.types.d.ts +93 -0
  110. package/dist/sandbox/sandbox.types.js +1 -0
  111. package/dist/sandbox/smoke.d.ts +76 -0
  112. package/dist/sandbox/smoke.js +380 -0
  113. package/dist/sandbox/worker-supervisor.d.ts +13 -0
  114. package/dist/sandbox/worker-supervisor.js +36 -0
  115. package/dist/sandbox/worker.d.ts +1 -0
  116. package/dist/sandbox/worker.js +225 -0
  117. package/dist/sandbox/workspace.service.d.ts +13 -0
  118. package/dist/sandbox/workspace.service.js +74 -0
  119. package/dist/session/managed-attempt-tools.d.ts +28 -0
  120. package/dist/session/managed-attempt-tools.js +1011 -0
  121. package/dist/session/managed-build-coordinator.d.ts +5 -0
  122. package/dist/session/managed-build-coordinator.js +105 -0
  123. package/dist/session/managed-build-receipt.d.ts +21 -0
  124. package/dist/session/managed-build-receipt.js +337 -0
  125. package/dist/session/managed-verification-timeouts.d.ts +8 -0
  126. package/dist/session/managed-verification-timeouts.js +17 -0
  127. package/dist/session/phase-completion.artifacts.d.ts +27 -0
  128. package/dist/session/phase-completion.artifacts.js +829 -0
  129. package/dist/session/phase-completion.d.ts +53 -42
  130. package/dist/session/phase-completion.helpers.d.ts +117 -0
  131. package/dist/session/phase-completion.helpers.js +707 -0
  132. package/dist/session/phase-completion.js +125 -3007
  133. package/dist/session/phase-completion.prompts.d.ts +14 -0
  134. package/dist/session/phase-completion.prompts.js +233 -0
  135. package/dist/session/phase-completion.runtime.d.ts +49 -0
  136. package/dist/session/phase-completion.runtime.js +833 -0
  137. package/dist/session/phase-completion.schemas.d.ts +3 -0
  138. package/dist/session/phase-completion.schemas.js +859 -0
  139. package/dist/session/phase-completion.validation.d.ts +38 -0
  140. package/dist/session/phase-completion.validation.js +918 -0
  141. package/dist/session/pre-publication-validation.d.ts +75 -0
  142. package/dist/session/pre-publication-validation.js +657 -0
  143. package/dist/session/provider-cli/bridge-cli.conformance-fixtures.d.ts +72 -0
  144. package/dist/session/provider-cli/bridge-cli.conformance-fixtures.js +462 -0
  145. package/dist/session/provider-cli/ci-repair-readiness.d.ts +16 -0
  146. package/dist/session/provider-cli/ci-repair-readiness.js +110 -0
  147. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +95 -0
  148. package/dist/session/provider-cli/provider-cli.artifacts.js +812 -0
  149. package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
  150. package/dist/session/provider-cli/provider-cli.deterministic-payloads.d.ts +12 -0
  151. package/dist/session/provider-cli/provider-cli.deterministic-payloads.js +279 -0
  152. package/dist/session/provider-cli/provider-cli.deterministic.d.ts +14 -0
  153. package/dist/session/provider-cli/provider-cli.deterministic.js +407 -0
  154. package/dist/session/provider-cli/provider-cli.process.d.ts +99 -0
  155. package/dist/session/provider-cli/provider-cli.process.js +874 -0
  156. package/dist/session/provider-cli/provider-cli.prompts.d.ts +52 -0
  157. package/dist/session/provider-cli/provider-cli.prompts.js +715 -0
  158. package/dist/session/provider-cli/provider-cli.qa-sidecars.d.ts +11 -0
  159. package/dist/session/provider-cli/provider-cli.qa-sidecars.js +66 -0
  160. package/dist/session/provider-cli/provider-cli.repair-epoch.d.ts +6 -0
  161. package/dist/session/provider-cli/provider-cli.repair-epoch.js +11 -0
  162. package/dist/session/provider-cli/provider-cli.runtime.d.ts +16 -0
  163. package/dist/session/provider-cli/provider-cli.runtime.js +78 -0
  164. package/dist/session/provider-cli/provider-cli.service.d.ts +47 -53
  165. package/dist/session/provider-cli/provider-cli.service.js +1257 -2497
  166. package/dist/session/provider-cli/provider-cli.types.d.ts +8 -0
  167. package/dist/session/provider-cli/provider-output-state.d.ts +32 -0
  168. package/dist/session/provider-cli/provider-output-state.js +817 -0
  169. package/dist/session/provider-cli/provider-usage.d.ts +7 -0
  170. package/dist/session/provider-cli/provider-usage.js +28 -5
  171. package/dist/session/session-redaction.d.ts +3 -0
  172. package/dist/session/session-redaction.js +448 -0
  173. package/dist/session/session.base.d.ts +6 -1
  174. package/dist/session/session.service.d.ts +8 -2
  175. package/dist/session/session.service.js +586 -59
  176. package/dist/session/session.types.d.ts +324 -3
  177. package/dist/session/session.types.js +939 -10
  178. package/dist/session/unreferenced-exports.d.ts +12 -0
  179. package/dist/session/unreferenced-exports.js +59 -0
  180. package/dist/shared/format/format-bytes.d.ts +1 -0
  181. package/dist/shared/format/format-bytes.js +9 -0
  182. package/dist/shared/git/git.js +2 -1
  183. package/dist/shared/path/path.d.ts +1 -0
  184. package/dist/shared/path/path.js +21 -1
  185. package/dist/shared/process/dotnet-toolchain.d.ts +17 -0
  186. package/dist/shared/process/dotnet-toolchain.js +307 -0
  187. package/dist/shared/process/host-process-recovery.d.ts +12 -0
  188. package/dist/shared/process/host-process-recovery.js +78 -0
  189. package/dist/shared/process/host-process-supervisor.d.ts +59 -0
  190. package/dist/shared/process/host-process-supervisor.js +330 -0
  191. package/dist/shared/process/process.d.ts +3 -0
  192. package/dist/shared/process/process.js +13 -7
  193. package/dist/shared/process/windows-launch.d.ts +8 -0
  194. package/dist/shared/process/windows-launch.js +41 -0
  195. package/dist/shared/provider-capabilities.d.ts +4 -0
  196. package/dist/shared/provider-capabilities.js +88 -0
  197. package/dist/shared/redaction/redaction.d.ts +1 -0
  198. package/dist/shared/redaction/redaction.js +47 -4
  199. package/dist/version.d.ts +1 -1
  200. package/dist/version.js +1 -1
  201. package/dist/worktree/artifact-injector.d.ts +3 -2
  202. package/dist/worktree/artifact-injector.js +82 -10
  203. package/dist/worktree/dependency-warmer.d.ts +53 -1
  204. package/dist/worktree/dependency-warmer.js +619 -11
  205. package/dist/worktree/index.d.ts +2 -1
  206. package/dist/worktree/index.js +1 -0
  207. package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
  208. package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
  209. package/dist/worktree/story-qa-reference-hydrator.d.ts +4 -0
  210. package/dist/worktree/story-qa-reference-hydrator.js +253 -0
  211. package/dist/worktree/worktree-paths.d.ts +4 -0
  212. package/dist/worktree/worktree-paths.js +30 -0
  213. package/dist/worktree/worktree.base.d.ts +3 -0
  214. package/dist/worktree/worktree.service.d.ts +16 -3
  215. package/dist/worktree/worktree.service.js +334 -54
  216. package/dist/worktree/worktree.types.d.ts +9 -1
  217. package/package.json +12 -7
  218. package/runtime/windows-job-supervisor.ps1 +722 -0
  219. package/runtime/windows-listener-owner.ps1 +33 -0
@@ -0,0 +1,812 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { artifactRootFor, artifactRootForOwner, debugCompletionPhaseOrder, draftCompletionGateOrder, qaCompletionGateOrder, readCompletionArtifact } from "../phase-completion.js";
4
+ import { ensureNoSymbolicLinkPathSegments, isPathUnderRoot, sha256File } from "../qa-sidecar-validation.js";
5
+ import { hasLaunchTarget } from "./provider-cli.qa-sidecars.js";
6
+ import { clip, isNodeError, messageFromError, normalizedString, parseJsonLine, stringField } from "./provider-output-state.js";
7
+ const maxTransportedQaSidecarBytes = 8 * 1024 * 1024;
8
+ const maxTransportedQaSidecarsTotalBytes = 16 * 1024 * 1024;
9
+ const developCyclePhaseOrder = ["implementation", "self_review", "validation"];
10
+ const nonPublishableWorktreePaths = [".forge", ".local"];
11
+ export async function emitDraftGateArtifacts(emit, worktreePath, workItemId) {
12
+ const draftArtifactFiles = {
13
+ draft_intent: "draft.intent.json",
14
+ draft_repo_context: "draft.repo-context.json",
15
+ draft_graph_context: "draft.graph-context.json",
16
+ draft_product_scope: "draft.product-scope.json",
17
+ draft_work_order_plan: "draft.work-order-plan.json",
18
+ draft_critic: "draft.critic.json",
19
+ draft_proposal: "draft.proposal.json"
20
+ };
21
+ for (const gate of draftCompletionGateOrder) {
22
+ const actualPath = path.join(worktreePath, ".forge", "work-items", workItemId, "artifacts", draftArtifactFiles[gate]);
23
+ if (!await fileExists(actualPath)) {
24
+ continue;
25
+ }
26
+ const relativePath = path.relative(worktreePath, actualPath).split(path.sep).join("/");
27
+ await emit({
28
+ role: "tool",
29
+ kind: "tool_result",
30
+ content: `Forge Draft completion tool recorded ${gate} artifact ${relativePath}.`,
31
+ eventType: "completion_tool_artifact_recorded",
32
+ data: JSON.stringify({ phase: "draft", draft_gate: gate, artifact_relative_paths: [relativePath] })
33
+ });
34
+ await emit({
35
+ role: "hook",
36
+ kind: "checkpoint",
37
+ content: `phase_gate: ${gate} artifact accepted with status pass.`,
38
+ eventType: "phase_gate"
39
+ });
40
+ const next = draftCompletionGateOrder[draftCompletionGateOrder.indexOf(gate) + 1];
41
+ if (next) {
42
+ await emit({
43
+ role: "hook",
44
+ kind: "checkpoint",
45
+ content: `phase_progression: ${gate} -> ${next}`,
46
+ eventType: "phase_gate"
47
+ });
48
+ }
49
+ }
50
+ }
51
+ export async function emitDecomposeGateArtifacts(emit, worktreePath, workItemId) {
52
+ const decomposeArtifactFiles = {
53
+ decompose_subject_intake: "decompose.subject-intake.json",
54
+ decompose_context_review: "decompose.context-review.json",
55
+ decompose_breakdown_strategy: "decompose.breakdown-strategy.json",
56
+ decompose_child_items: "decompose.child-items.json",
57
+ decompose_dependency_graph: "decompose.dependency-graph.json",
58
+ decompose_critic: "decompose.critic.json",
59
+ decompose_proposal: "decompose.proposal.json"
60
+ };
61
+ const gates = Object.keys(decomposeArtifactFiles);
62
+ for (const gate of gates) {
63
+ const actualPath = path.join(worktreePath, ".forge", "work-items", workItemId, "artifacts", decomposeArtifactFiles[gate]);
64
+ if (!await fileExists(actualPath)) {
65
+ continue;
66
+ }
67
+ const relativePath = path.relative(worktreePath, actualPath).split(path.sep).join("/");
68
+ await emit({
69
+ role: "tool",
70
+ kind: "tool_result",
71
+ content: `Forge Decompose completion tool recorded ${gate} artifact ${relativePath}.`,
72
+ eventType: "completion_tool_artifact_recorded",
73
+ data: JSON.stringify({ phase: "decompose", decompose_gate: gate, artifact_relative_paths: [relativePath] })
74
+ });
75
+ await emit({
76
+ role: "hook",
77
+ kind: "checkpoint",
78
+ content: `phase_gate: ${gate} artifact accepted with status pass.`,
79
+ eventType: "phase_gate"
80
+ });
81
+ const next = gates[gates.indexOf(gate) + 1];
82
+ if (next) {
83
+ await emit({
84
+ role: "hook",
85
+ kind: "checkpoint",
86
+ content: `phase_progression: ${gate} -> ${next}`,
87
+ eventType: "phase_gate"
88
+ });
89
+ }
90
+ }
91
+ }
92
+ export async function fileExists(filePath) {
93
+ try {
94
+ await fs.access(filePath);
95
+ return true;
96
+ }
97
+ catch {
98
+ return false;
99
+ }
100
+ }
101
+ export function summarizeStderr(stderr) {
102
+ const trimmed = stderr.trim();
103
+ return trimmed ? `: ${clip(trimmed, 1000)}` : "";
104
+ }
105
+ export async function waitForCompletionArtifact(worktreePath, workItemId, phase, signal) {
106
+ while (!signal?.aborted) {
107
+ const artifact = await readCompletionArtifact(worktreePath, workItemId, phase);
108
+ if (artifact) {
109
+ return artifact;
110
+ }
111
+ await delay(1000, signal);
112
+ }
113
+ throw new Error(`${phase} proposal artifact wait was cancelled.`);
114
+ }
115
+ export async function delay(ms, signal) {
116
+ if (!signal) {
117
+ await new Promise((resolve) => setTimeout(resolve, ms));
118
+ return;
119
+ }
120
+ const abortSignal = signal;
121
+ if (abortSignal.aborted) {
122
+ return;
123
+ }
124
+ await new Promise((resolve) => {
125
+ const timeout = setTimeout(done, ms);
126
+ const abort = () => done();
127
+ function done() {
128
+ clearTimeout(timeout);
129
+ abortSignal.removeEventListener("abort", abort);
130
+ resolve();
131
+ }
132
+ abortSignal.addEventListener("abort", abort, { once: true });
133
+ if (abortSignal.aborted) {
134
+ done();
135
+ }
136
+ });
137
+ }
138
+ export async function cleanupCompletionTools(completionTools, emit) {
139
+ try {
140
+ await completionTools.cleanup();
141
+ }
142
+ catch (error) {
143
+ await emit({
144
+ role: "runner",
145
+ kind: "checkpoint",
146
+ content: `Forge completion tool cleanup failed and was ignored: ${clip(messageFromError(error), 600)}`,
147
+ eventType: "completion_tools_cleanup_failed"
148
+ }).catch(() => undefined);
149
+ }
150
+ }
151
+ export async function commitWorktreeChanges(git, worktreePath, workItemId, alreadyStagedChangedFiles) {
152
+ const changedFiles = alreadyStagedChangedFiles ?? await stagePublishableWorktreeChanges(git, worktreePath);
153
+ if (changedFiles.length > 0) {
154
+ await git.run(worktreePath, ["commit", "-m", `provider cli changeset for ${workItemId.slice(0, 8)}`]);
155
+ }
156
+ const commitSha = (await git.run(worktreePath, ["rev-parse", "HEAD"])).trim();
157
+ return { commitSha, changedFiles };
158
+ }
159
+ export async function stagePublishableWorktreeChanges(git, worktreePath) {
160
+ await git.run(worktreePath, ["add", "-A"]);
161
+ for (const reservedPath of nonPublishableWorktreePaths) {
162
+ await git.run(worktreePath, ["reset", "--", reservedPath]).catch(() => "");
163
+ }
164
+ return (await git.run(worktreePath, ["diff", "--cached", "--name-only"]))
165
+ .split(/\r?\n/)
166
+ .map((line) => line.trim())
167
+ .filter(Boolean);
168
+ }
169
+ export function phaseCompleteEventType(phase) {
170
+ return phase === "implementation" ? "implementation_complete" : `${phase}_complete`;
171
+ }
172
+ export function isDebugCompletionPhaseForProvider(phase) {
173
+ return debugCompletionPhaseOrder.includes(phase);
174
+ }
175
+ const qaArtifactFileNames = {
176
+ plan: "qa.plan.json",
177
+ evidence: "qa.evidence.json",
178
+ findings: "qa.findings.json",
179
+ verdict: "qa.verdict.json",
180
+ report: "qa.report.md"
181
+ };
182
+ const qaGateArtifactFileNames = {
183
+ qa_plan: "qa.plan.json",
184
+ qa_regression: "qa.regression.json",
185
+ qa_api: "qa.api.json",
186
+ qa_manual_web: "qa.manual-web.json",
187
+ qa_bug_repro: "qa.bug-repro.json",
188
+ qa_findings: "qa.findings.json",
189
+ qa_verdict: "qa.verdict.json"
190
+ };
191
+ export async function emitQaArtifactRecorded(emit, worktreePath, artifact) {
192
+ const relativePath = path.relative(worktreePath, artifact.path) || path.basename(artifact.path);
193
+ await emit({
194
+ role: "tool",
195
+ kind: "tool_result",
196
+ content: `Forge QA artifact recorded ${relativePath} (${artifact.status}).`,
197
+ eventType: "qa_artifact_recorded"
198
+ });
199
+ await emit({
200
+ role: "hook",
201
+ kind: "checkpoint",
202
+ content: `phase_gate: qa artifact accepted with status ${artifact.status}.`,
203
+ eventType: "phase_gate"
204
+ });
205
+ }
206
+ export async function readQaCompletionArtifactBundle(worktreePath, workItemId, ownerType, ownerId) {
207
+ const artifact = await readCompletionArtifact(worktreePath, workItemId, "qa", ownerType, ownerId);
208
+ if (!artifact || !await hasCompleteQaArtifactBundle(worktreePath, workItemId, ownerType, ownerId)) {
209
+ return null;
210
+ }
211
+ const artifactRoot = ownerType && ownerId
212
+ ? artifactRootForOwner(worktreePath, ownerType, ownerId)
213
+ : artifactRootFor(worktreePath, workItemId);
214
+ const planJson = await fs.readFile(path.join(artifactRoot, qaArtifactFileNames.plan), "utf8");
215
+ const evidenceJson = await fs.readFile(path.join(artifactRoot, qaArtifactFileNames.evidence), "utf8");
216
+ const findingsJson = await fs.readFile(path.join(artifactRoot, qaArtifactFileNames.findings), "utf8");
217
+ const sidecars = await readQaBundleSidecars(worktreePath, workItemId, ownerType, ownerId);
218
+ return {
219
+ ...artifact,
220
+ planJson,
221
+ evidenceJson,
222
+ findingsJson,
223
+ sidecars
224
+ };
225
+ }
226
+ export async function hasCompleteQaArtifactBundle(worktreePath, workItemId, ownerType, ownerId) {
227
+ const artifactRoot = ownerType && ownerId
228
+ ? artifactRootForOwner(worktreePath, ownerType, ownerId)
229
+ : artifactRootFor(worktreePath, workItemId);
230
+ for (const fileName of Object.values(qaArtifactFileNames)) {
231
+ try {
232
+ const stat = await fs.stat(path.join(artifactRoot, fileName));
233
+ if (!stat.isFile()) {
234
+ return false;
235
+ }
236
+ }
237
+ catch (error) {
238
+ if (isNodeError(error) && error.code === "ENOENT") {
239
+ return false;
240
+ }
241
+ throw error;
242
+ }
243
+ }
244
+ return true;
245
+ }
246
+ export async function enforceQaBrowserLaunchTargetGate(dispatch, artifact) {
247
+ const qaBrowser = dispatch.qaBrowser;
248
+ if (!qaBrowser?.required || hasLaunchTarget(qaBrowser.launchTarget ?? null)) {
249
+ return;
250
+ }
251
+ const evidence = await readJsonObjectFile(path.join(path.dirname(artifact.path), qaArtifactFileNames.evidence));
252
+ if (hasPassingRepositoryPlaywrightEvidence(evidence)) {
253
+ return;
254
+ }
255
+ const verdict = parseJsonLine(artifact.verdictJson) ?? {};
256
+ const status = normalizedString(stringField(verdict, "status") ?? artifact.status ?? "");
257
+ const blockerReason = normalizedString(stringField(verdict, "blocker_reason") ?? "");
258
+ if (status !== "blocked" || blockerReason !== "missing_app_launch_target") {
259
+ throw new Error("QA browser testing is required but neither an app launch target nor passing repository Playwright evidence was provided; provider must return blocked with blocker_reason missing_app_launch_target.");
260
+ }
261
+ }
262
+ function hasPassingRepositoryPlaywrightEvidence(evidence) {
263
+ const passingBrowserCheck = arrayField(evidence, "browser_checks").some(check => normalizedString(stringField(check, "status") ?? "") === "pass"
264
+ && Boolean(stringField(check, "url") ?? stringField(check, "route")));
265
+ if (!passingBrowserCheck) {
266
+ return false;
267
+ }
268
+ return arrayField(evidence, "commands").some(command => {
269
+ const status = normalizedString(stringField(command, "status") ?? "");
270
+ const exitCode = command.exit_code;
271
+ const description = [stringField(command, "command"), stringField(command, "summary")]
272
+ .filter((value) => Boolean(value))
273
+ .join(" ");
274
+ return status === "pass"
275
+ && exitCode === 0
276
+ && /(?:playwright|test:e2e|e2e(?:\s|:|$)|browser(?:\s|-)?test)/iu.test(description);
277
+ });
278
+ }
279
+ export async function validateQaBundleSidecarReferences(worktreePath, workItemId, ownerType, ownerId) {
280
+ await readQaBundleSidecars(worktreePath, workItemId, ownerType, ownerId);
281
+ }
282
+ export async function readQaBundleSidecars(worktreePath, workItemId, ownerType, ownerId) {
283
+ const artifactRoot = ownerType && ownerId
284
+ ? artifactRootForOwner(worktreePath, ownerType, ownerId)
285
+ : artifactRootFor(worktreePath, workItemId);
286
+ const evidence = await readJsonObjectFile(path.join(artifactRoot, qaArtifactFileNames.evidence));
287
+ const sidecars = arrayField(evidence, "sidecars");
288
+ if (sidecars.length === 0) {
289
+ return [];
290
+ }
291
+ const qaSidecarRoot = path.resolve(artifactRoot, "qa");
292
+ const transported = [];
293
+ const transportedByPath = new Map();
294
+ let totalBytes = 0;
295
+ for (const [index, sidecar] of sidecars.entries()) {
296
+ const sidecarPath = stringField(sidecar, "path");
297
+ const expectedHash = stringField(sidecar, "content_hash");
298
+ const expectedBytes = sidecar.bytes;
299
+ if (!sidecarPath || !expectedHash || typeof expectedBytes !== "number" || !Number.isFinite(expectedBytes) || expectedBytes <= 0) {
300
+ throw new Error(`QA sidecar ${index} must include path, content_hash, and positive bytes.`);
301
+ }
302
+ if (path.isAbsolute(sidecarPath)) {
303
+ throw new Error(`QA sidecar ${index} path must be relative to the worktree.`);
304
+ }
305
+ const fullPath = path.resolve(worktreePath, sidecarPath);
306
+ if (!isPathUnderRoot(qaSidecarRoot, fullPath)) {
307
+ const ownerDirectory = ownerType && ownerId && ownerType.trim().toLowerCase() === "story" ? "stories" : "work-items";
308
+ const ownerSegment = ownerType && ownerId ? ownerId : workItemId;
309
+ throw new Error(`QA sidecar ${index} path must be under .forge/${ownerDirectory}/${ownerSegment}/artifacts/qa/.`);
310
+ }
311
+ const stat = await fs.lstat(fullPath).catch((error) => {
312
+ if (isNodeError(error) && error.code === "ENOENT") {
313
+ throw new Error(`QA sidecar ${index} file does not exist: ${sidecarPath}`);
314
+ }
315
+ throw error;
316
+ });
317
+ if (!stat.isFile() || stat.isSymbolicLink()) {
318
+ throw new Error(`QA sidecar ${index} must reference a regular file.`);
319
+ }
320
+ await ensureNoSymbolicLinkPathSegments(qaSidecarRoot, path.dirname(fullPath), `QA sidecar ${index} path must not traverse symbolic links.`);
321
+ if (stat.size !== expectedBytes) {
322
+ throw new Error(`QA sidecar ${index} bytes do not match the referenced file.`);
323
+ }
324
+ if (stat.size > maxTransportedQaSidecarBytes) {
325
+ throw new Error(`QA sidecar ${index} exceeds the ${maxTransportedQaSidecarBytes}-byte transport limit.`);
326
+ }
327
+ const actualHash = await sha256File(fullPath);
328
+ if (actualHash !== expectedHash.trim().toLowerCase()) {
329
+ throw new Error(`QA sidecar ${index} content_hash does not match the referenced file.`);
330
+ }
331
+ const canonicalPath = path.relative(worktreePath, fullPath).split(path.sep).join("/");
332
+ const duplicateKey = canonicalPath.toLowerCase();
333
+ const existing = transportedByPath.get(duplicateKey);
334
+ if (existing) {
335
+ if (existing.contentHash !== actualHash || existing.bytes !== stat.size) {
336
+ throw new Error(`QA sidecar ${index} conflicts with another payload for ${canonicalPath}.`);
337
+ }
338
+ continue;
339
+ }
340
+ totalBytes += stat.size;
341
+ if (totalBytes > maxTransportedQaSidecarsTotalBytes) {
342
+ throw new Error(`QA sidecars exceed the ${maxTransportedQaSidecarsTotalBytes}-byte total transport limit.`);
343
+ }
344
+ const content = await fs.readFile(fullPath);
345
+ transportedByPath.set(duplicateKey, { contentHash: actualHash, bytes: stat.size });
346
+ transported.push({
347
+ path: canonicalPath,
348
+ contentBase64: content.toString("base64"),
349
+ contentHash: actualHash,
350
+ bytes: stat.size
351
+ });
352
+ }
353
+ return transported;
354
+ }
355
+ export async function emitQaStructuredEvents(emit, artifact) {
356
+ const artifactRoot = path.dirname(artifact.path);
357
+ const plan = await readJsonObjectFile(path.join(artifactRoot, qaArtifactFileNames.plan));
358
+ const evidence = await readJsonObjectFile(path.join(artifactRoot, qaArtifactFileNames.evidence));
359
+ const findings = await readJsonObjectFile(path.join(artifactRoot, qaArtifactFileNames.findings));
360
+ const verdict = parseJsonLine(artifact.verdictJson) ?? {};
361
+ const status = stringField(verdict, "status") ?? artifact.status ?? "unknown";
362
+ const checklist = stringArrayField(plan, "checklist");
363
+ await emit({
364
+ role: "worker",
365
+ kind: "message",
366
+ content: checklist.length > 0
367
+ ? `QA plan: ${checklist.join("; ")}`
368
+ : "QA plan recorded.",
369
+ eventType: "qa_plan"
370
+ });
371
+ for (const browserCheck of arrayField(evidence, "browser_checks")) {
372
+ await emit({
373
+ role: "worker",
374
+ kind: "checkpoint",
375
+ content: `QA browser check: ${summarizeQaObject(browserCheck)}`,
376
+ eventType: "qa_browser_check"
377
+ });
378
+ }
379
+ for (const finding of [...arrayField(findings, "findings"), ...arrayField(findings, "candidate_bugs")]) {
380
+ await emit({
381
+ role: "worker",
382
+ kind: "checkpoint",
383
+ content: `QA finding: ${summarizeQaObject(finding)}`,
384
+ eventType: "qa_finding"
385
+ });
386
+ }
387
+ await emit({
388
+ role: "assistant",
389
+ kind: "message",
390
+ content: status === "blocked"
391
+ ? `QA blocked: ${stringField(verdict, "summary") ?? "No summary provided."}`
392
+ : `QA completed with status ${status}: ${stringField(verdict, "summary") ?? "No summary provided."}`,
393
+ eventType: status === "blocked" ? "qa_blocked" : "qa_complete"
394
+ });
395
+ }
396
+ export function qaArtifactRootForDispatch(worktreePath, workItemId, dispatch) {
397
+ return dispatch.cycleRun.ownerType && dispatch.cycleRun.ownerId
398
+ ? artifactRootForOwner(worktreePath, dispatch.cycleRun.ownerType, dispatch.cycleRun.ownerId)
399
+ : artifactRootFor(worktreePath, workItemId);
400
+ }
401
+ export async function readQaGateArtifact(artifactRoot, gate) {
402
+ const artifactPath = path.join(artifactRoot, qaGateArtifactFileNames[gate]);
403
+ try {
404
+ const stat = await fs.stat(artifactPath);
405
+ if (!stat.isFile()) {
406
+ return null;
407
+ }
408
+ }
409
+ catch (error) {
410
+ if (isNodeError(error) && error.code === "ENOENT") {
411
+ return null;
412
+ }
413
+ throw error;
414
+ }
415
+ const payload = await readJsonObjectFile(artifactPath);
416
+ return {
417
+ gate,
418
+ path: artifactPath,
419
+ status: stringField(payload, "status") ?? "unknown",
420
+ producedBy: stringField(payload, "produced_by") ?? stringField(payload, "producedBy") ?? undefined,
421
+ contentHash: stringField(payload, "content_hash") ?? undefined
422
+ };
423
+ }
424
+ export function nextQaGate(gate) {
425
+ const index = qaCompletionGateOrder.indexOf(gate);
426
+ return index >= 0 ? qaCompletionGateOrder[index + 1] ?? null : null;
427
+ }
428
+ export async function emitQaGateAccepted(emit, worktreePath, artifact, nextGate) {
429
+ const relativePath = path.relative(worktreePath, artifact.path).split(path.sep).join("/");
430
+ await emit({
431
+ role: "hook",
432
+ kind: artifact.status === "blocked" ? "error" : "checkpoint",
433
+ content: `phase_gate: ${artifact.gate} artifact accepted with status ${artifact.status}.`,
434
+ eventType: "phase_gate",
435
+ data: JSON.stringify({
436
+ phase: "qa",
437
+ qa_gate: artifact.gate,
438
+ status: artifact.status,
439
+ artifact_relative_paths: [relativePath],
440
+ content_hash: artifact.contentHash ?? null,
441
+ produced_by: artifact.producedBy ?? null
442
+ })
443
+ });
444
+ if (!nextGate) {
445
+ return;
446
+ }
447
+ await emit({
448
+ role: "hook",
449
+ kind: "checkpoint",
450
+ content: `phase_progression: ${artifact.gate} -> ${nextGate}`,
451
+ eventType: "phase_gate",
452
+ data: JSON.stringify({
453
+ phase: "qa",
454
+ from_gate: artifact.gate,
455
+ to_gate: nextGate
456
+ })
457
+ });
458
+ }
459
+ export class LiveQaArtifactEmitter {
460
+ worktreePath;
461
+ workItemId;
462
+ dispatch;
463
+ emit;
464
+ emittedBundle = false;
465
+ emittedGates = new Set();
466
+ timer = null;
467
+ pending = Promise.resolve();
468
+ constructor(worktreePath, workItemId, dispatch, emit) {
469
+ this.worktreePath = worktreePath;
470
+ this.workItemId = workItemId;
471
+ this.dispatch = dispatch;
472
+ this.emit = emit;
473
+ }
474
+ start() {
475
+ void this.pollQueued();
476
+ this.timer = setInterval(() => {
477
+ void this.pollQueued();
478
+ }, 250);
479
+ }
480
+ async stop() {
481
+ if (this.timer) {
482
+ clearInterval(this.timer);
483
+ this.timer = null;
484
+ }
485
+ await this.pollQueued();
486
+ }
487
+ hasEmitted() {
488
+ return this.emittedBundle;
489
+ }
490
+ pollQueued() {
491
+ this.pending = this.pending
492
+ .then(() => this.poll())
493
+ .catch(() => undefined);
494
+ return this.pending;
495
+ }
496
+ async poll() {
497
+ await this.emitAvailableGateEvents();
498
+ if (this.emittedBundle) {
499
+ return;
500
+ }
501
+ const artifact = await readQaCompletionArtifactBundle(this.worktreePath, this.workItemId, this.dispatch.cycleRun.ownerType, this.dispatch.cycleRun.ownerId);
502
+ if (!artifact) {
503
+ return;
504
+ }
505
+ await enforceQaBrowserLaunchTargetGate(this.dispatch, artifact);
506
+ this.emittedBundle = true;
507
+ await emitQaArtifactRecorded(this.emit, this.worktreePath, artifact);
508
+ await emitQaStructuredEvents(this.emit, artifact);
509
+ }
510
+ async emitAvailableGateEvents() {
511
+ const artifactRoot = qaArtifactRootForDispatch(this.worktreePath, this.workItemId, this.dispatch);
512
+ for (const gate of qaCompletionGateOrder) {
513
+ if (this.emittedGates.has(gate)) {
514
+ continue;
515
+ }
516
+ const artifact = await readQaGateArtifact(artifactRoot, gate);
517
+ if (!artifact) {
518
+ return;
519
+ }
520
+ this.emittedGates.add(gate);
521
+ await emitQaGateAccepted(this.emit, this.worktreePath, artifact, nextQaGate(gate));
522
+ }
523
+ }
524
+ }
525
+ export class LiveArchitectArtifactEmitter {
526
+ worktreePath;
527
+ workItemId;
528
+ emit;
529
+ emitted = false;
530
+ timer = null;
531
+ pending = Promise.resolve();
532
+ constructor(worktreePath, workItemId, emit) {
533
+ this.worktreePath = worktreePath;
534
+ this.workItemId = workItemId;
535
+ this.emit = emit;
536
+ }
537
+ start() {
538
+ void this.pollQueued();
539
+ this.timer = setInterval(() => {
540
+ void this.pollQueued();
541
+ }, 250);
542
+ }
543
+ async stop() {
544
+ if (this.timer) {
545
+ clearInterval(this.timer);
546
+ this.timer = null;
547
+ }
548
+ await this.pollQueued();
549
+ }
550
+ hasEmitted() {
551
+ return this.emitted;
552
+ }
553
+ pollQueued() {
554
+ this.pending = this.pending
555
+ .then(() => this.poll())
556
+ .catch(() => undefined);
557
+ return this.pending;
558
+ }
559
+ async poll() {
560
+ if (this.emitted || !await hasCompleteArchitectArtifactBundle(this.worktreePath, this.workItemId)) {
561
+ return;
562
+ }
563
+ const artifact = await readCompletionArtifact(this.worktreePath, this.workItemId, "architect");
564
+ if (!artifact) {
565
+ return;
566
+ }
567
+ this.emitted = true;
568
+ await emitCompletionArtifactRecorded(this.emit, this.worktreePath, artifact);
569
+ await emitArchitectStructuredEvents(this.emit, this.worktreePath, this.workItemId, artifact);
570
+ }
571
+ }
572
+ export async function hasCompleteArchitectArtifactBundle(worktreePath, workItemId) {
573
+ const artifactRoot = artifactRootFor(worktreePath, workItemId);
574
+ for (const fileName of ["implementation.plan.json", "qa.plan.json", "architect.verdict.json", "architect.report.md"]) {
575
+ try {
576
+ const stat = await fs.stat(path.join(artifactRoot, fileName));
577
+ if (!stat.isFile()) {
578
+ return false;
579
+ }
580
+ }
581
+ catch (error) {
582
+ if (isNodeError(error) && error.code === "ENOENT") {
583
+ return false;
584
+ }
585
+ throw error;
586
+ }
587
+ }
588
+ return true;
589
+ }
590
+ export async function emitArchitectStructuredEvents(emit, worktreePath, workItemId, artifact) {
591
+ const artifactRoot = artifactRootFor(worktreePath, workItemId);
592
+ const implementationPlan = await readJsonObjectFile(path.join(artifactRoot, "implementation.plan.json"));
593
+ const qaPlan = await readJsonObjectFile(path.join(artifactRoot, "qa.plan.json"));
594
+ const verdict = parseJsonLine(artifact.verdictJson) ?? {};
595
+ const status = stringField(verdict, "status") ?? artifact.status ?? "unknown";
596
+ const planId = stringField(implementationPlan, "plan_id") ?? "unknown";
597
+ await emit({
598
+ role: "worker",
599
+ kind: "checkpoint",
600
+ content: `Architect plan recorded: ${planId} with ${arrayField(implementationPlan, "phases").length} phases and ${arrayField(implementationPlan, "tasks").length} tasks.`,
601
+ eventType: "architect_plan"
602
+ });
603
+ await emit({
604
+ role: "worker",
605
+ kind: "checkpoint",
606
+ content: `Architect QA seed recorded with ${arrayField(qaPlan, "final_qa").length} final QA items.`,
607
+ eventType: "architect_qa_seed"
608
+ });
609
+ await emit({
610
+ role: "hook",
611
+ kind: status === "pass" ? "checkpoint" : "error",
612
+ content: `architect_mcp_complete: ${status === "pass" ? "accepted" : "recorded"} - ${stringField(verdict, "summary") ?? "Architect completion payload recorded."}`,
613
+ eventType: "architect_mcp_complete"
614
+ });
615
+ }
616
+ export async function readJsonObjectFile(filePath) {
617
+ const content = await fs.readFile(filePath, "utf8").catch(() => "");
618
+ return parseJsonLine(content) ?? {};
619
+ }
620
+ export function arrayField(source, key) {
621
+ const value = source[key];
622
+ if (!Array.isArray(value)) {
623
+ return [];
624
+ }
625
+ return value.filter((entry) => Boolean(entry) && typeof entry === "object" && !Array.isArray(entry));
626
+ }
627
+ export function stringArrayField(source, key) {
628
+ const value = source[key];
629
+ if (!Array.isArray(value)) {
630
+ return [];
631
+ }
632
+ return value.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
633
+ }
634
+ export function summarizeQaObject(source) {
635
+ return clip(stringField(source, "title")
636
+ ?? stringField(source, "summary")
637
+ ?? stringField(source, "id")
638
+ ?? JSON.stringify(source), 500);
639
+ }
640
+ export function qaArtifactRelativePath(workItemId, fileName, ownerType, ownerId) {
641
+ if (ownerType && ownerId) {
642
+ const ownerDirectory = ownerType.trim().toLowerCase() === "story" ? "stories" : "work-items";
643
+ return [".forge", ownerDirectory, ownerId, "artifacts", fileName].join("/");
644
+ }
645
+ return [".forge", "work-items", workItemId, "artifacts", fileName].join("/");
646
+ }
647
+ export async function emitCompletionArtifactRecorded(emit, worktreePath, artifact) {
648
+ const relativePath = path.relative(worktreePath, artifact.path) || path.basename(artifact.path);
649
+ await emit({
650
+ role: "tool",
651
+ kind: "checkpoint",
652
+ content: `Forge completion tool validation passed for ${artifact.phase ?? "unknown"} (${artifact.status}).`,
653
+ eventType: "completion_tool_validation_passed",
654
+ data: JSON.stringify({
655
+ phase: artifact.phase ?? "unknown",
656
+ status: artifact.status,
657
+ artifact_relative_paths: [relativePath.split(path.sep).join("/")]
658
+ })
659
+ });
660
+ await emit({
661
+ role: "tool",
662
+ kind: "tool_result",
663
+ content: `Forge completion tool recorded ${artifact.phase ?? "unknown"} artifact ${relativePath} (${artifact.status}).`,
664
+ eventType: "completion_tool_artifact_recorded",
665
+ data: JSON.stringify({
666
+ phase: artifact.phase ?? "unknown",
667
+ status: artifact.status,
668
+ artifact_relative_paths: [relativePath.split(path.sep).join("/")]
669
+ })
670
+ });
671
+ }
672
+ export async function emitPhaseCompletionEvent(emit, artifact) {
673
+ const phase = completionPhaseFromArtifact(artifact);
674
+ await emit({
675
+ role: "assistant",
676
+ kind: "message",
677
+ content: `${phase.replace(/_/g, " ")} completion artifact recorded with status ${artifact.status}.`,
678
+ eventType: phaseCompleteEventType(phase)
679
+ });
680
+ }
681
+ export async function emitPhaseGateEvents(emit, artifact) {
682
+ const phase = completionPhaseFromArtifact(artifact);
683
+ const status = (artifact.status ?? "").trim().toLowerCase() || "unknown";
684
+ await emit({
685
+ role: "hook",
686
+ kind: "checkpoint",
687
+ content: `phase_gate: ${phase} artifact accepted with status ${status}.`,
688
+ eventType: "phase_gate"
689
+ });
690
+ if (status !== "pass") {
691
+ return;
692
+ }
693
+ const progression = phase === "implementation"
694
+ ? "phase_progression: implementation -> self_review"
695
+ : phase === "self_review"
696
+ ? "phase_progression: self_review -> validation"
697
+ : phase === "debug_repro"
698
+ ? "phase_progression: debug_repro -> debug_root_cause"
699
+ : phase === "debug_root_cause"
700
+ ? "phase_progression: debug_root_cause -> debug_fix"
701
+ : phase === "debug_fix"
702
+ ? "phase_progression: debug_fix -> debug_regression"
703
+ : phase === "debug_regression"
704
+ ? "phase_progression: debug_regression -> QA ready"
705
+ : "phase_progression: validation -> QA ready";
706
+ await emit({
707
+ role: "hook",
708
+ kind: "checkpoint",
709
+ content: progression,
710
+ eventType: "phase_gate"
711
+ });
712
+ }
713
+ export function completionPhaseFromArtifact(artifact) {
714
+ const phase = artifact.phase?.trim();
715
+ return phase === "self_review" || phase === "validation" || phase === "implementation" || phase === "qa"
716
+ || phase === "architect" || phase === "debug_repro" || phase === "debug_root_cause" || phase === "debug_fix" || phase === "debug_regression"
717
+ ? phase
718
+ : "implementation";
719
+ }
720
+ export class LiveCompletionArtifactEmitter {
721
+ worktreePath;
722
+ workItemId;
723
+ emit;
724
+ phases;
725
+ emitted = new Set();
726
+ timer = null;
727
+ pending = Promise.resolve();
728
+ constructor(worktreePath, workItemId, phase, developCycle, debugCycle, emit) {
729
+ this.worktreePath = worktreePath;
730
+ this.workItemId = workItemId;
731
+ this.emit = emit;
732
+ if (debugCycle && isDebugCompletionPhaseForProvider(phase)) {
733
+ this.phases = debugCompletionPhaseOrder.slice(debugCompletionPhaseOrder.indexOf(phase));
734
+ }
735
+ else {
736
+ this.phases = developCycle
737
+ ? developCyclePhaseOrder.slice(developCyclePhaseOrder.indexOf(phase))
738
+ : [phase];
739
+ }
740
+ }
741
+ start() {
742
+ void this.pollQueued();
743
+ this.timer = setInterval(() => {
744
+ void this.pollQueued();
745
+ }, 250);
746
+ }
747
+ async stop() {
748
+ if (this.timer) {
749
+ clearInterval(this.timer);
750
+ this.timer = null;
751
+ }
752
+ await this.pollQueued();
753
+ }
754
+ hasEmitted(phase) {
755
+ return this.emitted.has(phase);
756
+ }
757
+ pollQueued() {
758
+ this.pending = this.pending
759
+ .then(() => this.poll())
760
+ .catch(() => undefined);
761
+ return this.pending;
762
+ }
763
+ async poll() {
764
+ for (const phase of this.phases) {
765
+ if (this.emitted.has(phase)) {
766
+ continue;
767
+ }
768
+ const artifact = await readCompletionArtifact(this.worktreePath, this.workItemId, phase);
769
+ if (!artifact) {
770
+ return;
771
+ }
772
+ this.emitted.add(phase);
773
+ await emitCompletionArtifactRecorded(this.emit, this.worktreePath, artifact);
774
+ await emitPhaseCompletionEvent(this.emit, artifact);
775
+ await emitPhaseGateEvents(this.emit, artifact);
776
+ if ((artifact.status ?? "").trim().toLowerCase() !== "pass") {
777
+ return;
778
+ }
779
+ }
780
+ }
781
+ }
782
+ export async function readCompletionArtifactsForRun(worktreePath, workItemId, phase, developCycle, debugCycle) {
783
+ if (!developCycle && !debugCycle) {
784
+ const artifact = await readCompletionArtifact(worktreePath, workItemId, phase);
785
+ if (!artifact) {
786
+ throw new Error(`Provider CLI completed but did not call the Forge ${phase} completion tool.`);
787
+ }
788
+ return [artifact];
789
+ }
790
+ const expectedPhases = debugCycle && isDebugCompletionPhaseForProvider(phase)
791
+ ? debugCompletionPhaseOrder.slice(debugCompletionPhaseOrder.indexOf(phase))
792
+ : ["implementation", "self_review", "validation"];
793
+ const artifacts = [];
794
+ for (const expectedPhase of expectedPhases) {
795
+ const artifact = await readCompletionArtifact(worktreePath, workItemId, expectedPhase);
796
+ if (!artifact) {
797
+ if (artifacts.length === 0) {
798
+ throw new Error(`Provider CLI completed but did not call the Forge ${expectedPhase} completion tool.`);
799
+ }
800
+ const previous = artifacts[artifacts.length - 1];
801
+ if ((previous.status ?? "").trim().toLowerCase() === "pass") {
802
+ throw new Error(`Provider CLI completed after ${previous.phase} passed but did not call the Forge ${expectedPhase} completion tool.`);
803
+ }
804
+ return artifacts;
805
+ }
806
+ artifacts.push(artifact);
807
+ if ((artifact.status ?? "").trim().toLowerCase() !== "pass") {
808
+ return artifacts;
809
+ }
810
+ }
811
+ return artifacts;
812
+ }