@loucompanion/forge-bridge 0.1.3-dev.f66395c3ac21 → 0.1.4-dev.04a1e5b9e484
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 +165 -1
- package/dist/bridge/bridge-chat.types.d.ts +111 -0
- package/dist/bridge/bridge-chat.types.js +401 -0
- package/dist/bridge/bridge.base.d.ts +9 -1
- package/dist/bridge/bridge.service.d.ts +129 -3
- package/dist/bridge/bridge.service.js +2427 -70
- package/dist/bridge/bridge.types.d.ts +218 -2
- package/dist/bridge/dispatch-result-outbox.d.ts +25 -0
- package/dist/bridge/dispatch-result-outbox.js +62 -0
- package/dist/bridge/dispatch-result-worker.d.ts +2 -0
- package/dist/bridge/dispatch-result-worker.js +122 -0
- package/dist/bridge/internals.d.ts +2 -0
- package/dist/bridge/internals.js +1 -0
- package/dist/bridge/readiness-result-outbox.d.ts +21 -0
- package/dist/bridge/readiness-result-outbox.js +54 -0
- package/dist/bridge/readiness-result-worker.d.ts +2 -0
- package/dist/bridge/readiness-result-worker.js +121 -0
- package/dist/bridge/runner-host-capabilities.d.ts +15 -0
- package/dist/bridge/runner-host-capabilities.js +93 -0
- 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.d.ts +2 -0
- package/dist/cli/cli.help.js +217 -0
- package/dist/cli/cli.service.d.ts +55 -7
- package/dist/cli/cli.service.js +429 -219
- package/dist/cli/local-config.d.ts +7 -0
- package/dist/cli/local-config.js +51 -3
- package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
- package/dist/runtime/aspnet-runtime-prebuild.js +341 -0
- package/dist/runtime/attempt-database-broker.d.ts +89 -0
- package/dist/runtime/attempt-database-broker.js +390 -0
- package/dist/runtime/attempt-node-runtime-launcher.d.ts +18 -0
- package/dist/runtime/attempt-node-runtime-launcher.js +140 -0
- package/dist/runtime/attempt-path-operation.d.ts +21 -0
- package/dist/runtime/attempt-path-operation.js +94 -0
- package/dist/runtime/attempt-private-directory.d.ts +6 -0
- package/dist/runtime/attempt-private-directory.js +54 -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 +102 -0
- package/dist/runtime/attempt-process-supervisor.js +421 -0
- package/dist/runtime/canonical-source-file.d.ts +3 -0
- package/dist/runtime/canonical-source-file.js +54 -0
- package/dist/runtime/docker-engine-probe.d.ts +90 -0
- package/dist/runtime/docker-engine-probe.js +333 -0
- package/dist/runtime/generated-output-reset.d.ts +20 -0
- package/dist/runtime/generated-output-reset.js +157 -0
- package/dist/runtime/host-database-capability-boundary.d.ts +16 -0
- package/dist/runtime/host-database-capability-boundary.js +62 -0
- package/dist/runtime/host-diagnostics.d.ts +11 -0
- package/dist/runtime/host-diagnostics.js +192 -0
- package/dist/runtime/host-execution-root.d.ts +4 -0
- package/dist/runtime/host-execution-root.js +39 -0
- package/dist/runtime/host-listener-inspector.d.ts +51 -0
- package/dist/runtime/host-listener-inspector.js +385 -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 +58 -0
- package/dist/runtime/host-runtime-contract.js +813 -0
- package/dist/runtime/host-runtime-lease.d.ts +217 -0
- package/dist/runtime/host-runtime-lease.js +2487 -0
- package/dist/runtime/host-runtime-promotion.d.ts +32 -0
- package/dist/runtime/host-runtime-promotion.js +206 -0
- package/dist/runtime/memory-capacity.d.ts +70 -0
- package/dist/runtime/memory-capacity.js +123 -0
- package/dist/runtime/native-test-postgres-fallback.d.ts +25 -0
- package/dist/runtime/native-test-postgres-fallback.js +91 -0
- package/dist/runtime/node-host-runtime-adapter.d.ts +54 -0
- package/dist/runtime/node-host-runtime-adapter.js +286 -0
- package/dist/runtime/pinned-directory-process.d.ts +9 -0
- package/dist/runtime/pinned-directory-process.js +61 -0
- package/dist/runtime/pinned-source-directory.d.ts +17 -0
- package/dist/runtime/pinned-source-directory.js +94 -0
- package/dist/runtime/playwright-host-runtime-adapter.d.ts +21 -0
- package/dist/runtime/playwright-host-runtime-adapter.js +268 -0
- package/dist/runtime/prepared-runtime-identity.d.ts +6 -0
- package/dist/runtime/prepared-runtime-identity.js +218 -0
- package/dist/runtime/prepared-runtime-readiness.d.ts +30 -0
- package/dist/runtime/prepared-runtime-readiness.js +285 -0
- package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
- package/dist/runtime/runtime-isolation-lease.js +161 -0
- package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
- package/dist/runtime/workspace-resource-estimator.js +174 -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 +84 -0
- package/dist/sandbox/preflight.js +521 -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 +6 -0
- package/dist/sandbox/repository-lock.js +129 -0
- package/dist/sandbox/runtime.d.ts +302 -0
- package/dist/sandbox/runtime.js +2368 -0
- package/dist/sandbox/sandbox.types.d.ts +93 -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 +74 -0
- package/dist/session/managed-attempt-tools.d.ts +28 -0
- package/dist/session/managed-attempt-tools.js +1016 -0
- package/dist/session/managed-build-coordinator.d.ts +5 -0
- package/dist/session/managed-build-coordinator.js +106 -0
- package/dist/session/managed-build-receipt.d.ts +21 -0
- package/dist/session/managed-build-receipt.js +337 -0
- package/dist/session/managed-verification-timeouts.d.ts +8 -0
- package/dist/session/managed-verification-timeouts.js +17 -0
- package/dist/session/phase-completion.artifacts.d.ts +27 -0
- package/dist/session/phase-completion.artifacts.js +829 -0
- package/dist/session/phase-completion.d.ts +53 -42
- package/dist/session/phase-completion.helpers.d.ts +117 -0
- package/dist/session/phase-completion.helpers.js +707 -0
- package/dist/session/phase-completion.js +125 -3007
- package/dist/session/phase-completion.prompts.d.ts +14 -0
- package/dist/session/phase-completion.prompts.js +233 -0
- package/dist/session/phase-completion.runtime.d.ts +49 -0
- package/dist/session/phase-completion.runtime.js +840 -0
- package/dist/session/phase-completion.schemas.d.ts +3 -0
- package/dist/session/phase-completion.schemas.js +859 -0
- package/dist/session/phase-completion.validation.d.ts +38 -0
- package/dist/session/phase-completion.validation.js +918 -0
- package/dist/session/pre-publication-validation.d.ts +75 -0
- package/dist/session/pre-publication-validation.js +661 -0
- package/dist/session/provider-cli/bridge-cli.conformance-fixtures.d.ts +72 -0
- package/dist/session/provider-cli/bridge-cli.conformance-fixtures.js +462 -0
- package/dist/session/provider-cli/ci-repair-readiness.d.ts +16 -0
- package/dist/session/provider-cli/ci-repair-readiness.js +110 -0
- package/dist/session/provider-cli/provider-cli.artifacts.d.ts +95 -0
- package/dist/session/provider-cli/provider-cli.artifacts.js +812 -0
- package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
- package/dist/session/provider-cli/provider-cli.deterministic-payloads.d.ts +12 -0
- package/dist/session/provider-cli/provider-cli.deterministic-payloads.js +279 -0
- package/dist/session/provider-cli/provider-cli.deterministic.d.ts +14 -0
- package/dist/session/provider-cli/provider-cli.deterministic.js +407 -0
- package/dist/session/provider-cli/provider-cli.process.d.ts +99 -0
- package/dist/session/provider-cli/provider-cli.process.js +874 -0
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +52 -0
- package/dist/session/provider-cli/provider-cli.prompts.js +715 -0
- package/dist/session/provider-cli/provider-cli.qa-sidecars.d.ts +11 -0
- package/dist/session/provider-cli/provider-cli.qa-sidecars.js +66 -0
- package/dist/session/provider-cli/provider-cli.repair-epoch.d.ts +6 -0
- package/dist/session/provider-cli/provider-cli.repair-epoch.js +11 -0
- package/dist/session/provider-cli/provider-cli.runtime.d.ts +22 -0
- package/dist/session/provider-cli/provider-cli.runtime.js +92 -0
- package/dist/session/provider-cli/provider-cli.service.d.ts +47 -53
- package/dist/session/provider-cli/provider-cli.service.js +1258 -2497
- package/dist/session/provider-cli/provider-cli.types.d.ts +8 -0
- package/dist/session/provider-cli/provider-output-state.d.ts +32 -0
- package/dist/session/provider-cli/provider-output-state.js +817 -0
- 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 +448 -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 +586 -59
- package/dist/session/session.types.d.ts +324 -3
- package/dist/session/session.types.js +939 -10
- package/dist/session/unreferenced-exports.d.ts +12 -0
- package/dist/session/unreferenced-exports.js +59 -0
- package/dist/shared/format/format-bytes.d.ts +1 -0
- package/dist/shared/format/format-bytes.js +9 -0
- package/dist/shared/git/git.js +2 -1
- package/dist/shared/path/path.d.ts +1 -0
- package/dist/shared/path/path.js +21 -1
- package/dist/shared/process/dotnet-toolchain.d.ts +17 -0
- package/dist/shared/process/dotnet-toolchain.js +307 -0
- package/dist/shared/process/host-process-recovery.d.ts +12 -0
- package/dist/shared/process/host-process-recovery.js +79 -0
- package/dist/shared/process/host-process-supervisor.d.ts +59 -0
- package/dist/shared/process/host-process-supervisor.js +331 -0
- package/dist/shared/process/process.d.ts +3 -0
- package/dist/shared/process/process.js +13 -7
- package/dist/shared/process/windows-launch.d.ts +8 -0
- package/dist/shared/process/windows-launch.js +41 -0
- package/dist/shared/provider-capabilities.d.ts +4 -0
- package/dist/shared/provider-capabilities.js +88 -0
- package/dist/shared/redaction/redaction.d.ts +1 -0
- package/dist/shared/redaction/redaction.js +47 -4
- package/dist/shared/time/monotonic.d.ts +1 -0
- package/dist/shared/time/monotonic.js +8 -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 +82 -10
- package/dist/worktree/dependency-warmer.d.ts +53 -1
- package/dist/worktree/dependency-warmer.js +619 -11
- 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/story-qa-reference-hydrator.d.ts +4 -0
- package/dist/worktree/story-qa-reference-hydrator.js +253 -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 +16 -3
- package/dist/worktree/worktree.service.js +334 -54
- package/dist/worktree/worktree.types.d.ts +9 -1
- package/package.json +12 -7
- package/runtime/windows-job-supervisor.ps1 +722 -0
- package/runtime/windows-listener-owner.ps1 +33 -0
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { redactText } from "../shared/redaction/redaction.js";
|
|
3
|
+
export class ForgeFailureError extends Error {
|
|
4
|
+
failureCode;
|
|
5
|
+
failureSource;
|
|
6
|
+
constructor(failureCode, message, failureSource = "bridge", options) {
|
|
7
|
+
super(message, options);
|
|
8
|
+
this.name = "ForgeFailureError";
|
|
9
|
+
this.failureCode = normalizeFailureCode(failureCode);
|
|
10
|
+
this.failureSource = failureSource;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
2
13
|
export function parseRunOnceDispatchPacket(value) {
|
|
3
14
|
const packet = asObject(value, "dispatch packet");
|
|
4
15
|
const schemaVersion = requiredNumber(packet, "schemaVersion");
|
|
5
|
-
if (schemaVersion !== 1) {
|
|
16
|
+
if (schemaVersion !== 1 && schemaVersion !== 2) {
|
|
6
17
|
throw new Error(`Unsupported dispatch schemaVersion: ${schemaVersion}`);
|
|
7
18
|
}
|
|
8
19
|
return {
|
|
9
|
-
schemaVersion
|
|
20
|
+
schemaVersion,
|
|
10
21
|
userId: requiredString(packet, "userId"),
|
|
11
22
|
workItem: parseWorkItem(packet.workItem),
|
|
12
23
|
repo: parseRepo(packet.repo),
|
|
@@ -14,30 +25,676 @@ export function parseRunOnceDispatchPacket(value) {
|
|
|
14
25
|
session: parseSession(packet.session),
|
|
15
26
|
lease: parseLease(packet.lease),
|
|
16
27
|
execution: parseExecution(packet.execution),
|
|
28
|
+
runtimeIntent: parseRuntimeIntent(packet.runtimeIntent),
|
|
17
29
|
serverLocal: parseServerLocal(packet.serverLocal),
|
|
30
|
+
sandbox: parseSandbox(packet.sandbox, schemaVersion),
|
|
18
31
|
pullRequest: parsePullRequest(packet.pullRequest),
|
|
19
32
|
apiKeySecretGrant: parseApiKeySecretGrant(packet.apiKeySecretGrant),
|
|
33
|
+
apiKeySecretGrantHandle: parseApiKeySecretGrantHandle(packet.apiKeySecretGrantHandle),
|
|
20
34
|
artifacts: parseArtifacts(packet.artifacts),
|
|
35
|
+
priorPhaseArtifacts: parsePriorPhaseArtifacts(packet.priorPhaseArtifacts),
|
|
21
36
|
warmPathOverrides: parseWarmPathOverrides(packet.warmPathOverrides),
|
|
22
37
|
qaBrowser: parseQaBrowser(packet.qaBrowser),
|
|
38
|
+
qaScope: parseQaScope(packet.qaScope ?? packet.qa_scope),
|
|
23
39
|
qaRework: parseQaRework(packet.qaRework),
|
|
24
40
|
architectRevision: parseArchitectRevision(packet.architectRevision),
|
|
41
|
+
architectDependencies: parseArchitectDependencies(packet.architectDependencies),
|
|
25
42
|
storyDevelop: parseStoryDevelop(packet.storyDevelop),
|
|
26
43
|
storyQa: parseStoryQa(packet.storyQa),
|
|
44
|
+
designerRefs: parseDesignerRefs(packet.designerRefs ?? packet.designer_refs),
|
|
27
45
|
draft: parseDraftPrompt(packet.draft)
|
|
28
46
|
};
|
|
29
47
|
}
|
|
30
|
-
export function
|
|
48
|
+
export function parseRunOnceResultPacket(value) {
|
|
49
|
+
const packet = asObject(value, "run-once result packet");
|
|
50
|
+
const schemaVersion = requiredNumber(packet, "schemaVersion");
|
|
51
|
+
if (schemaVersion !== 1 && schemaVersion !== 2) {
|
|
52
|
+
throw new Error(`Unsupported run-once result schemaVersion: ${schemaVersion}`);
|
|
53
|
+
}
|
|
54
|
+
const exitCode = requiredNumber(packet, "exitCode");
|
|
55
|
+
if (!Number.isSafeInteger(exitCode))
|
|
56
|
+
throw new Error("exitCode must be an integer.");
|
|
57
|
+
requiredString(packet, "summary");
|
|
58
|
+
if (!Array.isArray(packet.events))
|
|
59
|
+
throw new Error("events must be an array.");
|
|
60
|
+
for (const item of packet.events) {
|
|
61
|
+
if (typeof item !== "object" || item === null || !("wait" in item))
|
|
62
|
+
continue;
|
|
63
|
+
const event = asObject(item, "run-once result event");
|
|
64
|
+
if (event.wait !== undefined && event.wait !== null) {
|
|
65
|
+
parseWaitEnvelope(event.wait, requiredString(event, "eventType"));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (schemaVersion === 1) {
|
|
69
|
+
if (packet.failure !== undefined && packet.failure !== null) {
|
|
70
|
+
throw new Error("run-once result schemaVersion 1 cannot contain a v2 failure envelope.");
|
|
71
|
+
}
|
|
72
|
+
return packet;
|
|
73
|
+
}
|
|
74
|
+
if (exitCode === 0 && packet.failure !== undefined && packet.failure !== null) {
|
|
75
|
+
throw new Error("run-once result cannot report both success and a failure envelope.");
|
|
76
|
+
}
|
|
77
|
+
if (exitCode !== 0 && (packet.failure === undefined || packet.failure === null)) {
|
|
78
|
+
throw new Error("run-once result schemaVersion 2 requires a failure envelope for a nonzero exit.");
|
|
79
|
+
}
|
|
80
|
+
if (packet.failure !== undefined && packet.failure !== null)
|
|
81
|
+
parseFailureEnvelope(packet.failure);
|
|
82
|
+
return packet;
|
|
83
|
+
}
|
|
84
|
+
const waitKinds = new Map([
|
|
85
|
+
["host_runtime_admission_waiting", "host_runtime_admission"],
|
|
86
|
+
["host_runtime_admission_acquired", "host_runtime_admission"],
|
|
87
|
+
["host_runtime_serial_admission_waiting", "host_runtime_serial_admission"],
|
|
88
|
+
["host_runtime_prepared_probe_started", "host_runtime_prepared_probe"],
|
|
89
|
+
["host_runtime_prepared_probe_progress", "host_runtime_prepared_probe"],
|
|
90
|
+
["host_runtime_prepared_probe_completed", "host_runtime_prepared_probe"],
|
|
91
|
+
["host_runtime_prepared_admission_waiting", "host_runtime_prepared_admission"],
|
|
92
|
+
["host_runtime_prepared_admission_acquired", "host_runtime_prepared_admission"],
|
|
93
|
+
]);
|
|
94
|
+
export function waitKindForEvent(eventType) {
|
|
95
|
+
const kind = waitKinds.get(eventType);
|
|
96
|
+
if (!kind)
|
|
97
|
+
throw new Error(`Unsupported typed wait observation '${eventType}'.`);
|
|
98
|
+
return kind;
|
|
99
|
+
}
|
|
100
|
+
export function waitWakeCondition(kind) {
|
|
101
|
+
switch (kind) {
|
|
102
|
+
case "host_runtime_serial_admission":
|
|
103
|
+
return "project_singleton_released";
|
|
104
|
+
case "host_runtime_prepared_probe":
|
|
105
|
+
return "prepared_runtime_probe_completed";
|
|
106
|
+
default:
|
|
107
|
+
return "host_runtime_admission_available";
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function parseWaitEnvelope(value, eventType) {
|
|
111
|
+
const wait = asObject(value, "wait envelope");
|
|
112
|
+
if (requiredNumber(wait, "schemaVersion") !== 2) {
|
|
113
|
+
throw new Error("Unsupported wait envelope schemaVersion.");
|
|
114
|
+
}
|
|
115
|
+
let expectedKind;
|
|
116
|
+
try {
|
|
117
|
+
expectedKind = waitKindForEvent(eventType);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
throw new Error("wait envelope is only valid on a wait lifecycle observation.");
|
|
121
|
+
}
|
|
122
|
+
const kind = requiredString(wait, "kind");
|
|
123
|
+
const scope = requiredString(wait, "scope");
|
|
124
|
+
const wakeCondition = requiredString(wait, "wakeCondition");
|
|
125
|
+
if (kind !== expectedKind
|
|
126
|
+
|| !isWaitCode(kind, 80)
|
|
127
|
+
|| !isWaitCode(scope, 64)
|
|
128
|
+
|| !isWaitCode(wakeCondition, 80)
|
|
129
|
+
|| wakeCondition !== waitWakeCondition(kind)) {
|
|
130
|
+
throw new Error("wait envelope kind, scope, or wakeCondition is invalid.");
|
|
131
|
+
}
|
|
132
|
+
if (!Array.isArray(wait.resourceKeys)
|
|
133
|
+
|| wait.resourceKeys.length > 16
|
|
134
|
+
|| wait.resourceKeys.some(item => typeof item !== "string" || !isWaitCode(item, 256))
|
|
135
|
+
|| new Set(wait.resourceKeys).size !== wait.resourceKeys.length) {
|
|
136
|
+
throw new Error("wait envelope resourceKeys are invalid, duplicated, or exceed their bound.");
|
|
137
|
+
}
|
|
138
|
+
const queuePosition = wait.queuePosition;
|
|
139
|
+
if (queuePosition !== undefined && queuePosition !== null
|
|
140
|
+
&& (typeof queuePosition !== "number" || !Number.isSafeInteger(queuePosition) || queuePosition <= 0)) {
|
|
141
|
+
throw new Error("wait envelope queuePosition is invalid.");
|
|
142
|
+
}
|
|
143
|
+
const blockerSessionId = wait.blockerSessionId;
|
|
144
|
+
if (blockerSessionId !== undefined && blockerSessionId !== null
|
|
145
|
+
&& (typeof blockerSessionId !== "string"
|
|
146
|
+
|| !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu.test(blockerSessionId)
|
|
147
|
+
|| /^0{8}-0{4}-0{4}-0{4}-0{12}$/u.test(blockerSessionId))) {
|
|
148
|
+
throw new Error("wait envelope blockerSessionId is invalid.");
|
|
149
|
+
}
|
|
150
|
+
const blockerTitle = wait.blockerTitle;
|
|
151
|
+
if (blockerTitle !== undefined && blockerTitle !== null
|
|
152
|
+
&& (typeof blockerTitle !== "string" || blockerTitle.length > 500)) {
|
|
153
|
+
throw new Error("wait envelope blockerTitle is invalid.");
|
|
154
|
+
}
|
|
155
|
+
const retryAt = wait.retryAt;
|
|
156
|
+
if (retryAt !== undefined && retryAt !== null
|
|
157
|
+
&& (typeof retryAt !== "string" || !Number.isFinite(Date.parse(retryAt)))) {
|
|
158
|
+
throw new Error("wait envelope retryAt is invalid.");
|
|
159
|
+
}
|
|
160
|
+
const firstObservedAt = requiredString(wait, "firstObservedAt");
|
|
161
|
+
if (!Number.isFinite(Date.parse(firstObservedAt))) {
|
|
162
|
+
throw new Error("wait envelope firstObservedAt is invalid.");
|
|
163
|
+
}
|
|
164
|
+
const observationVersion = requiredNumber(wait, "observationVersion");
|
|
165
|
+
if (!Number.isSafeInteger(observationVersion) || observationVersion <= 0) {
|
|
166
|
+
throw new Error("wait envelope observationVersion is invalid.");
|
|
167
|
+
}
|
|
168
|
+
if (requiredString(wait, "contractOrigin") !== "bridge_v2") {
|
|
169
|
+
throw new Error("wait envelope contractOrigin must identify the v2 Bridge writer.");
|
|
170
|
+
}
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
function isWaitCode(value, maximumLength) {
|
|
174
|
+
return value.length <= maximumLength && /^[a-z][a-z0-9_.:-]*$/u.test(value);
|
|
175
|
+
}
|
|
176
|
+
function parseRuntimeIntent(value) {
|
|
177
|
+
if (value === undefined || value === null) {
|
|
178
|
+
return { executionClass: "managed_runtime", applicationRuntimeRequired: true };
|
|
179
|
+
}
|
|
180
|
+
const intent = asObject(value, "runtimeIntent");
|
|
181
|
+
const executionClass = requiredString(intent, "executionClass").trim().toLowerCase();
|
|
182
|
+
if (executionClass !== "analysis_only" && executionClass !== "managed_runtime") {
|
|
183
|
+
throw new Error("runtimeIntent.executionClass must be analysis_only or managed_runtime.");
|
|
184
|
+
}
|
|
185
|
+
const applicationRuntimeRequired = requiredBoolean(intent, "applicationRuntimeRequired");
|
|
186
|
+
if ((executionClass === "analysis_only") === applicationRuntimeRequired) {
|
|
187
|
+
throw new Error("runtimeIntent execution class and application runtime requirement are inconsistent.");
|
|
188
|
+
}
|
|
189
|
+
return { executionClass, applicationRuntimeRequired };
|
|
190
|
+
}
|
|
191
|
+
function parseSandbox(value, schemaVersion) {
|
|
192
|
+
if (value === undefined || value === null) {
|
|
193
|
+
if (schemaVersion === 2) {
|
|
194
|
+
throw new Error("sandbox is required for dispatch schemaVersion 2.");
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
const sandbox = asObject(value, "sandbox");
|
|
199
|
+
const executionMode = requiredString(sandbox, "executionMode");
|
|
200
|
+
if (executionMode !== "host" && executionMode !== "docker") {
|
|
201
|
+
throw new Error("sandbox.executionMode must be host or docker.");
|
|
202
|
+
}
|
|
203
|
+
const executionPolicy = requiredString(sandbox, "executionPolicy");
|
|
204
|
+
if (executionPolicy !== "host_allowed" && executionPolicy !== "sandbox_preferred" && executionPolicy !== "sandbox_required") {
|
|
205
|
+
throw new Error("sandbox.executionPolicy is invalid.");
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
dispatchAttemptId: requiredString(sandbox, "dispatchAttemptId"),
|
|
209
|
+
executionLeaseId: requiredString(sandbox, "executionLeaseId"),
|
|
210
|
+
runnerInstanceId: requiredString(sandbox, "runnerInstanceId"),
|
|
211
|
+
executionMode,
|
|
212
|
+
executionPolicy,
|
|
213
|
+
networkPolicy: parseSandboxNetworkPolicy(sandbox.networkPolicy),
|
|
214
|
+
requestedResources: parseSandboxResourceRequest(sandbox.requestedResources),
|
|
215
|
+
runtime: optionalString(sandbox, "runtime"),
|
|
216
|
+
sandboxConfigCommitSha: optionalString(sandbox, "sandboxConfigCommitSha"),
|
|
217
|
+
inputCommitSha: optionalString(sandbox, "inputCommitSha"),
|
|
218
|
+
contractDigest: optionalString(sandbox, "contractDigest"),
|
|
219
|
+
imageDigest: optionalString(sandbox, "imageDigest"),
|
|
220
|
+
publicationMode: parsePublicationMode(sandbox.publicationMode),
|
|
221
|
+
publishGrantHandle: parsePublishGrantHandle(sandbox.publishGrantHandle)
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function parsePublicationMode(value) {
|
|
225
|
+
if (value === undefined || value === null)
|
|
226
|
+
return "shared_filesystem";
|
|
227
|
+
if (value !== "bridge_push" && value !== "shared_filesystem")
|
|
228
|
+
throw new Error("sandbox.publicationMode is invalid.");
|
|
229
|
+
return value;
|
|
230
|
+
}
|
|
231
|
+
function parsePublishGrantHandle(value) {
|
|
232
|
+
if (value === undefined || value === null)
|
|
233
|
+
return null;
|
|
234
|
+
const handle = asObject(value, "sandbox.publishGrantHandle");
|
|
235
|
+
const grantId = requiredString(handle, "grantId");
|
|
236
|
+
const branch = requiredString(handle, "branch");
|
|
237
|
+
const expiresAt = requiredString(handle, "expiresAt");
|
|
238
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu.test(grantId)) {
|
|
239
|
+
throw new Error("sandbox.publishGrantHandle.grantId is invalid.");
|
|
240
|
+
}
|
|
241
|
+
if (branch.length > 240)
|
|
242
|
+
throw new Error("sandbox.publishGrantHandle.branch is too long.");
|
|
243
|
+
if (!Number.isFinite(Date.parse(expiresAt)))
|
|
244
|
+
throw new Error("sandbox.publishGrantHandle.expiresAt is invalid.");
|
|
245
|
+
return { grantId, branch, expiresAt };
|
|
246
|
+
}
|
|
247
|
+
function parseSandboxResourceRequest(value) {
|
|
248
|
+
if (value === undefined || value === null) {
|
|
249
|
+
return {
|
|
250
|
+
sandboxSlots: 1,
|
|
251
|
+
cpuMillis: 2000,
|
|
252
|
+
workerMemoryLimitBytes: 2 * 1024 ** 3,
|
|
253
|
+
serviceMemoryLimitBytes: 0,
|
|
254
|
+
runtimeOverheadBytes: 128 * 1024 ** 2,
|
|
255
|
+
memoryBytes: 2 * 1024 ** 3 + 128 * 1024 ** 2,
|
|
256
|
+
workspaceBytes: 10 * 1024 ** 3,
|
|
257
|
+
pids: 512,
|
|
258
|
+
buildSlots: 1
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
const resources = asObject(value, "sandbox.requestedResources");
|
|
262
|
+
const memoryBytes = boundedInteger(resources, "memoryBytes", 128 * 1024 ** 2, 512 * 1024 ** 3);
|
|
263
|
+
const hasMemoryBreakdown = ["workerMemoryLimitBytes", "serviceMemoryLimitBytes", "runtimeOverheadBytes"]
|
|
264
|
+
.some(key => resources[key] !== undefined && resources[key] !== null);
|
|
265
|
+
const workerMemoryLimitBytes = hasMemoryBreakdown
|
|
266
|
+
? boundedInteger(resources, "workerMemoryLimitBytes", 128 * 1024 ** 2, 512 * 1024 ** 3)
|
|
267
|
+
: memoryBytes;
|
|
268
|
+
const serviceMemoryLimitBytes = hasMemoryBreakdown
|
|
269
|
+
? boundedInteger(resources, "serviceMemoryLimitBytes", 0, 512 * 1024 ** 3)
|
|
270
|
+
: 0;
|
|
271
|
+
const runtimeOverheadBytes = hasMemoryBreakdown
|
|
272
|
+
? boundedInteger(resources, "runtimeOverheadBytes", 0, 64 * 1024 ** 3)
|
|
273
|
+
: 0;
|
|
274
|
+
if (workerMemoryLimitBytes + serviceMemoryLimitBytes + runtimeOverheadBytes !== memoryBytes) {
|
|
275
|
+
throw new Error("sandbox.requestedResources memory breakdown must equal memoryBytes.");
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
sandboxSlots: boundedInteger(resources, "sandboxSlots", 1, 4),
|
|
279
|
+
cpuMillis: boundedInteger(resources, "cpuMillis", 100, 64_000),
|
|
280
|
+
workerMemoryLimitBytes,
|
|
281
|
+
serviceMemoryLimitBytes,
|
|
282
|
+
runtimeOverheadBytes,
|
|
283
|
+
memoryBytes,
|
|
284
|
+
workspaceBytes: boundedInteger(resources, "workspaceBytes", 1024 ** 3, 10 * 1024 ** 4),
|
|
285
|
+
pids: boundedInteger(resources, "pids", 32, 65_536),
|
|
286
|
+
buildSlots: boundedInteger(resources, "buildSlots", 0, 4)
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function parseSandboxNetworkPolicy(value) {
|
|
290
|
+
if (value === undefined || value === null)
|
|
291
|
+
return { mode: "offline", allowedHosts: [] };
|
|
292
|
+
const policy = asObject(value, "sandbox.networkPolicy");
|
|
293
|
+
const mode = requiredString(policy, "mode");
|
|
294
|
+
if (mode !== "offline" && mode !== "public_allowlist" && mode !== "open_public") {
|
|
295
|
+
throw new Error("sandbox.networkPolicy.mode is invalid.");
|
|
296
|
+
}
|
|
297
|
+
const rawHosts = policy.allowedHosts ?? [];
|
|
298
|
+
if (!Array.isArray(rawHosts) || rawHosts.some((host) => typeof host !== "string")) {
|
|
299
|
+
throw new Error("sandbox.networkPolicy.allowedHosts must be a string array.");
|
|
300
|
+
}
|
|
301
|
+
const allowedHosts = [...new Set(rawHosts.map(host => host.trim().toLowerCase()).filter(Boolean))];
|
|
302
|
+
if (allowedHosts.length > 128 || allowedHosts.some(host => !/^(?:\*\.)?(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z](?:[a-z0-9-]{0,61}[a-z0-9])?$/u.test(host))) {
|
|
303
|
+
throw new Error("sandbox.networkPolicy.allowedHosts contains an invalid hostname.");
|
|
304
|
+
}
|
|
305
|
+
if (mode !== "public_allowlist" && allowedHosts.length > 0) {
|
|
306
|
+
throw new Error("sandbox.networkPolicy.allowedHosts requires public_allowlist mode.");
|
|
307
|
+
}
|
|
308
|
+
if (mode === "public_allowlist" && allowedHosts.length === 0) {
|
|
309
|
+
return { mode: "offline", allowedHosts: [] };
|
|
310
|
+
}
|
|
311
|
+
return { mode, allowedHosts };
|
|
312
|
+
}
|
|
313
|
+
const failureCodePattern = /^[a-z][a-z0-9_.-]{0,79}$/u;
|
|
314
|
+
const failureCategories = new Set(["domain", "provider", "transport", "cancellation", "ownership", "resource", "unknown"]);
|
|
315
|
+
const failurePhases = new Set(["offer", "build", "prepare", "agent", "validation", "import", "teardown", "unknown"]);
|
|
316
|
+
const failureSources = new Set(["bridge", "provider", "sandbox", "unknown"]);
|
|
317
|
+
const failureRetryabilities = new Set(["retryable", "non_retryable", "policy_required", "unknown"]);
|
|
318
|
+
const failureOperatorActions = new Set(["none", "retry_dispatch", "retry_with_policy", "inspect_failure", "fix_input", "fix_environment", "reauthenticate", "restore_ownership"]);
|
|
319
|
+
const maximumFailureMessageLength = 2000;
|
|
320
|
+
const maximumFailureCausalChainLength = 8;
|
|
321
|
+
const waitConditionCodes = new Set([
|
|
322
|
+
"host_runtime_admission_waiting",
|
|
323
|
+
"host_runtime_serial_admission_waiting",
|
|
324
|
+
"host_runtime_prepared_probe_started",
|
|
325
|
+
"host_runtime_prepared_probe_progress",
|
|
326
|
+
"host_runtime_prepared_admission_waiting",
|
|
327
|
+
"waiting_for_capacity",
|
|
328
|
+
]);
|
|
329
|
+
const stableFailureFamilyCodes = new Set([
|
|
330
|
+
"generated_output_reset_invalid",
|
|
331
|
+
"generated_output_reset_unsafe",
|
|
332
|
+
"sandbox_build_failed",
|
|
333
|
+
"sandbox_build_policy_failed",
|
|
334
|
+
"sandbox_builder_release_failed",
|
|
335
|
+
"sandbox_config_commit_invalid",
|
|
336
|
+
"sandbox_config_commit_missing",
|
|
337
|
+
"sandbox_config_commit_unapproved",
|
|
338
|
+
"contract_invalid",
|
|
339
|
+
"sandbox_input_failed",
|
|
340
|
+
"docker_capacity_unavailable",
|
|
341
|
+
"docker_engine_invalid_response",
|
|
342
|
+
"docker_engine_recovery_failed",
|
|
343
|
+
"docker_engine_recovery_timeout",
|
|
344
|
+
"docker_linux_required",
|
|
345
|
+
"docker_preflight_required",
|
|
346
|
+
"docker_rootless_required",
|
|
347
|
+
"docker_unavailable",
|
|
348
|
+
"docker_unreachable",
|
|
349
|
+
"sandbox_output_conflict",
|
|
350
|
+
"sandbox_output_invalid",
|
|
351
|
+
"sandbox_output_missing",
|
|
352
|
+
"sandbox_output_publish_forbidden",
|
|
353
|
+
"sandbox_output_rejected",
|
|
354
|
+
"sandbox_output_publish_rejected",
|
|
355
|
+
"sandbox_output_publish_stale",
|
|
356
|
+
"sandbox_output_publish_unavailable",
|
|
357
|
+
"sandbox_output_stale",
|
|
358
|
+
"sandbox_import_cancelled",
|
|
359
|
+
"sandbox_import_unavailable",
|
|
360
|
+
"teardown_failed",
|
|
361
|
+
"reaper_cleanup_degraded",
|
|
362
|
+
"reaper_runner_replaced",
|
|
363
|
+
"sandbox_egress_policy_invalid",
|
|
364
|
+
"sandbox_egress_proxy_unavailable",
|
|
365
|
+
"sandbox_event_flood",
|
|
366
|
+
"sandbox_event_invalid",
|
|
367
|
+
"sandbox_identity_invalid",
|
|
368
|
+
"sandbox_image_signature_failed",
|
|
369
|
+
"sandbox_log_limit",
|
|
370
|
+
"sandbox_preflight_failed",
|
|
371
|
+
"sandbox_security_profile_invalid",
|
|
372
|
+
"sandbox_security_profile_unavailable",
|
|
373
|
+
"sandbox_service_health_policy_invalid",
|
|
374
|
+
]);
|
|
375
|
+
export function failureResult(summary, events = [], facts = failureFactsForCode("bridge.execution_failed")) {
|
|
31
376
|
return {
|
|
32
|
-
schemaVersion:
|
|
377
|
+
schemaVersion: 2,
|
|
33
378
|
exitCode: 1,
|
|
34
|
-
summary,
|
|
35
|
-
events
|
|
379
|
+
summary: boundedFailureText(summary),
|
|
380
|
+
events,
|
|
381
|
+
failure: createFailureEnvelope(summary, facts),
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
export function createFailureEnvelope(message, facts, occurredAt = new Date().toISOString()) {
|
|
385
|
+
const code = normalizeFailureCode(facts.code);
|
|
386
|
+
if (waitConditionCodes.has(code)) {
|
|
387
|
+
throw new Error(`Wait condition '${code}' must be emitted as lifecycle state, not a terminal failure.`);
|
|
388
|
+
}
|
|
389
|
+
assertFailureFactsMatchPolicy(code, facts);
|
|
390
|
+
return {
|
|
391
|
+
schemaVersion: 2,
|
|
392
|
+
code,
|
|
393
|
+
message: boundedFailureText(message),
|
|
394
|
+
category: facts.category,
|
|
395
|
+
phase: facts.phase,
|
|
396
|
+
source: facts.source,
|
|
397
|
+
retryability: facts.retryability,
|
|
398
|
+
operatorAction: facts.operatorAction,
|
|
399
|
+
causalChain: (facts.causalChain ?? []).slice(0, maximumFailureCausalChainLength).map(cause => ({
|
|
400
|
+
code: normalizeFailureCode(cause.code),
|
|
401
|
+
message: boundedFailureText(cause.message),
|
|
402
|
+
source: cause.source,
|
|
403
|
+
})),
|
|
404
|
+
occurredAt,
|
|
405
|
+
contractOrigin: "bridge_v2",
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
export function failureFactsFromError(error, options = {}) {
|
|
409
|
+
if (options.cancelled) {
|
|
410
|
+
const cancellationSource = options.source ?? "bridge";
|
|
411
|
+
return {
|
|
412
|
+
...failureFactsForCode("bridge.dispatch_cancelled", cancellationSource),
|
|
413
|
+
causalChain: errorCauseChain(error, cancellationSource),
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
const source = error instanceof ForgeFailureError
|
|
417
|
+
? error.failureSource
|
|
418
|
+
: options.source ?? "bridge";
|
|
419
|
+
const code = error instanceof ForgeFailureError
|
|
420
|
+
? error.failureCode
|
|
421
|
+
: "bridge.execution_failed";
|
|
422
|
+
return {
|
|
423
|
+
...failureFactsForCode(code, source),
|
|
424
|
+
causalChain: errorCauseChain(error, source),
|
|
36
425
|
};
|
|
37
426
|
}
|
|
427
|
+
export function failureFactsForCode(code, source = "bridge") {
|
|
428
|
+
const policy = failurePolicyForCode(code);
|
|
429
|
+
if (policy.known && !policy.allowedSources.includes(source)) {
|
|
430
|
+
throw new Error(`Failure code '${policy.code}' cannot be attributed to source '${source}'.`);
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
code: policy.code,
|
|
434
|
+
category: policy.category,
|
|
435
|
+
phase: policy.phase,
|
|
436
|
+
source,
|
|
437
|
+
retryability: policy.retryability,
|
|
438
|
+
operatorAction: policy.operatorAction,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
export function failurePolicyForCode(code) {
|
|
442
|
+
code = normalizeFailureCode(code);
|
|
443
|
+
const classification = unvalidatedFailureFactsForCode(code, "bridge");
|
|
444
|
+
const known = classification.category !== "unknown" || code === "legacy_failure_unknown";
|
|
445
|
+
return {
|
|
446
|
+
code,
|
|
447
|
+
category: classification.category,
|
|
448
|
+
phase: classification.phase,
|
|
449
|
+
retryability: classification.retryability,
|
|
450
|
+
operatorAction: classification.operatorAction,
|
|
451
|
+
allowedSources: allowedSourcesForCode(code, known),
|
|
452
|
+
known,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
function unvalidatedFailureFactsForCode(code, source) {
|
|
456
|
+
code = normalizeFailureCode(code);
|
|
457
|
+
if (["bridge.dispatch_cancelled", "dispatch_cancelled", "dispatch_session_terminal", "dispatch.capacity_retargeted"].includes(code)) {
|
|
458
|
+
return facts(code, "cancellation", "teardown", source, "non_retryable", "none");
|
|
459
|
+
}
|
|
460
|
+
if (["bridge.dispatch_fence_conflict", "bridge.reservation_stale", "bridge.capacity_wait_lost", "bridge.dispatch_attempt_missing"].includes(code)) {
|
|
461
|
+
return facts(code, "ownership", "offer", source, "non_retryable", "restore_ownership");
|
|
462
|
+
}
|
|
463
|
+
if (["reservation_expired", "bridge.lease_expired", "bridge.stale_runner_instance", "bridge.runner_replaced"].includes(code)) {
|
|
464
|
+
return facts(code, "ownership", "offer", source, "retryable", "retry_dispatch");
|
|
465
|
+
}
|
|
466
|
+
if (["bridge.dispatch_failed", "bridge.execution_failed", "bridge.cli_execution_failed", "bridge_callback_timeout"].includes(code)) {
|
|
467
|
+
return facts(code, "transport", "unknown", source, "policy_required", "inspect_failure");
|
|
468
|
+
}
|
|
469
|
+
if (code === "bridge.unavailable") {
|
|
470
|
+
return facts(code, "transport", "offer", source, "retryable", "retry_dispatch");
|
|
471
|
+
}
|
|
472
|
+
if (["capacity_unavailable", "bridge.runner_draining", "docker_engine_unavailable", "docker_memory_low", "docker_memory_sample_unavailable", "docker_cpu_low", "docker_disk_low", "server_local.resource_pressure", "server_local.capacity_unavailable", "dispatch.preparation_resource_pressure", "sandbox_capacity_profile_unavailable", "host_runtime_admission_expired"].includes(code)) {
|
|
473
|
+
return facts(code, "resource", "offer", source, "retryable", "retry_dispatch");
|
|
474
|
+
}
|
|
475
|
+
if (["bridge.lease_retry_exhausted", "bridge.capacity_wait_exhausted"].includes(code)) {
|
|
476
|
+
return facts(code, "resource", "offer", source, "non_retryable", "inspect_failure");
|
|
477
|
+
}
|
|
478
|
+
if (["prepublication_infrastructure_failed", "prepublication_capability_unavailable"].includes(code)) {
|
|
479
|
+
return facts(code, "resource", "validation", source, "retryable", "retry_with_policy");
|
|
480
|
+
}
|
|
481
|
+
if (["prepublication_validation_failed", "prepublication_contract_invalid", "prepublication_execution_scope_invalid"].includes(code)) {
|
|
482
|
+
return facts(code, "domain", "validation", source, "non_retryable", "fix_input");
|
|
483
|
+
}
|
|
484
|
+
if (["generated_output_reset_failed", "generated_output_reset_unverified", "worktree_prepare_failed", "host_runtime_ledger_write_failed", "host_diagnostics_temporary_unavailable", "attempt_process_supervisor_start_failed", "attempt_process_supervisor_start_timeout"].includes(code)) {
|
|
485
|
+
return facts(code, "transport", "prepare", source, "retryable", "retry_dispatch");
|
|
486
|
+
}
|
|
487
|
+
if (stableFailureFamilyCodes.has(code) && code.startsWith("generated_output_reset_")) {
|
|
488
|
+
return facts(code, "domain", "prepare", source, "non_retryable", "fix_environment");
|
|
489
|
+
}
|
|
490
|
+
if (code === "provider_start_unavailable") {
|
|
491
|
+
return facts(code, "provider", "prepare", source, "retryable", "retry_dispatch");
|
|
492
|
+
}
|
|
493
|
+
if (code === "provider_configuration_invalid") {
|
|
494
|
+
return facts(code, "provider", "prepare", source, "non_retryable", "fix_environment");
|
|
495
|
+
}
|
|
496
|
+
if (["host_port_collision", "host_runtime_process_exited", "host_runtime_listener_timeout", "host_runtime_http_readiness_timeout", "host_runtime_prepared_source_changed", "host_runtime_prepared_probe_incomplete", "host_runtime_prepared_probe_cleanup_unconfirmed"].includes(code)) {
|
|
497
|
+
return facts(code, "resource", "prepare", source, "retryable", "retry_dispatch");
|
|
498
|
+
}
|
|
499
|
+
if (code === "host_runtime_project_start_failed") {
|
|
500
|
+
return facts(code, "domain", "prepare", source, "non_retryable", "fix_environment");
|
|
501
|
+
}
|
|
502
|
+
if (stableFailureFamilyCodes.has(code) && (code.startsWith("sandbox_build") || code.startsWith("image_"))) {
|
|
503
|
+
const policyFailure = code === "sandbox_build_policy_failed";
|
|
504
|
+
return facts(code, "resource", "build", source, policyFailure ? "non_retryable" : "retryable", policyFailure ? "fix_environment" : "retry_dispatch");
|
|
505
|
+
}
|
|
506
|
+
if (code === "provider_network_policy_denied"
|
|
507
|
+
|| stableFailureFamilyCodes.has(code) && (code.startsWith("sandbox_config") || code.startsWith("contract_") || code.startsWith("sandbox_input"))) {
|
|
508
|
+
return facts(code, "domain", "prepare", source, "non_retryable", "fix_environment");
|
|
509
|
+
}
|
|
510
|
+
if (stableFailureFamilyCodes.has(code) && code.startsWith("docker_")) {
|
|
511
|
+
return facts(code, "resource", "prepare", source, "non_retryable", "fix_environment");
|
|
512
|
+
}
|
|
513
|
+
if (code === "provider_network_unavailable") {
|
|
514
|
+
return facts(code, "transport", "offer", source, "retryable", "retry_dispatch");
|
|
515
|
+
}
|
|
516
|
+
if (code === "provider_request_not_found") {
|
|
517
|
+
return facts(code, "provider", "offer", source, "retryable", "retry_dispatch");
|
|
518
|
+
}
|
|
519
|
+
if (code === "provider_capacity_unavailable") {
|
|
520
|
+
return facts(code, "resource", "offer", source, "retryable", "retry_dispatch");
|
|
521
|
+
}
|
|
522
|
+
if (code === "provider_context_window_exhausted") {
|
|
523
|
+
return facts(code, "provider", "agent", source, "retryable", "retry_dispatch");
|
|
524
|
+
}
|
|
525
|
+
if (stableFailureFamilyCodes.has(code) && (code.startsWith("sandbox_output") || code.startsWith("sandbox_import"))) {
|
|
526
|
+
const retryable = code === "sandbox_import_unavailable" || code === "sandbox_output_publish_unavailable";
|
|
527
|
+
return facts(code, "transport", "import", source, retryable ? "retryable" : "non_retryable", retryable ? "retry_with_policy" : "inspect_failure");
|
|
528
|
+
}
|
|
529
|
+
if (stableFailureFamilyCodes.has(code) && (code.startsWith("teardown") || code.startsWith("reaper"))) {
|
|
530
|
+
return facts(code, "transport", "teardown", source, "retryable", "retry_with_policy");
|
|
531
|
+
}
|
|
532
|
+
if (["invalid_dispatch"].includes(code)) {
|
|
533
|
+
return facts(code, "domain", "offer", source, "non_retryable", "fix_input");
|
|
534
|
+
}
|
|
535
|
+
if ([
|
|
536
|
+
"bridge.api_key_auth_required",
|
|
537
|
+
"bridge.api_key_secret_grant_missing",
|
|
538
|
+
"bridge.api_key_secret_grant_denied",
|
|
539
|
+
"bridge.api_key_secret_grant_provider_mismatch",
|
|
540
|
+
"bridge.api_key_secret_missing",
|
|
541
|
+
"contract_plaintext_secret_rejected",
|
|
542
|
+
].includes(code)) {
|
|
543
|
+
return facts(code, "domain", "prepare", source, "non_retryable", "reauthenticate");
|
|
544
|
+
}
|
|
545
|
+
if (code === "sandbox_idle_timeout") {
|
|
546
|
+
return facts(code, "provider", "agent", source, "retryable", "retry_with_policy");
|
|
547
|
+
}
|
|
548
|
+
if (code === "sandbox_wall_timeout") {
|
|
549
|
+
return facts(code, "provider", "agent", source, "retryable", "retry_with_policy");
|
|
550
|
+
}
|
|
551
|
+
if (["sandbox_oom_killed", "sandbox_pid_exhausted", "sandbox_file_descriptor_exhausted", "sandbox_storage_exhausted"].includes(code)) {
|
|
552
|
+
return facts(code, "resource", "agent", source, "non_retryable", "inspect_failure");
|
|
553
|
+
}
|
|
554
|
+
if (code === "sandbox_auth_unavailable") {
|
|
555
|
+
return facts(code, "domain", "prepare", source, "non_retryable", "reauthenticate");
|
|
556
|
+
}
|
|
557
|
+
if (code === "sandbox_lease_expired") {
|
|
558
|
+
return facts(code, "ownership", "agent", source, "non_retryable", "restore_ownership");
|
|
559
|
+
}
|
|
560
|
+
if (code === "sandbox_runtime_failed") {
|
|
561
|
+
return facts(code, "provider", "agent", source, "non_retryable", "inspect_failure");
|
|
562
|
+
}
|
|
563
|
+
if (code === "worktree_branch_integration_conflict") {
|
|
564
|
+
return facts(code, "domain", "agent", source, "non_retryable", "fix_input");
|
|
565
|
+
}
|
|
566
|
+
if (stableFailureFamilyCodes.has(code) && code.startsWith("sandbox_")) {
|
|
567
|
+
return facts(code, "provider", "agent", source, "non_retryable", "inspect_failure");
|
|
568
|
+
}
|
|
569
|
+
return facts(code, "unknown", "unknown", source, "unknown", "inspect_failure");
|
|
570
|
+
}
|
|
571
|
+
function parseFailureEnvelope(value) {
|
|
572
|
+
const failure = asObject(value, "failure");
|
|
573
|
+
if (requiredNumber(failure, "schemaVersion") !== 2)
|
|
574
|
+
throw new Error("failure.schemaVersion must be 2.");
|
|
575
|
+
const code = requiredString(failure, "code");
|
|
576
|
+
const message = requiredString(failure, "message");
|
|
577
|
+
const category = requiredString(failure, "category");
|
|
578
|
+
const phase = requiredString(failure, "phase");
|
|
579
|
+
const source = requiredString(failure, "source");
|
|
580
|
+
const retryability = requiredString(failure, "retryability");
|
|
581
|
+
const operatorAction = requiredString(failure, "operatorAction");
|
|
582
|
+
const occurredAt = requiredString(failure, "occurredAt");
|
|
583
|
+
const contractOrigin = requiredString(failure, "contractOrigin");
|
|
584
|
+
if (!failureCodePattern.test(code))
|
|
585
|
+
throw new Error("failure.code is invalid.");
|
|
586
|
+
if (message.length > maximumFailureMessageLength)
|
|
587
|
+
throw new Error("failure.message is too long.");
|
|
588
|
+
if (!failureCategories.has(category) || !failurePhases.has(phase) || !failureSources.has(source)
|
|
589
|
+
|| !failureRetryabilities.has(retryability) || !failureOperatorActions.has(operatorAction)) {
|
|
590
|
+
throw new Error("failure contains an unsupported category, phase, source, retryability, or operator action.");
|
|
591
|
+
}
|
|
592
|
+
assertFailureFactsMatchPolicy(code, { code, category, phase, source, retryability, operatorAction });
|
|
593
|
+
if (!Number.isFinite(Date.parse(occurredAt)))
|
|
594
|
+
throw new Error("failure.occurredAt is invalid.");
|
|
595
|
+
if (contractOrigin !== "bridge_v2")
|
|
596
|
+
throw new Error("failure.contractOrigin must be bridge_v2.");
|
|
597
|
+
if (!Array.isArray(failure.causalChain) || failure.causalChain.length > maximumFailureCausalChainLength) {
|
|
598
|
+
throw new Error("failure.causalChain is invalid or too long.");
|
|
599
|
+
}
|
|
600
|
+
const causalChain = failure.causalChain.map((candidate, index) => {
|
|
601
|
+
const cause = asObject(candidate, `failure.causalChain[${index}]`);
|
|
602
|
+
const causeCode = requiredString(cause, "code");
|
|
603
|
+
const causeMessage = requiredString(cause, "message");
|
|
604
|
+
const causeSource = requiredString(cause, "source");
|
|
605
|
+
if (!failureCodePattern.test(causeCode) || causeMessage.length > maximumFailureMessageLength || !failureSources.has(causeSource)) {
|
|
606
|
+
throw new Error(`failure.causalChain[${index}] is invalid.`);
|
|
607
|
+
}
|
|
608
|
+
return { code: causeCode, message: causeMessage, source: causeSource };
|
|
609
|
+
});
|
|
610
|
+
return { schemaVersion: 2, code, message, category, phase, source, retryability, operatorAction, causalChain, occurredAt, contractOrigin };
|
|
611
|
+
}
|
|
612
|
+
function facts(code, category, phase, source, retryability, operatorAction) {
|
|
613
|
+
return { code, category, phase, source, retryability, operatorAction };
|
|
614
|
+
}
|
|
615
|
+
function allowedSourcesForCode(code, known) {
|
|
616
|
+
if (!known)
|
|
617
|
+
return ["bridge", "provider", "sandbox", "unknown"];
|
|
618
|
+
if (code.startsWith("provider_"))
|
|
619
|
+
return ["bridge", "provider", "sandbox"];
|
|
620
|
+
if (code.startsWith("sandbox_")
|
|
621
|
+
|| code.startsWith("docker_")
|
|
622
|
+
|| code.startsWith("image_")
|
|
623
|
+
|| code.startsWith("contract_")
|
|
624
|
+
|| code.startsWith("generated_output_reset_")
|
|
625
|
+
|| code.startsWith("teardown")
|
|
626
|
+
|| code.startsWith("reaper")) {
|
|
627
|
+
return ["bridge", "sandbox"];
|
|
628
|
+
}
|
|
629
|
+
return ["bridge"];
|
|
630
|
+
}
|
|
631
|
+
function assertFailureFactsMatchPolicy(code, factsToValidate) {
|
|
632
|
+
const policy = failurePolicyForCode(code);
|
|
633
|
+
if (!policy.known)
|
|
634
|
+
return;
|
|
635
|
+
if (factsToValidate.category !== policy.category
|
|
636
|
+
|| factsToValidate.phase !== policy.phase
|
|
637
|
+
|| factsToValidate.retryability !== policy.retryability
|
|
638
|
+
|| factsToValidate.operatorAction !== policy.operatorAction
|
|
639
|
+
|| !policy.allowedSources.includes(factsToValidate.source)) {
|
|
640
|
+
throw new Error(`Failure code '${policy.code}' contradicts the failure taxonomy or allowed-source registry.`);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
function normalizeFailureCode(value) {
|
|
644
|
+
const normalized = value.trim().toLowerCase().slice(0, 80);
|
|
645
|
+
return failureCodePattern.test(normalized) ? normalized : "bridge.execution_failed";
|
|
646
|
+
}
|
|
647
|
+
function boundedFailureText(value) {
|
|
648
|
+
const bounded = value.trim().slice(0, maximumFailureMessageLength);
|
|
649
|
+
return bounded || "Dispatch failed without a diagnostic message.";
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Rebuilds an error graph at a privileged execution boundary without
|
|
653
|
+
* retaining the raw error or any raw recursive cause. Forge failure metadata
|
|
654
|
+
* survives so the P7 taxonomy remains authoritative after sanitization.
|
|
655
|
+
*/
|
|
656
|
+
export function sanitizeFailureError(error, exactSecrets = []) {
|
|
657
|
+
return sanitizeFailureErrorAtDepth(error, exactSecrets, new Set(), 0);
|
|
658
|
+
}
|
|
659
|
+
function sanitizeFailureErrorAtDepth(error, exactSecrets, visited, depth) {
|
|
660
|
+
if (depth >= maximumFailureCausalChainLength) {
|
|
661
|
+
return new Error("Additional error causes were omitted.");
|
|
662
|
+
}
|
|
663
|
+
if (error !== null && typeof error === "object") {
|
|
664
|
+
if (visited.has(error))
|
|
665
|
+
return new Error("Circular error cause was omitted.");
|
|
666
|
+
visited.add(error);
|
|
667
|
+
}
|
|
668
|
+
const message = redactText(error instanceof Error ? error.message : String(error), exactSecrets);
|
|
669
|
+
const cause = error instanceof Error && error.cause !== undefined
|
|
670
|
+
? sanitizeFailureErrorAtDepth(error.cause, exactSecrets, visited, depth + 1)
|
|
671
|
+
: undefined;
|
|
672
|
+
const options = cause === undefined ? undefined : { cause };
|
|
673
|
+
if (error instanceof ForgeFailureError) {
|
|
674
|
+
return new ForgeFailureError(error.failureCode, message, error.failureSource, options);
|
|
675
|
+
}
|
|
676
|
+
if (error instanceof AggregateError) {
|
|
677
|
+
const errors = [...error.errors].map(item => sanitizeFailureErrorAtDepth(item, exactSecrets, visited, depth + 1));
|
|
678
|
+
return new AggregateError(errors, message, options);
|
|
679
|
+
}
|
|
680
|
+
return new Error(message, options);
|
|
681
|
+
}
|
|
682
|
+
function errorCauseChain(error, source) {
|
|
683
|
+
const causes = [];
|
|
684
|
+
let current = error;
|
|
685
|
+
while (current !== undefined && current !== null && causes.length < maximumFailureCausalChainLength) {
|
|
686
|
+
const message = current instanceof Error ? current.message : String(current);
|
|
687
|
+
const code = current instanceof ForgeFailureError ? current.failureCode : "cause_unknown";
|
|
688
|
+
const causeSource = current instanceof ForgeFailureError ? current.failureSource : source;
|
|
689
|
+
causes.push({ code, message: boundedFailureText(message), source: causeSource });
|
|
690
|
+
current = current instanceof Error ? current.cause : undefined;
|
|
691
|
+
}
|
|
692
|
+
return causes;
|
|
693
|
+
}
|
|
38
694
|
function parseWorkItem(value) {
|
|
39
695
|
const item = asObject(value, "workItem");
|
|
40
696
|
return {
|
|
697
|
+
projectId: optionalString(item, "projectId"),
|
|
41
698
|
id: requiredString(item, "id"),
|
|
42
699
|
kind: requiredString(item, "kind"),
|
|
43
700
|
title: requiredString(item, "title"),
|
|
@@ -61,6 +718,7 @@ function parseRepo(value) {
|
|
|
61
718
|
function parseCycleRun(value) {
|
|
62
719
|
const cycle = asObject(value, "cycleRun");
|
|
63
720
|
return {
|
|
721
|
+
effectivePlanAmendmentHash: optionalString(cycle, "effectivePlanAmendmentHash"),
|
|
64
722
|
id: requiredString(cycle, "id"),
|
|
65
723
|
ownerType: optionalString(cycle, "ownerType"),
|
|
66
724
|
ownerId: optionalString(cycle, "ownerId"),
|
|
@@ -70,6 +728,7 @@ function parseCycleRun(value) {
|
|
|
70
728
|
requestedBase: optionalString(cycle, "requestedBase") ?? "develop",
|
|
71
729
|
baseBranch: optionalString(cycle, "baseBranch") ?? "develop",
|
|
72
730
|
expectedCurrentBranch: optionalString(cycle, "expectedCurrentBranch"),
|
|
731
|
+
repairHeadSha: optionalString(cycle, "repairHeadSha"),
|
|
73
732
|
attempt: requiredNumber(cycle, "attempt"),
|
|
74
733
|
model: requiredString(cycle, "model")
|
|
75
734
|
};
|
|
@@ -79,7 +738,9 @@ function parseSession(value) {
|
|
|
79
738
|
return {
|
|
80
739
|
id: requiredString(session, "id"),
|
|
81
740
|
model: requiredString(session, "model"),
|
|
82
|
-
lane: requiredString(session, "lane")
|
|
741
|
+
lane: requiredString(session, "lane"),
|
|
742
|
+
resumedFromSessionId: optionalString(session, "resumedFromSessionId"),
|
|
743
|
+
resumeProviderThreadId: optionalString(session, "resumeProviderThreadId")
|
|
83
744
|
};
|
|
84
745
|
}
|
|
85
746
|
function parseLease(value) {
|
|
@@ -107,7 +768,8 @@ function parseExecution(value) {
|
|
|
107
768
|
role: requiredString(execution, "role"),
|
|
108
769
|
lane: requiredString(execution, "lane"),
|
|
109
770
|
requiresApiKeySecretGrant: requiredBoolean(execution, "requiresApiKeySecretGrant"),
|
|
110
|
-
runnerRepoLocalPath: optionalString(execution, "runnerRepoLocalPath")
|
|
771
|
+
runnerRepoLocalPath: optionalString(execution, "runnerRepoLocalPath"),
|
|
772
|
+
effort: optionalString(execution, "effort")
|
|
111
773
|
};
|
|
112
774
|
}
|
|
113
775
|
function parseDraftPrompt(value) {
|
|
@@ -118,9 +780,45 @@ function parseDraftPrompt(value) {
|
|
|
118
780
|
return {
|
|
119
781
|
kind: requiredString(draft, "kind"),
|
|
120
782
|
systemPrompt: requiredString(draft, "systemPrompt"),
|
|
121
|
-
userPrompt: requiredString(draft, "userPrompt")
|
|
783
|
+
userPrompt: requiredString(draft, "userPrompt"),
|
|
784
|
+
projectName: optionalString(draft, "projectName"),
|
|
785
|
+
repoName: optionalString(draft, "repoName"),
|
|
786
|
+
attachments: parseDraftAttachments(draft.attachments)
|
|
122
787
|
};
|
|
123
788
|
}
|
|
789
|
+
function parseDraftAttachments(value) {
|
|
790
|
+
if (value === undefined || value === null) {
|
|
791
|
+
return [];
|
|
792
|
+
}
|
|
793
|
+
if (!Array.isArray(value)) {
|
|
794
|
+
throw new Error("draft.attachments must be an array when provided.");
|
|
795
|
+
}
|
|
796
|
+
return value.map((entry, index) => {
|
|
797
|
+
const attachment = asObject(entry, `draft.attachments[${index}]`);
|
|
798
|
+
const kind = requiredString(attachment, "kind").trim().toLowerCase();
|
|
799
|
+
if (kind !== "text" && kind !== "image") {
|
|
800
|
+
throw new Error(`draft.attachments[${index}].kind must be text or image.`);
|
|
801
|
+
}
|
|
802
|
+
const textContent = optionalString(attachment, "textContent");
|
|
803
|
+
const contentBase64 = optionalString(attachment, "contentBase64");
|
|
804
|
+
if (kind === "text" && textContent === null) {
|
|
805
|
+
throw new Error(`draft.attachments[${index}].textContent is required for text attachments.`);
|
|
806
|
+
}
|
|
807
|
+
if (kind === "image" && contentBase64 === null) {
|
|
808
|
+
throw new Error(`draft.attachments[${index}].contentBase64 is required for image attachments.`);
|
|
809
|
+
}
|
|
810
|
+
return {
|
|
811
|
+
index: optionalNonNegativeInteger(attachment, "index") ?? index,
|
|
812
|
+
fileName: requiredString(attachment, "fileName"),
|
|
813
|
+
contentType: optionalString(attachment, "contentType") ?? "",
|
|
814
|
+
kind,
|
|
815
|
+
byteCount: optionalNonNegativeInteger(attachment, "byteCount") ?? 0,
|
|
816
|
+
summary: optionalString(attachment, "summary") ?? "",
|
|
817
|
+
textContent,
|
|
818
|
+
contentBase64
|
|
819
|
+
};
|
|
820
|
+
});
|
|
821
|
+
}
|
|
124
822
|
function parseApiKeySecretGrant(value) {
|
|
125
823
|
if (value === undefined || value === null) {
|
|
126
824
|
return null;
|
|
@@ -133,6 +831,16 @@ function parseApiKeySecretGrant(value) {
|
|
|
133
831
|
expiresAt: requiredString(grant, "expiresAt")
|
|
134
832
|
};
|
|
135
833
|
}
|
|
834
|
+
function parseApiKeySecretGrantHandle(value) {
|
|
835
|
+
if (value === undefined || value === null)
|
|
836
|
+
return null;
|
|
837
|
+
const grant = asObject(value, "apiKeySecretGrantHandle");
|
|
838
|
+
return {
|
|
839
|
+
grantId: requiredString(grant, "grantId"),
|
|
840
|
+
provider: requiredString(grant, "provider"),
|
|
841
|
+
expiresAt: requiredString(grant, "expiresAt")
|
|
842
|
+
};
|
|
843
|
+
}
|
|
136
844
|
function parseServerLocal(value) {
|
|
137
845
|
const serverLocal = asObject(value, "serverLocal");
|
|
138
846
|
return {
|
|
@@ -159,9 +867,49 @@ function parsePullRequest(value) {
|
|
|
159
867
|
requiredChecksTotal: requiredNumber(pullRequest, "requiredChecksTotal"),
|
|
160
868
|
unresolvedThreads: requiredNumber(pullRequest, "unresolvedThreads"),
|
|
161
869
|
humanApproved: requiredBoolean(pullRequest, "humanApproved"),
|
|
162
|
-
hasMergeConflicts: requiredBoolean(pullRequest, "hasMergeConflicts")
|
|
870
|
+
hasMergeConflicts: requiredBoolean(pullRequest, "hasMergeConflicts"),
|
|
871
|
+
headSha: optionalString(pullRequest, "headSha") ?? "",
|
|
872
|
+
reviewIssueKey: optionalString(pullRequest, "reviewIssueKey"),
|
|
873
|
+
reviewFindings: parsePullRequestReviewFindings(pullRequest.reviewFindings),
|
|
874
|
+
openWorkOrderStories: parseWorkOrderStories(pullRequest.openWorkOrderStories)
|
|
163
875
|
};
|
|
164
876
|
}
|
|
877
|
+
function parseWorkOrderStories(value) {
|
|
878
|
+
if (value === null || value === undefined)
|
|
879
|
+
return [];
|
|
880
|
+
if (!Array.isArray(value))
|
|
881
|
+
throw new Error("pullRequest.openWorkOrderStories must be an array when provided.");
|
|
882
|
+
return value.map((entry, index) => {
|
|
883
|
+
const story = asObject(entry, `pullRequest.openWorkOrderStories[${index}]`);
|
|
884
|
+
return {
|
|
885
|
+
workItemId: requiredString(story, "workItemId"),
|
|
886
|
+
title: optionalString(story, "title") ?? "",
|
|
887
|
+
acceptanceCriteria: optionalString(story, "acceptanceCriteria") ?? ""
|
|
888
|
+
};
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
function parsePullRequestReviewFindings(value) {
|
|
892
|
+
if (value === null || value === undefined)
|
|
893
|
+
return [];
|
|
894
|
+
if (!Array.isArray(value))
|
|
895
|
+
throw new Error("pullRequest.reviewFindings must be an array when provided.");
|
|
896
|
+
return value.map((entry, index) => {
|
|
897
|
+
const finding = asObject(entry, `pullRequest.reviewFindings[${index}]`);
|
|
898
|
+
return {
|
|
899
|
+
claimId: requiredString(finding, "claimId"),
|
|
900
|
+
kind: requiredString(finding, "kind"),
|
|
901
|
+
bodySha256: requiredString(finding, "bodySha256"),
|
|
902
|
+
body: optionalString(finding, "body") ?? "",
|
|
903
|
+
contentAvailable: requiredBoolean(finding, "contentAvailable"),
|
|
904
|
+
actionable: requiredBoolean(finding, "actionable"),
|
|
905
|
+
actionabilityReason: requiredString(finding, "actionabilityReason"),
|
|
906
|
+
author: optionalString(finding, "author"),
|
|
907
|
+
url: optionalString(finding, "url"),
|
|
908
|
+
path: optionalString(finding, "path"),
|
|
909
|
+
line: optionalPositiveInteger(finding, "line")
|
|
910
|
+
};
|
|
911
|
+
});
|
|
912
|
+
}
|
|
165
913
|
function parseWarmPathOverrides(value) {
|
|
166
914
|
if (value === undefined || value === null) {
|
|
167
915
|
return [];
|
|
@@ -197,6 +945,28 @@ function parseQaBrowser(value) {
|
|
|
197
945
|
missingTargetReason: optionalString(qaBrowser, "missingTargetReason") ?? optionalString(qaBrowser, "missing_target_reason")
|
|
198
946
|
};
|
|
199
947
|
}
|
|
948
|
+
function parseQaScope(value) {
|
|
949
|
+
if (value === undefined || value === null) {
|
|
950
|
+
return null;
|
|
951
|
+
}
|
|
952
|
+
const qaScope = asObject(value, "qaScope");
|
|
953
|
+
return {
|
|
954
|
+
schema_version: requiredNumber(qaScope, "schema_version"),
|
|
955
|
+
surface: parseOptionalStringArray(qaScope.surface, "qaScope.surface"),
|
|
956
|
+
required_evidence: parseOptionalStringArray(qaScope.required_evidence, "qaScope.required_evidence"),
|
|
957
|
+
required_capabilities: parseOptionalStringArray(qaScope.required_capabilities ?? qaScope.requiredCapabilities, "qaScope.required_capabilities"),
|
|
958
|
+
not_applicable: parseOptionalStringArray(qaScope.not_applicable, "qaScope.not_applicable"),
|
|
959
|
+
browser_required: optionalBoolean(qaScope, "browser_required") ?? optionalBoolean(qaScope, "browserRequired") ?? false,
|
|
960
|
+
api_required: optionalBoolean(qaScope, "api_required") ?? optionalBoolean(qaScope, "apiRequired") ?? false,
|
|
961
|
+
source: optionalString(qaScope, "source"),
|
|
962
|
+
authority: optionalString(qaScope, "authority"),
|
|
963
|
+
confidence: optionalString(qaScope, "confidence"),
|
|
964
|
+
confirmed_by_user: optionalBoolean(qaScope, "confirmed_by_user") ?? optionalBoolean(qaScope, "confirmedByUser"),
|
|
965
|
+
locked: optionalBoolean(qaScope, "locked"),
|
|
966
|
+
derived_from: parseOptionalObject(qaScope.derived_from ?? qaScope.derivedFrom, "qaScope.derived_from"),
|
|
967
|
+
rationale: optionalString(qaScope, "rationale")
|
|
968
|
+
};
|
|
969
|
+
}
|
|
200
970
|
function parseQaAppLaunchTarget(value) {
|
|
201
971
|
if (value === undefined || value === null) {
|
|
202
972
|
return null;
|
|
@@ -211,6 +981,7 @@ function parseQaAppLaunchTarget(value) {
|
|
|
211
981
|
command: optionalString(launchTarget, "command"),
|
|
212
982
|
working_directory: optionalString(launchTarget, "working_directory"),
|
|
213
983
|
ready_url: optionalString(launchTarget, "ready_url"),
|
|
984
|
+
managed_runtime_port: optionalString(launchTarget, "managed_runtime_port"),
|
|
214
985
|
routes: Array.isArray(routes)
|
|
215
986
|
? routes.map((entry, index) => {
|
|
216
987
|
if (typeof entry !== "string") {
|
|
@@ -260,6 +1031,34 @@ function parseArchitectRevision(value) {
|
|
|
260
1031
|
feedback: requiredString(revision, "feedback")
|
|
261
1032
|
};
|
|
262
1033
|
}
|
|
1034
|
+
function parseArchitectDependencies(value) {
|
|
1035
|
+
if (value === undefined || value === null) {
|
|
1036
|
+
return null;
|
|
1037
|
+
}
|
|
1038
|
+
const context = asObject(value, "architectDependencies");
|
|
1039
|
+
const prerequisites = context.prerequisites;
|
|
1040
|
+
if (!Array.isArray(prerequisites)) {
|
|
1041
|
+
throw new Error("architectDependencies.prerequisites must be an array.");
|
|
1042
|
+
}
|
|
1043
|
+
return {
|
|
1044
|
+
baseBranch: requiredString(context, "baseBranch").trim(),
|
|
1045
|
+
prerequisites: prerequisites.map((entry, index) => {
|
|
1046
|
+
const prerequisite = asObject(entry, `architectDependencies.prerequisites[${index}]`);
|
|
1047
|
+
return {
|
|
1048
|
+
storyId: requiredString(prerequisite, "storyId").trim(),
|
|
1049
|
+
title: requiredString(prerequisite, "title").trim(),
|
|
1050
|
+
scheduleIndex: optionalNonNegativeInteger(prerequisite, "scheduleIndex"),
|
|
1051
|
+
waveIndex: optionalNonNegativeInteger(prerequisite, "waveIndex"),
|
|
1052
|
+
featureScheduleIndex: optionalNonNegativeInteger(prerequisite, "featureScheduleIndex"),
|
|
1053
|
+
expectedOutput: optionalString(prerequisite, "expectedOutput"),
|
|
1054
|
+
handoffSummary: optionalString(prerequisite, "handoffSummary"),
|
|
1055
|
+
dependencyReason: optionalString(prerequisite, "dependencyReason"),
|
|
1056
|
+
pullRequestNumber: optionalPositiveInteger(prerequisite, "pullRequestNumber"),
|
|
1057
|
+
targetBranch: requiredString(prerequisite, "targetBranch").trim()
|
|
1058
|
+
};
|
|
1059
|
+
})
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
263
1062
|
function parseStoryDevelop(value) {
|
|
264
1063
|
if (value === undefined || value === null) {
|
|
265
1064
|
return null;
|
|
@@ -289,6 +1088,7 @@ function parseStoryQa(value) {
|
|
|
289
1088
|
}
|
|
290
1089
|
const storyQa = asObject(value, "storyQa");
|
|
291
1090
|
return {
|
|
1091
|
+
references: parseStoryQaReferences(storyQa.references),
|
|
292
1092
|
storyWorkItemId: requiredString(storyQa, "storyWorkItemId"),
|
|
293
1093
|
planId: requiredString(storyQa, "planId"),
|
|
294
1094
|
planContentHash: requiredString(storyQa, "planContentHash"),
|
|
@@ -301,6 +1101,26 @@ function parseStoryQa(value) {
|
|
|
301
1101
|
dependencies: parseStoryQaDependencies(storyQa.dependencies)
|
|
302
1102
|
};
|
|
303
1103
|
}
|
|
1104
|
+
function parseStoryQaReferences(value) {
|
|
1105
|
+
if (value === undefined || value === null)
|
|
1106
|
+
return null;
|
|
1107
|
+
const bundle = asObject(value, "storyQa.references");
|
|
1108
|
+
if (!Array.isArray(bundle.files))
|
|
1109
|
+
throw new Error("storyQa.references.files must be an array.");
|
|
1110
|
+
return {
|
|
1111
|
+
schemaVersion: requiredNumber(bundle, "schemaVersion"),
|
|
1112
|
+
cycleRunId: requiredString(bundle, "cycleRunId"),
|
|
1113
|
+
storyWorkItemId: requiredString(bundle, "storyWorkItemId"),
|
|
1114
|
+
projectId: requiredString(bundle, "projectId"), repoId: requiredString(bundle, "repoId"),
|
|
1115
|
+
planId: requiredString(bundle, "planId"), planContentHash: requiredString(bundle, "planContentHash"),
|
|
1116
|
+
effectiveAmendmentHash: requiredString(bundle, "effectiveAmendmentHash"), capturedAt: requiredString(bundle, "capturedAt"),
|
|
1117
|
+
files: bundle.files.map((entry, index) => {
|
|
1118
|
+
const file = asObject(entry, `storyQa.references.files[${index}]`);
|
|
1119
|
+
return { path: requiredString(file, "path"), body: requiredString(file, "body"),
|
|
1120
|
+
contentHash: requiredString(file, "contentHash"), provenance: requiredString(file, "provenance") };
|
|
1121
|
+
})
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
304
1124
|
function parseStoryQaChildTasks(value) {
|
|
305
1125
|
if (!Array.isArray(value)) {
|
|
306
1126
|
throw new Error("storyQa.childTasks must be an array.");
|
|
@@ -365,6 +1185,58 @@ function parseStoryQaDependencies(value) {
|
|
|
365
1185
|
};
|
|
366
1186
|
});
|
|
367
1187
|
}
|
|
1188
|
+
function parseDesignerRefs(value) {
|
|
1189
|
+
if (value === undefined || value === null) {
|
|
1190
|
+
return null;
|
|
1191
|
+
}
|
|
1192
|
+
const context = asObject(value, "designerRefs");
|
|
1193
|
+
const refs = context.refs;
|
|
1194
|
+
if (!Array.isArray(refs)) {
|
|
1195
|
+
throw new Error("designerRefs.refs must be an array.");
|
|
1196
|
+
}
|
|
1197
|
+
return {
|
|
1198
|
+
schemaVersion: requiredNumber(context, "schemaVersion"),
|
|
1199
|
+
kind: requiredString(context, "kind"),
|
|
1200
|
+
ownerType: requiredString(context, "ownerType"),
|
|
1201
|
+
ownerId: requiredString(context, "ownerId"),
|
|
1202
|
+
projectId: requiredString(context, "projectId"),
|
|
1203
|
+
repoId: requiredString(context, "repoId"),
|
|
1204
|
+
title: requiredString(context, "title"),
|
|
1205
|
+
refs: refs.map((entry, index) => parseDesignerRefAttachment(entry, index))
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
function parseDesignerRefAttachment(value, index) {
|
|
1209
|
+
const attachment = asObject(value, `designerRefs.refs[${index}]`);
|
|
1210
|
+
return {
|
|
1211
|
+
ref: parseDesignerRef(attachment.ref, index),
|
|
1212
|
+
reason: optionalString(attachment, "reason") ?? "",
|
|
1213
|
+
required: requiredBoolean(attachment, "required"),
|
|
1214
|
+
source: optionalString(attachment, "source") ?? "",
|
|
1215
|
+
lineageKind: optionalString(attachment, "lineageKind") ?? optionalString(attachment, "lineage_kind") ?? "",
|
|
1216
|
+
inheritedFromItemType: optionalString(attachment, "inheritedFromItemType") ?? optionalString(attachment, "inherited_from_item_type"),
|
|
1217
|
+
inheritedFromItemId: optionalString(attachment, "inheritedFromItemId") ?? optionalString(attachment, "inherited_from_item_id"),
|
|
1218
|
+
inheritedFromDesignerRefId: optionalString(attachment, "inheritedFromDesignerRefId") ?? optionalString(attachment, "inherited_from_designer_ref_id"),
|
|
1219
|
+
addedByLane: optionalString(attachment, "addedByLane") ?? optionalString(attachment, "added_by_lane") ?? "",
|
|
1220
|
+
sortOrder: requiredNumber(attachment, "sortOrder"),
|
|
1221
|
+
createdAt: requiredString(attachment, "createdAt")
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
function parseDesignerRef(value, index) {
|
|
1225
|
+
const designerRef = asObject(value, `designerRefs.refs[${index}].ref`);
|
|
1226
|
+
return {
|
|
1227
|
+
id: requiredString(designerRef, "id"),
|
|
1228
|
+
projectId: requiredString(designerRef, "projectId"),
|
|
1229
|
+
repoId: requiredString(designerRef, "repoId"),
|
|
1230
|
+
refKey: requiredString(designerRef, "refKey"),
|
|
1231
|
+
kind: requiredString(designerRef, "kind"),
|
|
1232
|
+
path: requiredString(designerRef, "path"),
|
|
1233
|
+
anchor: optionalString(designerRef, "anchor"),
|
|
1234
|
+
title: optionalString(designerRef, "title") ?? "",
|
|
1235
|
+
contentHash: requiredString(designerRef, "contentHash"),
|
|
1236
|
+
commitSha: requiredString(designerRef, "commitSha"),
|
|
1237
|
+
createdAt: requiredString(designerRef, "createdAt")
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
368
1240
|
function parseStringArray(value, label) {
|
|
369
1241
|
if (!Array.isArray(value)) {
|
|
370
1242
|
throw new Error(`${label} must be an array.`);
|
|
@@ -376,6 +1248,18 @@ function parseStringArray(value, label) {
|
|
|
376
1248
|
return entry.trim();
|
|
377
1249
|
});
|
|
378
1250
|
}
|
|
1251
|
+
function parseOptionalStringArray(value, label) {
|
|
1252
|
+
if (value === undefined || value === null) {
|
|
1253
|
+
return [];
|
|
1254
|
+
}
|
|
1255
|
+
return parseStringArray(value, label);
|
|
1256
|
+
}
|
|
1257
|
+
function parseOptionalObject(value, label) {
|
|
1258
|
+
if (value === undefined || value === null) {
|
|
1259
|
+
return null;
|
|
1260
|
+
}
|
|
1261
|
+
return asObject(value, label);
|
|
1262
|
+
}
|
|
379
1263
|
function parseArtifacts(value) {
|
|
380
1264
|
if (value === undefined || value === null) {
|
|
381
1265
|
return [];
|
|
@@ -397,6 +1281,34 @@ function parseArtifacts(value) {
|
|
|
397
1281
|
};
|
|
398
1282
|
});
|
|
399
1283
|
}
|
|
1284
|
+
function parsePriorPhaseArtifacts(value) {
|
|
1285
|
+
if (value === undefined || value === null) {
|
|
1286
|
+
return [];
|
|
1287
|
+
}
|
|
1288
|
+
if (!Array.isArray(value)) {
|
|
1289
|
+
throw new Error("priorPhaseArtifacts must be an array when provided.");
|
|
1290
|
+
}
|
|
1291
|
+
return value.map((entry, index) => {
|
|
1292
|
+
const artifact = asObject(entry, `priorPhaseArtifacts[${index}]`);
|
|
1293
|
+
const phase = requiredString(artifact, "phase").trim().toLowerCase().replace(/-/g, "_");
|
|
1294
|
+
if (phase !== "implementation" && phase !== "self_review" && phase !== "validation") {
|
|
1295
|
+
throw new Error(`priorPhaseArtifacts[${index}].phase must be implementation, self_review, or validation.`);
|
|
1296
|
+
}
|
|
1297
|
+
return {
|
|
1298
|
+
workItemId: requiredString(artifact, "workItemId"),
|
|
1299
|
+
artifactId: requiredString(artifact, "artifactId"),
|
|
1300
|
+
cycleRunId: requiredString(artifact, "cycleRunId"),
|
|
1301
|
+
phase,
|
|
1302
|
+
status: requiredString(artifact, "status"),
|
|
1303
|
+
producedAt: requiredString(artifact, "producedAt"),
|
|
1304
|
+
sourcePath: requiredString(artifact, "sourcePath"),
|
|
1305
|
+
verdictJson: requiredString(artifact, "verdictJson"),
|
|
1306
|
+
report: optionalString(artifact, "report") ?? "",
|
|
1307
|
+
verdictContentHash: requiredString(artifact, "verdictContentHash").toLowerCase(),
|
|
1308
|
+
reportContentHash: requiredString(artifact, "reportContentHash").toLowerCase()
|
|
1309
|
+
};
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
400
1312
|
function asObject(value, label) {
|
|
401
1313
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
402
1314
|
throw new Error(`${label} must be an object.`);
|
|
@@ -447,6 +1359,16 @@ function optionalPositiveInteger(source, key) {
|
|
|
447
1359
|
}
|
|
448
1360
|
return value;
|
|
449
1361
|
}
|
|
1362
|
+
function optionalNonNegativeInteger(source, key) {
|
|
1363
|
+
const value = source[key];
|
|
1364
|
+
if (value === undefined || value === null) {
|
|
1365
|
+
return null;
|
|
1366
|
+
}
|
|
1367
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
1368
|
+
throw new Error(`${key} must be a non-negative integer when provided.`);
|
|
1369
|
+
}
|
|
1370
|
+
return value;
|
|
1371
|
+
}
|
|
450
1372
|
function requiredNumber(source, key) {
|
|
451
1373
|
const value = source[key];
|
|
452
1374
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
@@ -454,3 +1376,10 @@ function requiredNumber(source, key) {
|
|
|
454
1376
|
}
|
|
455
1377
|
return value;
|
|
456
1378
|
}
|
|
1379
|
+
function boundedInteger(source, key, minimum, maximum) {
|
|
1380
|
+
const value = requiredNumber(source, key);
|
|
1381
|
+
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
|
1382
|
+
throw new Error(`${key} must be an integer from ${minimum} through ${maximum}.`);
|
|
1383
|
+
}
|
|
1384
|
+
return value;
|
|
1385
|
+
}
|