@loucompanion/forge-bridge 0.1.3-dev.ba7d36726eda → 0.1.3-dev.bca7d0ccee53

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 (73) hide show
  1. package/dist/bridge/bridge.base.d.ts +2 -1
  2. package/dist/bridge/bridge.service.d.ts +8 -0
  3. package/dist/bridge/bridge.service.js +184 -12
  4. package/dist/bridge/bridge.types.d.ts +37 -0
  5. package/dist/cli/cli.help.js +1 -1
  6. package/dist/cli/cli.service.js +1 -1
  7. package/dist/cli/local-config.js +2 -1
  8. package/dist/runtime/attempt-node-runtime-launcher.d.ts +3 -1
  9. package/dist/runtime/attempt-node-runtime-launcher.js +62 -11
  10. package/dist/runtime/attempt-process-supervisor-registry.js +1 -1
  11. package/dist/runtime/attempt-process-supervisor.d.ts +12 -0
  12. package/dist/runtime/attempt-process-supervisor.js +75 -15
  13. package/dist/runtime/generated-output-reset.d.ts +10 -0
  14. package/dist/runtime/generated-output-reset.js +103 -0
  15. package/dist/runtime/host-listener-inspector.d.ts +16 -0
  16. package/dist/runtime/host-listener-inspector.js +143 -4
  17. package/dist/runtime/host-runtime-contract.d.ts +9 -1
  18. package/dist/runtime/host-runtime-contract.js +411 -6
  19. package/dist/runtime/host-runtime-lease.d.ts +12 -1
  20. package/dist/runtime/host-runtime-lease.js +271 -50
  21. package/dist/runtime/host-runtime-promotion.d.ts +2 -0
  22. package/dist/runtime/host-runtime-promotion.js +35 -4
  23. package/dist/runtime/node-host-runtime-adapter.d.ts +7 -0
  24. package/dist/runtime/node-host-runtime-adapter.js +71 -8
  25. package/dist/runtime/playwright-host-runtime-adapter.js +84 -0
  26. package/dist/sandbox/preflight.d.ts +9 -0
  27. package/dist/sandbox/preflight.js +39 -4
  28. package/dist/sandbox/workspace.service.js +7 -2
  29. package/dist/session/managed-attempt-tools.d.ts +17 -0
  30. package/dist/session/managed-attempt-tools.js +712 -0
  31. package/dist/session/phase-completion.artifacts.js +9 -2
  32. package/dist/session/phase-completion.d.ts +11 -2
  33. package/dist/session/phase-completion.helpers.d.ts +5 -0
  34. package/dist/session/phase-completion.helpers.js +45 -9
  35. package/dist/session/phase-completion.js +12 -3
  36. package/dist/session/phase-completion.prompts.d.ts +3 -3
  37. package/dist/session/phase-completion.prompts.js +21 -6
  38. package/dist/session/phase-completion.runtime.d.ts +13 -1
  39. package/dist/session/phase-completion.runtime.js +183 -8
  40. package/dist/session/phase-completion.schemas.js +108 -3
  41. package/dist/session/phase-completion.validation.d.ts +4 -1
  42. package/dist/session/phase-completion.validation.js +144 -18
  43. package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
  44. package/dist/session/provider-cli/ci-repair-readiness.js +95 -0
  45. package/dist/session/provider-cli/provider-cli.deterministic.js +47 -1
  46. package/dist/session/provider-cli/provider-cli.process.d.ts +14 -0
  47. package/dist/session/provider-cli/provider-cli.process.js +176 -18
  48. package/dist/session/provider-cli/provider-cli.prompts.js +30 -8
  49. package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
  50. package/dist/session/provider-cli/provider-cli.service.d.ts +19 -2
  51. package/dist/session/provider-cli/provider-cli.service.js +390 -83
  52. package/dist/session/provider-cli/provider-cli.types.d.ts +3 -0
  53. package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
  54. package/dist/session/provider-cli/provider-output-state.js +30 -2
  55. package/dist/session/session.base.d.ts +2 -1
  56. package/dist/session/session.service.d.ts +2 -1
  57. package/dist/session/session.service.js +303 -30
  58. package/dist/session/session.types.d.ts +38 -0
  59. package/dist/session/session.types.js +17 -0
  60. package/dist/shared/process/host-process-supervisor.d.ts +18 -1
  61. package/dist/shared/process/host-process-supervisor.js +94 -7
  62. package/dist/shared/process/process.d.ts +1 -0
  63. package/dist/shared/process/process.js +1 -0
  64. package/dist/version.d.ts +1 -1
  65. package/dist/version.js +1 -1
  66. package/dist/worktree/artifact-injector.d.ts +3 -2
  67. package/dist/worktree/artifact-injector.js +43 -4
  68. package/dist/worktree/dependency-warmer.d.ts +10 -1
  69. package/dist/worktree/dependency-warmer.js +94 -5
  70. package/dist/worktree/worktree.service.d.ts +1 -0
  71. package/dist/worktree/worktree.service.js +12 -1
  72. package/package.json +1 -1
  73. package/runtime/windows-job-supervisor.ps1 +89 -20
@@ -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") {
@@ -611,22 +729,30 @@ export function validateQaPayloadShape(payload, status) {
611
729
  throw new Error("QA pass/needs_rework requires at least one command, browser check, or evidence note.");
612
730
  }
613
731
  }
732
+ const commandIssues = [];
614
733
  for (const [index, command] of commands.entries()) {
615
- requireEvidenceRecord(command, `commands[${index}]`);
734
+ if (!command || typeof command !== "object" || Array.isArray(command)) {
735
+ commandIssues.push(`commands[${index}] must be an object.`);
736
+ continue;
737
+ }
616
738
  const record = command;
617
- requireNonEmptyStringRecord(record, "command", `commands[${index}].`);
618
- requireNonEmptyStringRecord(record, "status", `commands[${index}].`);
619
- requireNonEmptyStringRecord(record, "summary", `commands[${index}].`);
739
+ if (!optionalString(record, "command"))
740
+ commandIssues.push(`commands[${index}].command is required.`);
741
+ if (!optionalString(record, "status"))
742
+ commandIssues.push(`commands[${index}].status is required.`);
743
+ if (!optionalString(record, "summary"))
744
+ commandIssues.push(`commands[${index}].summary is required.`);
620
745
  if (typeof record.exit_code !== "number" || !Number.isFinite(record.exit_code)) {
621
- throw new Error(`commands[${index}].exit_code is required.`);
746
+ commandIssues.push(`commands[${index}].exit_code is required.`);
622
747
  }
623
- if (status === "pass" && record.exit_code !== 0) {
624
- throw new Error(`commands[${index}].exit_code must be 0 when QA status is pass.`);
748
+ else if (status === "pass" && record.exit_code !== 0) {
749
+ commandIssues.push(`commands[${index}].exit_code must be 0 when QA status is pass.`);
625
750
  }
626
751
  if (status === "pass" && normalizedString(record.status) !== "pass") {
627
- throw new Error(`commands[${index}].status must be pass when QA status is pass.`);
752
+ commandIssues.push(`commands[${index}].status must be pass when QA status is pass.`);
628
753
  }
629
754
  }
755
+ throwCompletionPayloadIssues(commandIssues);
630
756
  for (const [index, browserCheck] of browserChecks.entries()) {
631
757
  requireEvidenceRecord(browserCheck, `browser_checks[${index}]`);
632
758
  const record = browserCheck;
@@ -720,5 +846,5 @@ export function validateOptionalWorkItemId(payload, expectedWorkItemId) {
720
846
  throw new Error(`completion payload work item ${actual} does not match dispatch work item ${expectedWorkItemId}.`);
721
847
  }
722
848
  }
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";
849
+ 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
850
  export * from "./phase-completion.helpers.js";
@@ -0,0 +1,8 @@
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 interface CiRepairReadinessResult {
4
+ readonly path: string;
5
+ readonly environment: Readonly<Record<string, string>>;
6
+ readonly manifest: Readonly<Record<string, unknown>>;
7
+ }
8
+ export declare function writeCiRepairReadinessManifest(dispatch: RunOnceDispatchPacket, worktreePath: string, specPath: string, runtimeEnvironment: Readonly<Record<string, string>> | undefined): Promise<CiRepairReadinessResult>;
@@ -0,0 +1,95 @@
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 async function writeCiRepairReadinessManifest(dispatch, worktreePath, specPath, runtimeEnvironment) {
10
+ const metadataRoot = path.dirname(specPath);
11
+ const manifestPath = path.join(metadataRoot, "ci-repair.readiness.json");
12
+ const workflowPath = path.join(worktreePath, ".github", "workflows", "ci.yml");
13
+ const browserReady = runtimeEnvironment?.FORGE_PLAYWRIGHT_BROWSER_READY === "true";
14
+ const providerOnlySpecsJson = JSON.stringify(ciRepairProviderOnlySpecs);
15
+ const apiUrl = runtimeEnvironment?.FORGE_E2E_API_URL ?? runtimeEnvironment?.FORGE_API_URL ?? null;
16
+ const webUrl = runtimeEnvironment?.FORGE_E2E_WEB_URL
17
+ ?? runtimeEnvironment?.FORGE_WEB_URL
18
+ ?? runtimeEnvironment?.FORGE_PLAYWRIGHT_BASE_URL
19
+ ?? null;
20
+ const environment = {
21
+ ...(runtimeEnvironment ?? {}),
22
+ ...(apiUrl ? { FORGE_E2E_API_URL: apiUrl } : {}),
23
+ ...(webUrl ? { FORGE_E2E_WEB_URL: webUrl } : {}),
24
+ FORGE_CI_REPAIR_READINESS_PATH: manifestPath,
25
+ FORGE_CI_REPAIR_HANDOFF_PATH: path.join(metadataRoot, "artifacts", "ci-repair.handoff.json"),
26
+ FORGE_CI_REPAIR_PROVIDER_ONLY_SPECS: providerOnlySpecsJson,
27
+ };
28
+ const manifest = {
29
+ schema_version: 1,
30
+ generated_by: "forge-bridge",
31
+ work_item_id: dispatch.workItem.id,
32
+ session_id: dispatch.session.id,
33
+ spec: {
34
+ path: relativePath(worktreePath, specPath),
35
+ ready: await isFile(specPath),
36
+ },
37
+ handoff: {
38
+ path: relativePath(worktreePath, environment.FORGE_CI_REPAIR_HANDOFF_PATH),
39
+ completion_tool: "forge.complete_ci_repair",
40
+ required_before_commit_or_push: true,
41
+ },
42
+ pull_request: {
43
+ provider: dispatch.pullRequest?.provider ?? "unknown",
44
+ number: dispatch.pullRequest?.number ?? null,
45
+ url: dispatch.pullRequest?.url ?? null,
46
+ source_branch: dispatch.pullRequest?.sourceBranch ?? dispatch.cycleRun.baseBranch,
47
+ target_branch: dispatch.pullRequest?.targetBranch ?? dispatch.cycleRun.requestedBase,
48
+ check_state: dispatch.pullRequest?.checkState ?? "unknown",
49
+ required_checks_passed: dispatch.pullRequest?.requiredChecksPassed ?? 0,
50
+ required_checks_total: dispatch.pullRequest?.requiredChecksTotal ?? 0,
51
+ failed_job_discovery: "Inspect provider check runs and record exact job/step in the completion payload.",
52
+ },
53
+ workflow: {
54
+ path: relativePath(worktreePath, workflowPath),
55
+ ready: await isFile(workflowPath),
56
+ requirement: "Record the failed step and every earlier guard/drift step in workflow_validation.",
57
+ },
58
+ runtime: {
59
+ mode: runtimeEnvironment?.FORGE_RUNTIME_MODE ?? "unknown",
60
+ application_runtime_ready: Boolean(apiUrl || webUrl),
61
+ api_url: apiUrl,
62
+ web_url: webUrl,
63
+ playwright_config: runtimeEnvironment?.FORGE_PLAYWRIGHT_CONFIG ?? null,
64
+ playwright_browser_cache: runtimeEnvironment?.PLAYWRIGHT_BROWSERS_PATH ?? null,
65
+ playwright_browser_ready: browserReady,
66
+ playwright_browser_install_command: browserReady
67
+ ? null
68
+ : runtimeEnvironment?.FORGE_PLAYWRIGHT_BROWSER_INSTALL_COMMAND ?? "npx playwright install chromium",
69
+ attempt_database_schema: runtimeEnvironment?.FORGE_ATTEMPT_DATABASE_SCHEMA ?? null,
70
+ attempt_database_readiness_path: runtimeEnvironment?.FORGE_ATTEMPT_DATABASE_READINESS_PATH ?? null,
71
+ },
72
+ local_equivalence: {
73
+ default: "supported",
74
+ nested_bridge: "provider_only",
75
+ provider_only_specs: ciRepairProviderOnlySpecs,
76
+ provider_only_specs_fingerprint: `sha256:${createHash("sha256").update(providerOnlySpecsJson).digest("hex")}`,
77
+ reason: "A managed Forge attempt cannot safely create a second Bridge/runner control plane inside itself. Provider CI remains authoritative for these specs.",
78
+ policy: "Do not delete, skip, or weaken provider CI coverage. Record provider_only steps in workflow_validation with this reason.",
79
+ },
80
+ };
81
+ await fs.mkdir(metadataRoot, { recursive: true });
82
+ await fs.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
83
+ return { path: manifestPath, environment, manifest };
84
+ }
85
+ async function isFile(candidate) {
86
+ try {
87
+ return (await fs.stat(candidate)).isFile();
88
+ }
89
+ catch {
90
+ return false;
91
+ }
92
+ }
93
+ function relativePath(worktreePath, candidate) {
94
+ return path.relative(worktreePath, candidate).replaceAll(path.sep, "/");
95
+ }
@@ -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") {
@@ -17,6 +17,7 @@ export interface ProviderProcessRunner {
17
17
  run(request: ProviderProcessRunRequest, handlers?: ProviderProcessHandlers, signal?: AbortSignal): Promise<ProviderProcessRunResult>;
18
18
  }
19
19
  export interface ProviderProcessHandlers {
20
+ onStarted?(): Promise<void> | void;
20
21
  onStdoutLine?(line: string): Promise<void> | void;
21
22
  onStderrLine?(line: string): Promise<void> | void;
22
23
  }
@@ -30,5 +31,18 @@ export declare class StreamingProviderProcessRunner implements ProviderProcessRu
30
31
  run(request: ProviderProcessRunRequest, handlers?: ProviderProcessHandlers, signal?: AbortSignal): Promise<ProviderProcessRunResult>;
31
32
  }
32
33
  export declare function resolveCodexNpmDirectLaunch(commandPath: string, args: readonly string[]): LaunchCommand | null;
34
+ /**
35
+ * Avoids cmd.exe's nested-quote handling for package-manager shims. That
36
+ * quoting loses executable paths containing spaces after the command is
37
+ * wrapped by the Windows Job Object supervisor.
38
+ */
39
+ export declare function resolvePackageManagerDirectLaunch(commandPath: string, args: readonly string[]): LaunchCommand | null;
40
+ /**
41
+ * Resolves Windows commands without spawning where.exe. Runtime launches can
42
+ * happen while the machine is under enough process pressure for where.exe to
43
+ * miss its timeout; returning the unresolved package-manager name in that case
44
+ * makes Node fall back to a shell shim that cannot survive Job Object wrapping.
45
+ */
46
+ export declare function resolveWindowsCommandFromPath(command: string, pathValue?: string, pathExtValue?: string): string | null;
33
47
  export declare function chooseWindowsCommandCandidate(candidates: readonly string[]): string | null;
34
48
  export {};