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