@bridge_gpt/mcp-server 0.2.34 → 0.2.37
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 +490 -366
- package/build/agent-capabilities/probe-context.js +8 -1
- package/build/agent-capabilities/probes.js +7 -1
- package/build/agents.generated.js +1 -1
- package/build/base-url.js +79 -0
- package/build/bridge-api-urls.js +9 -0
- package/build/chain-orchestrator.js +93 -15
- package/build/claude-review-workflow.js +264 -0
- package/build/claude-user-config-doctor.js +317 -0
- package/build/cli-release.js +53 -0
- package/build/commands.generated.js +6 -5
- package/build/conductor/bridge-api-client.js +393 -4
- package/build/conductor/deny-enforcement-preflight.js +1 -0
- package/build/conductor/done-gate.js +44 -5
- package/build/conductor/epic-reconcile.js +6 -0
- package/build/conductor/install-doctor.js +462 -0
- package/build/conductor-bin.js +3 -3
- package/build/conductor-bundle-artifacts.js +37 -15
- package/build/credential-store.js +205 -4
- package/build/direct-ticket-tools.js +70 -0
- package/build/doctor.js +473 -81
- package/build/executor/cli.js +83 -6
- package/build/executor/credentials.js +39 -11
- package/build/executor/deps.js +32 -1
- package/build/executor/env.js +71 -28
- package/build/executor/heartbeat.js +138 -17
- package/build/executor/http-client.js +49 -8
- package/build/executor/index.js +4 -0
- package/build/executor/job-errors.js +4 -0
- package/build/executor/job-runner.js +530 -20
- package/build/executor/observation.js +130 -0
- package/build/executor/permissions.js +114 -8
- package/build/executor/preflight.js +127 -8
- package/build/executor/prompt-spec.js +51 -0
- package/build/executor/runner.js +23 -2
- package/build/executor/service-unit.js +876 -0
- package/build/executor/test-clock.js +75 -3
- package/build/executor/types.js +4 -18
- package/build/executor/worker-command.js +73 -12
- package/build/executor/worker-config-isolation.js +287 -0
- package/build/executor/worker-finalization.js +68 -14
- package/build/executor/worktree.js +46 -4
- package/build/index.js +1039 -237
- package/build/init.js +503 -111
- package/build/install-bridge-conductor.js +491 -0
- package/build/install-bridge.js +1188 -247
- package/build/install-reexec.js +233 -0
- package/build/launcher-config-inspection.js +351 -0
- package/build/mcp-host-config.js +11 -1
- package/build/mcp-install-state.js +32 -0
- package/build/mcp-invoke.js +49 -6
- package/build/mcp-provisioning.js +52 -13
- package/build/mcp-registration-doctor.js +14 -5
- package/build/notifications.js +553 -0
- package/build/pipeline-orchestrator.js +146 -4
- package/build/pipeline-utils.js +3 -0
- package/build/pipelines.generated.js +32 -13
- package/build/plan-execution-ledger.js +550 -0
- package/build/plan-phase-routing.js +272 -0
- package/build/plane/alembic-head.js +110 -0
- package/build/plane/build-freshness.js +167 -0
- package/build/plane/cli.js +480 -0
- package/build/plane/defaults.js +266 -0
- package/build/plane/manifest.js +377 -0
- package/build/plane/member-logs.js +147 -0
- package/build/plane/member-roster.js +147 -0
- package/build/plane/preflight.js +289 -0
- package/build/plane/shutdown.js +195 -0
- package/build/plane/status.js +125 -0
- package/build/plane/supervisor.js +569 -0
- package/build/plane/test-fakes.js +156 -0
- package/build/plane/types.js +75 -0
- package/build/readme.generated.js +1 -1
- package/build/run-unit-tests-launcher.js +259 -0
- package/build/setup-epic.js +772 -28
- package/build/sfcc/log-gate.js +38 -11
- package/build/sfcc/log-query.js +55 -15
- package/build/sfcc/ocapi-shape.js +51 -14
- package/build/sfcc/output.js +41 -11
- package/build/sfcc/permissions.js +24 -2
- package/build/sfcc/read-projection.js +181 -0
- package/build/sfcc/read-result.js +158 -0
- package/build/sfcc/reads-custom-object-def.js +29 -18
- package/build/sfcc/reads-site-preference.js +75 -29
- package/build/sfcc/reads-system-object.js +40 -34
- package/build/sfcc/sfcc-result.js +106 -0
- package/build/sfcc/tool-wrapper.js +56 -13
- package/build/sfcc/write-grants.js +45 -22
- package/build/sfcc/write-guard.js +21 -13
- package/build/sfcc/write-result.js +61 -14
- package/build/sfcc/write-tool-common.js +126 -32
- package/build/sfcc/writes-system-object.js +11 -50
- package/build/start-tickets-prereqs.js +129 -0
- package/build/start-tickets.js +17 -13
- package/build/ticket-backend-metadata.js +59 -0
- package/build/ticket-key-utils.js +92 -0
- package/build/tool-error-envelope.js +71 -0
- package/build/tool-surface-gating.js +72 -0
- package/build/update-status.js +102 -0
- package/build/upgrade-advice.js +47 -0
- package/build/upgrade-cli.js +412 -110
- package/build/version.generated.js +1 -1
- package/build/worktree-core.js +73 -0
- package/docs/CONDUCTOR.md +132 -6
- package/docs/install/mcp-tool-integrations.md +29 -21
- package/package.json +9 -6
- package/pipelines/implement-ticket.json +19 -4
- package/build/conductor/supervisor-judgment-python.js +0 -141
- package/build/conductor/supervisor-judgment.js +0 -215
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `plane` CLI surface (BAPI-756).
|
|
3
|
+
*
|
|
4
|
+
* Two responsibilities live here and nothing else: a pure argument parser, and
|
|
5
|
+
* a coordinator that sequences preflight → claim → launch (or status, or down)
|
|
6
|
+
* behind one exception-contained entrypoint.
|
|
7
|
+
*
|
|
8
|
+
* All operator output goes through injected sinks. The global console helpers
|
|
9
|
+
* are banned throughout this directory — a static review step greps for them —
|
|
10
|
+
* because this package also hosts an MCP stdio server: anything that writes to
|
|
11
|
+
* the process's real stdout from shared code can corrupt the protocol transport
|
|
12
|
+
* if it is ever reused on the server path.
|
|
13
|
+
*/
|
|
14
|
+
import { randomUUID } from "crypto";
|
|
15
|
+
import { PLANE_ENTRYPOINT_ACTION, PLANE_ID_ENV_VAR, PLANE_MANIFEST_SCHEMA_VERSION, PLANE_OBSERVER_CHANNEL_TYPE_ENV, PLANE_OBSERVER_COMMAND, PLANE_OBSERVER_DESTINATION_ENV, PLANE_RUNTIME_ACTION, PLANE_RUNTIME_DIR, } from "./types.js";
|
|
16
|
+
import { PLANE_RUNTIME_ENTRYPOINT_REFUSAL } from "./build-freshness.js";
|
|
17
|
+
import { relativeLogPathFor } from "./manifest.js";
|
|
18
|
+
import { claimPlaneManifest } from "./manifest.js";
|
|
19
|
+
import { runPlanePreflight } from "./preflight.js";
|
|
20
|
+
import { buildPlaneMemberRoster } from "./member-roster.js";
|
|
21
|
+
import { getPlaneStatus, formatPlaneStatus } from "./status.js";
|
|
22
|
+
import { shutdownPlane, formatPlaneShutdown } from "./shutdown.js";
|
|
23
|
+
import { launchPlaneSupervisor, runPlaneRuntime, } from "./supervisor.js";
|
|
24
|
+
import { createMemberLogDeps, createPlaneClock, createPlaneExecFile, createPlaneFsDeps, createPlaneProcessDeps, createPlaneSpawn, probeTcpPort, registerSignalHandler, resolveHomedir, resolveNodeExecutable, resolvePackageEntrypoint, } from "./defaults.js";
|
|
25
|
+
import { resolveBapiCredentials } from "../credential-store.js";
|
|
26
|
+
import { resolveRequiredStartTicketsRepoName } from "../start-tickets-repo.js";
|
|
27
|
+
/** Upper bound on executor lanes. Guards a typo from forking a fleet. */
|
|
28
|
+
export const PLANE_MAX_EXECUTORS = 32;
|
|
29
|
+
/** Default executor lane count when `--executors` is omitted. */
|
|
30
|
+
export const PLANE_DEFAULT_EXECUTORS = 1;
|
|
31
|
+
/**
|
|
32
|
+
* Parse everything after the `plane` token.
|
|
33
|
+
*
|
|
34
|
+
* Pure by construction: every rejection happens here, before the coordinator
|
|
35
|
+
* can touch the filesystem, open a socket, resolve a credential, or spawn a
|
|
36
|
+
* child. A malformed invocation therefore has no side effect at all.
|
|
37
|
+
*/
|
|
38
|
+
export function parsePlaneArgs(argv) {
|
|
39
|
+
if (argv.length === 0) {
|
|
40
|
+
return { kind: "error", message: "missing action (expected `up`, `status`, or `down`)" };
|
|
41
|
+
}
|
|
42
|
+
if (argv.includes("--help") || argv.includes("-h") || argv[0] === "help") {
|
|
43
|
+
return { kind: "help" };
|
|
44
|
+
}
|
|
45
|
+
const action = argv[0];
|
|
46
|
+
const rest = argv.slice(1);
|
|
47
|
+
if (action === "status" || action === "down") {
|
|
48
|
+
if (rest.length > 0) {
|
|
49
|
+
return {
|
|
50
|
+
kind: "error",
|
|
51
|
+
message: `\`plane ${action}\` takes no options (got '${rest[0]}')`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return { kind: action };
|
|
55
|
+
}
|
|
56
|
+
// Private, and deliberately absent from the usage text and from the
|
|
57
|
+
// expected-action list in the unknown-action error below: it exists for the
|
|
58
|
+
// bundled-artifact regression guard, not for operators.
|
|
59
|
+
if (action === PLANE_ENTRYPOINT_ACTION) {
|
|
60
|
+
if (rest.length > 0) {
|
|
61
|
+
return {
|
|
62
|
+
kind: "error",
|
|
63
|
+
message: `\`plane ${action}\` takes no options (got '${rest[0]}')`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return { kind: "entrypoint" };
|
|
67
|
+
}
|
|
68
|
+
if (action !== "up" && action !== PLANE_RUNTIME_ACTION) {
|
|
69
|
+
return {
|
|
70
|
+
kind: "error",
|
|
71
|
+
message: `unknown action '${action}' (expected \`up\`, \`status\`, or \`down\`)`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
let executors = null;
|
|
75
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
76
|
+
const arg = rest[i];
|
|
77
|
+
if (arg === "--executors") {
|
|
78
|
+
if (executors !== null) {
|
|
79
|
+
return { kind: "error", message: "--executors may be supplied only once" };
|
|
80
|
+
}
|
|
81
|
+
const raw = rest[i + 1];
|
|
82
|
+
i += 1;
|
|
83
|
+
if (raw === undefined) {
|
|
84
|
+
return { kind: "error", message: "--executors requires a value" };
|
|
85
|
+
}
|
|
86
|
+
const parsed = parseExecutorCount(raw);
|
|
87
|
+
if (parsed === null) {
|
|
88
|
+
return {
|
|
89
|
+
kind: "error",
|
|
90
|
+
message: `--executors must be a whole number between 1 and ${PLANE_MAX_EXECUTORS} ` +
|
|
91
|
+
`(got '${raw}')`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
executors = parsed;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (arg.startsWith("-")) {
|
|
98
|
+
return { kind: "error", message: `unknown option '${arg}' for \`plane ${action}\`` };
|
|
99
|
+
}
|
|
100
|
+
return { kind: "error", message: `unexpected argument '${arg}' for \`plane ${action}\`` };
|
|
101
|
+
}
|
|
102
|
+
const options = { executors: executors ?? PLANE_DEFAULT_EXECUTORS };
|
|
103
|
+
return action === "up" ? { kind: "up", options } : { kind: "runtime", options };
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Strict base-10 whole-number parse. Returns `null` for anything that is not an
|
|
107
|
+
* exact in-range integer — no coercion, no rounding, no exponent notation.
|
|
108
|
+
* `Number("1e2")` is 100 and `Number("")` is 0, so the textual shape is checked
|
|
109
|
+
* before the numeric value is trusted.
|
|
110
|
+
*/
|
|
111
|
+
function parseExecutorCount(raw) {
|
|
112
|
+
if (!/^\d+$/.test(raw))
|
|
113
|
+
return null;
|
|
114
|
+
const value = Number.parseInt(raw, 10);
|
|
115
|
+
if (!Number.isSafeInteger(value))
|
|
116
|
+
return null;
|
|
117
|
+
if (value < 1 || value > PLANE_MAX_EXECUTORS)
|
|
118
|
+
return null;
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Operator-facing usage. The safety claims here are pinned by a unit test: they
|
|
123
|
+
* are the contract an operator reads before trusting the supervisor with a run,
|
|
124
|
+
* so they cannot drift away from the implementation silently.
|
|
125
|
+
*/
|
|
126
|
+
export function getPlaneUsage() {
|
|
127
|
+
return [
|
|
128
|
+
"Usage: mcp-server plane <up|status|down> [options]",
|
|
129
|
+
"",
|
|
130
|
+
"Brings up (and winds down) the attended conductor plane in one command:",
|
|
131
|
+
"the Bridge API server, the reconciler worker, and one executor per lane.",
|
|
132
|
+
"",
|
|
133
|
+
"Actions:",
|
|
134
|
+
" plane up [--executors N] Preflight, then start the plane. Refuses as a",
|
|
135
|
+
" whole if any spawn-blocking check fails.",
|
|
136
|
+
" plane status Read the manifest and report each member's state.",
|
|
137
|
+
" plane down Signal the recorded process group, escalating to",
|
|
138
|
+
" SIGKILL, and clear the manifest.",
|
|
139
|
+
"",
|
|
140
|
+
"Options:",
|
|
141
|
+
` --executors N Executor lanes to start (default ${PLANE_DEFAULT_EXECUTORS}, max ${PLANE_MAX_EXECUTORS}).`,
|
|
142
|
+
" -h, --help Show this help.",
|
|
143
|
+
"",
|
|
144
|
+
"Behavior you can rely on:",
|
|
145
|
+
" * The server runs WITHOUT --reload. Reload is not configurable here, because",
|
|
146
|
+
" it drops long-running MCP calls mid-flight.",
|
|
147
|
+
" * Executors run from this repository's local build tree",
|
|
148
|
+
" (mcp_server/build/index.js) — never npx, never a published package.",
|
|
149
|
+
" * A member that exits is reported loudly and is NOT restarted. There is no",
|
|
150
|
+
" auto-restart, retry, or backoff: a crash is a finding, not noise.",
|
|
151
|
+
" * The dead-man observer is NOT part of the plane. It is started and stopped",
|
|
152
|
+
" by you, so its failure domain stays independent of the supervisor's.",
|
|
153
|
+
` Start it separately: ${PLANE_OBSERVER_COMMAND}`,
|
|
154
|
+
"",
|
|
155
|
+
`Runtime artifacts (manifest + per-member logs) live under ${PLANE_RUNTIME_DIR}/.`,
|
|
156
|
+
].join("\n");
|
|
157
|
+
}
|
|
158
|
+
/** The observer reminder printed at the end of a successful startup banner. */
|
|
159
|
+
export function getPlaneObserverReminder() {
|
|
160
|
+
return [
|
|
161
|
+
"",
|
|
162
|
+
"Dead-man observer (independent — not started, listed, or stopped by the plane):",
|
|
163
|
+
` ${PLANE_OBSERVER_COMMAND}`,
|
|
164
|
+
` with ${PLANE_OBSERVER_CHANNEL_TYPE_ENV} (slack_webhook|generic_webhook) and`,
|
|
165
|
+
` ${PLANE_OBSERVER_DESTINATION_ENV} (the NAME of an env var holding the URL).`,
|
|
166
|
+
];
|
|
167
|
+
}
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
// Coordinator
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
/** Wording every refusal ends with, so "nothing started" is greppable. */
|
|
172
|
+
export const PLANE_NOTHING_STARTED = "no processes were started";
|
|
173
|
+
/**
|
|
174
|
+
* Public `plane` entrypoint.
|
|
175
|
+
*
|
|
176
|
+
* The ordering here IS the all-or-nothing contract: parse, then preflight, and
|
|
177
|
+
* only then create `.bridge/plane/`, claim the manifest, open logs, or spawn.
|
|
178
|
+
* Nothing before a clean preflight has a side effect.
|
|
179
|
+
*/
|
|
180
|
+
export async function runPlaneCli(argv, overrides = {}) {
|
|
181
|
+
const sinks = overrides.sinks ?? {
|
|
182
|
+
// Bound at the boundary, never inside a plane module: this package also
|
|
183
|
+
// hosts an MCP stdio server, and shared code that writes to the process's
|
|
184
|
+
// real stdout would corrupt that transport if it were ever reused there.
|
|
185
|
+
stdout: (line) => process.stdout.write(`${line}\n`),
|
|
186
|
+
stderr: (line) => process.stderr.write(`${line}\n`),
|
|
187
|
+
};
|
|
188
|
+
try {
|
|
189
|
+
const parsed = parsePlaneArgs(argv);
|
|
190
|
+
switch (parsed.kind) {
|
|
191
|
+
case "help":
|
|
192
|
+
sinks.stdout(getPlaneUsage());
|
|
193
|
+
return 0;
|
|
194
|
+
case "error":
|
|
195
|
+
sinks.stderr(`Error: ${parsed.message}`);
|
|
196
|
+
sinks.stderr("");
|
|
197
|
+
sinks.stderr(getPlaneUsage());
|
|
198
|
+
return 1;
|
|
199
|
+
// Each of these is AWAITED, not merely returned. Returning the promise
|
|
200
|
+
// would hand it to the caller unresolved, so a rejection would escape the
|
|
201
|
+
// sanitizing catch below and surface as a raw stack — which is exactly the
|
|
202
|
+
// path most likely to carry a credential out of a resolver or spawn error.
|
|
203
|
+
case "status":
|
|
204
|
+
return await runStatusAction(sinks, overrides);
|
|
205
|
+
case "down":
|
|
206
|
+
return await runDownAction(sinks, overrides);
|
|
207
|
+
case "up":
|
|
208
|
+
return await runUpAction(parsed.options.executors, sinks, overrides);
|
|
209
|
+
case "runtime":
|
|
210
|
+
return await runRuntimeAction(parsed.options.executors, overrides);
|
|
211
|
+
// Dispatched here, ahead of every action that touches the repository, so
|
|
212
|
+
// the diagnostic answers from an empty directory with no credentials.
|
|
213
|
+
case "entrypoint":
|
|
214
|
+
return runEntrypointDiagnosticAction(sinks, overrides);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
// Sanitized by construction: an exception message can carry a credential
|
|
219
|
+
// that leaked into a resolver error or a spawn failure, so none of it is
|
|
220
|
+
// rendered — only this fixed line.
|
|
221
|
+
sinks.stderr("plane failed: an unexpected internal error occurred.");
|
|
222
|
+
return 1;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Report the runtime re-exec target the *executing artifact* selects, and do
|
|
227
|
+
* nothing else (BAPI-768).
|
|
228
|
+
*
|
|
229
|
+
* This is the question the bundled-artifact regression test asks. It runs the
|
|
230
|
+
* same production resolver preflight uses, so a bundle whose resolution is
|
|
231
|
+
* broken cannot answer correctly here and pass there. It creates no
|
|
232
|
+
* `.bridge/plane/`, claims no manifest, opens no log, and spawns nothing —
|
|
233
|
+
* which is what lets the guard run from an empty temporary directory with no
|
|
234
|
+
* credentials and no port 8000, and therefore never skip.
|
|
235
|
+
*/
|
|
236
|
+
export function runEntrypointDiagnosticAction(sinks, overrides = {}) {
|
|
237
|
+
const resolve = overrides.resolveRuntimeEntrypoint ?? (() => resolvePackageEntrypoint());
|
|
238
|
+
const resolution = resolve();
|
|
239
|
+
if (!resolution.ok) {
|
|
240
|
+
sinks.stderr(`plane entrypoint UNRESOLVED: ${PLANE_RUNTIME_ENTRYPOINT_REFUSAL}`);
|
|
241
|
+
return 1;
|
|
242
|
+
}
|
|
243
|
+
sinks.stdout(resolution.entrypoint);
|
|
244
|
+
return 0;
|
|
245
|
+
}
|
|
246
|
+
async function runStatusAction(sinks, overrides) {
|
|
247
|
+
const repoRoot = overrides.cwd ?? process.cwd();
|
|
248
|
+
if (overrides.status) {
|
|
249
|
+
sinks.stdout(await overrides.status(repoRoot));
|
|
250
|
+
return 0;
|
|
251
|
+
}
|
|
252
|
+
const result = await getPlaneStatus(repoRoot, {
|
|
253
|
+
fs: createPlaneFsDeps(),
|
|
254
|
+
proc: createPlaneProcessDeps(),
|
|
255
|
+
});
|
|
256
|
+
const text = formatPlaneStatus(result);
|
|
257
|
+
if (!result.ok) {
|
|
258
|
+
sinks.stderr(text);
|
|
259
|
+
return 1;
|
|
260
|
+
}
|
|
261
|
+
sinks.stdout(text);
|
|
262
|
+
return 0;
|
|
263
|
+
}
|
|
264
|
+
async function runDownAction(sinks, overrides) {
|
|
265
|
+
const repoRoot = overrides.cwd ?? process.cwd();
|
|
266
|
+
if (overrides.down) {
|
|
267
|
+
const injected = await overrides.down(repoRoot);
|
|
268
|
+
(injected.ok ? sinks.stdout : sinks.stderr)(injected.text);
|
|
269
|
+
return injected.ok ? 0 : 1;
|
|
270
|
+
}
|
|
271
|
+
const result = await shutdownPlane(repoRoot, {
|
|
272
|
+
fs: createPlaneFsDeps(),
|
|
273
|
+
proc: createPlaneProcessDeps(),
|
|
274
|
+
clock: createPlaneClock(),
|
|
275
|
+
});
|
|
276
|
+
const text = formatPlaneShutdown(result);
|
|
277
|
+
(result.ok ? sinks.stdout : sinks.stderr)(text);
|
|
278
|
+
return result.ok ? 0 : 1;
|
|
279
|
+
}
|
|
280
|
+
async function runUpAction(executors, sinks, overrides) {
|
|
281
|
+
const repoRoot = overrides.cwd ?? process.cwd();
|
|
282
|
+
const env = overrides.env ?? process.env;
|
|
283
|
+
const preflight = overrides.preflight
|
|
284
|
+
? await overrides.preflight(repoRoot)
|
|
285
|
+
: await runPlanePreflight(repoRoot, buildPreflightDeps(env));
|
|
286
|
+
for (const diagnostic of preflight.diagnostics.filter((d) => d.severity === "warning")) {
|
|
287
|
+
sinks.stdout(formatDiagnostic(diagnostic));
|
|
288
|
+
}
|
|
289
|
+
if (!preflight.ok) {
|
|
290
|
+
sinks.stderr("");
|
|
291
|
+
sinks.stderr("plane up REFUSED — preflight found blocking problems:");
|
|
292
|
+
for (const diagnostic of preflight.diagnostics.filter((d) => d.severity === "blocking")) {
|
|
293
|
+
sinks.stderr(formatDiagnostic(diagnostic));
|
|
294
|
+
}
|
|
295
|
+
sinks.stderr("");
|
|
296
|
+
sinks.stderr(`Fix every item above and retry. ${PLANE_NOTHING_STARTED}.`);
|
|
297
|
+
return 1;
|
|
298
|
+
}
|
|
299
|
+
const planeId = (overrides.newPlaneId ?? randomUUID)();
|
|
300
|
+
const launch = overrides.launch
|
|
301
|
+
? await overrides.launch(preflight, executors)
|
|
302
|
+
: await launchPlane(preflight, executors, planeId, sinks, env);
|
|
303
|
+
if (!launch.ok) {
|
|
304
|
+
for (const line of formatPlaneLaunchFailure(launch))
|
|
305
|
+
sinks.stderr(line);
|
|
306
|
+
return 1;
|
|
307
|
+
}
|
|
308
|
+
return launch.exitCode;
|
|
309
|
+
}
|
|
310
|
+
/** Stable label so a startup death is greppable in a terminal scrollback. */
|
|
311
|
+
export const PLANE_STARTUP_FAILED = "plane up FAILED — the runtime never came up";
|
|
312
|
+
/**
|
|
313
|
+
* Render a launch failure with everything an operator needs to act.
|
|
314
|
+
*
|
|
315
|
+
* The old rendering was a single `plane up FAILED: <error>` line, which was
|
|
316
|
+
* accurate only for the spawn-level failures it was written for. A runtime that
|
|
317
|
+
* dies after spawning has an exit status, a stderr tail, and a trace file, and
|
|
318
|
+
* omitting them is what turned BAPI-768 into an investigation instead of a
|
|
319
|
+
* message. Everything here is already redacted and bounded by the supervisor;
|
|
320
|
+
* this function only lays it out.
|
|
321
|
+
*/
|
|
322
|
+
export function formatPlaneLaunchFailure(launch) {
|
|
323
|
+
if (!launch.startup) {
|
|
324
|
+
// No child exit information exists, so none is invented — this is the
|
|
325
|
+
// pre-existing generic spawn-failure path, preserved verbatim in substance.
|
|
326
|
+
return [`plane up FAILED: ${launch.error}`];
|
|
327
|
+
}
|
|
328
|
+
const { exitCode, signal, stderrExcerpt, tracePath } = launch.startup;
|
|
329
|
+
const status = signal !== null ? `signal ${signal}` : `exit code ${exitCode ?? "unknown"}`;
|
|
330
|
+
const lines = [`${PLANE_STARTUP_FAILED}: ${launch.error} (${status}).`, ` trace ${tracePath}`];
|
|
331
|
+
if (stderrExcerpt.trim().length > 0) {
|
|
332
|
+
lines.push(" runtime stderr:");
|
|
333
|
+
for (const line of stderrExcerpt.split("\n")) {
|
|
334
|
+
if (line.length > 0)
|
|
335
|
+
lines.push(` ${line}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// Not PLANE_NOTHING_STARTED: members may briefly have existed before the
|
|
339
|
+
// runtime rolled them back, and claiming otherwise would be the same species
|
|
340
|
+
// of untruth as the banner this ticket removed.
|
|
341
|
+
lines.push("The plane is not running. Read the trace above, then retry `plane up`.");
|
|
342
|
+
return lines;
|
|
343
|
+
}
|
|
344
|
+
/** Claim the manifest, print the banner, then run the detached supervisor. */
|
|
345
|
+
async function launchPlane(preflight, executors, planeId, sinks, env) {
|
|
346
|
+
const fs = createPlaneFsDeps();
|
|
347
|
+
const clock = createPlaneClock();
|
|
348
|
+
const context = preflight.context;
|
|
349
|
+
const nodeExecutable = resolveNodeExecutable();
|
|
350
|
+
const roster = buildPlaneMemberRoster({
|
|
351
|
+
context,
|
|
352
|
+
executors,
|
|
353
|
+
parentEnv: env,
|
|
354
|
+
nodeExecutable,
|
|
355
|
+
});
|
|
356
|
+
const timestamp = clock.now().toISOString();
|
|
357
|
+
const claim = await claimPlaneManifest({
|
|
358
|
+
manifest: {
|
|
359
|
+
schemaVersion: PLANE_MANIFEST_SCHEMA_VERSION,
|
|
360
|
+
planeId,
|
|
361
|
+
repoRoot: context.repoRoot,
|
|
362
|
+
// Placeholder until the runtime exists; the launcher rewrites both fields
|
|
363
|
+
// with the detached runtime's pid the moment it has one.
|
|
364
|
+
supervisorPid: process.pid,
|
|
365
|
+
supervisorPgid: process.pid,
|
|
366
|
+
createdAt: timestamp,
|
|
367
|
+
updatedAt: timestamp,
|
|
368
|
+
members: roster.map((spec) => ({
|
|
369
|
+
name: spec.name,
|
|
370
|
+
pid: null,
|
|
371
|
+
state: "spawning",
|
|
372
|
+
exitCode: null,
|
|
373
|
+
exitSignal: null,
|
|
374
|
+
logPath: relativeLogPathFor(spec.name),
|
|
375
|
+
})),
|
|
376
|
+
},
|
|
377
|
+
fs,
|
|
378
|
+
proc: createPlaneProcessDeps(),
|
|
379
|
+
});
|
|
380
|
+
// No runtime was spawned, so there is no exit status, stderr, or trace to
|
|
381
|
+
// report — `startup: null` is the honest shape, not a placeholder.
|
|
382
|
+
if (!claim.ok)
|
|
383
|
+
return { ok: false, error: claim.message, startup: null };
|
|
384
|
+
return launchPlaneSupervisor({ context, manifest: claim.manifest, executors }, {
|
|
385
|
+
fs,
|
|
386
|
+
clock,
|
|
387
|
+
spawn: createPlaneSpawn(),
|
|
388
|
+
sinks,
|
|
389
|
+
log: createMemberLogDeps(),
|
|
390
|
+
onSignal: registerSignalHandler,
|
|
391
|
+
nodeExecutable,
|
|
392
|
+
// Resolved and validated by preflight, never re-derived here: a second
|
|
393
|
+
// resolution could disagree with the one the refusal was based on.
|
|
394
|
+
entrypoint: context.runtimeEntrypoint,
|
|
395
|
+
parentEnv: env,
|
|
396
|
+
// The banner is now a consequence of the runtime's readiness handshake
|
|
397
|
+
// rather than of the launcher reaching the end of its own function. That
|
|
398
|
+
// is the whole point: `spawn()` succeeding never meant the plane was up.
|
|
399
|
+
onReady: ({ supervisorPid, supervisorPgid }) => printStartupBanner(sinks, context, roster.length, executors, {
|
|
400
|
+
supervisorPid,
|
|
401
|
+
supervisorPgid,
|
|
402
|
+
}),
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
function printStartupBanner(sinks, context, memberCount, executors, launch) {
|
|
406
|
+
// Deliberately built from fixed labels plus non-secret facts. No environment
|
|
407
|
+
// object and no credential value is rendered — the resolved key exists only
|
|
408
|
+
// in the child environments.
|
|
409
|
+
sinks.stdout("");
|
|
410
|
+
sinks.stdout(`Plane up — ${memberCount} members (${executors} executor lane(s))`);
|
|
411
|
+
sinks.stdout(` repository ${context.repoName} (${context.repoRoot})`);
|
|
412
|
+
sinks.stdout(` server ${context.baseUrl} (no --reload)`);
|
|
413
|
+
sinks.stdout(` supervisor pid ${launch.supervisorPid}, process group ${launch.supervisorPgid}`);
|
|
414
|
+
sinks.stdout(` logs ${PLANE_RUNTIME_DIR}/`);
|
|
415
|
+
sinks.stdout(" crash policy members are NOT restarted; a member exit is reported loudly.");
|
|
416
|
+
for (const line of getPlaneObserverReminder())
|
|
417
|
+
sinks.stdout(line);
|
|
418
|
+
sinks.stdout("");
|
|
419
|
+
}
|
|
420
|
+
/** The private detached-runtime action. Never part of the documented surface. */
|
|
421
|
+
async function runRuntimeAction(executors, overrides) {
|
|
422
|
+
const repoRoot = overrides.cwd ?? process.cwd();
|
|
423
|
+
if (overrides.runtime)
|
|
424
|
+
return overrides.runtime(repoRoot, executors);
|
|
425
|
+
const env = overrides.env ?? process.env;
|
|
426
|
+
const planeId = env[PLANE_ID_ENV_VAR];
|
|
427
|
+
const sinks = {
|
|
428
|
+
stdout: (line) => process.stdout.write(`${line}\n`),
|
|
429
|
+
stderr: (line) => process.stderr.write(`${line}\n`),
|
|
430
|
+
};
|
|
431
|
+
if (typeof planeId !== "string" || planeId.length === 0) {
|
|
432
|
+
sinks.stderr(`plane runtime refused: ${PLANE_ID_ENV_VAR} was not provided by the launcher.`);
|
|
433
|
+
return 1;
|
|
434
|
+
}
|
|
435
|
+
const preflight = await runPlanePreflight(repoRoot, buildPreflightDeps(env));
|
|
436
|
+
if (!preflight.ok) {
|
|
437
|
+
// The launcher already ran preflight; reaching here means the world changed
|
|
438
|
+
// between claim and spawn. Refuse rather than start a half-valid plane.
|
|
439
|
+
sinks.stderr(`plane runtime refused: preflight no longer passes. ${PLANE_NOTHING_STARTED}.`);
|
|
440
|
+
return 1;
|
|
441
|
+
}
|
|
442
|
+
const roster = buildPlaneMemberRoster({
|
|
443
|
+
context: preflight.context,
|
|
444
|
+
executors,
|
|
445
|
+
parentEnv: env,
|
|
446
|
+
nodeExecutable: resolveNodeExecutable(),
|
|
447
|
+
});
|
|
448
|
+
const result = await runPlaneRuntime(repoRoot, roster, {
|
|
449
|
+
fs: createPlaneFsDeps(),
|
|
450
|
+
clock: createPlaneClock(),
|
|
451
|
+
proc: createPlaneProcessDeps(),
|
|
452
|
+
spawn: createPlaneSpawn(),
|
|
453
|
+
sinks,
|
|
454
|
+
log: createMemberLogDeps(),
|
|
455
|
+
onSignal: registerSignalHandler,
|
|
456
|
+
probePort: probeTcpPort,
|
|
457
|
+
selfPid: process.pid,
|
|
458
|
+
planeId,
|
|
459
|
+
});
|
|
460
|
+
return result.exitCode;
|
|
461
|
+
}
|
|
462
|
+
function buildPreflightDeps(env) {
|
|
463
|
+
return {
|
|
464
|
+
env,
|
|
465
|
+
platform: process.platform,
|
|
466
|
+
homedir: resolveHomedir,
|
|
467
|
+
fs: createPlaneFsDeps(),
|
|
468
|
+
clock: createPlaneClock(),
|
|
469
|
+
proc: createPlaneProcessDeps(),
|
|
470
|
+
execFile: createPlaneExecFile(),
|
|
471
|
+
probePort: probeTcpPort,
|
|
472
|
+
resolveRuntimeEntrypoint: () => resolvePackageEntrypoint(),
|
|
473
|
+
resolveRepoName: resolveRequiredStartTicketsRepoName,
|
|
474
|
+
resolveCredentials: resolveBapiCredentials,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
function formatDiagnostic(diagnostic) {
|
|
478
|
+
const label = diagnostic.severity === "blocking" ? "BLOCKED" : "WARN ";
|
|
479
|
+
return ` ${label} [${diagnostic.check}] ${diagnostic.message}`;
|
|
480
|
+
}
|