@loucompanion/forge-bridge 0.1.3-dev.f2b8822ce953 → 0.1.3-dev.fbe15023cdfe
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 +78 -0
- package/dist/bridge/bridge.base.d.ts +6 -1
- package/dist/bridge/bridge.service.d.ts +60 -3
- package/dist/bridge/bridge.service.js +970 -24
- package/dist/bridge/bridge.types.d.ts +172 -1
- package/dist/cleanup/cleanup.service.d.ts +13 -2
- package/dist/cleanup/cleanup.service.js +435 -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 +47 -1
- package/dist/cli/cli.service.d.ts +42 -2
- package/dist/cli/cli.service.js +351 -22
- package/dist/cli/local-config.d.ts +6 -0
- package/dist/cli/local-config.js +47 -3
- package/dist/runtime/attempt-node-runtime-launcher.d.ts +17 -0
- package/dist/runtime/attempt-node-runtime-launcher.js +101 -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 +86 -0
- package/dist/runtime/attempt-process-supervisor.js +350 -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 +44 -0
- package/dist/runtime/host-listener-inspector.js +324 -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 +133 -0
- package/dist/runtime/host-runtime-lease.js +1460 -0
- package/dist/runtime/host-runtime-promotion.d.ts +23 -0
- package/dist/runtime/host-runtime-promotion.js +159 -0
- package/dist/runtime/node-host-runtime-adapter.d.ts +51 -0
- package/dist/runtime/node-host-runtime-adapter.js +268 -0
- package/dist/runtime/playwright-host-runtime-adapter.d.ts +20 -0
- package/dist/runtime/playwright-host-runtime-adapter.js +220 -0
- package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
- package/dist/runtime/runtime-isolation-lease.js +158 -0
- package/dist/sandbox/command-runner.d.ts +4 -0
- package/dist/sandbox/command-runner.js +47 -0
- package/dist/sandbox/contract.d.ts +5 -0
- package/dist/sandbox/contract.js +276 -0
- package/dist/sandbox/event-budget.d.ts +11 -0
- package/dist/sandbox/event-budget.js +27 -0
- package/dist/sandbox/image-policy.d.ts +35 -0
- package/dist/sandbox/image-policy.js +247 -0
- package/dist/sandbox/index.d.ts +5 -0
- package/dist/sandbox/index.js +5 -0
- package/dist/sandbox/lease-watchdog.d.ts +18 -0
- package/dist/sandbox/lease-watchdog.js +67 -0
- package/dist/sandbox/preflight.d.ts +54 -0
- package/dist/sandbox/preflight.js +335 -0
- package/dist/sandbox/process.d.ts +20 -0
- package/dist/sandbox/process.js +209 -0
- package/dist/sandbox/provider-auth/codex.d.ts +26 -0
- package/dist/sandbox/provider-auth/codex.js +212 -0
- package/dist/sandbox/repository-lock.d.ts +5 -0
- package/dist/sandbox/repository-lock.js +119 -0
- package/dist/sandbox/runtime.d.ts +297 -0
- package/dist/sandbox/runtime.js +2338 -0
- package/dist/sandbox/sandbox.types.d.ts +86 -0
- package/dist/sandbox/sandbox.types.js +1 -0
- package/dist/sandbox/smoke.d.ts +76 -0
- package/dist/sandbox/smoke.js +380 -0
- package/dist/sandbox/worker-supervisor.d.ts +13 -0
- package/dist/sandbox/worker-supervisor.js +36 -0
- package/dist/sandbox/worker.d.ts +1 -0
- package/dist/sandbox/worker.js +225 -0
- package/dist/sandbox/workspace.service.d.ts +13 -0
- package/dist/sandbox/workspace.service.js +72 -0
- package/dist/session/managed-attempt-tools.d.ts +12 -0
- package/dist/session/managed-attempt-tools.js +561 -0
- package/dist/session/phase-completion.artifacts.js +32 -4
- package/dist/session/phase-completion.d.ts +4 -2
- package/dist/session/phase-completion.helpers.js +27 -3
- 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 +20 -6
- package/dist/session/phase-completion.runtime.d.ts +12 -1
- package/dist/session/phase-completion.runtime.js +129 -7
- package/dist/session/phase-completion.schemas.js +132 -5
- package/dist/session/phase-completion.validation.d.ts +4 -1
- package/dist/session/phase-completion.validation.js +106 -2
- package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
- package/dist/session/provider-cli/ci-repair-readiness.js +90 -0
- package/dist/session/provider-cli/provider-cli.artifacts.d.ts +1 -1
- package/dist/session/provider-cli/provider-cli.artifacts.js +28 -4
- package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
- package/dist/session/provider-cli/provider-cli.deterministic.d.ts +2 -0
- package/dist/session/provider-cli/provider-cli.deterministic.js +66 -9
- package/dist/session/provider-cli/provider-cli.process.d.ts +17 -0
- package/dist/session/provider-cli/provider-cli.process.js +223 -14
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +17 -2
- package/dist/session/provider-cli/provider-cli.prompts.js +226 -18
- 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 +3 -2
- package/dist/session/provider-cli/provider-cli.service.d.ts +19 -3
- package/dist/session/provider-cli/provider-cli.service.js +503 -98
- package/dist/session/provider-cli/provider-cli.types.d.ts +6 -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-redaction.d.ts +3 -0
- package/dist/session/session-redaction.js +23 -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 +463 -58
- package/dist/session/session.types.d.ts +144 -1
- package/dist/session/session.types.js +279 -3
- package/dist/shared/path/path.js +3 -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 +57 -0
- package/dist/shared/process/host-process-supervisor.js +282 -0
- package/dist/shared/process/process.d.ts +1 -0
- package/dist/shared/process/process.js +1 -0
- package/dist/shared/process/windows-launch.d.ts +1 -1
- package/dist/shared/process/windows-launch.js +6 -3
- package/dist/shared/provider-capabilities.d.ts +1 -0
- package/dist/shared/provider-capabilities.js +3 -0
- package/dist/shared/redaction/redaction.js +10 -3
- 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 +21 -1
- package/dist/worktree/dependency-warmer.js +138 -5
- package/dist/worktree/index.d.ts +1 -0
- 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 +7 -2
- package/dist/worktree/worktree.service.js +72 -44
- package/dist/worktree/worktree.types.d.ts +3 -1
- package/package.json +6 -3
- package/runtime/windows-job-supervisor.ps1 +356 -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,7 +251,7 @@ export function validateQaGatePayloadShape(gate, payload, status) {
|
|
|
179
251
|
}
|
|
180
252
|
}
|
|
181
253
|
}
|
|
182
|
-
export function validateQaGatePassingCommands(commands) {
|
|
254
|
+
export function validateQaGatePassingCommands(commands, options = {}) {
|
|
183
255
|
for (const [index, command] of commands.entries()) {
|
|
184
256
|
requireEvidenceRecord(command, `commands[${index}]`);
|
|
185
257
|
const record = command;
|
|
@@ -189,6 +261,13 @@ export function validateQaGatePassingCommands(commands) {
|
|
|
189
261
|
if (typeof record.exit_code !== "number" || !Number.isFinite(record.exit_code)) {
|
|
190
262
|
throw new Error(`commands[${index}].exit_code is required.`);
|
|
191
263
|
}
|
|
264
|
+
const passed = record.exit_code === 0 && normalizedString(record.status) === "pass";
|
|
265
|
+
if (!passed
|
|
266
|
+
&& options.allowClassifiedNonBlockingFailures
|
|
267
|
+
&& optionalString(record, "classification")) {
|
|
268
|
+
validateClassifiedNonBlockingQaFailure(record, index);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
192
271
|
if (record.exit_code !== 0) {
|
|
193
272
|
throw new Error(`commands[${index}].exit_code must be 0 when QA gate status is pass.`);
|
|
194
273
|
}
|
|
@@ -197,6 +276,31 @@ export function validateQaGatePassingCommands(commands) {
|
|
|
197
276
|
}
|
|
198
277
|
}
|
|
199
278
|
}
|
|
279
|
+
function validateClassifiedNonBlockingQaFailure(record, index) {
|
|
280
|
+
const classification = normalizedString(optionalString(record, "classification"));
|
|
281
|
+
if (classification !== "out_of_scope_failure" && classification !== "baseline_failure") {
|
|
282
|
+
throw new Error(`commands[${index}] is non-passing; QA regression pass requires classification out_of_scope_failure or baseline_failure with corroborating evidence.`);
|
|
283
|
+
}
|
|
284
|
+
requireNonEmptyStringRecord(record, "candidate_bug_id", `commands[${index}].`);
|
|
285
|
+
requireNonEmptyStringRecord(record, "scope_evidence", `commands[${index}].`);
|
|
286
|
+
if (record.acceptance_criteria_independent !== true) {
|
|
287
|
+
throw new Error(`commands[${index}].acceptance_criteria_independent must be true for a non-blocking QA regression failure.`);
|
|
288
|
+
}
|
|
289
|
+
if (classification === "out_of_scope_failure") {
|
|
290
|
+
requireNonEmptyStringRecord(record, "repro_command", `commands[${index}].`);
|
|
291
|
+
if (typeof record.repro_exit_code !== "number" || !Number.isFinite(record.repro_exit_code) || record.repro_exit_code === 0) {
|
|
292
|
+
throw new Error(`commands[${index}].repro_exit_code must be a non-zero number for an out-of-scope failure.`);
|
|
293
|
+
}
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
requireNonEmptyStringRecord(record, "baseline_ref", `commands[${index}].`);
|
|
297
|
+
if (typeof record.baseline_exit_code !== "number" || !Number.isFinite(record.baseline_exit_code) || record.baseline_exit_code === 0) {
|
|
298
|
+
throw new Error(`commands[${index}].baseline_exit_code must be a non-zero number for a baseline failure.`);
|
|
299
|
+
}
|
|
300
|
+
if (record.failure_signature_match !== true) {
|
|
301
|
+
throw new Error(`commands[${index}].failure_signature_match must be true for a baseline failure.`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
200
304
|
export function validateArchitectPayloadShape(payload, status) {
|
|
201
305
|
if (status !== "pass") {
|
|
202
306
|
requireArchitectNonPassEvidence(payload);
|
|
@@ -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,90 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export const ciRepairProviderOnlySpecs = [
|
|
4
|
+
"**/bridge-dispatch.spec.ts",
|
|
5
|
+
"**/develop-agent-run.spec.ts",
|
|
6
|
+
"**/runner-availability.spec.ts",
|
|
7
|
+
];
|
|
8
|
+
export async function writeCiRepairReadinessManifest(dispatch, worktreePath, specPath, runtimeEnvironment) {
|
|
9
|
+
const metadataRoot = path.dirname(specPath);
|
|
10
|
+
const manifestPath = path.join(metadataRoot, "ci-repair.readiness.json");
|
|
11
|
+
const workflowPath = path.join(worktreePath, ".github", "workflows", "ci.yml");
|
|
12
|
+
const browserReady = runtimeEnvironment?.FORGE_PLAYWRIGHT_BROWSER_READY === "true";
|
|
13
|
+
const apiUrl = runtimeEnvironment?.FORGE_E2E_API_URL ?? runtimeEnvironment?.FORGE_API_URL ?? null;
|
|
14
|
+
const webUrl = runtimeEnvironment?.FORGE_E2E_WEB_URL
|
|
15
|
+
?? runtimeEnvironment?.FORGE_WEB_URL
|
|
16
|
+
?? runtimeEnvironment?.FORGE_PLAYWRIGHT_BASE_URL
|
|
17
|
+
?? null;
|
|
18
|
+
const environment = {
|
|
19
|
+
...(runtimeEnvironment ?? {}),
|
|
20
|
+
...(apiUrl ? { FORGE_E2E_API_URL: apiUrl } : {}),
|
|
21
|
+
...(webUrl ? { FORGE_E2E_WEB_URL: webUrl } : {}),
|
|
22
|
+
FORGE_CI_REPAIR_READINESS_PATH: manifestPath,
|
|
23
|
+
FORGE_CI_REPAIR_HANDOFF_PATH: path.join(metadataRoot, "artifacts", "ci-repair.handoff.json"),
|
|
24
|
+
FORGE_CI_REPAIR_PROVIDER_ONLY_SPECS: JSON.stringify(ciRepairProviderOnlySpecs),
|
|
25
|
+
};
|
|
26
|
+
const manifest = {
|
|
27
|
+
schema_version: 1,
|
|
28
|
+
generated_by: "forge-bridge",
|
|
29
|
+
work_item_id: dispatch.workItem.id,
|
|
30
|
+
session_id: dispatch.session.id,
|
|
31
|
+
spec: {
|
|
32
|
+
path: relativePath(worktreePath, specPath),
|
|
33
|
+
ready: await isFile(specPath),
|
|
34
|
+
},
|
|
35
|
+
handoff: {
|
|
36
|
+
path: relativePath(worktreePath, environment.FORGE_CI_REPAIR_HANDOFF_PATH),
|
|
37
|
+
completion_tool: "forge.complete_ci_repair",
|
|
38
|
+
required_before_commit_or_push: true,
|
|
39
|
+
},
|
|
40
|
+
pull_request: {
|
|
41
|
+
provider: dispatch.pullRequest?.provider ?? "unknown",
|
|
42
|
+
number: dispatch.pullRequest?.number ?? null,
|
|
43
|
+
url: dispatch.pullRequest?.url ?? null,
|
|
44
|
+
source_branch: dispatch.pullRequest?.sourceBranch ?? dispatch.cycleRun.baseBranch,
|
|
45
|
+
target_branch: dispatch.pullRequest?.targetBranch ?? dispatch.cycleRun.requestedBase,
|
|
46
|
+
check_state: dispatch.pullRequest?.checkState ?? "unknown",
|
|
47
|
+
required_checks_passed: dispatch.pullRequest?.requiredChecksPassed ?? 0,
|
|
48
|
+
required_checks_total: dispatch.pullRequest?.requiredChecksTotal ?? 0,
|
|
49
|
+
failed_job_discovery: "Inspect provider check runs and record exact job/step in the completion payload.",
|
|
50
|
+
},
|
|
51
|
+
workflow: {
|
|
52
|
+
path: relativePath(worktreePath, workflowPath),
|
|
53
|
+
ready: await isFile(workflowPath),
|
|
54
|
+
requirement: "Record the failed step and every earlier guard/drift step in workflow_validation.",
|
|
55
|
+
},
|
|
56
|
+
runtime: {
|
|
57
|
+
mode: runtimeEnvironment?.FORGE_RUNTIME_MODE ?? "unknown",
|
|
58
|
+
application_runtime_ready: Boolean(apiUrl || webUrl),
|
|
59
|
+
api_url: apiUrl,
|
|
60
|
+
web_url: webUrl,
|
|
61
|
+
playwright_config: runtimeEnvironment?.FORGE_PLAYWRIGHT_CONFIG ?? null,
|
|
62
|
+
playwright_browser_cache: runtimeEnvironment?.PLAYWRIGHT_BROWSERS_PATH ?? null,
|
|
63
|
+
playwright_browser_ready: browserReady,
|
|
64
|
+
playwright_browser_install_command: browserReady
|
|
65
|
+
? null
|
|
66
|
+
: runtimeEnvironment?.FORGE_PLAYWRIGHT_BROWSER_INSTALL_COMMAND ?? "npx playwright install chromium",
|
|
67
|
+
},
|
|
68
|
+
local_equivalence: {
|
|
69
|
+
default: "supported",
|
|
70
|
+
nested_bridge: "provider_only",
|
|
71
|
+
provider_only_specs: ciRepairProviderOnlySpecs,
|
|
72
|
+
reason: "A managed Forge attempt cannot safely create a second Bridge/runner control plane inside itself. Provider CI remains authoritative for these specs.",
|
|
73
|
+
policy: "Do not delete, skip, or weaken provider CI coverage. Record provider_only steps in workflow_validation with this reason.",
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
await fs.mkdir(metadataRoot, { recursive: true });
|
|
77
|
+
await fs.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
78
|
+
return { path: manifestPath, environment, manifest };
|
|
79
|
+
}
|
|
80
|
+
async function isFile(candidate) {
|
|
81
|
+
try {
|
|
82
|
+
return (await fs.stat(candidate)).isFile();
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function relativePath(worktreePath, candidate) {
|
|
89
|
+
return path.relative(worktreePath, candidate).replaceAll(path.sep, "/");
|
|
90
|
+
}
|
|
@@ -18,7 +18,7 @@ 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
23
|
export declare function emitQaStructuredEvents(emit: ProviderCliRunContext["emit"], artifact: RunnerDispatchArtifact): Promise<void>;
|
|
24
24
|
interface QaGateArtifactSnapshot {
|
|
@@ -5,6 +5,7 @@ import { ensureNoSymbolicLinkPathSegments, isPathUnderRoot, sha256File } from ".
|
|
|
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
7
|
const developCyclePhaseOrder = ["implementation", "self_review", "validation"];
|
|
8
|
+
const nonPublishableWorktreePaths = [".forge", ".local"];
|
|
8
9
|
export async function emitDraftGateArtifacts(emit, worktreePath, workItemId) {
|
|
9
10
|
const draftArtifactFiles = {
|
|
10
11
|
draft_intent: "draft.intent.json",
|
|
@@ -147,7 +148,9 @@ export async function cleanupCompletionTools(completionTools, emit) {
|
|
|
147
148
|
}
|
|
148
149
|
export async function commitWorktreeChanges(git, worktreePath, workItemId) {
|
|
149
150
|
await git.run(worktreePath, ["add", "-A"]);
|
|
150
|
-
|
|
151
|
+
for (const reservedPath of nonPublishableWorktreePaths) {
|
|
152
|
+
await git.run(worktreePath, ["reset", "--", reservedPath]).catch(() => "");
|
|
153
|
+
}
|
|
151
154
|
const changedFiles = (await git.run(worktreePath, ["diff", "--cached", "--name-only"]))
|
|
152
155
|
.split(/\r?\n/)
|
|
153
156
|
.map((line) => line.trim())
|
|
@@ -237,18 +240,39 @@ export async function hasCompleteQaArtifactBundle(worktreePath, workItemId, owne
|
|
|
237
240
|
}
|
|
238
241
|
return true;
|
|
239
242
|
}
|
|
240
|
-
export function enforceQaBrowserLaunchTargetGate(dispatch, artifact) {
|
|
243
|
+
export async function enforceQaBrowserLaunchTargetGate(dispatch, artifact) {
|
|
241
244
|
const qaBrowser = dispatch.qaBrowser;
|
|
242
245
|
if (!qaBrowser?.required || hasLaunchTarget(qaBrowser.launchTarget ?? null)) {
|
|
243
246
|
return;
|
|
244
247
|
}
|
|
248
|
+
const evidence = await readJsonObjectFile(path.join(path.dirname(artifact.path), qaArtifactFileNames.evidence));
|
|
249
|
+
if (hasPassingRepositoryPlaywrightEvidence(evidence)) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
245
252
|
const verdict = parseJsonLine(artifact.verdictJson) ?? {};
|
|
246
253
|
const status = normalizedString(stringField(verdict, "status") ?? artifact.status ?? "");
|
|
247
254
|
const blockerReason = normalizedString(stringField(verdict, "blocker_reason") ?? "");
|
|
248
255
|
if (status !== "blocked" || blockerReason !== "missing_app_launch_target") {
|
|
249
|
-
throw new Error("QA browser testing is required but
|
|
256
|
+
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.");
|
|
250
257
|
}
|
|
251
258
|
}
|
|
259
|
+
function hasPassingRepositoryPlaywrightEvidence(evidence) {
|
|
260
|
+
const passingBrowserCheck = arrayField(evidence, "browser_checks").some(check => normalizedString(stringField(check, "status") ?? "") === "pass"
|
|
261
|
+
&& Boolean(stringField(check, "url") ?? stringField(check, "route")));
|
|
262
|
+
if (!passingBrowserCheck) {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
return arrayField(evidence, "commands").some(command => {
|
|
266
|
+
const status = normalizedString(stringField(command, "status") ?? "");
|
|
267
|
+
const exitCode = command.exit_code;
|
|
268
|
+
const description = [stringField(command, "command"), stringField(command, "summary")]
|
|
269
|
+
.filter((value) => Boolean(value))
|
|
270
|
+
.join(" ");
|
|
271
|
+
return status === "pass"
|
|
272
|
+
&& exitCode === 0
|
|
273
|
+
&& /(?:playwright|test:e2e|e2e(?:\s|:|$)|browser(?:\s|-)?test)/iu.test(description);
|
|
274
|
+
});
|
|
275
|
+
}
|
|
252
276
|
export async function validateQaBundleSidecarReferences(worktreePath, workItemId, ownerType, ownerId) {
|
|
253
277
|
const artifactRoot = ownerType && ownerId
|
|
254
278
|
? artifactRootForOwner(worktreePath, ownerType, ownerId)
|
|
@@ -444,7 +468,7 @@ export class LiveQaArtifactEmitter {
|
|
|
444
468
|
if (!artifact) {
|
|
445
469
|
return;
|
|
446
470
|
}
|
|
447
|
-
enforceQaBrowserLaunchTargetGate(this.dispatch, artifact);
|
|
471
|
+
await enforceQaBrowserLaunchTargetGate(this.dispatch, artifact);
|
|
448
472
|
this.emittedBundle = true;
|
|
449
473
|
await emitQaArtifactRecorded(this.emit, this.worktreePath, artifact);
|
|
450
474
|
await emitQaStructuredEvents(this.emit, artifact);
|
|
@@ -2,6 +2,7 @@ import type { ProviderCliDraftContext, ProviderCliRunContext, ProviderCliRunResu
|
|
|
2
2
|
export interface ProviderCliService {
|
|
3
3
|
runImplementation(context: ProviderCliRunContext): Promise<ProviderCliRunResult>;
|
|
4
4
|
runArchitect(context: ProviderCliRunContext): Promise<ProviderCliRunResult>;
|
|
5
|
+
runCiRepair(context: ProviderCliRunContext): Promise<ProviderCliRunResult>;
|
|
5
6
|
runConflictResolve(context: ProviderCliRunContext): Promise<ProviderCliRunResult>;
|
|
6
7
|
runQa(context: ProviderCliRunContext): Promise<ProviderCliRunResult>;
|
|
7
8
|
runDraft(context: ProviderCliDraftContext): Promise<ProviderCliRunResult>;
|
|
@@ -8,5 +8,7 @@ export declare class DeterministicProviderCliService implements ProviderCliServi
|
|
|
8
8
|
runImplementation(context: ProviderCliRunContext): Promise<ProviderCliRunResult>;
|
|
9
9
|
runArchitect(context: ProviderCliRunContext): Promise<ProviderCliRunResult>;
|
|
10
10
|
runQa(context: ProviderCliRunContext): Promise<ProviderCliRunResult>;
|
|
11
|
+
runCiRepair(context: ProviderCliRunContext): Promise<ProviderCliRunResult>;
|
|
11
12
|
runConflictResolve(context: ProviderCliRunContext): Promise<ProviderCliRunResult>;
|
|
13
|
+
private runPullRequestRepair;
|
|
12
14
|
}
|
|
@@ -5,9 +5,10 @@ import { buildSpecBody, selectLaneCommand } from "../../worktree/artifact-inject
|
|
|
5
5
|
import { completionPhaseFromDispatch, debugCompletionPhaseOrder, expectedCompletionToolName, expectedDebugCompletionToolName, expectedQaCompletionToolName, qaCompletionGateOrder, readCompletionArtifact, usesDebugCycleCompletion, usesDevelopCycleCompletion, writeCompletionArtifact } from "../phase-completion.js";
|
|
6
6
|
import { emitArchitectStructuredEvents, emitCompletionArtifactRecorded, emitQaArtifactRecorded, emitQaGateAccepted, emitQaStructuredEvents, isDebugCompletionPhaseForProvider, nextQaGate, phaseCompleteEventType, qaArtifactRootForDispatch, readQaCompletionArtifactBundle, readQaGateArtifact } from "./provider-cli.artifacts.js";
|
|
7
7
|
import { deterministicBrowserCheck, deterministicBrowserSidecar, hasLaunchTarget, writeQaSidecarFiles } from "./provider-cli.qa-sidecars.js";
|
|
8
|
-
import { buildProviderCliCommand, formatProviderCliCommand, isPullRequestCommentsDispatch, singleLine } from "./provider-cli.prompts.js";
|
|
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;
|
|
@@ -202,26 +203,82 @@ export class DeterministicProviderCliService {
|
|
|
202
203
|
artifacts: [qaArtifact]
|
|
203
204
|
};
|
|
204
205
|
}
|
|
206
|
+
async runCiRepair(context) {
|
|
207
|
+
return await this.runPullRequestRepair(context, "ci_repair");
|
|
208
|
+
}
|
|
205
209
|
async runConflictResolve(context) {
|
|
210
|
+
return await this.runPullRequestRepair(context, "conflict_resolve");
|
|
211
|
+
}
|
|
212
|
+
async runPullRequestRepair(context, route) {
|
|
206
213
|
const { dispatch, worktree } = context;
|
|
214
|
+
const repairChecks = route === "ci_repair" || isPullRequestCheckRepairDispatch(dispatch);
|
|
207
215
|
const addressComments = isPullRequestCommentsDispatch(dispatch);
|
|
208
|
-
|
|
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
|
+
}
|
|
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)}`]);
|
|
209
242
|
await context.emit(buildProviderUsageEvent(dispatch, estimatedDeterministicUsage()));
|
|
210
243
|
await context.emit({
|
|
211
244
|
role: "assistant",
|
|
212
245
|
kind: "message",
|
|
213
|
-
content:
|
|
214
|
-
? `Deterministic
|
|
215
|
-
:
|
|
216
|
-
|
|
246
|
+
content: repairChecks
|
|
247
|
+
? `Deterministic CI repair completed for ${worktree.branchName}.`
|
|
248
|
+
: addressComments
|
|
249
|
+
? `Deterministic PR comment addressing completed for ${worktree.branchName}.`
|
|
250
|
+
: `Deterministic conflict resolve completed for ${worktree.branchName}.`,
|
|
251
|
+
eventType: repairChecks ? "pr_checks_complete" : addressComments ? "pr_comments_complete" : "conflict_resolve_complete"
|
|
217
252
|
});
|
|
218
253
|
return {
|
|
219
|
-
summary:
|
|
220
|
-
? `forge-bridge
|
|
221
|
-
:
|
|
254
|
+
summary: repairChecks
|
|
255
|
+
? `forge-bridge repaired PR checks for ${worktree.branchName}.`
|
|
256
|
+
: addressComments
|
|
257
|
+
? `forge-bridge addressed PR comments for ${worktree.branchName}.`
|
|
258
|
+
: `forge-bridge resolved conflicts for ${worktree.branchName}.`,
|
|
259
|
+
spec: repairSpec,
|
|
260
|
+
artifact: repairArtifact,
|
|
261
|
+
artifacts: repairArtifact ? [repairArtifact] : [],
|
|
222
262
|
};
|
|
223
263
|
}
|
|
224
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
|
+
}
|
|
225
282
|
function deterministicDraftPayload(dispatch) {
|
|
226
283
|
const kind = dispatch.draft?.kind ?? "work_item";
|
|
227
284
|
if (kind === "work_order_decomposition") {
|
|
@@ -5,6 +5,8 @@ export interface ProviderProcessRunRequest {
|
|
|
5
5
|
readonly args: readonly string[];
|
|
6
6
|
readonly cwd: string;
|
|
7
7
|
readonly env?: Readonly<Record<string, string>>;
|
|
8
|
+
readonly stdin?: string;
|
|
9
|
+
readonly captureOutput?: boolean;
|
|
8
10
|
}
|
|
9
11
|
export interface ProviderProcessRunResult {
|
|
10
12
|
readonly exitCode: number;
|
|
@@ -15,6 +17,7 @@ export interface ProviderProcessRunner {
|
|
|
15
17
|
run(request: ProviderProcessRunRequest, handlers?: ProviderProcessHandlers, signal?: AbortSignal): Promise<ProviderProcessRunResult>;
|
|
16
18
|
}
|
|
17
19
|
export interface ProviderProcessHandlers {
|
|
20
|
+
onStarted?(): Promise<void> | void;
|
|
18
21
|
onStdoutLine?(line: string): Promise<void> | void;
|
|
19
22
|
onStderrLine?(line: string): Promise<void> | void;
|
|
20
23
|
}
|
|
@@ -27,5 +30,19 @@ export declare function buildProviderChildEnvironment(requestEnv?: Readonly<Reco
|
|
|
27
30
|
export declare class StreamingProviderProcessRunner implements ProviderProcessRunner {
|
|
28
31
|
run(request: ProviderProcessRunRequest, handlers?: ProviderProcessHandlers, signal?: AbortSignal): Promise<ProviderProcessRunResult>;
|
|
29
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;
|
|
30
47
|
export declare function chooseWindowsCommandCandidate(candidates: readonly string[]): string | null;
|
|
31
48
|
export {};
|