@loucompanion/forge-bridge 0.1.3-dev.8777774c67bc → 0.1.3-dev.88e93f53db5b

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 (137) hide show
  1. package/README.md +51 -0
  2. package/dist/bridge/bridge.base.d.ts +4 -1
  3. package/dist/bridge/bridge.service.d.ts +50 -3
  4. package/dist/bridge/bridge.service.js +730 -87
  5. package/dist/bridge/bridge.types.d.ts +63 -1
  6. package/dist/cleanup/cleanup.service.d.ts +14 -2
  7. package/dist/cleanup/cleanup.service.js +496 -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 +37 -3
  14. package/dist/cli/cli.service.d.ts +30 -2
  15. package/dist/cli/cli.service.js +191 -27
  16. package/dist/cli/local-config.d.ts +4 -0
  17. package/dist/cli/local-config.js +41 -1
  18. package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
  19. package/dist/runtime/aspnet-runtime-prebuild.js +340 -0
  20. package/dist/runtime/attempt-node-runtime-launcher.d.ts +17 -0
  21. package/dist/runtime/attempt-node-runtime-launcher.js +105 -0
  22. package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
  23. package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
  24. package/dist/runtime/attempt-process-supervisor.d.ts +97 -0
  25. package/dist/runtime/attempt-process-supervisor.js +384 -0
  26. package/dist/runtime/generated-output-reset.d.ts +10 -0
  27. package/dist/runtime/generated-output-reset.js +103 -0
  28. package/dist/runtime/host-listener-inspector.d.ts +46 -0
  29. package/dist/runtime/host-listener-inspector.js +349 -0
  30. package/dist/runtime/host-port-broker.d.ts +76 -0
  31. package/dist/runtime/host-port-broker.js +333 -0
  32. package/dist/runtime/host-runtime-contract.d.ts +47 -0
  33. package/dist/runtime/host-runtime-contract.js +700 -0
  34. package/dist/runtime/host-runtime-lease.d.ts +194 -0
  35. package/dist/runtime/host-runtime-lease.js +2138 -0
  36. package/dist/runtime/host-runtime-promotion.d.ts +23 -0
  37. package/dist/runtime/host-runtime-promotion.js +159 -0
  38. package/dist/runtime/memory-capacity.d.ts +68 -0
  39. package/dist/runtime/memory-capacity.js +121 -0
  40. package/dist/runtime/node-host-runtime-adapter.d.ts +54 -0
  41. package/dist/runtime/node-host-runtime-adapter.js +284 -0
  42. package/dist/runtime/playwright-host-runtime-adapter.d.ts +21 -0
  43. package/dist/runtime/playwright-host-runtime-adapter.js +225 -0
  44. package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
  45. package/dist/runtime/runtime-isolation-lease.js +158 -0
  46. package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
  47. package/dist/runtime/workspace-resource-estimator.js +174 -0
  48. package/dist/sandbox/image-policy.d.ts +3 -1
  49. package/dist/sandbox/image-policy.js +4 -2
  50. package/dist/sandbox/preflight.d.ts +49 -1
  51. package/dist/sandbox/preflight.js +313 -31
  52. package/dist/sandbox/process.js +125 -40
  53. package/dist/sandbox/repository-lock.js +41 -3
  54. package/dist/sandbox/runtime.d.ts +85 -4
  55. package/dist/sandbox/runtime.js +806 -126
  56. package/dist/sandbox/sandbox.types.d.ts +10 -0
  57. package/dist/sandbox/smoke.d.ts +76 -0
  58. package/dist/sandbox/smoke.js +380 -0
  59. package/dist/sandbox/worker-supervisor.js +1 -1
  60. package/dist/sandbox/worker.js +2 -5
  61. package/dist/sandbox/workspace.service.d.ts +1 -0
  62. package/dist/sandbox/workspace.service.js +28 -5
  63. package/dist/session/managed-attempt-tools.d.ts +18 -0
  64. package/dist/session/managed-attempt-tools.js +854 -0
  65. package/dist/session/managed-build-coordinator.d.ts +5 -0
  66. package/dist/session/managed-build-coordinator.js +105 -0
  67. package/dist/session/managed-build-receipt.d.ts +21 -0
  68. package/dist/session/managed-build-receipt.js +335 -0
  69. package/dist/session/phase-completion.artifacts.d.ts +17 -0
  70. package/dist/session/phase-completion.artifacts.js +233 -24
  71. package/dist/session/phase-completion.d.ts +11 -2
  72. package/dist/session/phase-completion.helpers.d.ts +5 -0
  73. package/dist/session/phase-completion.helpers.js +51 -10
  74. package/dist/session/phase-completion.js +12 -3
  75. package/dist/session/phase-completion.prompts.d.ts +3 -3
  76. package/dist/session/phase-completion.prompts.js +21 -6
  77. package/dist/session/phase-completion.runtime.d.ts +13 -1
  78. package/dist/session/phase-completion.runtime.js +189 -8
  79. package/dist/session/phase-completion.schemas.js +108 -3
  80. package/dist/session/phase-completion.validation.d.ts +4 -1
  81. package/dist/session/phase-completion.validation.js +151 -21
  82. package/dist/session/provider-cli/ci-repair-readiness.d.ts +14 -0
  83. package/dist/session/provider-cli/ci-repair-readiness.js +105 -0
  84. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +3 -2
  85. package/dist/session/provider-cli/provider-cli.artifacts.js +65 -7
  86. package/dist/session/provider-cli/provider-cli.deterministic.js +47 -1
  87. package/dist/session/provider-cli/provider-cli.process.d.ts +23 -0
  88. package/dist/session/provider-cli/provider-cli.process.js +420 -32
  89. package/dist/session/provider-cli/provider-cli.prompts.d.ts +4 -1
  90. package/dist/session/provider-cli/provider-cli.prompts.js +99 -19
  91. package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
  92. package/dist/session/provider-cli/provider-cli.runtime.d.ts +1 -1
  93. package/dist/session/provider-cli/provider-cli.runtime.js +6 -3
  94. package/dist/session/provider-cli/provider-cli.service.d.ts +20 -2
  95. package/dist/session/provider-cli/provider-cli.service.js +464 -102
  96. package/dist/session/provider-cli/provider-cli.types.d.ts +5 -0
  97. package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
  98. package/dist/session/provider-cli/provider-output-state.js +91 -16
  99. package/dist/session/provider-cli/provider-usage.d.ts +7 -0
  100. package/dist/session/provider-cli/provider-usage.js +28 -5
  101. package/dist/session/session-redaction.d.ts +3 -0
  102. package/dist/session/session-redaction.js +38 -0
  103. package/dist/session/session.base.d.ts +6 -1
  104. package/dist/session/session.service.d.ts +8 -2
  105. package/dist/session/session.service.js +522 -41
  106. package/dist/session/session.types.d.ts +70 -0
  107. package/dist/session/session.types.js +76 -4
  108. package/dist/shared/format/format-bytes.d.ts +1 -0
  109. package/dist/shared/format/format-bytes.js +9 -0
  110. package/dist/shared/path/path.d.ts +1 -0
  111. package/dist/shared/path/path.js +21 -1
  112. package/dist/shared/process/host-process-recovery.d.ts +12 -0
  113. package/dist/shared/process/host-process-recovery.js +78 -0
  114. package/dist/shared/process/host-process-supervisor.d.ts +59 -0
  115. package/dist/shared/process/host-process-supervisor.js +330 -0
  116. package/dist/shared/process/process.d.ts +1 -0
  117. package/dist/shared/process/process.js +1 -0
  118. package/dist/shared/redaction/redaction.js +12 -2
  119. package/dist/version.d.ts +1 -1
  120. package/dist/version.js +1 -1
  121. package/dist/worktree/artifact-injector.d.ts +3 -2
  122. package/dist/worktree/artifact-injector.js +43 -4
  123. package/dist/worktree/dependency-warmer.d.ts +28 -1
  124. package/dist/worktree/dependency-warmer.js +308 -9
  125. package/dist/worktree/index.d.ts +2 -1
  126. package/dist/worktree/index.js +1 -0
  127. package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
  128. package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
  129. package/dist/worktree/worktree-paths.d.ts +4 -0
  130. package/dist/worktree/worktree-paths.js +30 -0
  131. package/dist/worktree/worktree.base.d.ts +3 -0
  132. package/dist/worktree/worktree.service.d.ts +9 -2
  133. package/dist/worktree/worktree.service.js +217 -48
  134. package/dist/worktree/worktree.types.d.ts +5 -1
  135. package/package.json +5 -3
  136. package/runtime/windows-job-supervisor.ps1 +444 -0
  137. package/runtime/windows-listener-owner.ps1 +33 -0
@@ -51,6 +51,10 @@ export function validatePayloadShape(phase, payload) {
51
51
  requireNonPassEvidence(payload);
52
52
  return;
53
53
  }
54
+ if (phase === "ci_repair") {
55
+ validateCiRepairPayloadShape(payload);
56
+ return;
57
+ }
54
58
  if (isDebugCompletionPhase(phase)) {
55
59
  validateDebugPayloadShape(phase, payload);
56
60
  return;
@@ -97,6 +101,72 @@ export function validatePayloadShape(phase, payload) {
97
101
  requirePassingCommands(payload);
98
102
  requireSatisfiedAcceptanceCriteria(payload);
99
103
  }
104
+ export function validateCiRepairPayloadShape(payload) {
105
+ requireArray(payload, "failed_checks");
106
+ requireNonEmptyArray(payload, "failed_checks");
107
+ for (const [index, failedCheck] of payload.failed_checks.entries()) {
108
+ requireEvidenceRecord(failedCheck, `failed_checks[${index}]`);
109
+ const record = failedCheck;
110
+ requireNonEmptyStringRecord(record, "name", `failed_checks[${index}].`);
111
+ requireNonEmptyStringRecord(record, "provider_state", `failed_checks[${index}].`);
112
+ }
113
+ requireObject(payload, "root_cause");
114
+ const rootCause = requiredRecord(payload, "root_cause");
115
+ requireNonEmptyStringRecord(rootCause, "classification", "root_cause.");
116
+ requireNonEmptyStringRecord(rootCause, "summary", "root_cause.");
117
+ requireNonEmptyStringArrayRecord(rootCause, "evidence", "root_cause.");
118
+ requireArray(payload, "changed_files");
119
+ if (arrayLength(payload, "changed_files") === 0) {
120
+ requireObject(payload, "no_code_action");
121
+ const noCodeAction = requiredRecord(payload, "no_code_action");
122
+ requireNonEmptyStringRecord(noCodeAction, "action", "no_code_action.");
123
+ requireNonEmptyStringRecord(noCodeAction, "reason", "no_code_action.");
124
+ }
125
+ else {
126
+ for (const [index, changedFile] of payload.changed_files.entries()) {
127
+ requireEvidenceRecord(changedFile, `changed_files[${index}]`);
128
+ const record = changedFile;
129
+ requireNonEmptyStringRecord(record, "path", `changed_files[${index}].`);
130
+ requireNonEmptyStringRecord(record, "summary", `changed_files[${index}].`);
131
+ }
132
+ }
133
+ for (const key of ["narrow_validation", "broader_validation"]) {
134
+ requireArray(payload, key);
135
+ requireNonEmptyArray(payload, key);
136
+ validateQaGatePassingCommands(payload[key]);
137
+ }
138
+ requireArray(payload, "workflow_validation");
139
+ requireNonEmptyArray(payload, "workflow_validation");
140
+ for (const [index, entry] of payload.workflow_validation.entries()) {
141
+ requireEvidenceRecord(entry, `workflow_validation[${index}]`);
142
+ const record = entry;
143
+ requireNonEmptyStringRecord(record, "step", `workflow_validation[${index}].`);
144
+ requireKnownStringRecord(record, "equivalence", new Set(["supported", "substitute", "provider_only"]), `workflow_validation[${index}].`);
145
+ requireNonEmptyStringRecord(record, "status", `workflow_validation[${index}].`);
146
+ const equivalence = normalizedString(optionalString(record, "equivalence"));
147
+ if (equivalence === "provider_only") {
148
+ requireNonEmptyStringRecord(record, "reason", `workflow_validation[${index}].`);
149
+ continue;
150
+ }
151
+ requireNonEmptyStringRecord(record, "command", `workflow_validation[${index}].`);
152
+ requireNonEmptyStringRecord(record, "summary", `workflow_validation[${index}].`);
153
+ if (typeof record.exit_code !== "number" || !Number.isFinite(record.exit_code)) {
154
+ throw new Error(`workflow_validation[${index}].exit_code is required for ${equivalence} evidence.`);
155
+ }
156
+ if (record.exit_code !== 0 || normalizedString(optionalString(record, "status")) !== "pass") {
157
+ throw new Error(`workflow_validation[${index}] must pass locally when equivalence is ${equivalence}.`);
158
+ }
159
+ if (equivalence === "substitute") {
160
+ requireNonEmptyStringRecord(record, "substitute", `workflow_validation[${index}].`);
161
+ }
162
+ }
163
+ requireArray(payload, "remaining_risks");
164
+ requireAllowedStatus(payload, "provider_state", ["ready_for_rerun", "rerun_requested", "checks_pending"]);
165
+ requireBoolean(payload, "ready_for_push");
166
+ if (payload.ready_for_push !== true) {
167
+ throw new Error("ci_repair pass requires ready_for_push: true.");
168
+ }
169
+ }
100
170
  export function validateDebugPayloadShape(phase, payload) {
101
171
  if (phase === "debug_repro") {
102
172
  requireArray(payload, "repro_steps");
@@ -169,7 +239,9 @@ export function validateQaGatePayloadShape(gate, payload, status) {
169
239
  if (commands.length === 0) {
170
240
  throw new Error(`${qaGateToolNames[gate]} pass requires commands with at least one entry.`);
171
241
  }
172
- validateQaGatePassingCommands(commands);
242
+ validateQaGatePassingCommands(commands, {
243
+ allowClassifiedNonBlockingFailures: gate === "qa_regression"
244
+ });
173
245
  }
174
246
  if (gate === "qa_manual_web" && status === "pass") {
175
247
  const browserChecks = qaArray(payload, "browser_checks");
@@ -179,23 +251,69 @@ export function validateQaGatePayloadShape(gate, payload, status) {
179
251
  }
180
252
  }
181
253
  }
182
- export function validateQaGatePassingCommands(commands) {
254
+ export function validateQaGatePassingCommands(commands, options = {}) {
255
+ const issues = [];
183
256
  for (const [index, command] of commands.entries()) {
184
- requireEvidenceRecord(command, `commands[${index}]`);
257
+ if (!command || typeof command !== "object" || Array.isArray(command)) {
258
+ issues.push(`commands[${index}] must be an object.`);
259
+ continue;
260
+ }
185
261
  const record = command;
186
- requireNonEmptyStringRecord(record, "command", `commands[${index}].`);
187
- requireNonEmptyStringRecord(record, "status", `commands[${index}].`);
188
- requireNonEmptyStringRecord(record, "summary", `commands[${index}].`);
262
+ if (!optionalString(record, "command"))
263
+ issues.push(`commands[${index}].command is required.`);
264
+ if (!optionalString(record, "status"))
265
+ issues.push(`commands[${index}].status is required.`);
266
+ if (!optionalString(record, "summary"))
267
+ issues.push(`commands[${index}].summary is required.`);
189
268
  if (typeof record.exit_code !== "number" || !Number.isFinite(record.exit_code)) {
190
- throw new Error(`commands[${index}].exit_code is required.`);
269
+ issues.push(`commands[${index}].exit_code is required.`);
270
+ continue;
271
+ }
272
+ const passed = record.exit_code === 0 && normalizedString(record.status) === "pass";
273
+ if (!passed
274
+ && options.allowClassifiedNonBlockingFailures
275
+ && optionalString(record, "classification")) {
276
+ try {
277
+ validateClassifiedNonBlockingQaFailure(record, index);
278
+ }
279
+ catch (error) {
280
+ issues.push(error instanceof Error ? error.message : String(error));
281
+ }
282
+ continue;
191
283
  }
192
284
  if (record.exit_code !== 0) {
193
- throw new Error(`commands[${index}].exit_code must be 0 when QA gate status is pass.`);
285
+ issues.push(`commands[${index}].exit_code must be 0 when QA gate status is pass.`);
194
286
  }
195
287
  if (normalizedString(record.status) !== "pass") {
196
- throw new Error(`commands[${index}].status must be pass when QA gate status is pass.`);
288
+ issues.push(`commands[${index}].status must be pass when QA gate status is pass.`);
197
289
  }
198
290
  }
291
+ throwCompletionPayloadIssues(issues);
292
+ }
293
+ function validateClassifiedNonBlockingQaFailure(record, index) {
294
+ const classification = normalizedString(optionalString(record, "classification"));
295
+ if (classification !== "out_of_scope_failure" && classification !== "baseline_failure") {
296
+ throw new Error(`commands[${index}] is non-passing; QA regression pass requires classification out_of_scope_failure or baseline_failure with corroborating evidence.`);
297
+ }
298
+ requireNonEmptyStringRecord(record, "candidate_bug_id", `commands[${index}].`);
299
+ requireNonEmptyStringRecord(record, "scope_evidence", `commands[${index}].`);
300
+ if (record.acceptance_criteria_independent !== true) {
301
+ throw new Error(`commands[${index}].acceptance_criteria_independent must be true for a non-blocking QA regression failure.`);
302
+ }
303
+ if (classification === "out_of_scope_failure") {
304
+ requireNonEmptyStringRecord(record, "repro_command", `commands[${index}].`);
305
+ if (typeof record.repro_exit_code !== "number" || !Number.isFinite(record.repro_exit_code) || record.repro_exit_code === 0) {
306
+ throw new Error(`commands[${index}].repro_exit_code must be a non-zero number for an out-of-scope failure.`);
307
+ }
308
+ return;
309
+ }
310
+ requireNonEmptyStringRecord(record, "baseline_ref", `commands[${index}].`);
311
+ if (typeof record.baseline_exit_code !== "number" || !Number.isFinite(record.baseline_exit_code) || record.baseline_exit_code === 0) {
312
+ throw new Error(`commands[${index}].baseline_exit_code must be a non-zero number for a baseline failure.`);
313
+ }
314
+ if (record.failure_signature_match !== true) {
315
+ throw new Error(`commands[${index}].failure_signature_match must be true for a baseline failure.`);
316
+ }
199
317
  }
200
318
  export function validateArchitectPayloadShape(payload, status) {
201
319
  if (status !== "pass") {
@@ -214,18 +332,22 @@ export function validateArchitectPayloadShape(payload, status) {
214
332
  throw new Error("implementation_plan.tasks: expected non-empty array; correction: include at least one task with id and tests.");
215
333
  }
216
334
  const taskIds = new Set();
217
- for (const task of implementationPlan.tasks) {
335
+ const taskIndexById = new Map();
336
+ for (const [taskIndex, task] of implementationPlan.tasks.entries()) {
218
337
  if (!task || typeof task !== "object" || Array.isArray(task)) {
219
338
  throw new Error("implementation_plan.tasks[]: expected object entries; correction: each task must be an object with id and tests.");
220
339
  }
221
340
  const taskRecord = task;
222
341
  requireNonEmptyStringRecord(taskRecord, "id", "implementation_plan.tasks[].");
223
342
  const taskId = optionalString(taskRecord, "id") ?? "";
224
- if (taskIds.has(taskId)) {
225
- throw new Error(`implementation_plan.tasks: duplicate task id '${taskId}'.`);
343
+ const firstTaskIndex = taskIndexById.get(taskId);
344
+ if (firstTaskIndex !== undefined) {
345
+ throw new Error(`implementation_plan.tasks[${taskIndex}].id duplicates implementation_plan.tasks[${firstTaskIndex}].id '${taskId}'; correction: assign the duplicate a unique stable task id, update every phase/dependency reference to that task, and resubmit forge.complete_architect_plan.`);
226
346
  }
227
347
  taskIds.add(taskId);
348
+ taskIndexById.set(taskId, taskIndex);
228
349
  validateArchitectTaskDevelopScope(taskRecord, taskId);
350
+ requireNonEmptyStringArrayRecord(taskRecord, "acceptance_criteria", `implementation_plan.tasks[${taskId}].`);
229
351
  validateArchitectVerificationScope(taskRecord, taskId);
230
352
  requireArchitectArray(taskRecord, "tests", payload, { path: `implementation_plan.tasks[${optionalString(taskRecord, "id") ?? "?"}].tests` });
231
353
  if (arrayLength(taskRecord, "tests") === 0) {
@@ -611,22 +733,30 @@ export function validateQaPayloadShape(payload, status) {
611
733
  throw new Error("QA pass/needs_rework requires at least one command, browser check, or evidence note.");
612
734
  }
613
735
  }
736
+ const commandIssues = [];
614
737
  for (const [index, command] of commands.entries()) {
615
- requireEvidenceRecord(command, `commands[${index}]`);
738
+ if (!command || typeof command !== "object" || Array.isArray(command)) {
739
+ commandIssues.push(`commands[${index}] must be an object.`);
740
+ continue;
741
+ }
616
742
  const record = command;
617
- requireNonEmptyStringRecord(record, "command", `commands[${index}].`);
618
- requireNonEmptyStringRecord(record, "status", `commands[${index}].`);
619
- requireNonEmptyStringRecord(record, "summary", `commands[${index}].`);
743
+ if (!optionalString(record, "command"))
744
+ commandIssues.push(`commands[${index}].command is required.`);
745
+ if (!optionalString(record, "status"))
746
+ commandIssues.push(`commands[${index}].status is required.`);
747
+ if (!optionalString(record, "summary"))
748
+ commandIssues.push(`commands[${index}].summary is required.`);
620
749
  if (typeof record.exit_code !== "number" || !Number.isFinite(record.exit_code)) {
621
- throw new Error(`commands[${index}].exit_code is required.`);
750
+ commandIssues.push(`commands[${index}].exit_code is required.`);
622
751
  }
623
- if (status === "pass" && record.exit_code !== 0) {
624
- throw new Error(`commands[${index}].exit_code must be 0 when QA status is pass.`);
752
+ else if (status === "pass" && record.exit_code !== 0) {
753
+ commandIssues.push(`commands[${index}].exit_code must be 0 when QA status is pass.`);
625
754
  }
626
755
  if (status === "pass" && normalizedString(record.status) !== "pass") {
627
- throw new Error(`commands[${index}].status must be pass when QA status is pass.`);
756
+ commandIssues.push(`commands[${index}].status must be pass when QA status is pass.`);
628
757
  }
629
758
  }
759
+ throwCompletionPayloadIssues(commandIssues);
630
760
  for (const [index, browserCheck] of browserChecks.entries()) {
631
761
  requireEvidenceRecord(browserCheck, `browser_checks[${index}]`);
632
762
  const record = browserCheck;
@@ -720,5 +850,5 @@ export function validateOptionalWorkItemId(payload, expectedWorkItemId) {
720
850
  throw new Error(`completion payload work item ${actual} does not match dispatch work item ${expectedWorkItemId}.`);
721
851
  }
722
852
  }
723
- import { requireField, requireNonPassEvidence, requireImplementationEvidence, requirePassingCommands, requireArray, requireNonEmptyArray, arrayLength, requireAllowedStatus, requireObject, requiredRecord, requireBoolean, requireNumericOne, requireSatisfiedAcceptanceCriteria, validateResolvedFindings, isDebugCompletionPhase, qaEvidenceSource, qaArray, qaObjectArray, qaPolicyWarnings, qaStringArray, requireEvidenceRecord, requireNonEmptyStringRecord, requireKnownStringRecord, requireNonEmptyStringArrayRecord, requireSubstantiveEvidenceRecord, normalizedString, requiredString, optionalString, stripUndefined, } from "./phase-completion.helpers.js";
853
+ import { requireField, requireNonPassEvidence, requireImplementationEvidence, requirePassingCommands, requireArray, requireNonEmptyArray, arrayLength, requireAllowedStatus, requireObject, requiredRecord, requireBoolean, requireNumericOne, requireSatisfiedAcceptanceCriteria, validateResolvedFindings, isDebugCompletionPhase, qaEvidenceSource, qaArray, qaObjectArray, qaPolicyWarnings, qaStringArray, requireEvidenceRecord, requireNonEmptyStringRecord, requireKnownStringRecord, requireNonEmptyStringArrayRecord, requireSubstantiveEvidenceRecord, normalizedString, requiredString, optionalString, stripUndefined, throwCompletionPayloadIssues, } from "./phase-completion.helpers.js";
724
854
  export * from "./phase-completion.helpers.js";
@@ -0,0 +1,14 @@
1
+ import type { RunOnceDispatchPacket } from "../session.types.js";
2
+ export declare const ciRepairProviderOnlySpecs: readonly ["**/bridge-dispatch.spec.ts", "**/develop-agent-run.spec.ts", "**/runner-availability.spec.ts"];
3
+ export declare const ciRepairLocalPreflightCommands: readonly [{
4
+ readonly id: "bridge-dispatch-without-nested-control-plane";
5
+ readonly cwd: "tests/e2e";
6
+ readonly command: readonly ["pnpm", "exec", "playwright", "test", "specs/bridge-dispatch.spec.ts", "--grep-invert", "online bridge completes a newly created task"];
7
+ readonly coverage: "Runs Bridge API/setup, offline recovery, CSRF, and repo-root approval coverage while excluding only the nested live Bridge process.";
8
+ }];
9
+ export interface CiRepairReadinessResult {
10
+ readonly path: string;
11
+ readonly environment: Readonly<Record<string, string>>;
12
+ readonly manifest: Readonly<Record<string, unknown>>;
13
+ }
14
+ export declare function writeCiRepairReadinessManifest(dispatch: RunOnceDispatchPacket, worktreePath: string, specPath: string, runtimeEnvironment: Readonly<Record<string, string>> | undefined): Promise<CiRepairReadinessResult>;
@@ -0,0 +1,105 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ export const ciRepairProviderOnlySpecs = [
5
+ "**/bridge-dispatch.spec.ts",
6
+ "**/develop-agent-run.spec.ts",
7
+ "**/runner-availability.spec.ts",
8
+ ];
9
+ export const ciRepairLocalPreflightCommands = [{
10
+ id: "bridge-dispatch-without-nested-control-plane",
11
+ cwd: "tests/e2e",
12
+ command: [
13
+ "pnpm", "exec", "playwright", "test", "specs/bridge-dispatch.spec.ts",
14
+ "--grep-invert", "online bridge completes a newly created task",
15
+ ],
16
+ coverage: "Runs Bridge API/setup, offline recovery, CSRF, and repo-root approval coverage while excluding only the nested live Bridge process.",
17
+ }];
18
+ export async function writeCiRepairReadinessManifest(dispatch, worktreePath, specPath, runtimeEnvironment) {
19
+ const metadataRoot = path.dirname(specPath);
20
+ const manifestPath = path.join(metadataRoot, "ci-repair.readiness.json");
21
+ const workflowPath = path.join(worktreePath, ".github", "workflows", "ci.yml");
22
+ const browserReady = runtimeEnvironment?.FORGE_PLAYWRIGHT_BROWSER_READY === "true";
23
+ const providerOnlySpecsJson = JSON.stringify(ciRepairProviderOnlySpecs);
24
+ const apiUrl = runtimeEnvironment?.FORGE_E2E_API_URL ?? runtimeEnvironment?.FORGE_API_URL ?? null;
25
+ const webUrl = runtimeEnvironment?.FORGE_E2E_WEB_URL
26
+ ?? runtimeEnvironment?.FORGE_WEB_URL
27
+ ?? runtimeEnvironment?.FORGE_PLAYWRIGHT_BASE_URL
28
+ ?? null;
29
+ const environment = {
30
+ ...(runtimeEnvironment ?? {}),
31
+ ...(apiUrl ? { FORGE_E2E_API_URL: apiUrl } : {}),
32
+ ...(webUrl ? { FORGE_E2E_WEB_URL: webUrl } : {}),
33
+ FORGE_CI_REPAIR_READINESS_PATH: manifestPath,
34
+ FORGE_CI_REPAIR_HANDOFF_PATH: path.join(metadataRoot, "artifacts", "ci-repair.handoff.json"),
35
+ FORGE_CI_REPAIR_PROVIDER_ONLY_SPECS: providerOnlySpecsJson,
36
+ };
37
+ const manifest = {
38
+ schema_version: 1,
39
+ generated_by: "forge-bridge",
40
+ work_item_id: dispatch.workItem.id,
41
+ session_id: dispatch.session.id,
42
+ spec: {
43
+ path: relativePath(worktreePath, specPath),
44
+ ready: await isFile(specPath),
45
+ },
46
+ handoff: {
47
+ path: relativePath(worktreePath, environment.FORGE_CI_REPAIR_HANDOFF_PATH),
48
+ completion_tool: "forge.complete_ci_repair",
49
+ required_before_commit_or_push: true,
50
+ },
51
+ pull_request: {
52
+ provider: dispatch.pullRequest?.provider ?? "unknown",
53
+ number: dispatch.pullRequest?.number ?? null,
54
+ url: dispatch.pullRequest?.url ?? null,
55
+ source_branch: dispatch.pullRequest?.sourceBranch ?? dispatch.cycleRun.baseBranch,
56
+ target_branch: dispatch.pullRequest?.targetBranch ?? dispatch.cycleRun.requestedBase,
57
+ check_state: dispatch.pullRequest?.checkState ?? "unknown",
58
+ required_checks_passed: dispatch.pullRequest?.requiredChecksPassed ?? 0,
59
+ required_checks_total: dispatch.pullRequest?.requiredChecksTotal ?? 0,
60
+ failed_job_discovery: "Inspect provider check runs and record exact job/step in the completion payload.",
61
+ },
62
+ workflow: {
63
+ path: relativePath(worktreePath, workflowPath),
64
+ ready: await isFile(workflowPath),
65
+ requirement: "Record the failed step and every earlier guard/drift step in workflow_validation.",
66
+ },
67
+ runtime: {
68
+ mode: runtimeEnvironment?.FORGE_RUNTIME_MODE ?? "unknown",
69
+ application_runtime_ready: Boolean(apiUrl || webUrl),
70
+ api_url: apiUrl,
71
+ web_url: webUrl,
72
+ playwright_config: runtimeEnvironment?.FORGE_PLAYWRIGHT_CONFIG ?? null,
73
+ playwright_browser_cache: runtimeEnvironment?.PLAYWRIGHT_BROWSERS_PATH ?? null,
74
+ playwright_browser_ready: browserReady,
75
+ playwright_browser_install_command: browserReady
76
+ ? null
77
+ : runtimeEnvironment?.FORGE_PLAYWRIGHT_BROWSER_INSTALL_COMMAND ?? "npx playwright install chromium",
78
+ attempt_database_schema: runtimeEnvironment?.FORGE_ATTEMPT_DATABASE_SCHEMA ?? null,
79
+ attempt_database_readiness_path: runtimeEnvironment?.FORGE_ATTEMPT_DATABASE_READINESS_PATH ?? null,
80
+ },
81
+ local_equivalence: {
82
+ default: "supported",
83
+ nested_bridge: "provider_only",
84
+ provider_only_specs: ciRepairProviderOnlySpecs,
85
+ provider_only_specs_fingerprint: `sha256:${createHash("sha256").update(providerOnlySpecsJson).digest("hex")}`,
86
+ local_preflight_commands: ciRepairLocalPreflightCommands,
87
+ reason: "A managed Forge attempt cannot safely create a second Bridge/runner control plane inside itself. Only cases that launch a nested live Bridge remain provider-only; the surrounding API/setup coverage has a runnable local preflight.",
88
+ policy: "Run every applicable local_preflight_command. Do not delete, skip, or weaken provider CI coverage; record only the exact nested-control-plane case as provider_only in workflow_validation.",
89
+ },
90
+ };
91
+ await fs.mkdir(metadataRoot, { recursive: true });
92
+ await fs.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
93
+ return { path: manifestPath, environment, manifest };
94
+ }
95
+ async function isFile(candidate) {
96
+ try {
97
+ return (await fs.stat(candidate)).isFile();
98
+ }
99
+ catch {
100
+ return false;
101
+ }
102
+ }
103
+ function relativePath(worktreePath, candidate) {
104
+ return path.relative(worktreePath, candidate).replaceAll(path.sep, "/");
105
+ }
@@ -1,6 +1,6 @@
1
1
  import { GitClient } from "../../shared/git/git.js";
2
2
  import { type CompletionPhase, type CompletionToolRuntime, type DebugCompletionPhase, type QaCompletionGate } from "../phase-completion.js";
3
- import type { RunOnceDispatchPacket, RunnerDispatchArtifact } from "../session.types.js";
3
+ import type { RunOnceDispatchPacket, RunnerDispatchArtifact, RunnerDispatchSidecar } from "../session.types.js";
4
4
  import type { ProviderCliRunContext } from "./provider-cli.types.js";
5
5
  export declare function emitDraftGateArtifacts(emit: ProviderCliRunContext["emit"], worktreePath: string, workItemId: string): Promise<void>;
6
6
  export declare function emitDecomposeGateArtifacts(emit: ProviderCliRunContext["emit"], worktreePath: string, workItemId: string): Promise<void>;
@@ -18,8 +18,9 @@ export declare function isDebugCompletionPhaseForProvider(phase: CompletionPhase
18
18
  export declare function emitQaArtifactRecorded(emit: ProviderCliRunContext["emit"], worktreePath: string, artifact: RunnerDispatchArtifact): Promise<void>;
19
19
  export declare function readQaCompletionArtifactBundle(worktreePath: string, workItemId: string, ownerType?: string | null, ownerId?: string | null): Promise<RunnerDispatchArtifact | null>;
20
20
  export declare function hasCompleteQaArtifactBundle(worktreePath: string, workItemId: string, ownerType?: string | null, ownerId?: string | null): Promise<boolean>;
21
- export declare function enforceQaBrowserLaunchTargetGate(dispatch: RunOnceDispatchPacket, artifact: RunnerDispatchArtifact): void;
21
+ export declare function enforceQaBrowserLaunchTargetGate(dispatch: RunOnceDispatchPacket, artifact: RunnerDispatchArtifact): Promise<void>;
22
22
  export declare function validateQaBundleSidecarReferences(worktreePath: string, workItemId: string, ownerType?: string | null, ownerId?: string | null): Promise<void>;
23
+ export declare function readQaBundleSidecars(worktreePath: string, workItemId: string, ownerType?: string | null, ownerId?: string | null): Promise<RunnerDispatchSidecar[]>;
23
24
  export declare function emitQaStructuredEvents(emit: ProviderCliRunContext["emit"], artifact: RunnerDispatchArtifact): Promise<void>;
24
25
  interface QaGateArtifactSnapshot {
25
26
  readonly gate: QaCompletionGate;
@@ -4,7 +4,10 @@ import { artifactRootFor, artifactRootForOwner, debugCompletionPhaseOrder, draft
4
4
  import { ensureNoSymbolicLinkPathSegments, isPathUnderRoot, sha256File } from "../qa-sidecar-validation.js";
5
5
  import { hasLaunchTarget } from "./provider-cli.qa-sidecars.js";
6
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;
7
9
  const developCyclePhaseOrder = ["implementation", "self_review", "validation"];
10
+ const nonPublishableWorktreePaths = [".forge", ".local"];
8
11
  export async function emitDraftGateArtifacts(emit, worktreePath, workItemId) {
9
12
  const draftArtifactFiles = {
10
13
  draft_intent: "draft.intent.json",
@@ -147,7 +150,9 @@ export async function cleanupCompletionTools(completionTools, emit) {
147
150
  }
148
151
  export async function commitWorktreeChanges(git, worktreePath, workItemId) {
149
152
  await git.run(worktreePath, ["add", "-A"]);
150
- await git.run(worktreePath, ["reset", "--", ".forge"]).catch(() => "");
153
+ for (const reservedPath of nonPublishableWorktreePaths) {
154
+ await git.run(worktreePath, ["reset", "--", reservedPath]).catch(() => "");
155
+ }
151
156
  const changedFiles = (await git.run(worktreePath, ["diff", "--cached", "--name-only"]))
152
157
  .split(/\r?\n/)
153
158
  .map((line) => line.trim())
@@ -209,12 +214,13 @@ export async function readQaCompletionArtifactBundle(worktreePath, workItemId, o
209
214
  const planJson = await fs.readFile(path.join(artifactRoot, qaArtifactFileNames.plan), "utf8");
210
215
  const evidenceJson = await fs.readFile(path.join(artifactRoot, qaArtifactFileNames.evidence), "utf8");
211
216
  const findingsJson = await fs.readFile(path.join(artifactRoot, qaArtifactFileNames.findings), "utf8");
212
- await validateQaBundleSidecarReferences(worktreePath, workItemId, ownerType, ownerId);
217
+ const sidecars = await readQaBundleSidecars(worktreePath, workItemId, ownerType, ownerId);
213
218
  return {
214
219
  ...artifact,
215
220
  planJson,
216
221
  evidenceJson,
217
- findingsJson
222
+ findingsJson,
223
+ sidecars
218
224
  };
219
225
  }
220
226
  export async function hasCompleteQaArtifactBundle(worktreePath, workItemId, ownerType, ownerId) {
@@ -237,28 +243,55 @@ export async function hasCompleteQaArtifactBundle(worktreePath, workItemId, owne
237
243
  }
238
244
  return true;
239
245
  }
240
- export function enforceQaBrowserLaunchTargetGate(dispatch, artifact) {
246
+ export async function enforceQaBrowserLaunchTargetGate(dispatch, artifact) {
241
247
  const qaBrowser = dispatch.qaBrowser;
242
248
  if (!qaBrowser?.required || hasLaunchTarget(qaBrowser.launchTarget ?? null)) {
243
249
  return;
244
250
  }
251
+ const evidence = await readJsonObjectFile(path.join(path.dirname(artifact.path), qaArtifactFileNames.evidence));
252
+ if (hasPassingRepositoryPlaywrightEvidence(evidence)) {
253
+ return;
254
+ }
245
255
  const verdict = parseJsonLine(artifact.verdictJson) ?? {};
246
256
  const status = normalizedString(stringField(verdict, "status") ?? artifact.status ?? "");
247
257
  const blockerReason = normalizedString(stringField(verdict, "blocker_reason") ?? "");
248
258
  if (status !== "blocked" || blockerReason !== "missing_app_launch_target") {
249
- throw new Error("QA browser testing is required but no app launch target was provided; provider must return blocked with blocker_reason 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;
250
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
+ });
251
278
  }
252
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) {
253
283
  const artifactRoot = ownerType && ownerId
254
284
  ? artifactRootForOwner(worktreePath, ownerType, ownerId)
255
285
  : artifactRootFor(worktreePath, workItemId);
256
286
  const evidence = await readJsonObjectFile(path.join(artifactRoot, qaArtifactFileNames.evidence));
257
287
  const sidecars = arrayField(evidence, "sidecars");
258
288
  if (sidecars.length === 0) {
259
- return;
289
+ return [];
260
290
  }
261
291
  const qaSidecarRoot = path.resolve(artifactRoot, "qa");
292
+ const transported = [];
293
+ const transportedByPath = new Map();
294
+ let totalBytes = 0;
262
295
  for (const [index, sidecar] of sidecars.entries()) {
263
296
  const sidecarPath = stringField(sidecar, "path");
264
297
  const expectedHash = stringField(sidecar, "content_hash");
@@ -288,11 +321,36 @@ export async function validateQaBundleSidecarReferences(worktreePath, workItemId
288
321
  if (stat.size !== expectedBytes) {
289
322
  throw new Error(`QA sidecar ${index} bytes do not match the referenced file.`);
290
323
  }
324
+ if (stat.size > maxTransportedQaSidecarBytes) {
325
+ throw new Error(`QA sidecar ${index} exceeds the ${maxTransportedQaSidecarBytes}-byte transport limit.`);
326
+ }
291
327
  const actualHash = await sha256File(fullPath);
292
328
  if (actualHash !== expectedHash.trim().toLowerCase()) {
293
329
  throw new Error(`QA sidecar ${index} content_hash does not match the referenced file.`);
294
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
+ });
295
352
  }
353
+ return transported;
296
354
  }
297
355
  export async function emitQaStructuredEvents(emit, artifact) {
298
356
  const artifactRoot = path.dirname(artifact.path);
@@ -444,7 +502,7 @@ export class LiveQaArtifactEmitter {
444
502
  if (!artifact) {
445
503
  return;
446
504
  }
447
- enforceQaBrowserLaunchTargetGate(this.dispatch, artifact);
505
+ await enforceQaBrowserLaunchTargetGate(this.dispatch, artifact);
448
506
  this.emittedBundle = true;
449
507
  await emitQaArtifactRecorded(this.emit, this.worktreePath, artifact);
450
508
  await emitQaStructuredEvents(this.emit, artifact);
@@ -8,6 +8,7 @@ import { deterministicBrowserCheck, deterministicBrowserSidecar, hasLaunchTarget
8
8
  import { buildProviderCliCommand, formatProviderCliCommand, isPullRequestCheckRepairDispatch, isPullRequestCommentsDispatch, singleLine } from "./provider-cli.prompts.js";
9
9
  import { estimatedDeterministicUsage, exactRedactionSecrets } from "./provider-cli.runtime.js";
10
10
  import { buildProviderUsageEvent } from "./provider-usage.js";
11
+ import { writeCiRepairReadinessManifest } from "./ci-repair-readiness.js";
11
12
  import { deterministicArchitectPayload, deterministicCompletionPayload, deterministicQaGatePayload } from "./provider-cli.deterministic-payloads.js";
12
13
  export class DeterministicProviderCliService {
13
14
  git;
@@ -212,6 +213,31 @@ export class DeterministicProviderCliService {
212
213
  const { dispatch, worktree } = context;
213
214
  const repairChecks = route === "ci_repair" || isPullRequestCheckRepairDispatch(dispatch);
214
215
  const addressComments = isPullRequestCommentsDispatch(dispatch);
216
+ let repairArtifact = null;
217
+ let repairSpec = null;
218
+ if (repairChecks) {
219
+ const metadataRoot = path.join(worktree.worktreePath, ".forge", "work-items", dispatch.workItem.id);
220
+ await fs.mkdir(metadataRoot, { recursive: true });
221
+ const laneCommand = selectLaneCommand(dispatch.artifacts, dispatch.execution.lane, dispatch.cycleRun.phase);
222
+ if (!laneCommand)
223
+ throw new Error("Deterministic CI repair could not materialize the approved lane spec.");
224
+ const specBody = buildSpecBody(laneCommand, dispatch.workItem, "ci_repair");
225
+ const specPath = path.join(metadataRoot, "spec.md");
226
+ await fs.writeFile(specPath, specBody, "utf8");
227
+ await fs.writeFile(path.join(metadataRoot, "spec.meta.json"), `${JSON.stringify({ schema_version: 1, status: "approved", produced_by: "forge-lane-command" })}\n`, "utf8");
228
+ repairSpec = { repoPath: worktree.worktreePath, path: specPath, body: specBody, producedBy: "forge-lane-command", status: "approved" };
229
+ await writeCiRepairReadinessManifest(dispatch, worktree.worktreePath, specPath, context.runtimeEnvironment);
230
+ const reference = await writeCompletionArtifact({
231
+ worktreePath: worktree.worktreePath,
232
+ workItemId: dispatch.workItem.id,
233
+ phase: "ci_repair",
234
+ producedBy: "forge-deterministic-provider",
235
+ }, expectedCompletionToolName("ci_repair"), deterministicCiRepairPayload());
236
+ repairArtifact = await readCompletionArtifact(worktree.worktreePath, dispatch.workItem.id, "ci_repair");
237
+ if (!repairArtifact)
238
+ throw new Error(`Deterministic CI repair did not produce ${reference.path}.`);
239
+ await emitCompletionArtifactRecorded(context.emit, worktree.worktreePath, repairArtifact);
240
+ }
215
241
  await this.git.run(worktree.worktreePath, ["commit", "--allow-empty", "-m", `forge bridge ${repairChecks ? "ci repair" : addressComments ? "pr comments" : "conflict resolve"} for ${dispatch.workItem.id.slice(0, 8)}`]);
216
242
  await context.emit(buildProviderUsageEvent(dispatch, estimatedDeterministicUsage()));
217
243
  await context.emit({
@@ -229,10 +255,30 @@ export class DeterministicProviderCliService {
229
255
  ? `forge-bridge repaired PR checks for ${worktree.branchName}.`
230
256
  : addressComments
231
257
  ? `forge-bridge addressed PR comments for ${worktree.branchName}.`
232
- : `forge-bridge resolved conflicts for ${worktree.branchName}.`
258
+ : `forge-bridge resolved conflicts for ${worktree.branchName}.`,
259
+ spec: repairSpec,
260
+ artifact: repairArtifact,
261
+ artifacts: repairArtifact ? [repairArtifact] : [],
233
262
  };
234
263
  }
235
264
  }
265
+ function deterministicCiRepairPayload() {
266
+ const command = { command: "forge deterministic ci-repair validation", exit_code: 0, status: "pass", summary: "Deterministic CI repair fixture passed." };
267
+ return {
268
+ status: "pass",
269
+ summary: "Deterministic CI repair handoff is ready for provider checks.",
270
+ failed_checks: [{ name: "CI / deterministic", provider_state: "failure", workflow: ".github/workflows/ci.yml", job: "deterministic", step: "fixture" }],
271
+ root_cause: { classification: "test expectation mismatch", summary: "Deterministic fixture failure repaired.", evidence: ["Fixture provider reported the expected failed check."] },
272
+ changed_files: [],
273
+ no_code_action: { action: "deterministic_fixture", reason: "The deterministic provider models the repair without a product diff." },
274
+ narrow_validation: [command],
275
+ workflow_validation: [{ step: "deterministic fixture", equivalence: "supported", command: command.command, exit_code: 0, status: "pass", summary: command.summary }],
276
+ broader_validation: [command],
277
+ remaining_risks: [],
278
+ provider_state: "ready_for_rerun",
279
+ ready_for_push: true,
280
+ };
281
+ }
236
282
  function deterministicDraftPayload(dispatch) {
237
283
  const kind = dispatch.draft?.kind ?? "work_item";
238
284
  if (kind === "work_order_decomposition") {