@loucompanion/forge-bridge 0.1.3-dev.9bc5014a2270 → 0.1.3-dev.a58d7cd1092f

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 (168) hide show
  1. package/README.md +127 -0
  2. package/dist/bridge/bridge.base.d.ts +8 -1
  3. package/dist/bridge/bridge.service.d.ts +93 -3
  4. package/dist/bridge/bridge.service.js +1358 -47
  5. package/dist/bridge/bridge.types.d.ts +202 -1
  6. package/dist/bridge/runner-host-capabilities.d.ts +15 -0
  7. package/dist/bridge/runner-host-capabilities.js +93 -0
  8. package/dist/cleanup/cleanup.service.d.ts +14 -2
  9. package/dist/cleanup/cleanup.service.js +496 -3
  10. package/dist/cleanup/cleanup.types.d.ts +10 -0
  11. package/dist/cleanup/index.d.ts +1 -1
  12. package/dist/cleanup/index.js +1 -1
  13. package/dist/cleanup/internals.d.ts +2 -2
  14. package/dist/cleanup/internals.js +1 -1
  15. package/dist/cli/cli.help.js +47 -1
  16. package/dist/cli/cli.service.d.ts +45 -2
  17. package/dist/cli/cli.service.js +378 -26
  18. package/dist/cli/local-config.d.ts +7 -0
  19. package/dist/cli/local-config.js +51 -3
  20. package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
  21. package/dist/runtime/aspnet-runtime-prebuild.js +340 -0
  22. package/dist/runtime/attempt-node-runtime-launcher.d.ts +17 -0
  23. package/dist/runtime/attempt-node-runtime-launcher.js +105 -0
  24. package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
  25. package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
  26. package/dist/runtime/attempt-process-supervisor.d.ts +97 -0
  27. package/dist/runtime/attempt-process-supervisor.js +386 -0
  28. package/dist/runtime/docker-engine-probe.d.ts +83 -0
  29. package/dist/runtime/docker-engine-probe.js +300 -0
  30. package/dist/runtime/generated-output-reset.d.ts +10 -0
  31. package/dist/runtime/generated-output-reset.js +103 -0
  32. package/dist/runtime/host-listener-inspector.d.ts +46 -0
  33. package/dist/runtime/host-listener-inspector.js +349 -0
  34. package/dist/runtime/host-port-broker.d.ts +76 -0
  35. package/dist/runtime/host-port-broker.js +333 -0
  36. package/dist/runtime/host-runtime-contract.d.ts +47 -0
  37. package/dist/runtime/host-runtime-contract.js +700 -0
  38. package/dist/runtime/host-runtime-lease.d.ts +197 -0
  39. package/dist/runtime/host-runtime-lease.js +2203 -0
  40. package/dist/runtime/host-runtime-promotion.d.ts +23 -0
  41. package/dist/runtime/host-runtime-promotion.js +159 -0
  42. package/dist/runtime/memory-capacity.d.ts +68 -0
  43. package/dist/runtime/memory-capacity.js +121 -0
  44. package/dist/runtime/node-host-runtime-adapter.d.ts +54 -0
  45. package/dist/runtime/node-host-runtime-adapter.js +284 -0
  46. package/dist/runtime/playwright-host-runtime-adapter.d.ts +21 -0
  47. package/dist/runtime/playwright-host-runtime-adapter.js +231 -0
  48. package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
  49. package/dist/runtime/runtime-isolation-lease.js +158 -0
  50. package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
  51. package/dist/runtime/workspace-resource-estimator.js +174 -0
  52. package/dist/sandbox/command-runner.d.ts +4 -0
  53. package/dist/sandbox/command-runner.js +47 -0
  54. package/dist/sandbox/contract.d.ts +5 -0
  55. package/dist/sandbox/contract.js +276 -0
  56. package/dist/sandbox/event-budget.d.ts +11 -0
  57. package/dist/sandbox/event-budget.js +27 -0
  58. package/dist/sandbox/image-policy.d.ts +35 -0
  59. package/dist/sandbox/image-policy.js +247 -0
  60. package/dist/sandbox/index.d.ts +5 -0
  61. package/dist/sandbox/index.js +5 -0
  62. package/dist/sandbox/lease-watchdog.d.ts +18 -0
  63. package/dist/sandbox/lease-watchdog.js +67 -0
  64. package/dist/sandbox/preflight.d.ts +84 -0
  65. package/dist/sandbox/preflight.js +521 -0
  66. package/dist/sandbox/process.d.ts +20 -0
  67. package/dist/sandbox/process.js +209 -0
  68. package/dist/sandbox/provider-auth/codex.d.ts +26 -0
  69. package/dist/sandbox/provider-auth/codex.js +212 -0
  70. package/dist/sandbox/repository-lock.d.ts +5 -0
  71. package/dist/sandbox/repository-lock.js +121 -0
  72. package/dist/sandbox/runtime.d.ts +297 -0
  73. package/dist/sandbox/runtime.js +2338 -0
  74. package/dist/sandbox/sandbox.types.d.ts +93 -0
  75. package/dist/sandbox/sandbox.types.js +1 -0
  76. package/dist/sandbox/smoke.d.ts +76 -0
  77. package/dist/sandbox/smoke.js +380 -0
  78. package/dist/sandbox/worker-supervisor.d.ts +13 -0
  79. package/dist/sandbox/worker-supervisor.js +36 -0
  80. package/dist/sandbox/worker.d.ts +1 -0
  81. package/dist/sandbox/worker.js +225 -0
  82. package/dist/sandbox/workspace.service.d.ts +13 -0
  83. package/dist/sandbox/workspace.service.js +72 -0
  84. package/dist/session/managed-attempt-tools.d.ts +21 -0
  85. package/dist/session/managed-attempt-tools.js +912 -0
  86. package/dist/session/managed-build-coordinator.d.ts +5 -0
  87. package/dist/session/managed-build-coordinator.js +105 -0
  88. package/dist/session/managed-build-receipt.d.ts +21 -0
  89. package/dist/session/managed-build-receipt.js +335 -0
  90. package/dist/session/phase-completion.artifacts.d.ts +17 -0
  91. package/dist/session/phase-completion.artifacts.js +256 -26
  92. package/dist/session/phase-completion.d.ts +12 -2
  93. package/dist/session/phase-completion.helpers.d.ts +5 -0
  94. package/dist/session/phase-completion.helpers.js +51 -10
  95. package/dist/session/phase-completion.js +14 -3
  96. package/dist/session/phase-completion.prompts.d.ts +3 -3
  97. package/dist/session/phase-completion.prompts.js +24 -8
  98. package/dist/session/phase-completion.runtime.d.ts +13 -1
  99. package/dist/session/phase-completion.runtime.js +189 -8
  100. package/dist/session/phase-completion.schemas.js +117 -4
  101. package/dist/session/phase-completion.validation.d.ts +4 -1
  102. package/dist/session/phase-completion.validation.js +153 -22
  103. package/dist/session/pre-publication-validation.d.ts +46 -0
  104. package/dist/session/pre-publication-validation.js +208 -0
  105. package/dist/session/provider-cli/ci-repair-readiness.d.ts +16 -0
  106. package/dist/session/provider-cli/ci-repair-readiness.js +110 -0
  107. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +3 -2
  108. package/dist/session/provider-cli/provider-cli.artifacts.js +65 -7
  109. package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
  110. package/dist/session/provider-cli/provider-cli.deterministic.d.ts +2 -0
  111. package/dist/session/provider-cli/provider-cli.deterministic.js +66 -9
  112. package/dist/session/provider-cli/provider-cli.process.d.ts +26 -0
  113. package/dist/session/provider-cli/provider-cli.process.js +474 -23
  114. package/dist/session/provider-cli/provider-cli.prompts.d.ts +16 -2
  115. package/dist/session/provider-cli/provider-cli.prompts.js +224 -24
  116. package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
  117. package/dist/session/provider-cli/provider-cli.runtime.d.ts +1 -1
  118. package/dist/session/provider-cli/provider-cli.runtime.js +6 -3
  119. package/dist/session/provider-cli/provider-cli.service.d.ts +26 -3
  120. package/dist/session/provider-cli/provider-cli.service.js +646 -90
  121. package/dist/session/provider-cli/provider-cli.types.d.ts +5 -0
  122. package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
  123. package/dist/session/provider-cli/provider-output-state.js +91 -16
  124. package/dist/session/provider-cli/provider-usage.d.ts +7 -0
  125. package/dist/session/provider-cli/provider-usage.js +28 -5
  126. package/dist/session/session-redaction.d.ts +3 -0
  127. package/dist/session/session-redaction.js +38 -0
  128. package/dist/session/session.base.d.ts +6 -1
  129. package/dist/session/session.service.d.ts +8 -2
  130. package/dist/session/session.service.js +530 -42
  131. package/dist/session/session.types.d.ts +124 -1
  132. package/dist/session/session.types.js +231 -4
  133. package/dist/shared/format/format-bytes.d.ts +1 -0
  134. package/dist/shared/format/format-bytes.js +9 -0
  135. package/dist/shared/path/path.d.ts +1 -0
  136. package/dist/shared/path/path.js +21 -1
  137. package/dist/shared/process/dotnet-toolchain.d.ts +14 -0
  138. package/dist/shared/process/dotnet-toolchain.js +179 -0
  139. package/dist/shared/process/host-process-recovery.d.ts +12 -0
  140. package/dist/shared/process/host-process-recovery.js +78 -0
  141. package/dist/shared/process/host-process-supervisor.d.ts +59 -0
  142. package/dist/shared/process/host-process-supervisor.js +330 -0
  143. package/dist/shared/process/process.d.ts +1 -0
  144. package/dist/shared/process/process.js +1 -0
  145. package/dist/shared/process/windows-launch.d.ts +1 -1
  146. package/dist/shared/process/windows-launch.js +6 -3
  147. package/dist/shared/provider-capabilities.d.ts +1 -0
  148. package/dist/shared/provider-capabilities.js +3 -0
  149. package/dist/shared/redaction/redaction.js +16 -4
  150. package/dist/version.d.ts +1 -1
  151. package/dist/version.js +1 -1
  152. package/dist/worktree/artifact-injector.d.ts +3 -2
  153. package/dist/worktree/artifact-injector.js +43 -4
  154. package/dist/worktree/dependency-warmer.d.ts +36 -2
  155. package/dist/worktree/dependency-warmer.js +347 -11
  156. package/dist/worktree/index.d.ts +2 -1
  157. package/dist/worktree/index.js +1 -0
  158. package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
  159. package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
  160. package/dist/worktree/worktree-paths.d.ts +4 -0
  161. package/dist/worktree/worktree-paths.js +30 -0
  162. package/dist/worktree/worktree.base.d.ts +3 -0
  163. package/dist/worktree/worktree.service.d.ts +13 -2
  164. package/dist/worktree/worktree.service.js +238 -47
  165. package/dist/worktree/worktree.types.d.ts +5 -1
  166. package/package.json +8 -4
  167. package/runtime/windows-job-supervisor.ps1 +444 -0
  168. package/runtime/windows-listener-owner.ps1 +33 -0
@@ -1,4 +1,4 @@
1
- import { qaBlockerReasons, qaCandidateBugActions, qaFindingScopes, qaFindingSeverities, qaGateToolNames, qaScopeAuthorities, qaScopeConfidence, qaScopeEvidence, qaScopeFields, qaScopeSources, qaScopeSurfaces } from "./phase-completion.js";
1
+ import { qaBlockerReasons, qaCandidateBugActions, qaFindingScopes, qaFindingSeverities, qaGateToolNames, qaScopeAuthorities, qaScopeCapabilities, qaScopeConfidence, qaScopeEvidence, qaScopeFields, qaScopeSources, qaScopeSurfaces } from "./phase-completion.js";
2
2
  export function normalizeCompletionStatus(value, phase) {
3
3
  const normalized = value.trim().toLowerCase().replace(/-/g, "_");
4
4
  if (phase === "qa") {
@@ -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) {
@@ -269,6 +391,7 @@ export function validateQaScopeRecord(scope, pathLabel) {
269
391
  }
270
392
  const surface = qaScopeStringArray(scope, "surface", pathLabel, qaScopeSurfaces);
271
393
  const requiredEvidence = qaScopeStringArray(scope, "required_evidence", pathLabel, qaScopeEvidence);
394
+ qaScopeStringArray(scope, "required_capabilities", pathLabel, qaScopeCapabilities);
272
395
  const notApplicable = qaScopeStringArray(scope, "not_applicable", pathLabel, qaScopeEvidence);
273
396
  const requiredSet = new Set(requiredEvidence);
274
397
  for (const evidence of notApplicable) {
@@ -611,22 +734,30 @@ export function validateQaPayloadShape(payload, status) {
611
734
  throw new Error("QA pass/needs_rework requires at least one command, browser check, or evidence note.");
612
735
  }
613
736
  }
737
+ const commandIssues = [];
614
738
  for (const [index, command] of commands.entries()) {
615
- requireEvidenceRecord(command, `commands[${index}]`);
739
+ if (!command || typeof command !== "object" || Array.isArray(command)) {
740
+ commandIssues.push(`commands[${index}] must be an object.`);
741
+ continue;
742
+ }
616
743
  const record = command;
617
- requireNonEmptyStringRecord(record, "command", `commands[${index}].`);
618
- requireNonEmptyStringRecord(record, "status", `commands[${index}].`);
619
- requireNonEmptyStringRecord(record, "summary", `commands[${index}].`);
744
+ if (!optionalString(record, "command"))
745
+ commandIssues.push(`commands[${index}].command is required.`);
746
+ if (!optionalString(record, "status"))
747
+ commandIssues.push(`commands[${index}].status is required.`);
748
+ if (!optionalString(record, "summary"))
749
+ commandIssues.push(`commands[${index}].summary is required.`);
620
750
  if (typeof record.exit_code !== "number" || !Number.isFinite(record.exit_code)) {
621
- throw new Error(`commands[${index}].exit_code is required.`);
751
+ commandIssues.push(`commands[${index}].exit_code is required.`);
622
752
  }
623
- if (status === "pass" && record.exit_code !== 0) {
624
- throw new Error(`commands[${index}].exit_code must be 0 when QA status is pass.`);
753
+ else if (status === "pass" && record.exit_code !== 0) {
754
+ commandIssues.push(`commands[${index}].exit_code must be 0 when QA status is pass.`);
625
755
  }
626
756
  if (status === "pass" && normalizedString(record.status) !== "pass") {
627
- throw new Error(`commands[${index}].status must be pass when QA status is pass.`);
757
+ commandIssues.push(`commands[${index}].status must be pass when QA status is pass.`);
628
758
  }
629
759
  }
760
+ throwCompletionPayloadIssues(commandIssues);
630
761
  for (const [index, browserCheck] of browserChecks.entries()) {
631
762
  requireEvidenceRecord(browserCheck, `browser_checks[${index}]`);
632
763
  const record = browserCheck;
@@ -720,5 +851,5 @@ export function validateOptionalWorkItemId(payload, expectedWorkItemId) {
720
851
  throw new Error(`completion payload work item ${actual} does not match dispatch work item ${expectedWorkItemId}.`);
721
852
  }
722
853
  }
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";
854
+ 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
855
  export * from "./phase-completion.helpers.js";
@@ -0,0 +1,46 @@
1
+ import type { ProviderProcessRunner } from "./provider-cli/provider-cli.process.js";
2
+ import type { RunnerDispatchEvent } from "./session.types.js";
3
+ export type PrePublicationProfile = "implementation" | "comment_repair" | "ci_repair" | "conflict_repair";
4
+ interface ValidationStep {
5
+ readonly command: string;
6
+ readonly fallbackCommand?: string;
7
+ readonly args: readonly string[];
8
+ readonly fallbackArgs?: readonly string[];
9
+ readonly assertUnchangedPaths?: readonly string[];
10
+ }
11
+ interface ValidationCheck {
12
+ readonly id: string;
13
+ readonly profiles: readonly string[];
14
+ readonly changedPathPatterns: readonly string[];
15
+ readonly requiredCapability?: string;
16
+ readonly providerOnly?: boolean;
17
+ readonly steps: readonly ValidationStep[];
18
+ }
19
+ interface ValidationContract {
20
+ readonly schemaVersion: number;
21
+ readonly contractId: string;
22
+ readonly checks: readonly ValidationCheck[];
23
+ }
24
+ export interface PrePublicationCommandResult {
25
+ readonly exitCode: number;
26
+ readonly stdout?: string;
27
+ readonly stderr?: string;
28
+ }
29
+ export interface PrePublicationCommandRunner {
30
+ run(command: string, args: readonly string[], cwd: string, signal?: AbortSignal, environment?: Readonly<Record<string, string>>): Promise<PrePublicationCommandResult>;
31
+ }
32
+ export interface PrePublicationExecutionContext {
33
+ readonly environment?: Readonly<Record<string, string>>;
34
+ readonly processRunner?: ProviderProcessRunner;
35
+ }
36
+ export declare class PrePublicationValidator {
37
+ private readonly commands;
38
+ constructor(commands?: PrePublicationCommandRunner);
39
+ validate(worktreePath: string, profile: PrePublicationProfile, capabilities: ReadonlySet<string>, emit?: (event: RunnerDispatchEvent) => Promise<void>, signal?: AbortSignal, execution?: PrePublicationExecutionContext): Promise<readonly string[]>;
40
+ private changedPaths;
41
+ private runStep;
42
+ private runCommand;
43
+ }
44
+ export declare function applicableChecks(contract: ValidationContract, profile: PrePublicationProfile | "github_actions", changedPaths: readonly string[]): readonly ValidationCheck[];
45
+ export declare function loadValidationContract(worktreePath: string): Promise<ValidationContract | null>;
46
+ export {};
@@ -0,0 +1,208 @@
1
+ import { execFile } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { promisify } from "node:util";
6
+ const execFileAsync = promisify(execFile);
7
+ const contractRelativePath = ".forge/validation/ci-parity.v1.json";
8
+ const requiredCheckIds = new Set([
9
+ "file-size-inventory",
10
+ "owned-suite-registration",
11
+ "dotnet-build-and-unit",
12
+ "openapi-and-generated-types",
13
+ "client",
14
+ "bridge",
15
+ "playwright-e2e",
16
+ "github-provider-policy",
17
+ ]);
18
+ const approvedContractDigest = "014727b066b2b659b8772987650b85540f1bf70864c678187fde108a9b1d84d3";
19
+ class ProcessPrePublicationCommandRunner {
20
+ async run(command, args, cwd, signal, environment) {
21
+ try {
22
+ const windowsBatch = process.platform === "win32" && (command === "npm" || command === "pnpm");
23
+ const executable = windowsBatch ? (process.env.ComSpec ?? "cmd.exe") : command;
24
+ const commandArgs = windowsBatch
25
+ ? ["/d", "/s", "/c", [`${command}.cmd`, ...args].map(quoteWindowsCommandArgument).join(" ")]
26
+ : [...args];
27
+ const result = await execFileAsync(executable, commandArgs, {
28
+ cwd,
29
+ encoding: "utf8",
30
+ maxBuffer: 4 * 1024 * 1024,
31
+ signal,
32
+ env: environment ? { ...process.env, ...environment } : process.env,
33
+ });
34
+ return { exitCode: 0, stdout: result.stdout, stderr: result.stderr };
35
+ }
36
+ catch (error) {
37
+ const failure = error;
38
+ if (failure.name === "AbortError")
39
+ throw error;
40
+ if (failure.code === "ENOENT") {
41
+ return { exitCode: 127, stderr: `executable_not_found: ${command}` };
42
+ }
43
+ return {
44
+ exitCode: typeof failure.code === "number" ? failure.code : 1,
45
+ stdout: failure.stdout,
46
+ stderr: failure.stderr ?? failure.message,
47
+ };
48
+ }
49
+ }
50
+ }
51
+ function quoteWindowsCommandArgument(value) {
52
+ return /^[A-Za-z0-9_./:@=-]+$/u.test(value) ? value : `"${value.replaceAll('"', '""')}"`;
53
+ }
54
+ export class PrePublicationValidator {
55
+ commands;
56
+ constructor(commands = new ProcessPrePublicationCommandRunner()) {
57
+ this.commands = commands;
58
+ }
59
+ async validate(worktreePath, profile, capabilities, emit, signal, execution) {
60
+ const contract = await loadValidationContract(worktreePath);
61
+ if (contract === null) {
62
+ await emit?.(validationEvent("prepublication_validation_legacy_contract", "legacy-provider-validation", `This checkout predates ${contractRelativePath}; Bridge preserved legacy provider validation without source mutation.`));
63
+ return [];
64
+ }
65
+ const changedPaths = await this.changedPaths(worktreePath, signal, execution);
66
+ const selected = applicableChecks(contract, profile, changedPaths);
67
+ const completed = [];
68
+ for (const check of selected) {
69
+ if (check.providerOnly) {
70
+ await emit?.(validationEvent("prepublication_validation_provider_only", check.id, `Preserved provider-only validation '${check.id}' for the provider check run.`));
71
+ continue;
72
+ }
73
+ if (check.requiredCapability && !capabilities.has(check.requiredCapability)) {
74
+ throw new Error(`prepublication_capability_unavailable: ${check.id} requires ${check.requiredCapability}; publication is holding until a capable runner is available.`);
75
+ }
76
+ await emit?.(validationEvent("prepublication_validation_started", check.id, `Running pre-publication CI parity check '${check.id}'.`));
77
+ for (const step of check.steps) {
78
+ const result = await this.runStep(step, worktreePath, signal, execution);
79
+ if (result.exitCode !== 0) {
80
+ const detail = `${result.stderr ?? result.stdout ?? ""}`.trim().slice(-1600);
81
+ throw new Error(`prepublication_validation_failed: ${check.id} exited ${result.exitCode}${detail ? `: ${detail}` : ""}`);
82
+ }
83
+ }
84
+ completed.push(check.id);
85
+ await emit?.(validationEvent("prepublication_validation_passed", check.id, `Pre-publication CI parity check '${check.id}' passed.`));
86
+ }
87
+ return completed;
88
+ }
89
+ async changedPaths(worktreePath, signal, execution) {
90
+ const requests = [
91
+ ["diff", "--name-only", "HEAD", "--"],
92
+ ["diff", "--name-only", "HEAD^", "HEAD", "--"],
93
+ ["status", "--porcelain=v1", "--untracked-files=all"],
94
+ ];
95
+ const paths = new Set();
96
+ for (const args of requests) {
97
+ const result = await this.runCommand("git", args, worktreePath, signal, execution);
98
+ if (result.exitCode !== 0 && !isMissingParentRevision(args, result)) {
99
+ throw new Error(`prepublication_validation_failed: unable to inventory changed paths: ${result.stderr ?? result.stdout ?? "git failed"}`);
100
+ }
101
+ if (result.exitCode !== 0)
102
+ continue;
103
+ for (const line of `${result.stdout ?? ""}`.split(/\r?\n/u).filter(Boolean)) {
104
+ const raw = args[0] === "status" ? line.slice(3).split(" -> ").at(-1) ?? "" : line;
105
+ const normalized = raw.trim().replaceAll("\\", "/");
106
+ if (normalized)
107
+ paths.add(normalized);
108
+ }
109
+ }
110
+ return [...paths];
111
+ }
112
+ async runStep(step, cwd, signal, execution) {
113
+ const before = await readValidationFiles(cwd, step.assertUnchangedPaths ?? []);
114
+ const result = await this.runCommand(step.command, step.args, cwd, signal, execution);
115
+ if (result.exitCode === 127 && step.fallbackCommand) {
116
+ const fallback = await this.runCommand(step.fallbackCommand, step.fallbackArgs ?? step.args, cwd, signal, execution);
117
+ return await assertValidationFilesUnchanged(cwd, before, fallback);
118
+ }
119
+ return await assertValidationFilesUnchanged(cwd, before, result);
120
+ }
121
+ async runCommand(command, args, cwd, signal, execution) {
122
+ if (execution?.processRunner) {
123
+ const result = await execution.processRunner.run({
124
+ command,
125
+ args,
126
+ cwd,
127
+ env: execution.environment,
128
+ captureOutput: true,
129
+ }, undefined, signal);
130
+ return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
131
+ }
132
+ return await this.commands.run(command, args, cwd, signal, execution?.environment);
133
+ }
134
+ }
135
+ function isMissingParentRevision(args, result) {
136
+ if (args.length !== 5
137
+ || args[0] !== "diff"
138
+ || args[1] !== "--name-only"
139
+ || args[2] !== "HEAD^"
140
+ || args[3] !== "HEAD"
141
+ || args[4] !== "--") {
142
+ return false;
143
+ }
144
+ const detail = `${result.stderr ?? result.stdout ?? ""}`;
145
+ return /(?:ambiguous argument|bad revision|unknown revision|invalid object name).*HEAD\^/iu.test(detail);
146
+ }
147
+ async function readValidationFiles(cwd, relativePaths) {
148
+ const values = new Map();
149
+ for (const relativePath of relativePaths) {
150
+ values.set(relativePath, await fs.readFile(path.join(cwd, relativePath), "utf8"));
151
+ }
152
+ return values;
153
+ }
154
+ async function assertValidationFilesUnchanged(cwd, before, result) {
155
+ if (result.exitCode !== 0)
156
+ return result;
157
+ for (const [relativePath, original] of before) {
158
+ const current = await fs.readFile(path.join(cwd, relativePath), "utf8");
159
+ if (current !== original) {
160
+ return {
161
+ exitCode: 1,
162
+ stderr: `generated_drift: ${relativePath} changed during validation; regenerate and review it before publication.`,
163
+ };
164
+ }
165
+ }
166
+ return result;
167
+ }
168
+ export function applicableChecks(contract, profile, changedPaths) {
169
+ return contract.checks.filter(check => check.profiles.includes(profile)
170
+ && changedPaths.some(changedPath => check.changedPathPatterns.some(pattern => new RegExp(pattern, "u").test(changedPath))));
171
+ }
172
+ export async function loadValidationContract(worktreePath) {
173
+ let source;
174
+ try {
175
+ source = await fs.readFile(path.join(worktreePath, contractRelativePath), "utf8");
176
+ }
177
+ catch (error) {
178
+ if (error.code === "ENOENT")
179
+ return null;
180
+ throw error;
181
+ }
182
+ const contract = JSON.parse(source);
183
+ const ids = new Set(contract.checks?.map(check => check.id));
184
+ const contractDigest = createHash("sha256")
185
+ .update(JSON.stringify(canonicalizeJson(contract)))
186
+ .digest("hex");
187
+ if (contract.schemaVersion !== 1
188
+ || contract.contractId !== "forge-ci-parity-v1"
189
+ || contractDigest !== approvedContractDigest
190
+ || requiredCheckIds.size !== ids.size
191
+ || [...requiredCheckIds].some(id => !ids.has(id))
192
+ || contract.checks.some(check => !check.id || !Array.isArray(check.steps) || !Array.isArray(check.changedPathPatterns))) {
193
+ throw new Error(`prepublication_contract_invalid: the Forge CI parity contract does not match the approved definition (digest ${contractDigest}, expected ${approvedContractDigest}).`);
194
+ }
195
+ return contract;
196
+ }
197
+ function canonicalizeJson(value) {
198
+ if (Array.isArray(value))
199
+ return value.map(canonicalizeJson);
200
+ if (value !== null && typeof value === "object") {
201
+ const record = value;
202
+ return Object.fromEntries(Object.keys(record).sort().map(key => [key, canonicalizeJson(record[key])]));
203
+ }
204
+ return value;
205
+ }
206
+ function validationEvent(eventType, checkId, content) {
207
+ return { role: "runner", kind: "checkpoint", content, eventType, data: JSON.stringify({ checkId, contractId: "forge-ci-parity-v1" }) };
208
+ }
@@ -0,0 +1,16 @@
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 CiRepairLocalPreflightCommand[];
4
+ export interface CiRepairLocalPreflightCommand {
5
+ readonly id: string;
6
+ readonly cwd: string;
7
+ readonly command: readonly string[];
8
+ readonly coverage: string;
9
+ }
10
+ export interface CiRepairReadinessResult {
11
+ readonly path: string;
12
+ readonly environment: Readonly<Record<string, string>>;
13
+ readonly manifest: Readonly<Record<string, unknown>>;
14
+ }
15
+ export declare function writeCiRepairReadinessManifest(dispatch: RunOnceDispatchPacket, worktreePath: string, specPath: string, runtimeEnvironment: Readonly<Record<string, string>> | undefined): Promise<CiRepairReadinessResult>;
16
+ export declare function assertCiRepairPreflightContract(providerOnlySpecs: readonly string[], localPreflightCommands: readonly CiRepairLocalPreflightCommand[]): void;
@@ -0,0 +1,110 @@
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
+ export async function writeCiRepairReadinessManifest(dispatch, worktreePath, specPath, runtimeEnvironment) {
11
+ assertCiRepairPreflightContract(ciRepairProviderOnlySpecs, ciRepairLocalPreflightCommands);
12
+ const metadataRoot = path.dirname(specPath);
13
+ const manifestPath = path.join(metadataRoot, "ci-repair.readiness.json");
14
+ const workflowPath = path.join(worktreePath, ".github", "workflows", "ci.yml");
15
+ const browserReady = runtimeEnvironment?.FORGE_PLAYWRIGHT_BROWSER_READY === "true";
16
+ const providerOnlySpecsJson = JSON.stringify(ciRepairProviderOnlySpecs);
17
+ const apiUrl = runtimeEnvironment?.FORGE_E2E_API_URL ?? runtimeEnvironment?.FORGE_API_URL ?? null;
18
+ const webUrl = runtimeEnvironment?.FORGE_E2E_WEB_URL
19
+ ?? runtimeEnvironment?.FORGE_WEB_URL
20
+ ?? runtimeEnvironment?.FORGE_PLAYWRIGHT_BASE_URL
21
+ ?? null;
22
+ const environment = {
23
+ ...(runtimeEnvironment ?? {}),
24
+ ...(apiUrl ? { FORGE_E2E_API_URL: apiUrl } : {}),
25
+ ...(webUrl ? { FORGE_E2E_WEB_URL: webUrl } : {}),
26
+ FORGE_CI_REPAIR_READINESS_PATH: manifestPath,
27
+ FORGE_CI_REPAIR_HANDOFF_PATH: path.join(metadataRoot, "artifacts", "ci-repair.handoff.json"),
28
+ FORGE_CI_REPAIR_PROVIDER_ONLY_SPECS: providerOnlySpecsJson,
29
+ };
30
+ const manifest = {
31
+ schema_version: 1,
32
+ generated_by: "forge-bridge",
33
+ work_item_id: dispatch.workItem.id,
34
+ session_id: dispatch.session.id,
35
+ spec: {
36
+ path: relativePath(worktreePath, specPath),
37
+ ready: await isFile(specPath),
38
+ },
39
+ handoff: {
40
+ path: relativePath(worktreePath, environment.FORGE_CI_REPAIR_HANDOFF_PATH),
41
+ completion_tool: "forge.complete_ci_repair",
42
+ required_before_commit_or_push: true,
43
+ },
44
+ pull_request: {
45
+ provider: dispatch.pullRequest?.provider ?? "unknown",
46
+ number: dispatch.pullRequest?.number ?? null,
47
+ url: dispatch.pullRequest?.url ?? null,
48
+ source_branch: dispatch.pullRequest?.sourceBranch ?? dispatch.cycleRun.baseBranch,
49
+ target_branch: dispatch.pullRequest?.targetBranch ?? dispatch.cycleRun.requestedBase,
50
+ check_state: dispatch.pullRequest?.checkState ?? "unknown",
51
+ required_checks_passed: dispatch.pullRequest?.requiredChecksPassed ?? 0,
52
+ required_checks_total: dispatch.pullRequest?.requiredChecksTotal ?? 0,
53
+ failed_job_discovery: "Inspect provider check runs and record exact job/step in the completion payload.",
54
+ },
55
+ workflow: {
56
+ path: relativePath(worktreePath, workflowPath),
57
+ ready: await isFile(workflowPath),
58
+ requirement: "Record the failed step and every earlier guard/drift step in workflow_validation.",
59
+ },
60
+ runtime: {
61
+ mode: runtimeEnvironment?.FORGE_RUNTIME_MODE ?? "unknown",
62
+ application_runtime_ready: Boolean(apiUrl || webUrl),
63
+ api_url: apiUrl,
64
+ web_url: webUrl,
65
+ playwright_config: runtimeEnvironment?.FORGE_PLAYWRIGHT_CONFIG ?? null,
66
+ playwright_browser_cache: runtimeEnvironment?.PLAYWRIGHT_BROWSERS_PATH ?? null,
67
+ playwright_browser_ready: browserReady,
68
+ playwright_browser_install_command: browserReady
69
+ ? null
70
+ : runtimeEnvironment?.FORGE_PLAYWRIGHT_BROWSER_INSTALL_COMMAND ?? "npx playwright install chromium",
71
+ attempt_database_schema: runtimeEnvironment?.FORGE_ATTEMPT_DATABASE_SCHEMA ?? null,
72
+ attempt_database_readiness_path: runtimeEnvironment?.FORGE_ATTEMPT_DATABASE_READINESS_PATH ?? null,
73
+ },
74
+ local_equivalence: {
75
+ default: "supported",
76
+ nested_bridge: "provider_only",
77
+ provider_only_specs: ciRepairProviderOnlySpecs,
78
+ provider_only_specs_fingerprint: `sha256:${createHash("sha256").update(providerOnlySpecsJson).digest("hex")}`,
79
+ local_preflight_commands: ciRepairLocalPreflightCommands,
80
+ reason: "A managed Forge attempt disables its hosted reconciler and cannot exercise specs that require the live Bridge/runner control plane. Those specs remain authoritative in provider CI; attempt-local unit, contract, API, and browser coverage remains supported.",
81
+ policy: "Run every listed local_preflight_command. Treat every provider_only_specs pattern as unavailable in the managed attempt, do not override its exclusion or weaken provider CI, and record the provider check that supplies that proof in workflow_validation.",
82
+ },
83
+ };
84
+ await fs.mkdir(metadataRoot, { recursive: true });
85
+ await fs.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
86
+ return { path: manifestPath, environment, manifest };
87
+ }
88
+ export function assertCiRepairPreflightContract(providerOnlySpecs, localPreflightCommands) {
89
+ const providerOnlyFileNames = new Set(providerOnlySpecs.map(fileName));
90
+ for (const preflight of localPreflightCommands) {
91
+ const conflictingArgument = preflight.command.find(argument => providerOnlyFileNames.has(fileName(argument)));
92
+ if (conflictingArgument) {
93
+ throw new Error(`ci_repair_readiness_invalid: local preflight '${preflight.id}' references provider-only spec '${conflictingArgument}'.`);
94
+ }
95
+ }
96
+ }
97
+ async function isFile(candidate) {
98
+ try {
99
+ return (await fs.stat(candidate)).isFile();
100
+ }
101
+ catch {
102
+ return false;
103
+ }
104
+ }
105
+ function relativePath(worktreePath, candidate) {
106
+ return path.relative(worktreePath, candidate).replaceAll(path.sep, "/");
107
+ }
108
+ function fileName(candidate) {
109
+ return path.posix.basename(candidate.replaceAll("\\", "/"));
110
+ }