@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.
- package/dist/bridge/bridge.base.d.ts +2 -1
- package/dist/bridge/bridge.service.d.ts +8 -0
- package/dist/bridge/bridge.service.js +184 -12
- package/dist/bridge/bridge.types.d.ts +37 -0
- package/dist/cli/cli.help.js +1 -1
- package/dist/cli/cli.service.js +1 -1
- package/dist/cli/local-config.js +2 -1
- package/dist/runtime/attempt-node-runtime-launcher.d.ts +3 -1
- package/dist/runtime/attempt-node-runtime-launcher.js +62 -11
- package/dist/runtime/attempt-process-supervisor-registry.js +1 -1
- package/dist/runtime/attempt-process-supervisor.d.ts +12 -0
- package/dist/runtime/attempt-process-supervisor.js +75 -15
- package/dist/runtime/generated-output-reset.d.ts +10 -0
- package/dist/runtime/generated-output-reset.js +103 -0
- package/dist/runtime/host-listener-inspector.d.ts +16 -0
- package/dist/runtime/host-listener-inspector.js +143 -4
- package/dist/runtime/host-runtime-contract.d.ts +9 -1
- package/dist/runtime/host-runtime-contract.js +411 -6
- package/dist/runtime/host-runtime-lease.d.ts +12 -1
- package/dist/runtime/host-runtime-lease.js +271 -50
- package/dist/runtime/host-runtime-promotion.d.ts +2 -0
- package/dist/runtime/host-runtime-promotion.js +35 -4
- package/dist/runtime/node-host-runtime-adapter.d.ts +7 -0
- package/dist/runtime/node-host-runtime-adapter.js +71 -8
- package/dist/runtime/playwright-host-runtime-adapter.js +84 -0
- package/dist/sandbox/preflight.d.ts +9 -0
- package/dist/sandbox/preflight.js +39 -4
- package/dist/sandbox/workspace.service.js +7 -2
- package/dist/session/managed-attempt-tools.d.ts +17 -0
- package/dist/session/managed-attempt-tools.js +712 -0
- package/dist/session/phase-completion.artifacts.js +9 -2
- package/dist/session/phase-completion.d.ts +11 -2
- package/dist/session/phase-completion.helpers.d.ts +5 -0
- package/dist/session/phase-completion.helpers.js +45 -9
- package/dist/session/phase-completion.js +12 -3
- package/dist/session/phase-completion.prompts.d.ts +3 -3
- package/dist/session/phase-completion.prompts.js +21 -6
- package/dist/session/phase-completion.runtime.d.ts +13 -1
- package/dist/session/phase-completion.runtime.js +183 -8
- package/dist/session/phase-completion.schemas.js +108 -3
- package/dist/session/phase-completion.validation.d.ts +4 -1
- package/dist/session/phase-completion.validation.js +144 -18
- package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
- package/dist/session/provider-cli/ci-repair-readiness.js +95 -0
- package/dist/session/provider-cli/provider-cli.deterministic.js +47 -1
- package/dist/session/provider-cli/provider-cli.process.d.ts +14 -0
- package/dist/session/provider-cli/provider-cli.process.js +176 -18
- package/dist/session/provider-cli/provider-cli.prompts.js +30 -8
- package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
- package/dist/session/provider-cli/provider-cli.service.d.ts +19 -2
- package/dist/session/provider-cli/provider-cli.service.js +390 -83
- package/dist/session/provider-cli/provider-cli.types.d.ts +3 -0
- package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
- package/dist/session/provider-cli/provider-output-state.js +30 -2
- package/dist/session/session.base.d.ts +2 -1
- package/dist/session/session.service.d.ts +2 -1
- package/dist/session/session.service.js +303 -30
- package/dist/session/session.types.d.ts +38 -0
- package/dist/session/session.types.js +17 -0
- package/dist/shared/process/host-process-supervisor.d.ts +18 -1
- package/dist/shared/process/host-process-supervisor.js +94 -7
- package/dist/shared/process/process.d.ts +1 -0
- package/dist/shared/process/process.js +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/artifact-injector.d.ts +3 -2
- package/dist/worktree/artifact-injector.js +43 -4
- package/dist/worktree/dependency-warmer.d.ts +10 -1
- package/dist/worktree/dependency-warmer.js +94 -5
- package/dist/worktree/worktree.service.d.ts +1 -0
- package/dist/worktree/worktree.service.js +12 -1
- package/package.json +1 -1
- 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
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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
|
-
|
|
746
|
+
commandIssues.push(`commands[${index}].exit_code is required.`);
|
|
622
747
|
}
|
|
623
|
-
if (status === "pass" && record.exit_code !== 0) {
|
|
624
|
-
|
|
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
|
-
|
|
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 {};
|