@bridge_gpt/mcp-server 0.2.52 → 0.2.53
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 +42 -12
- package/build/commands.generated.js +6 -4
- package/build/conductor/bridge-api-client.js +61 -0
- package/build/conductor/cli.js +23 -0
- package/build/conductor/doctor.js +428 -5
- package/build/conductor/install-doctor.js +65 -656
- package/build/conductor/readiness-cli.js +152 -0
- package/build/conductor/readiness-sections.js +666 -0
- package/build/conductor/readiness.js +710 -0
- package/build/conductor/tools.js +56 -3
- package/build/conductor-bin.js +21 -17
- package/build/index.js +4651 -4580
- package/build/install-doctor.js +154 -2
- package/build/pipelines.generated.js +3 -3
- package/build/plane/alembic-head.js +40 -11
- package/build/plane/build-freshness.js +22 -11
- package/build/plane/preflight.js +363 -48
- package/build/plane/types.js +36 -0
- package/build/readiness-check.js +412 -0
- package/build/readme.generated.js +1 -1
- package/build/version.generated.js +3 -3
- package/package.json +2 -2
- package/pipelines/{full-automation.json → idea-to-pr.json} +1 -1
package/build/plane/preflight.js
CHANGED
|
@@ -12,13 +12,71 @@
|
|
|
12
12
|
* it — including any manifest belonging to a plane that is already running.
|
|
13
13
|
*/
|
|
14
14
|
import path from "path";
|
|
15
|
-
import { PLANE_SERVER_PORT_ENV_VAR, } from "./types.js";
|
|
15
|
+
import { PLANE_SERVER_PORT_ENV_VAR, planeCheckFinding, planeCheckPassed, } from "./types.js";
|
|
16
16
|
import { checkPlaneBuildFreshness, checkPlaneRuntimeEntrypoint } from "./build-freshness.js";
|
|
17
17
|
import { checkAlembicHead } from "./alembic-head.js";
|
|
18
18
|
import { manifestHasLiveProcess, readPlaneManifest } from "./manifest.js";
|
|
19
19
|
import { detectClaudeLogin, formatClaudeLoginAdvisory } from "../claude-login.js";
|
|
20
|
+
import { createReadinessCheck, createReadinessCheckSafely, } from "../readiness-check.js";
|
|
20
21
|
/** Files that must exist for a path to be this repository's root. */
|
|
21
22
|
const REQUIRED_REPO_FILES = ["main.py", "worker.py", "alembic.ini"];
|
|
23
|
+
/** Fixed remediation for a `plane up` run started outside the repository root. */
|
|
24
|
+
export const PLANE_REPOSITORY_ROOT_REMEDIATION = "run `plane up` from the Bridge API repository root (the directory holding main.py, worker.py, and alembic.ini).";
|
|
25
|
+
/** Fixed remediation for an unresolvable Bridge repository identity. */
|
|
26
|
+
export const PLANE_REPO_IDENTITY_REMEDIATION = "set BAPI_REPO_NAME in this shell, or add a valid .bridge/config at the repository root.";
|
|
27
|
+
/** Fixed remediation for an unresolvable Bridge credential. */
|
|
28
|
+
export const PLANE_CREDENTIAL_REMEDIATION = "set BAPI_API_KEY in this shell, or store it with `mcp-server credentials`; a spawned shell never sees .mcp.json env.";
|
|
29
|
+
/** Fixed remediation for the resolver's credential-file permission advisory. */
|
|
30
|
+
export const PLANE_CREDENTIAL_PERMISSIONS_REMEDIATION = "restrict the Bridge credentials file to owner-only permissions (chmod 600), then rerun.";
|
|
31
|
+
/** Fixed remediation for an advisory Claude-login gap. */
|
|
32
|
+
export const PLANE_CLAUDE_LOGIN_REMEDIATION = "run `claude login` on this host so plane members inherit an authenticated session; startup continues either way.";
|
|
33
|
+
/** Fixed remediation for an occupied local server port. */
|
|
34
|
+
export const PLANE_SERVER_PORT_OCCUPIED_REMEDIATION = `stop the server that owns the port (or wind down its plane) and retry, or set ${PLANE_SERVER_PORT_ENV_VAR} to a free port — the port may belong to a SIBLING WORKTREE.`;
|
|
35
|
+
/** Fixed remediation for a port whose availability could not be determined. */
|
|
36
|
+
export const PLANE_SERVER_PORT_UNKNOWN_REMEDIATION = `retry, or set ${PLANE_SERVER_PORT_ENV_VAR} to a port you know is free; startup continues and uvicorn fails loudly if the port is taken.`;
|
|
37
|
+
/** Fixed remediation for a configured port that could not be resolved at all. */
|
|
38
|
+
export const PLANE_SERVER_ENDPOINT_REMEDIATION = `set ${PLANE_SERVER_PORT_ENV_VAR} to a valid port number (1-65535), or unset it to use the default.`;
|
|
39
|
+
/** Fixed remediation for an unreadable or malformed plane manifest. */
|
|
40
|
+
export const PLANE_MANIFEST_UNREADABLE_REMEDIATION = "inspect .bridge/plane/plane.json and remove it by hand once you have confirmed no plane is running; preflight signalled no process and changed no file.";
|
|
41
|
+
/** Fixed remediation for a plane that is still running. */
|
|
42
|
+
export const PLANE_ALREADY_RUNNING_REMEDIATION = "run `plane status` to inspect the running plane, or `plane down` to wind it down first.";
|
|
43
|
+
/** Ids the credential check reports under, kept stable across every branch. */
|
|
44
|
+
const PLANE_CREDENTIAL_OUTCOME_IDS = {
|
|
45
|
+
identity: "repo-identity",
|
|
46
|
+
credential: "bridge-credentials",
|
|
47
|
+
permissions: "credential-permissions",
|
|
48
|
+
};
|
|
49
|
+
/** Fixed detail for a prerequisite skipped because the repository root is invalid. */
|
|
50
|
+
const PLANE_ROOT_DEPENDENT_SKIP_DETAIL = "not run — every remaining check is relative to a valid repository root";
|
|
51
|
+
/**
|
|
52
|
+
* Every prerequisite `runPlanePreflight` short-circuits past when the
|
|
53
|
+
* repository root is invalid, with the name it is reported under.
|
|
54
|
+
*
|
|
55
|
+
* A table rather than a derived list: these are the checks that must still
|
|
56
|
+
* APPEAR in the outcome set, and deriving them from the ones that ran is exactly
|
|
57
|
+
* the "absence means pass" inference this whole change removes.
|
|
58
|
+
*/
|
|
59
|
+
const ROOT_DEPENDENT_CHECKS = [
|
|
60
|
+
["worktree-presence", "worktree-presence", "Git work tree"],
|
|
61
|
+
["claude-login", "claude-login", "Claude login"],
|
|
62
|
+
// The credential check reports THREE independently actionable facts under one
|
|
63
|
+
// check name, so all three ids are listed. Listing only the check name would
|
|
64
|
+
// leave `repo-identity` and `credential-permissions` absent from the outcome
|
|
65
|
+
// set, and the adapter would then report them as unknown FAILURES on a run
|
|
66
|
+
// where they were merely never reached.
|
|
67
|
+
["bridge-credentials", PLANE_CREDENTIAL_OUTCOME_IDS.identity, "Bridge repository identity"],
|
|
68
|
+
["bridge-credentials", PLANE_CREDENTIAL_OUTCOME_IDS.credential, "Bridge API credential"],
|
|
69
|
+
[
|
|
70
|
+
"bridge-credentials",
|
|
71
|
+
PLANE_CREDENTIAL_OUTCOME_IDS.permissions,
|
|
72
|
+
"Bridge credential file permissions",
|
|
73
|
+
],
|
|
74
|
+
["executor-build", "executor-build", "Executor build freshness"],
|
|
75
|
+
["runtime-entrypoint", "runtime-entrypoint", "Runtime re-exec entrypoint"],
|
|
76
|
+
["server-port", "server-port", "Local server port"],
|
|
77
|
+
["alembic-head", "alembic-head", "Database migration head"],
|
|
78
|
+
["existing-plane", "existing-plane", "Existing plane"],
|
|
79
|
+
];
|
|
22
80
|
/** Bound on the port probe so a black-holed port cannot stall bring-up. */
|
|
23
81
|
export const PLANE_PORT_PROBE_TIMEOUT_MS = 1_500;
|
|
24
82
|
/**
|
|
@@ -31,25 +89,50 @@ export const PLANE_PORT_PROBE_TIMEOUT_MS = 1_500;
|
|
|
31
89
|
*/
|
|
32
90
|
export async function runPlanePreflight(repoRoot, deps) {
|
|
33
91
|
const diagnostics = [];
|
|
92
|
+
const outcomes = [];
|
|
34
93
|
const add = (diagnostic) => {
|
|
35
94
|
if (diagnostic)
|
|
36
95
|
diagnostics.push(diagnostic);
|
|
37
96
|
};
|
|
38
|
-
|
|
39
|
-
|
|
97
|
+
// BAPI-1055: every check's own report, recorded whole. `add` still filters
|
|
98
|
+
// nulls for the legacy diagnostic list; `record` never infers anything from
|
|
99
|
+
// that filtering — the outcomes come from the check itself.
|
|
100
|
+
const record = (report) => {
|
|
101
|
+
add(report.diagnostic);
|
|
102
|
+
outcomes.push(...report.outcomes);
|
|
103
|
+
return report;
|
|
104
|
+
};
|
|
105
|
+
const rootCheck = record(await checkRepositoryRoot(repoRoot, deps));
|
|
40
106
|
// Every remaining check is relative to the repository root. Without a valid
|
|
41
107
|
// one there is nothing coherent left to check, so report and stop here rather
|
|
42
108
|
// than emitting a cascade of derived failures the operator cannot act on.
|
|
43
|
-
if (rootCheck)
|
|
44
|
-
|
|
109
|
+
if (rootCheck.diagnostic) {
|
|
110
|
+
// The dependent prerequisites are recorded as explicitly SKIPPED rather
|
|
111
|
+
// than left absent (BAPI-1055). No probe is issued for any of them — no
|
|
112
|
+
// filesystem read, no process spawn, no credential resolution, no port
|
|
113
|
+
// connect — so the read-only, no-side-effect contract is unchanged; only
|
|
114
|
+
// the reporting is. Without this, a run that checked the least would look
|
|
115
|
+
// identical to a run in which everything passed.
|
|
116
|
+
for (const [check, id, label] of ROOT_DEPENDENT_CHECKS) {
|
|
117
|
+
outcomes.push({
|
|
118
|
+
check,
|
|
119
|
+
id,
|
|
120
|
+
label,
|
|
121
|
+
status: "skip",
|
|
122
|
+
detail: PLANE_ROOT_DEPENDENT_SKIP_DETAIL,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return { ok: false, diagnostics, outcomes };
|
|
126
|
+
}
|
|
45
127
|
// BAPI-1054: aggregated with every other independent check rather than
|
|
46
128
|
// short-circuiting. `checkRepositoryRoot` above proved the marker files are
|
|
47
129
|
// here; this proves the directory is a real work tree. They are independent
|
|
48
130
|
// facts and both can be true at once, which is exactly what the all-or-nothing
|
|
49
131
|
// contract exists to report together.
|
|
50
|
-
|
|
51
|
-
|
|
132
|
+
record(await checkRepositoryWorktree(repoRoot, deps));
|
|
133
|
+
record(await checkClaudeLogin(deps));
|
|
52
134
|
const credentials = await checkBridgeCredentials(repoRoot, deps);
|
|
135
|
+
outcomes.push(...credentials.outcomes);
|
|
53
136
|
// Advisories first, then the failure. An insecure credentials file and an
|
|
54
137
|
// unresolvable credential are independent facts and can both be true, so the
|
|
55
138
|
// advisory is appended unconditionally rather than only on the success path.
|
|
@@ -57,26 +140,33 @@ export async function runPlanePreflight(repoRoot, deps) {
|
|
|
57
140
|
add(advisory);
|
|
58
141
|
if (!credentials.ok)
|
|
59
142
|
add(credentials.diagnostic);
|
|
60
|
-
|
|
143
|
+
record(await checkPlaneBuildFreshness(repoRoot, { fs: deps.fs }));
|
|
61
144
|
// Read-only, and evaluated alongside the other independent checks so an
|
|
62
145
|
// unresolvable re-exec target aggregates with them instead of short-circuiting.
|
|
63
146
|
const runtimeEntrypoint = deps.resolveRuntimeEntrypoint();
|
|
64
|
-
|
|
147
|
+
record(checkPlaneRuntimeEntrypoint(runtimeEntrypoint));
|
|
65
148
|
// An unresolvable override is a configuration failure, not a port failure:
|
|
66
149
|
// there is no port to probe, so the probe is skipped entirely rather than
|
|
67
150
|
// falling back to 8000 and reporting on a port the operator did not ask for.
|
|
68
151
|
if (deps.endpoint.ok) {
|
|
69
|
-
|
|
152
|
+
record(await checkServerPort(deps, deps.endpoint.endpoint));
|
|
70
153
|
}
|
|
71
154
|
else {
|
|
72
|
-
|
|
155
|
+
// The endpoint resolver owns this diagnostic's prose; the structured fix is
|
|
156
|
+
// authored here beside it rather than derived downstream by an aggregator.
|
|
157
|
+
record(planeCheckFinding({
|
|
158
|
+
check: "server-port",
|
|
159
|
+
severity: "blocking",
|
|
160
|
+
message: deps.endpoint.message,
|
|
161
|
+
remediation: PLANE_SERVER_ENDPOINT_REMEDIATION,
|
|
162
|
+
}, "Local server port", "the configured local server endpoint could not be resolved"));
|
|
73
163
|
}
|
|
74
|
-
|
|
164
|
+
record(await checkAlembicHead(repoRoot, {
|
|
75
165
|
execFile: deps.execFile,
|
|
76
166
|
fileExists: (filePath) => fileExists(filePath, deps.fs),
|
|
77
167
|
platform: deps.platform,
|
|
78
168
|
}));
|
|
79
|
-
|
|
169
|
+
record(await checkExistingPlane(repoRoot, deps));
|
|
80
170
|
const blocking = diagnostics.filter((d) => d.severity === "blocking");
|
|
81
171
|
// `!runtimeEntrypoint.ok` is already covered by the blocking count; it is
|
|
82
172
|
// repeated here so the compiler narrows the union rather than requiring a
|
|
@@ -85,11 +175,12 @@ export async function runPlanePreflight(repoRoot, deps) {
|
|
|
85
175
|
// here, like `!runtimeEntrypoint.ok`, so the compiler narrows the union rather
|
|
86
176
|
// than requiring a non-null assertion on the context field below.
|
|
87
177
|
if (blocking.length > 0 || !credentials.ok || !runtimeEntrypoint.ok || !deps.endpoint.ok) {
|
|
88
|
-
return { ok: false, diagnostics };
|
|
178
|
+
return { ok: false, diagnostics, outcomes };
|
|
89
179
|
}
|
|
90
180
|
return {
|
|
91
181
|
ok: true,
|
|
92
182
|
diagnostics,
|
|
183
|
+
outcomes,
|
|
93
184
|
context: {
|
|
94
185
|
repoRoot,
|
|
95
186
|
repoName: credentials.repoName,
|
|
@@ -116,12 +207,14 @@ export async function runPlanePreflight(repoRoot, deps) {
|
|
|
116
207
|
* parent would otherwise spawn `uvicorn main:app` against nothing.
|
|
117
208
|
*/
|
|
118
209
|
async function checkRepositoryRoot(repoRoot, deps) {
|
|
210
|
+
const label = "Repository root";
|
|
119
211
|
if (!path.isAbsolute(repoRoot)) {
|
|
120
|
-
return {
|
|
212
|
+
return planeCheckFinding({
|
|
121
213
|
check: "repository-root",
|
|
122
214
|
severity: "blocking",
|
|
123
215
|
message: "the repository root could not be resolved to an absolute path",
|
|
124
|
-
|
|
216
|
+
remediation: PLANE_REPOSITORY_ROOT_REMEDIATION,
|
|
217
|
+
}, label, "the repository root could not be resolved to an absolute path");
|
|
125
218
|
}
|
|
126
219
|
const missing = [];
|
|
127
220
|
for (const file of REQUIRED_REPO_FILES) {
|
|
@@ -131,14 +224,18 @@ async function checkRepositoryRoot(repoRoot, deps) {
|
|
|
131
224
|
if (!(await fileExists(path.join(repoRoot, "mcp_server"), deps.fs))) {
|
|
132
225
|
missing.push("mcp_server/");
|
|
133
226
|
}
|
|
134
|
-
if (missing.length === 0)
|
|
135
|
-
return
|
|
136
|
-
|
|
227
|
+
if (missing.length === 0) {
|
|
228
|
+
return planeCheckPassed("repository-root", label, "all Bridge API root marker files are present");
|
|
229
|
+
}
|
|
230
|
+
return planeCheckFinding({
|
|
137
231
|
check: "repository-root",
|
|
138
232
|
severity: "blocking",
|
|
139
233
|
message: `this does not look like the Bridge API repository root — missing ${missing.join(", ")}. ` +
|
|
140
234
|
"Run `plane up` from the repository root.",
|
|
141
|
-
|
|
235
|
+
remediation: PLANE_REPOSITORY_ROOT_REMEDIATION,
|
|
236
|
+
}, label,
|
|
237
|
+
// Marker file NAMES only — never the resolved root, which is an absolute path.
|
|
238
|
+
`missing root marker(s): ${missing.join(", ")}`);
|
|
142
239
|
}
|
|
143
240
|
/** Remediation for a directory that is not a Git work tree. Fixed prose. */
|
|
144
241
|
export const PLANE_WORKTREE_REMEDIATION = "cd into the intended worktree (or create it with `git worktree add`) and rerun.";
|
|
@@ -160,6 +257,7 @@ export const PLANE_WORKTREE_REMEDIATION = "cd into the intended worktree (or cre
|
|
|
160
257
|
* operator performs it.
|
|
161
258
|
*/
|
|
162
259
|
export async function checkRepositoryWorktree(repoRoot, deps) {
|
|
260
|
+
const label = "Git work tree";
|
|
163
261
|
const result = await deps.execFile("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
164
262
|
cwd: repoRoot,
|
|
165
263
|
});
|
|
@@ -168,14 +266,19 @@ export async function checkRepositoryWorktree(repoRoot, deps) {
|
|
|
168
266
|
// stderr is deliberately NOT interpolated: it is unbounded text from a
|
|
169
267
|
// subprocess, and the remediation does not depend on which way it failed.
|
|
170
268
|
if (!result.ok || result.stdout.trim() !== "true") {
|
|
171
|
-
return {
|
|
269
|
+
return planeCheckFinding({
|
|
172
270
|
check: "worktree-presence",
|
|
173
271
|
severity: "blocking",
|
|
174
272
|
message: `${repoRoot} is not inside a Git work tree (git rev-parse --is-inside-work-tree ` +
|
|
175
273
|
`did not report 'true'). No worktree was created — ${PLANE_WORKTREE_REMEDIATION}`,
|
|
176
|
-
|
|
274
|
+
remediation: PLANE_WORKTREE_REMEDIATION,
|
|
275
|
+
}, label,
|
|
276
|
+
// The legacy `message` keeps naming `repoRoot` for the operator reading a
|
|
277
|
+
// terminal; the consolidated detail excludes it, because that root is an
|
|
278
|
+
// absolute path and the consolidated report is a wider surface.
|
|
279
|
+
"this directory is not inside a Git work tree");
|
|
177
280
|
}
|
|
178
|
-
return
|
|
281
|
+
return planeCheckPassed("worktree-presence", label, "inside a Git work tree");
|
|
179
282
|
}
|
|
180
283
|
/**
|
|
181
284
|
* Advisory-only Claude login marker (BAPI-791).
|
|
@@ -191,14 +294,41 @@ export async function checkRepositoryWorktree(repoRoot, deps) {
|
|
|
191
294
|
* credential check `plane up` performs.
|
|
192
295
|
*/
|
|
193
296
|
export async function checkClaudeLogin(deps) {
|
|
297
|
+
const label = "Claude login";
|
|
194
298
|
const result = await detectClaudeLogin({ homedir: deps.homedir, readFile: deps.fs.readFile });
|
|
195
|
-
if (result.detected)
|
|
196
|
-
return
|
|
197
|
-
|
|
299
|
+
if (result.detected) {
|
|
300
|
+
return planeCheckPassed("claude-login", label, "a local Claude login marker was detected");
|
|
301
|
+
}
|
|
302
|
+
// Still a WARNING, never blocking: the advisory gains a structured fix, it
|
|
303
|
+
// does not gain the power to refuse a bring-up.
|
|
304
|
+
return planeCheckFinding({
|
|
198
305
|
check: "claude-login",
|
|
199
306
|
severity: "warning",
|
|
200
307
|
message: formatClaudeLoginAdvisory(result),
|
|
201
|
-
|
|
308
|
+
remediation: PLANE_CLAUDE_LOGIN_REMEDIATION,
|
|
309
|
+
}, label, "no local Claude login marker was detected");
|
|
310
|
+
}
|
|
311
|
+
/** Build the credential check's permission-advisory outcome. */
|
|
312
|
+
function credentialPermissionsOutcome(advisories) {
|
|
313
|
+
// Counted, never quoted: the resolver's advisory text is buffered output this
|
|
314
|
+
// module does not author, so the consolidated outcome reports only that it
|
|
315
|
+
// fired. The advisory itself still reaches the operator as a diagnostic.
|
|
316
|
+
return advisories.length === 0
|
|
317
|
+
? {
|
|
318
|
+
check: "bridge-credentials",
|
|
319
|
+
id: PLANE_CREDENTIAL_OUTCOME_IDS.permissions,
|
|
320
|
+
label: "Bridge credential file permissions",
|
|
321
|
+
status: "pass",
|
|
322
|
+
detail: "the credential resolver raised no permission advisory",
|
|
323
|
+
}
|
|
324
|
+
: {
|
|
325
|
+
check: "bridge-credentials",
|
|
326
|
+
id: PLANE_CREDENTIAL_OUTCOME_IDS.permissions,
|
|
327
|
+
label: "Bridge credential file permissions",
|
|
328
|
+
status: "warn",
|
|
329
|
+
detail: `the credential resolver raised ${advisories.length} permission advisory line(s)`,
|
|
330
|
+
remediation: PLANE_CREDENTIAL_PERMISSIONS_REMEDIATION,
|
|
331
|
+
};
|
|
202
332
|
}
|
|
203
333
|
/**
|
|
204
334
|
* Resolve the repository identity, then the Bridge credential for it.
|
|
@@ -233,7 +363,8 @@ export async function checkBridgeCredentials(repoRoot, deps) {
|
|
|
233
363
|
});
|
|
234
364
|
if (!repo.ok) {
|
|
235
365
|
// Nothing has called the credential resolver yet, so there is no advisory
|
|
236
|
-
// source to have captured from
|
|
366
|
+
// source to have captured from — and the credential outcome is `skip`
|
|
367
|
+
// rather than `fail`, because the resolver was never asked.
|
|
237
368
|
return {
|
|
238
369
|
ok: false,
|
|
239
370
|
advisories: [],
|
|
@@ -242,7 +373,26 @@ export async function checkBridgeCredentials(repoRoot, deps) {
|
|
|
242
373
|
severity: "blocking",
|
|
243
374
|
message: "the Bridge repository identity could not be resolved — set BAPI_REPO_NAME or add a " +
|
|
244
375
|
"valid .bridge/config at the repository root.",
|
|
376
|
+
remediation: PLANE_REPO_IDENTITY_REMEDIATION,
|
|
245
377
|
},
|
|
378
|
+
outcomes: [
|
|
379
|
+
{
|
|
380
|
+
check: "bridge-credentials",
|
|
381
|
+
id: PLANE_CREDENTIAL_OUTCOME_IDS.identity,
|
|
382
|
+
label: "Bridge repository identity",
|
|
383
|
+
status: "fail",
|
|
384
|
+
detail: "the Bridge repository identity could not be resolved",
|
|
385
|
+
remediation: PLANE_REPO_IDENTITY_REMEDIATION,
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
check: "bridge-credentials",
|
|
389
|
+
id: PLANE_CREDENTIAL_OUTCOME_IDS.credential,
|
|
390
|
+
label: "Bridge API credential",
|
|
391
|
+
status: "skip",
|
|
392
|
+
detail: "not resolved — there is no repository identity to resolve a credential for",
|
|
393
|
+
},
|
|
394
|
+
credentialPermissionsOutcome([]),
|
|
395
|
+
],
|
|
246
396
|
};
|
|
247
397
|
}
|
|
248
398
|
const warnings = createResolverWarningBuffer();
|
|
@@ -267,36 +417,92 @@ export async function checkBridgeCredentials(repoRoot, deps) {
|
|
|
267
417
|
// Sanitized: a resolver throw becomes a category, never exception text
|
|
268
418
|
// (which could echo file contents). Advisories captured before the throw
|
|
269
419
|
// are still real observations and are kept.
|
|
420
|
+
const advisories = warnings.drain();
|
|
270
421
|
return {
|
|
271
422
|
ok: false,
|
|
272
|
-
advisories
|
|
423
|
+
advisories,
|
|
273
424
|
diagnostic: {
|
|
274
425
|
check: "bridge-credentials",
|
|
275
426
|
severity: "blocking",
|
|
276
427
|
message: `Bridge credentials for target bapi:${repo.repoName} could not be resolved ` +
|
|
277
428
|
"(resolver unavailable).",
|
|
429
|
+
remediation: PLANE_CREDENTIAL_REMEDIATION,
|
|
278
430
|
},
|
|
431
|
+
outcomes: [
|
|
432
|
+
identityResolvedOutcome(),
|
|
433
|
+
{
|
|
434
|
+
check: "bridge-credentials",
|
|
435
|
+
id: PLANE_CREDENTIAL_OUTCOME_IDS.credential,
|
|
436
|
+
label: "Bridge API credential",
|
|
437
|
+
status: "fail",
|
|
438
|
+
// Category, never exception text: a resolver throw can echo file content.
|
|
439
|
+
detail: "the credential resolver was unavailable",
|
|
440
|
+
remediation: PLANE_CREDENTIAL_REMEDIATION,
|
|
441
|
+
},
|
|
442
|
+
credentialPermissionsOutcome(advisories),
|
|
443
|
+
],
|
|
279
444
|
};
|
|
280
445
|
}
|
|
281
446
|
if (!result.ok) {
|
|
447
|
+
const advisories = warnings.drain();
|
|
282
448
|
return {
|
|
283
449
|
ok: false,
|
|
284
|
-
advisories
|
|
450
|
+
advisories,
|
|
285
451
|
diagnostic: {
|
|
286
452
|
check: "bridge-credentials",
|
|
287
453
|
severity: "blocking",
|
|
288
454
|
message: `Bridge credentials for target bapi:${repo.repoName} could not be resolved ` +
|
|
289
455
|
`(${result.kind}). Set BAPI_API_KEY in this shell, or store it with ` +
|
|
290
456
|
"`mcp-server credentials`. A spawned shell never sees .mcp.json env.",
|
|
457
|
+
remediation: PLANE_CREDENTIAL_REMEDIATION,
|
|
291
458
|
},
|
|
459
|
+
outcomes: [
|
|
460
|
+
identityResolvedOutcome(),
|
|
461
|
+
{
|
|
462
|
+
check: "bridge-credentials",
|
|
463
|
+
id: PLANE_CREDENTIAL_OUTCOME_IDS.credential,
|
|
464
|
+
label: "Bridge API credential",
|
|
465
|
+
status: "fail",
|
|
466
|
+
// `result.kind` is the resolver's own closed category vocabulary; the
|
|
467
|
+
// repository name is deliberately omitted from the consolidated detail.
|
|
468
|
+
detail: `no Bridge API credential resolved (${result.kind})`,
|
|
469
|
+
remediation: PLANE_CREDENTIAL_REMEDIATION,
|
|
470
|
+
},
|
|
471
|
+
credentialPermissionsOutcome(advisories),
|
|
472
|
+
],
|
|
292
473
|
};
|
|
293
474
|
}
|
|
475
|
+
const advisories = warnings.drain();
|
|
294
476
|
return {
|
|
295
477
|
ok: true,
|
|
296
478
|
repoName: repo.repoName,
|
|
297
479
|
apiKey: result.credentials.apiKey,
|
|
298
480
|
source: result.credentials.source,
|
|
299
|
-
advisories
|
|
481
|
+
advisories,
|
|
482
|
+
outcomes: [
|
|
483
|
+
identityResolvedOutcome(),
|
|
484
|
+
{
|
|
485
|
+
check: "bridge-credentials",
|
|
486
|
+
id: PLANE_CREDENTIAL_OUTCOME_IDS.credential,
|
|
487
|
+
label: "Bridge API credential",
|
|
488
|
+
status: "pass",
|
|
489
|
+
// The SOURCE, never the value: `source` is `"env"` or `"file"`.
|
|
490
|
+
detail: `resolved from ${result.credentials.source} (the value is never read into a report)`,
|
|
491
|
+
},
|
|
492
|
+
credentialPermissionsOutcome(advisories),
|
|
493
|
+
],
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
/** The identity outcome for every branch reached after identity resolved. */
|
|
497
|
+
function identityResolvedOutcome() {
|
|
498
|
+
return {
|
|
499
|
+
check: "bridge-credentials",
|
|
500
|
+
id: PLANE_CREDENTIAL_OUTCOME_IDS.identity,
|
|
501
|
+
label: "Bridge repository identity",
|
|
502
|
+
status: "pass",
|
|
503
|
+
// The resolved repository NAME is omitted deliberately: it is not needed to
|
|
504
|
+
// act on a passing check, and this outcome flows into a wider report.
|
|
505
|
+
detail: "the Bridge repository identity resolved",
|
|
300
506
|
};
|
|
301
507
|
}
|
|
302
508
|
/**
|
|
@@ -353,26 +559,35 @@ function createResolverWarningBuffer() {
|
|
|
353
559
|
* refuse a perfectly good bring-up.
|
|
354
560
|
*/
|
|
355
561
|
export async function checkServerPort(deps, endpoint) {
|
|
562
|
+
const label = "Local server port";
|
|
356
563
|
const target = `${endpoint.host}:${endpoint.port}`;
|
|
357
564
|
const result = await deps.probePort(endpoint.host, endpoint.port, PLANE_PORT_PROBE_TIMEOUT_MS);
|
|
358
|
-
if (result.kind === "refused")
|
|
359
|
-
return
|
|
565
|
+
if (result.kind === "refused") {
|
|
566
|
+
return planeCheckPassed("server-port", label, `${target} is free`);
|
|
567
|
+
}
|
|
568
|
+
// Severity is unchanged in both branches: `connected` still blocks, an
|
|
569
|
+
// indeterminate probe still only warns.
|
|
360
570
|
if (result.kind === "connected") {
|
|
361
|
-
return {
|
|
571
|
+
return planeCheckFinding({
|
|
362
572
|
check: "server-port",
|
|
363
573
|
severity: "blocking",
|
|
364
574
|
message: `${target} is already accepting connections. ` +
|
|
365
575
|
"That port may belong to a SIBLING WORKTREE's server — check before you kill it. " +
|
|
366
576
|
`Stop the existing server (or wind down its plane) and retry, or set ${PLANE_SERVER_PORT_ENV_VAR} ` +
|
|
367
577
|
"to a free port.",
|
|
368
|
-
|
|
578
|
+
remediation: PLANE_SERVER_PORT_OCCUPIED_REMEDIATION,
|
|
579
|
+
}, label, `${target} is already accepting connections`);
|
|
369
580
|
}
|
|
370
|
-
return {
|
|
581
|
+
return planeCheckFinding({
|
|
371
582
|
check: "server-port",
|
|
372
583
|
severity: "warning",
|
|
373
584
|
message: `could not determine whether ${target} is free ` +
|
|
374
585
|
`(${result.error}); startup continues and uvicorn will fail loudly if the port is taken.`,
|
|
375
|
-
|
|
586
|
+
remediation: PLANE_SERVER_PORT_UNKNOWN_REMEDIATION,
|
|
587
|
+
}, label,
|
|
588
|
+
// The probe's own `error` text stays in the legacy message for the operator
|
|
589
|
+
// at the terminal and is not copied into the consolidated detail.
|
|
590
|
+
`could not determine whether ${target} is free`);
|
|
376
591
|
}
|
|
377
592
|
/**
|
|
378
593
|
* Refuse to start over a plane that is still alive.
|
|
@@ -382,17 +597,23 @@ export async function checkServerPort(deps, endpoint) {
|
|
|
382
597
|
* where the classification can be re-checked. Preflight never mutates disk.
|
|
383
598
|
*/
|
|
384
599
|
export async function checkExistingPlane(repoRoot, deps) {
|
|
600
|
+
const label = "Existing plane";
|
|
385
601
|
const read = await readPlaneManifest(repoRoot, deps.fs);
|
|
386
|
-
|
|
387
|
-
|
|
602
|
+
// Three distinct states, three distinct fixes (BAPI-1055): unreadable/malformed,
|
|
603
|
+
// stale, and live. Manifest validation, the liveness probe, and the
|
|
604
|
+
// never-touch-disk rule are all unchanged.
|
|
605
|
+
if (read.kind === "missing") {
|
|
606
|
+
return planeCheckPassed("existing-plane", label, "no plane manifest is present");
|
|
607
|
+
}
|
|
388
608
|
if (read.kind !== "valid") {
|
|
389
|
-
return {
|
|
609
|
+
return planeCheckFinding({
|
|
390
610
|
check: "existing-plane",
|
|
391
611
|
severity: "blocking",
|
|
392
612
|
message: `an existing .bridge/plane/plane.json could not be validated (${read.error}). ` +
|
|
393
613
|
"No process was signalled and the file was left untouched — inspect it, then remove " +
|
|
394
614
|
"it by hand if no plane is running.",
|
|
395
|
-
|
|
615
|
+
remediation: PLANE_MANIFEST_UNREADABLE_REMEDIATION,
|
|
616
|
+
}, label, "an existing plane manifest could not be validated");
|
|
396
617
|
}
|
|
397
618
|
// BAPI-882: a manifest carrying THIS process's own plane id is its own claim,
|
|
398
619
|
// not a competitor. Checked after validation and BEFORE the liveness probe,
|
|
@@ -411,24 +632,40 @@ export async function checkExistingPlane(repoRoot, deps) {
|
|
|
411
632
|
// A live manifest with a DIFFERENT plane id still blocks, unchanged — that is
|
|
412
633
|
// a genuine second plane and the whole point of the check.
|
|
413
634
|
if (deps.ownPlaneId !== undefined && read.manifest.planeId === deps.ownPlaneId) {
|
|
414
|
-
return
|
|
635
|
+
return planeCheckPassed("existing-plane", label, "the existing manifest carries this launch's own plane id");
|
|
415
636
|
}
|
|
416
637
|
if (!manifestHasLiveProcess(read.manifest, deps.proc)) {
|
|
417
638
|
// Stale. Reported as information so the operator understands why an old
|
|
418
639
|
// manifest is about to be replaced; the claim revalidates before it does.
|
|
640
|
+
// No remediation: the operator has nothing to do — the claim replaces it.
|
|
419
641
|
return {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
"
|
|
642
|
+
diagnostic: {
|
|
643
|
+
check: "existing-plane",
|
|
644
|
+
severity: "warning",
|
|
645
|
+
message: "a previous plane manifest is present but every recorded process is gone; it will be " +
|
|
646
|
+
"replaced after a final liveness re-check.",
|
|
647
|
+
},
|
|
648
|
+
outcomes: [
|
|
649
|
+
{
|
|
650
|
+
check: "existing-plane",
|
|
651
|
+
id: "existing-plane",
|
|
652
|
+
label,
|
|
653
|
+
status: "warn",
|
|
654
|
+
detail: "a stale plane manifest is present and will be replaced after a liveness re-check",
|
|
655
|
+
},
|
|
656
|
+
],
|
|
424
657
|
};
|
|
425
658
|
}
|
|
426
|
-
return {
|
|
659
|
+
return planeCheckFinding({
|
|
427
660
|
check: "existing-plane",
|
|
428
661
|
severity: "blocking",
|
|
429
662
|
message: `a plane is already running (supervisor pid ${read.manifest.supervisorPid}). ` +
|
|
430
663
|
"Run `plane status` to inspect it, or `plane down` to wind it down first.",
|
|
431
|
-
|
|
664
|
+
remediation: PLANE_ALREADY_RUNNING_REMEDIATION,
|
|
665
|
+
}, label,
|
|
666
|
+
// The supervisor pid stays in the legacy message; the consolidated detail
|
|
667
|
+
// reports the fact, not the process identifier.
|
|
668
|
+
"a plane is already running for this repository");
|
|
432
669
|
}
|
|
433
670
|
async function fileExists(filePath, fs) {
|
|
434
671
|
try {
|
|
@@ -439,3 +676,81 @@ async function fileExists(filePath, fs) {
|
|
|
439
676
|
return false;
|
|
440
677
|
}
|
|
441
678
|
}
|
|
679
|
+
// ---------------------------------------------------------------------------
|
|
680
|
+
// Canonical readiness projection (BAPI-1055)
|
|
681
|
+
// ---------------------------------------------------------------------------
|
|
682
|
+
/**
|
|
683
|
+
* The complete plane prerequisite set, keyed by outcome id, in render order.
|
|
684
|
+
*
|
|
685
|
+
* Stable like the install- and conductor-side descriptor sets, and for the same
|
|
686
|
+
* reason: a preflight that returned early, threw, or was never run must still
|
|
687
|
+
* yield every prerequisite as an explicit unknown.
|
|
688
|
+
*/
|
|
689
|
+
export const PLANE_READINESS_DESCRIPTORS = [
|
|
690
|
+
{ id: "repository-root", label: "Repository root" },
|
|
691
|
+
{ id: "worktree-presence", label: "Git work tree" },
|
|
692
|
+
{ id: "claude-login", label: "Claude login" },
|
|
693
|
+
{ id: "repo-identity", label: "Bridge repository identity" },
|
|
694
|
+
{ id: "bridge-credentials", label: "Bridge API credential" },
|
|
695
|
+
{ id: "credential-permissions", label: "Bridge credential file permissions" },
|
|
696
|
+
{ id: "executor-build", label: "Executor build freshness" },
|
|
697
|
+
{ id: "runtime-entrypoint", label: "Runtime re-exec entrypoint" },
|
|
698
|
+
{ id: "server-port", label: "Local server port" },
|
|
699
|
+
{ id: "alembic-head", label: "Database migration head" },
|
|
700
|
+
{ id: "existing-plane", label: "Existing plane" },
|
|
701
|
+
];
|
|
702
|
+
/** Fixed detail for a plane prerequisite the preflight never reported on. */
|
|
703
|
+
const PLANE_UNREPORTED_DETAIL = "not reported — the plane preflight produced no outcome for this prerequisite";
|
|
704
|
+
/** Fixed remediation for a plane prerequisite the preflight never reported on. */
|
|
705
|
+
const PLANE_UNREPORTED_REMEDIATION = "run `plane up` (or `conductor readiness` again) to re-collect the plane preflight; this " +
|
|
706
|
+
"prerequisite's state is unknown, not healthy.";
|
|
707
|
+
/**
|
|
708
|
+
* Project a plane preflight result into canonical readiness checks.
|
|
709
|
+
*
|
|
710
|
+
* PURE, and it NEVER runs a preflight of its own: it reads the outcomes an
|
|
711
|
+
* already-executed `runPlanePreflight` emitted. Two rules matter here and both
|
|
712
|
+
* exist because of how this could quietly go wrong:
|
|
713
|
+
*
|
|
714
|
+
* - A pass is only ever read from an explicit `status: "pass"` outcome. The
|
|
715
|
+
* absence of a `PlaneDiagnostic` proves nothing — `add()` keeps only non-null
|
|
716
|
+
* diagnostics, and a failed repository-root check returns before most checks
|
|
717
|
+
* run — so a report built on silence would look healthiest on the runs where
|
|
718
|
+
* the least was actually checked.
|
|
719
|
+
* - A launch-permitting `warning` maps to `warn`, never `fail` and never `skip`,
|
|
720
|
+
* and nothing here feeds back into the preflight's own `ok` calculation.
|
|
721
|
+
*/
|
|
722
|
+
export function mapPlanePreflightToReadinessChecks(result) {
|
|
723
|
+
const byId = new Map();
|
|
724
|
+
for (const outcome of result?.outcomes ?? []) {
|
|
725
|
+
if (!byId.has(outcome.id))
|
|
726
|
+
byId.set(outcome.id, outcome);
|
|
727
|
+
}
|
|
728
|
+
return PLANE_READINESS_DESCRIPTORS.map(({ id, label }) => {
|
|
729
|
+
const outcome = byId.get(id);
|
|
730
|
+
if (!outcome) {
|
|
731
|
+
return createReadinessCheck({
|
|
732
|
+
id: `plane.${id}`,
|
|
733
|
+
source: "plane",
|
|
734
|
+
label,
|
|
735
|
+
status: "fail",
|
|
736
|
+
detail: PLANE_UNREPORTED_DETAIL,
|
|
737
|
+
remediation: PLANE_UNREPORTED_REMEDIATION,
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
return createReadinessCheckSafely({
|
|
741
|
+
id: `plane.${id}`,
|
|
742
|
+
source: "plane",
|
|
743
|
+
label: outcome.label || label,
|
|
744
|
+
status: outcome.status,
|
|
745
|
+
...(outcome.detail ? { detail: outcome.detail } : {}),
|
|
746
|
+
// A failure without a fix would violate the contract and be replaced by a
|
|
747
|
+
// generic "unreadable" check, losing the real finding; the fallback keeps
|
|
748
|
+
// the finding and names the recollection step instead.
|
|
749
|
+
...(outcome.status === "fail"
|
|
750
|
+
? { remediation: outcome.remediation ?? PLANE_UNREPORTED_REMEDIATION }
|
|
751
|
+
: outcome.status !== "pass" && outcome.remediation
|
|
752
|
+
? { remediation: outcome.remediation }
|
|
753
|
+
: {}),
|
|
754
|
+
});
|
|
755
|
+
});
|
|
756
|
+
}
|