@bridge_gpt/mcp-server 0.2.41 → 0.2.43
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 +330 -191
- package/build/agent-capabilities/cli.js +2 -1
- package/build/agent-launchers/claude-executor-adapter.js +17 -4
- package/build/agents.generated.js +2 -2
- package/build/claude-review-workflow.js +510 -45
- package/build/claude-user-config-doctor.js +42 -11
- package/build/cli-release.js +2 -1
- package/build/commands.generated.js +6 -5
- package/build/conduct-epic/bridge-client.js +354 -113
- package/build/conduct-epic/checkpoint-store.js +17 -0
- package/build/conduct-epic/cli.js +947 -99
- package/build/conduct-epic/cut-protocol.js +327 -0
- package/build/conduct-epic/spawn.js +14 -2
- package/build/conductor/bridge-api-client.js +148 -1
- package/build/conductor/cli.js +109 -1
- package/build/conductor/doctor.js +101 -16
- package/build/conductor/epic-reconcile.js +72 -19
- package/build/conductor/epic-runtime.js +15 -3
- package/build/conductor/errors.js +47 -0
- package/build/conductor/git-hooks.js +205 -11
- package/build/conductor/install-doctor.js +230 -1
- package/build/conductor/local-merge.js +130 -28
- package/build/conductor/recovery-cli.js +313 -0
- package/build/conductor/recovery-operations.js +219 -0
- package/build/conductor/tools.js +32 -3
- package/build/conductor/worker-ledger-cli.js +27 -1
- package/build/conductor-bin.js +20 -16
- package/build/credentials-cli.js +3 -2
- package/build/docs.generated.js +2 -1
- package/build/doctor.js +120 -44
- package/build/drive-epic.js +375 -0
- package/build/executor/cli.js +48 -1
- package/build/executor/env.js +21 -0
- package/build/executor/http-client.js +71 -3
- package/build/executor/index-scope.js +39 -0
- package/build/executor/job-errors.js +9 -0
- package/build/executor/job-log-registry.js +69 -0
- package/build/executor/job-runner.js +198 -29
- package/build/executor/live-worker-registry.js +83 -0
- package/build/executor/observation.js +259 -6
- package/build/executor/platform.js +147 -3
- package/build/executor/process.js +58 -14
- package/build/executor/runner.js +454 -48
- package/build/executor/test-clock.js +3 -2
- package/build/executor/worker-finalization.js +233 -56
- package/build/executor/worktree.js +8 -1
- package/build/index-scope-contract.js +96 -0
- package/build/index.js +2277 -270
- package/build/init.js +83 -22
- package/build/install-bridge-conductor.js +323 -14
- package/build/install-bridge.js +225 -47
- package/build/install-doctor.js +23 -9
- package/build/install-reexec.js +2 -1
- package/build/launcher-config-inspection.js +83 -22
- package/build/mcp-host-config.js +331 -67
- package/build/mcp-host-targets.js +45 -21
- package/build/mcp-identity.js +92 -0
- package/build/mcp-install-state.js +94 -1
- package/build/mcp-invoke.js +2 -1
- package/build/mcp-provisioning.js +45 -12
- package/build/mcp-registration-doctor.js +35 -13
- package/build/mcp-server-invocation.js +4 -2
- package/build/merge-pull-request.js +208 -9
- package/build/pipelines.generated.js +305 -15
- package/build/plane/cli.js +73 -7
- package/build/plane/defaults.js +18 -5
- package/build/plane/manifest.js +90 -0
- package/build/plane/preflight.js +100 -10
- package/build/plane/shutdown.js +71 -3
- package/build/plane/test-fakes.js +9 -1
- package/build/readme.generated.js +1 -1
- package/build/regression-check.js +3 -2
- package/build/review-tickets.js +8 -7
- package/build/run-unit-tests-launcher.js +149 -6
- package/build/schedule-run.js +3 -2
- package/build/setup-epic.js +531 -82
- package/build/sfcc/tool-wrapper.js +15 -0
- package/build/start-tickets-prereqs.js +11 -6
- package/build/start-tickets.js +91 -85
- package/build/update-check.js +3 -2
- package/build/upgrade-advice.js +2 -1
- package/build/upgrade-cli.js +50 -18
- package/build/version.generated.js +2 -1
- package/build/worktree-core.js +31 -17
- package/docs/CONDUCTOR.md +22 -0
- package/docs/install/mcp-tool-integrations.md +19 -3
- package/package.json +2 -2
- package/pipelines/greenfield-setup.json +286 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Bridge API operator-recovery operations (BAPI-872).
|
|
3
|
+
*
|
|
4
|
+
* Centralizes stop, abandon, and ticket unpark/adopt-current-head-and-unpark so
|
|
5
|
+
* the conductor CLI recovery verbs (`recovery-cli.ts`) and `plane down`
|
|
6
|
+
* (`../plane/shutdown.ts`, wired through `../plane/cli.ts`) share EXACTLY the
|
|
7
|
+
* same stop semantics, and so this module — never a caller — owns every
|
|
8
|
+
* ticket-recovery `row_version` read, retry, and idempotency-key decision.
|
|
9
|
+
*
|
|
10
|
+
* This module is a pure result-classification layer over the typed Bridge API
|
|
11
|
+
* client (`bridge-api-client.ts`). It prints nothing, never touches argv, and
|
|
12
|
+
* never renders operator-facing text — every result here is a discriminated
|
|
13
|
+
* union carrying only semantic outcomes and identifiers, and deliberately never
|
|
14
|
+
* a `row_version` or other CAS-internal value. Rendering belongs to the two
|
|
15
|
+
* callers: `recovery-cli.ts` and `plane/shutdown.ts`.
|
|
16
|
+
*/
|
|
17
|
+
import { randomUUID } from "crypto";
|
|
18
|
+
import { adoptCurrentHeadAndUnparkTicket, ConductorBridgeApiError, fetchEpicRunState, safeDiagnosticMessage, stopEpicRun, unparkEpicTicket, updateEpicRunStatus, } from "./bridge-api-client.js";
|
|
19
|
+
/**
|
|
20
|
+
* Bounded total attempts for a ticket-recovery CAS retry loop. Each attempt
|
|
21
|
+
* begins with a fresh, authoritative state read — so this bounds full
|
|
22
|
+
* read-then-mutate cycles, not raw HTTP calls.
|
|
23
|
+
*/
|
|
24
|
+
export const RECOVERY_TICKET_RETRY_LIMIT = 3;
|
|
25
|
+
/**
|
|
26
|
+
* Stop an epic run through the existing `stop` endpoint, classifying every
|
|
27
|
+
* outcome the endpoint can produce: a freshly committed stop, an idempotent
|
|
28
|
+
* repeat, a terminal refusal, or an unreachable/failed API.
|
|
29
|
+
*
|
|
30
|
+
* State is re-read ONLY on a terminal refusal, to report the run's actual
|
|
31
|
+
* `done`/`abandoned` status — never on the ordinary committed or idempotent
|
|
32
|
+
* paths, which need no second read.
|
|
33
|
+
*/
|
|
34
|
+
export async function stopEpicRunRecovery(access, options) {
|
|
35
|
+
try {
|
|
36
|
+
const response = await stopEpicRun(access, {
|
|
37
|
+
epicRunId: options.epicRunId,
|
|
38
|
+
reason: options.reason,
|
|
39
|
+
});
|
|
40
|
+
return response.committed
|
|
41
|
+
? { ok: true, kind: "committed", epicRunId: options.epicRunId }
|
|
42
|
+
: { ok: true, kind: "already-stopped", epicRunId: options.epicRunId };
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
if (err instanceof ConductorBridgeApiError &&
|
|
46
|
+
err.status === 409 &&
|
|
47
|
+
err.errorCode === "RUN_TERMINAL") {
|
|
48
|
+
const status = await readTerminalRunStatus(access, options.epicRunId);
|
|
49
|
+
return { ok: false, kind: "terminal", epicRunId: options.epicRunId, status };
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
kind: "unavailable",
|
|
54
|
+
epicRunId: options.epicRunId,
|
|
55
|
+
message: safeDiagnosticMessage(err, "stop request failed"),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Best-effort read of a run's actual status for a terminal-refusal report.
|
|
61
|
+
* Never throws: an unreadable state still renders SOME terminal report rather
|
|
62
|
+
* than losing the stop refusal itself to a secondary read failure.
|
|
63
|
+
*/
|
|
64
|
+
async function readTerminalRunStatus(access, epicRunId) {
|
|
65
|
+
try {
|
|
66
|
+
const state = await fetchEpicRunState(access, epicRunId);
|
|
67
|
+
return state.epic_run.status;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return "terminal";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Abandon an epic run through the existing status-CAS PATCH lane
|
|
75
|
+
* (`updateEpicRunStatus`), reading the run's current status first so the PATCH
|
|
76
|
+
* always carries a freshly observed `expectedStatus` — never issued without
|
|
77
|
+
* one. An already-abandoned run is a safe no-op success; a lost CAS (the
|
|
78
|
+
* status moved concurrently) fails safely rather than retrying an unobserved
|
|
79
|
+
* transition or re-issuing an unguarded PATCH.
|
|
80
|
+
*/
|
|
81
|
+
export async function abandonEpicRunRecovery(access, options) {
|
|
82
|
+
let state;
|
|
83
|
+
try {
|
|
84
|
+
state = await fetchEpicRunState(access, options.epicRunId);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
kind: "unavailable",
|
|
90
|
+
epicRunId: options.epicRunId,
|
|
91
|
+
message: safeDiagnosticMessage(err, "could not read run state"),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const currentStatus = state.epic_run.status;
|
|
95
|
+
if (currentStatus === "abandoned") {
|
|
96
|
+
return { ok: true, kind: "already-abandoned", epicRunId: options.epicRunId };
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
await updateEpicRunStatus(access, {
|
|
100
|
+
epicKey: options.epicRunId,
|
|
101
|
+
status: "abandoned",
|
|
102
|
+
expectedStatus: currentStatus,
|
|
103
|
+
});
|
|
104
|
+
return { ok: true, kind: "abandoned", epicRunId: options.epicRunId };
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
if (err instanceof ConductorBridgeApiError && err.status === 400) {
|
|
108
|
+
// The status-CAS PATCH refused: the run's status moved concurrently since
|
|
109
|
+
// the read above. Never retried here — an unobserved transition must not
|
|
110
|
+
// be guessed at, and this is precisely why abandon reads state fresh on
|
|
111
|
+
// every invocation rather than caching or reusing a prior read.
|
|
112
|
+
return {
|
|
113
|
+
ok: false,
|
|
114
|
+
kind: "concurrent-change",
|
|
115
|
+
epicRunId: options.epicRunId,
|
|
116
|
+
message: "the run's status changed concurrently; re-check its state and retry",
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
ok: false,
|
|
121
|
+
kind: "unavailable",
|
|
122
|
+
epicRunId: options.epicRunId,
|
|
123
|
+
message: safeDiagnosticMessage(err, "abandon request failed"),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The shared bounded-retry engine behind both ticket recovery operations.
|
|
129
|
+
*
|
|
130
|
+
* Every attempt — including every retry after a CAS conflict — begins with a
|
|
131
|
+
* fresh, authoritative `fetchEpicRunState` read. A conflict's own
|
|
132
|
+
* `current_row_version` is NEVER substituted for that reread: the reread is
|
|
133
|
+
* what proves the version is authoritative, not merely the latest one the
|
|
134
|
+
* conflict payload happened to report. Bounded to
|
|
135
|
+
* {@link RECOVERY_TICKET_RETRY_LIMIT} full read-then-mutate cycles; exhausting
|
|
136
|
+
* it is reported as `concurrent-change-exhausted`, never as an infinite loop.
|
|
137
|
+
*
|
|
138
|
+
* ONE idempotency key is generated per invocation and reused across every
|
|
139
|
+
* retry within it — a distinct recovery-operation call gets its own key.
|
|
140
|
+
*/
|
|
141
|
+
async function ticketRecoveryWithRetry(access, options, mutate) {
|
|
142
|
+
const idempotencyKey = randomUUID();
|
|
143
|
+
for (let attempt = 0; attempt < RECOVERY_TICKET_RETRY_LIMIT; attempt += 1) {
|
|
144
|
+
let state;
|
|
145
|
+
try {
|
|
146
|
+
state = await fetchEpicRunState(access, options.epicRunId);
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
return {
|
|
150
|
+
ok: false,
|
|
151
|
+
kind: "unavailable",
|
|
152
|
+
epicRunId: options.epicRunId,
|
|
153
|
+
ticketKey: options.ticketKey,
|
|
154
|
+
message: safeDiagnosticMessage(err, "could not read run state"),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
const ticket = state.ticket_statuses.find((t) => t.ticket_key === options.ticketKey);
|
|
158
|
+
if (!ticket) {
|
|
159
|
+
return {
|
|
160
|
+
ok: false,
|
|
161
|
+
kind: "ticket-not-found",
|
|
162
|
+
epicRunId: options.epicRunId,
|
|
163
|
+
ticketKey: options.ticketKey,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
let result;
|
|
167
|
+
try {
|
|
168
|
+
result = await mutate(access, {
|
|
169
|
+
epicRunId: options.epicRunId,
|
|
170
|
+
ticketKey: options.ticketKey,
|
|
171
|
+
expectedRowVersion: ticket.row_version,
|
|
172
|
+
idempotencyKey,
|
|
173
|
+
reason: options.reason,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
return {
|
|
178
|
+
ok: false,
|
|
179
|
+
kind: "unavailable",
|
|
180
|
+
epicRunId: options.epicRunId,
|
|
181
|
+
ticketKey: options.ticketKey,
|
|
182
|
+
message: safeDiagnosticMessage(err, "recovery request failed"),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
if (result.ok) {
|
|
186
|
+
return {
|
|
187
|
+
ok: true,
|
|
188
|
+
kind: "unparked",
|
|
189
|
+
epicRunId: options.epicRunId,
|
|
190
|
+
ticketKey: options.ticketKey,
|
|
191
|
+
status: result.ticket_status.status,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
// `result.kind === "cas-conflict"`: a concurrent write moved the ticket's
|
|
195
|
+
// row_version between our read and our mutate. Loop back to a fresh read —
|
|
196
|
+
// never reuse `result.current_row_version` here.
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
ok: false,
|
|
200
|
+
kind: "concurrent-change-exhausted",
|
|
201
|
+
epicRunId: options.epicRunId,
|
|
202
|
+
ticketKey: options.ticketKey,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Unpark a parked `needs_human` ticket, with a bounded authoritative-reread
|
|
207
|
+
* retry on a concurrent `row_version` bump.
|
|
208
|
+
*/
|
|
209
|
+
export async function unparkEpicTicketWithRetry(access, options) {
|
|
210
|
+
return ticketRecoveryWithRetry(access, options, (a, args) => unparkEpicTicket(a, args));
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Adopt the current PR head and unpark a parked ticket, sharing the identical
|
|
214
|
+
* reread-and-retry discipline. The adopt-current-head client — never the
|
|
215
|
+
* ordinary unpark client — is called for every attempt.
|
|
216
|
+
*/
|
|
217
|
+
export async function adoptCurrentHeadAndUnparkWithRetry(access, options) {
|
|
218
|
+
return ticketRecoveryWithRetry(access, options, (a, args) => adoptCurrentHeadAndUnparkTicket(a, args));
|
|
219
|
+
}
|
package/build/conductor/tools.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { z } from "zod";
|
|
12
12
|
import { SEMANTIC_EVENT_TYPES } from "./taxonomy.js";
|
|
13
|
-
import { ConductorValidationError, toConductorErrorEnvelope } from "./errors.js";
|
|
13
|
+
import { ConductorValidationError, ConductorWorkerContextRequiredError, toConductorErrorEnvelope, } from "./errors.js";
|
|
14
14
|
import { emitConductorEvent, pollConductorEvents, waitForConductorEvent, getSupervisorSnapshot, sendWorkerMessage, } from "./store.js";
|
|
15
15
|
import { normalizePrNumber, normalizeSha } from "./git-ci-types.js";
|
|
16
16
|
import { waitForDoneGate, resolveDispatchRunIdForBinding } from "./pr-ci-producer.js";
|
|
@@ -266,6 +266,15 @@ function registerWaitForDoneGateTool(registerTool) {
|
|
|
266
266
|
// deterministic id are derived purely and the id is forwarded to the CLI,
|
|
267
267
|
// so dedup happens server-side on the events.id UNIQUE constraint — the
|
|
268
268
|
// worker path performs NO in-process ledger read or write.
|
|
269
|
+
//
|
|
270
|
+
// BAPI-772: this emit leg inherits the runtime contract from
|
|
271
|
+
// `resolveWorkerLedgerCliRuntime`, which is reached only when an emit is
|
|
272
|
+
// actually attempted — read-only gate work (binding resolution, CI
|
|
273
|
+
// polling) is never preemptively rejected. In a plain session with no
|
|
274
|
+
// worker env at all, that resolver raises WORKER_CONTEXT_REQUIRED, so the
|
|
275
|
+
// gate explains itself instead of surfacing an opaque 503; a partially
|
|
276
|
+
// configured or corrupted worker runtime still fails loud with
|
|
277
|
+
// LEDGER_SUBPROCESS_RUNTIME_UNAVAILABLE. Nothing falls back in-process.
|
|
269
278
|
emitIfNew: (input, dimensions) => emitConductorEventIfNewViaCli(input, dimensions),
|
|
270
279
|
});
|
|
271
280
|
return jsonResult({
|
|
@@ -351,8 +360,28 @@ function registerCheckMessagesTool(registerTool) {
|
|
|
351
360
|
limit: z.number().int().positive().max(100).optional().describe("Max messages to deliver/ack (default 10, max 100)."),
|
|
352
361
|
},
|
|
353
362
|
}, withConductorToolErrorHandling(async (args) => {
|
|
354
|
-
const
|
|
355
|
-
const
|
|
363
|
+
const runIdArg = args.run_id;
|
|
364
|
+
const workerIdArg = args.worker_id;
|
|
365
|
+
const runIdEnv = process.env.BAPI_CONDUCTOR_RUN_ID;
|
|
366
|
+
const workerIdEnv = process.env.BAPI_CONDUCTOR_WORKER_ID;
|
|
367
|
+
// BAPI-772: a call with NEITHER argument supplied and NEITHER env var set is
|
|
368
|
+
// a plain MCP session — nothing is broken, the tool is simply worker-scoped.
|
|
369
|
+
// Return the typed guidance envelope instead of an opaque validation error.
|
|
370
|
+
// Any other shape (an explicit blank id, one id present and the other not, a
|
|
371
|
+
// partially-set worker env) is still malformed identity and keeps the
|
|
372
|
+
// existing fail-loud VALIDATION_ERROR: a broken worker context must never be
|
|
373
|
+
// relabeled as an ordinary session.
|
|
374
|
+
if (runIdArg === undefined &&
|
|
375
|
+
workerIdArg === undefined &&
|
|
376
|
+
runIdEnv === undefined &&
|
|
377
|
+
workerIdEnv === undefined) {
|
|
378
|
+
throw new ConductorWorkerContextRequiredError([
|
|
379
|
+
"BAPI_CONDUCTOR_RUN_ID",
|
|
380
|
+
"BAPI_CONDUCTOR_WORKER_ID",
|
|
381
|
+
]);
|
|
382
|
+
}
|
|
383
|
+
const runId = runIdArg ?? runIdEnv ?? "";
|
|
384
|
+
const workerId = workerIdArg ?? workerIdEnv ?? "";
|
|
356
385
|
if (runId.trim().length === 0 || workerId.trim().length === 0) {
|
|
357
386
|
throw new ConductorValidationError("Conductor worker identity is unavailable: provide run_id + worker_id, or set BAPI_CONDUCTOR_RUN_ID and BAPI_CONDUCTOR_WORKER_ID.");
|
|
358
387
|
}
|
|
@@ -16,6 +16,12 @@
|
|
|
16
16
|
* to the worker's own `process.execPath`. Falling back would re-introduce the
|
|
17
17
|
* native load into the worker Node — the exact failure mode this slice removes.
|
|
18
18
|
*
|
|
19
|
+
* BAPI-772 adds ONE distinction on top of that contract, and it changes no
|
|
20
|
+
* fallback behavior: when BOTH worker variables are undefined there is no worker
|
|
21
|
+
* at all (a plain MCP session), which raises the typed
|
|
22
|
+
* {@link ConductorWorkerContextRequiredError} guidance envelope instead. Every
|
|
23
|
+
* partially-configured or invalid runtime keeps failing loud exactly as before.
|
|
24
|
+
*
|
|
19
25
|
* Security: argv is always a fixed LIST (`execFile`, never `shell: true`, never a
|
|
20
26
|
* command string). Event JSON payloads cross via STDIN (`--data-json-stdin`), so
|
|
21
27
|
* raw payloads/secrets never enter the process argument list. Every failure is
|
|
@@ -24,7 +30,7 @@
|
|
|
24
30
|
*/
|
|
25
31
|
import * as nodeChildProcess from "node:child_process";
|
|
26
32
|
import { isAbsolute as pathIsAbsolute } from "node:path";
|
|
27
|
-
import { ConductorLedgerSubprocessRuntimeError, } from "./errors.js";
|
|
33
|
+
import { ConductorLedgerSubprocessRuntimeError, ConductorWorkerContextRequiredError, } from "./errors.js";
|
|
28
34
|
import { makeProducerDedupeKey, makeStableProducerEventId, } from "./producer-ledger.js";
|
|
29
35
|
/** The env key that carries the captured conductor Node executable path. */
|
|
30
36
|
export const CONDUCTOR_NODE_PATH_ENV = "CONDUCTOR_NODE_PATH";
|
|
@@ -60,10 +66,30 @@ function nonEmpty(value) {
|
|
|
60
66
|
* non-empty. On any failure this throws a typed
|
|
61
67
|
* {@link ConductorLedgerSubprocessRuntimeError}; it NEVER falls back to
|
|
62
68
|
* `process.execPath`.
|
|
69
|
+
*
|
|
70
|
+
* BAPI-772 splits ABSENCE from BREAKAGE, and only at the top:
|
|
71
|
+
*
|
|
72
|
+
* - BOTH variables undefined → {@link ConductorWorkerContextRequiredError}.
|
|
73
|
+
* There is no worker runtime because there is no worker: this is a plain MCP
|
|
74
|
+
* session, and the caller deserves guidance, not a 503.
|
|
75
|
+
* - anything else — one variable present and the other not, an empty or
|
|
76
|
+
* whitespace-only value, a relative path, an unusable CLI file — keeps the
|
|
77
|
+
* existing fail-loud `LEDGER_SUBPROCESS_RUNTIME_UNAVAILABLE` mapping. A
|
|
78
|
+
* half-configured or corrupted worker runtime is a real fault and must never
|
|
79
|
+
* be relabeled as an ordinary plain session.
|
|
63
80
|
*/
|
|
64
81
|
export function resolveWorkerLedgerCliRuntime(deps = {}) {
|
|
65
82
|
const env = deps.env ?? process.env;
|
|
66
83
|
const isAbsolute = deps.isAbsolute ?? pathIsAbsolute;
|
|
84
|
+
// `undefined` specifically — an env var SET to "" is present-but-invalid and
|
|
85
|
+
// falls through to the fail-loud path below.
|
|
86
|
+
if (env[CONDUCTOR_NODE_PATH_ENV] === undefined &&
|
|
87
|
+
env[BAPI_CONDUCTOR_CLI_FILE_ENV] === undefined) {
|
|
88
|
+
throw new ConductorWorkerContextRequiredError([
|
|
89
|
+
CONDUCTOR_NODE_PATH_ENV,
|
|
90
|
+
BAPI_CONDUCTOR_CLI_FILE_ENV,
|
|
91
|
+
]);
|
|
92
|
+
}
|
|
67
93
|
const nodePathRaw = env[CONDUCTOR_NODE_PATH_ENV];
|
|
68
94
|
if (!nonEmpty(nodePathRaw)) {
|
|
69
95
|
throw new ConductorLedgerSubprocessRuntimeError("missing", CONDUCTOR_NODE_PATH_ENV);
|