@bridge_gpt/mcp-server 0.2.51 → 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 +59 -13
- package/build/agent-capabilities/probe-context.js +15 -7
- package/build/agent-capabilities/probes.js +42 -6
- package/build/agent-launchers/claude-executor-adapter.js +98 -14
- package/build/commands.generated.js +7 -5
- package/build/conduct-epic/cut-protocol.js +17 -3
- package/build/conductor/bridge-api-client.js +232 -5
- package/build/conductor/cli.js +23 -0
- package/build/conductor/deny-enforcement-preflight.js +107 -10
- package/build/conductor/doctor.js +428 -5
- package/build/conductor/install-doctor.js +65 -656
- package/build/conductor/local-merge.js +170 -11
- 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/connect-bitbucket-api.js +370 -0
- package/build/connect-bitbucket.js +437 -0
- package/build/docs.generated.js +1 -1
- package/build/doctor.js +40 -1
- package/build/drive-epic.js +423 -11
- package/build/env-file-link.js +164 -0
- package/build/epic-integration-pr.js +10 -0
- package/build/executor/cli.js +41 -6
- package/build/executor/deps.js +5 -1
- package/build/executor/env-file-guard.js +113 -0
- package/build/executor/env.js +78 -1
- package/build/executor/heartbeat.js +9 -0
- package/build/executor/http-client.js +90 -22
- package/build/executor/job-errors.js +43 -2
- package/build/executor/job-runner.js +130 -28
- package/build/executor/merge-job.js +67 -16
- package/build/executor/permissions.js +106 -0
- package/build/executor/preflight.js +38 -13
- package/build/executor/resume-pre-spawn.js +2 -1
- package/build/executor/runner.js +175 -4
- package/build/executor/service-unit.js +15 -0
- package/build/executor/terminal-mutation.js +22 -1
- package/build/executor/types.js +86 -0
- package/build/executor/worker-command.js +21 -5
- package/build/executor/worker-guard-hook.js +939 -0
- package/build/executor/worker-log.js +56 -0
- package/build/executor/worktree.js +11 -0
- package/build/git-reachability.js +147 -0
- package/build/index.js +4734 -4270
- package/build/install-bridge.js +95 -0
- package/build/install-doctor.js +154 -2
- package/build/pipelines.generated.js +6 -4
- package/build/plan-epic-conductor-eligibility.js +37 -7
- package/build/plane/alembic-head.js +40 -11
- package/build/plane/build-freshness.js +22 -11
- package/build/plane/cli.js +78 -15
- package/build/plane/defaults.js +165 -0
- package/build/plane/manifest.js +63 -8
- package/build/plane/member-logs.js +6 -0
- package/build/plane/member-roster.js +195 -11
- package/build/plane/preflight.js +402 -44
- package/build/plane/shutdown.js +25 -3
- package/build/plane/status.js +11 -0
- package/build/plane/supervisor.js +343 -14
- package/build/plane/test-fakes.js +43 -0
- package/build/plane/types.js +118 -11
- package/build/pr-base-contract.js +20 -0
- package/build/readiness-check.js +412 -0
- package/build/readme.generated.js +1 -1
- package/build/review-synthesis-config.js +60 -0
- package/build/scripts/executor-protocol-contract-driver.js +311 -0
- package/build/setup-epic.js +560 -139
- package/build/sfcc/log-query.js +2 -1
- package/build/start-tickets-conductor.js +11 -2
- package/build/start-tickets.js +69 -2
- package/build/version.generated.js +3 -3
- package/build/worker-containment-diagnostic.js +97 -0
- package/build/worker-guard-hook-bin.js +6 -0
- package/docs/CONDUCTOR.md +27 -0
- package/docs/install/mcp-tool-integrations.md +3 -2
- package/package.json +4 -3
- 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,19 +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
|
-
|
|
45
|
-
|
|
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
|
+
}
|
|
127
|
+
// BAPI-1054: aggregated with every other independent check rather than
|
|
128
|
+
// short-circuiting. `checkRepositoryRoot` above proved the marker files are
|
|
129
|
+
// here; this proves the directory is a real work tree. They are independent
|
|
130
|
+
// facts and both can be true at once, which is exactly what the all-or-nothing
|
|
131
|
+
// contract exists to report together.
|
|
132
|
+
record(await checkRepositoryWorktree(repoRoot, deps));
|
|
133
|
+
record(await checkClaudeLogin(deps));
|
|
46
134
|
const credentials = await checkBridgeCredentials(repoRoot, deps);
|
|
135
|
+
outcomes.push(...credentials.outcomes);
|
|
47
136
|
// Advisories first, then the failure. An insecure credentials file and an
|
|
48
137
|
// unresolvable credential are independent facts and can both be true, so the
|
|
49
138
|
// advisory is appended unconditionally rather than only on the success path.
|
|
@@ -51,26 +140,33 @@ export async function runPlanePreflight(repoRoot, deps) {
|
|
|
51
140
|
add(advisory);
|
|
52
141
|
if (!credentials.ok)
|
|
53
142
|
add(credentials.diagnostic);
|
|
54
|
-
|
|
143
|
+
record(await checkPlaneBuildFreshness(repoRoot, { fs: deps.fs }));
|
|
55
144
|
// Read-only, and evaluated alongside the other independent checks so an
|
|
56
145
|
// unresolvable re-exec target aggregates with them instead of short-circuiting.
|
|
57
146
|
const runtimeEntrypoint = deps.resolveRuntimeEntrypoint();
|
|
58
|
-
|
|
147
|
+
record(checkPlaneRuntimeEntrypoint(runtimeEntrypoint));
|
|
59
148
|
// An unresolvable override is a configuration failure, not a port failure:
|
|
60
149
|
// there is no port to probe, so the probe is skipped entirely rather than
|
|
61
150
|
// falling back to 8000 and reporting on a port the operator did not ask for.
|
|
62
151
|
if (deps.endpoint.ok) {
|
|
63
|
-
|
|
152
|
+
record(await checkServerPort(deps, deps.endpoint.endpoint));
|
|
64
153
|
}
|
|
65
154
|
else {
|
|
66
|
-
|
|
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"));
|
|
67
163
|
}
|
|
68
|
-
|
|
164
|
+
record(await checkAlembicHead(repoRoot, {
|
|
69
165
|
execFile: deps.execFile,
|
|
70
166
|
fileExists: (filePath) => fileExists(filePath, deps.fs),
|
|
71
167
|
platform: deps.platform,
|
|
72
168
|
}));
|
|
73
|
-
|
|
169
|
+
record(await checkExistingPlane(repoRoot, deps));
|
|
74
170
|
const blocking = diagnostics.filter((d) => d.severity === "blocking");
|
|
75
171
|
// `!runtimeEntrypoint.ok` is already covered by the blocking count; it is
|
|
76
172
|
// repeated here so the compiler narrows the union rather than requiring a
|
|
@@ -79,11 +175,12 @@ export async function runPlanePreflight(repoRoot, deps) {
|
|
|
79
175
|
// here, like `!runtimeEntrypoint.ok`, so the compiler narrows the union rather
|
|
80
176
|
// than requiring a non-null assertion on the context field below.
|
|
81
177
|
if (blocking.length > 0 || !credentials.ok || !runtimeEntrypoint.ok || !deps.endpoint.ok) {
|
|
82
|
-
return { ok: false, diagnostics };
|
|
178
|
+
return { ok: false, diagnostics, outcomes };
|
|
83
179
|
}
|
|
84
180
|
return {
|
|
85
181
|
ok: true,
|
|
86
182
|
diagnostics,
|
|
183
|
+
outcomes,
|
|
87
184
|
context: {
|
|
88
185
|
repoRoot,
|
|
89
186
|
repoName: credentials.repoName,
|
|
@@ -110,12 +207,14 @@ export async function runPlanePreflight(repoRoot, deps) {
|
|
|
110
207
|
* parent would otherwise spawn `uvicorn main:app` against nothing.
|
|
111
208
|
*/
|
|
112
209
|
async function checkRepositoryRoot(repoRoot, deps) {
|
|
210
|
+
const label = "Repository root";
|
|
113
211
|
if (!path.isAbsolute(repoRoot)) {
|
|
114
|
-
return {
|
|
212
|
+
return planeCheckFinding({
|
|
115
213
|
check: "repository-root",
|
|
116
214
|
severity: "blocking",
|
|
117
215
|
message: "the repository root could not be resolved to an absolute path",
|
|
118
|
-
|
|
216
|
+
remediation: PLANE_REPOSITORY_ROOT_REMEDIATION,
|
|
217
|
+
}, label, "the repository root could not be resolved to an absolute path");
|
|
119
218
|
}
|
|
120
219
|
const missing = [];
|
|
121
220
|
for (const file of REQUIRED_REPO_FILES) {
|
|
@@ -125,14 +224,61 @@ async function checkRepositoryRoot(repoRoot, deps) {
|
|
|
125
224
|
if (!(await fileExists(path.join(repoRoot, "mcp_server"), deps.fs))) {
|
|
126
225
|
missing.push("mcp_server/");
|
|
127
226
|
}
|
|
128
|
-
if (missing.length === 0)
|
|
129
|
-
return
|
|
130
|
-
|
|
227
|
+
if (missing.length === 0) {
|
|
228
|
+
return planeCheckPassed("repository-root", label, "all Bridge API root marker files are present");
|
|
229
|
+
}
|
|
230
|
+
return planeCheckFinding({
|
|
131
231
|
check: "repository-root",
|
|
132
232
|
severity: "blocking",
|
|
133
233
|
message: `this does not look like the Bridge API repository root — missing ${missing.join(", ")}. ` +
|
|
134
234
|
"Run `plane up` from the repository root.",
|
|
135
|
-
|
|
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(", ")}`);
|
|
239
|
+
}
|
|
240
|
+
/** Remediation for a directory that is not a Git work tree. Fixed prose. */
|
|
241
|
+
export const PLANE_WORKTREE_REMEDIATION = "cd into the intended worktree (or create it with `git worktree add`) and rerun.";
|
|
242
|
+
/**
|
|
243
|
+
* Confirm the repository root is inside a real Git work tree (BAPI-1054).
|
|
244
|
+
*
|
|
245
|
+
* `checkRepositoryRoot` cannot answer this. Its four marker paths — `main.py`,
|
|
246
|
+
* `worker.py`, `alembic.ini`, `mcp_server/` — are present in every LINKED
|
|
247
|
+
* worktree and in any plain copy of the tree, so it proves the files are here,
|
|
248
|
+
* never that Git owns them. A plane brought up over an unpacked archive would
|
|
249
|
+
* pass that check and then fail at the first `git` operation a member performs.
|
|
250
|
+
*
|
|
251
|
+
* Linked worktrees are valid and must stay valid: `--is-inside-work-tree` is
|
|
252
|
+
* true in a linked worktree (where `.git` is a FILE, not a directory), which is
|
|
253
|
+
* why the predicate is this command and not a `.git` directory probe.
|
|
254
|
+
*
|
|
255
|
+
* READ-ONLY, like every other check here: it creates no worktree and repairs
|
|
256
|
+
* nothing. A directory that is not a work tree is named, with the fix, and the
|
|
257
|
+
* operator performs it.
|
|
258
|
+
*/
|
|
259
|
+
export async function checkRepositoryWorktree(repoRoot, deps) {
|
|
260
|
+
const label = "Git work tree";
|
|
261
|
+
const result = await deps.execFile("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
262
|
+
cwd: repoRoot,
|
|
263
|
+
});
|
|
264
|
+
// A failed command and a `false` answer are the same operator-facing fact —
|
|
265
|
+
// this is not a work tree — and are reported identically. The command's own
|
|
266
|
+
// stderr is deliberately NOT interpolated: it is unbounded text from a
|
|
267
|
+
// subprocess, and the remediation does not depend on which way it failed.
|
|
268
|
+
if (!result.ok || result.stdout.trim() !== "true") {
|
|
269
|
+
return planeCheckFinding({
|
|
270
|
+
check: "worktree-presence",
|
|
271
|
+
severity: "blocking",
|
|
272
|
+
message: `${repoRoot} is not inside a Git work tree (git rev-parse --is-inside-work-tree ` +
|
|
273
|
+
`did not report 'true'). No worktree was created — ${PLANE_WORKTREE_REMEDIATION}`,
|
|
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");
|
|
280
|
+
}
|
|
281
|
+
return planeCheckPassed("worktree-presence", label, "inside a Git work tree");
|
|
136
282
|
}
|
|
137
283
|
/**
|
|
138
284
|
* Advisory-only Claude login marker (BAPI-791).
|
|
@@ -148,14 +294,41 @@ async function checkRepositoryRoot(repoRoot, deps) {
|
|
|
148
294
|
* credential check `plane up` performs.
|
|
149
295
|
*/
|
|
150
296
|
export async function checkClaudeLogin(deps) {
|
|
297
|
+
const label = "Claude login";
|
|
151
298
|
const result = await detectClaudeLogin({ homedir: deps.homedir, readFile: deps.fs.readFile });
|
|
152
|
-
if (result.detected)
|
|
153
|
-
return
|
|
154
|
-
|
|
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({
|
|
155
305
|
check: "claude-login",
|
|
156
306
|
severity: "warning",
|
|
157
307
|
message: formatClaudeLoginAdvisory(result),
|
|
158
|
-
|
|
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
|
+
};
|
|
159
332
|
}
|
|
160
333
|
/**
|
|
161
334
|
* Resolve the repository identity, then the Bridge credential for it.
|
|
@@ -190,7 +363,8 @@ export async function checkBridgeCredentials(repoRoot, deps) {
|
|
|
190
363
|
});
|
|
191
364
|
if (!repo.ok) {
|
|
192
365
|
// Nothing has called the credential resolver yet, so there is no advisory
|
|
193
|
-
// 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.
|
|
194
368
|
return {
|
|
195
369
|
ok: false,
|
|
196
370
|
advisories: [],
|
|
@@ -199,7 +373,26 @@ export async function checkBridgeCredentials(repoRoot, deps) {
|
|
|
199
373
|
severity: "blocking",
|
|
200
374
|
message: "the Bridge repository identity could not be resolved — set BAPI_REPO_NAME or add a " +
|
|
201
375
|
"valid .bridge/config at the repository root.",
|
|
376
|
+
remediation: PLANE_REPO_IDENTITY_REMEDIATION,
|
|
202
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
|
+
],
|
|
203
396
|
};
|
|
204
397
|
}
|
|
205
398
|
const warnings = createResolverWarningBuffer();
|
|
@@ -224,36 +417,92 @@ export async function checkBridgeCredentials(repoRoot, deps) {
|
|
|
224
417
|
// Sanitized: a resolver throw becomes a category, never exception text
|
|
225
418
|
// (which could echo file contents). Advisories captured before the throw
|
|
226
419
|
// are still real observations and are kept.
|
|
420
|
+
const advisories = warnings.drain();
|
|
227
421
|
return {
|
|
228
422
|
ok: false,
|
|
229
|
-
advisories
|
|
423
|
+
advisories,
|
|
230
424
|
diagnostic: {
|
|
231
425
|
check: "bridge-credentials",
|
|
232
426
|
severity: "blocking",
|
|
233
427
|
message: `Bridge credentials for target bapi:${repo.repoName} could not be resolved ` +
|
|
234
428
|
"(resolver unavailable).",
|
|
429
|
+
remediation: PLANE_CREDENTIAL_REMEDIATION,
|
|
235
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
|
+
],
|
|
236
444
|
};
|
|
237
445
|
}
|
|
238
446
|
if (!result.ok) {
|
|
447
|
+
const advisories = warnings.drain();
|
|
239
448
|
return {
|
|
240
449
|
ok: false,
|
|
241
|
-
advisories
|
|
450
|
+
advisories,
|
|
242
451
|
diagnostic: {
|
|
243
452
|
check: "bridge-credentials",
|
|
244
453
|
severity: "blocking",
|
|
245
454
|
message: `Bridge credentials for target bapi:${repo.repoName} could not be resolved ` +
|
|
246
455
|
`(${result.kind}). Set BAPI_API_KEY in this shell, or store it with ` +
|
|
247
456
|
"`mcp-server credentials`. A spawned shell never sees .mcp.json env.",
|
|
457
|
+
remediation: PLANE_CREDENTIAL_REMEDIATION,
|
|
248
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
|
+
],
|
|
249
473
|
};
|
|
250
474
|
}
|
|
475
|
+
const advisories = warnings.drain();
|
|
251
476
|
return {
|
|
252
477
|
ok: true,
|
|
253
478
|
repoName: repo.repoName,
|
|
254
479
|
apiKey: result.credentials.apiKey,
|
|
255
480
|
source: result.credentials.source,
|
|
256
|
-
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",
|
|
257
506
|
};
|
|
258
507
|
}
|
|
259
508
|
/**
|
|
@@ -310,26 +559,35 @@ function createResolverWarningBuffer() {
|
|
|
310
559
|
* refuse a perfectly good bring-up.
|
|
311
560
|
*/
|
|
312
561
|
export async function checkServerPort(deps, endpoint) {
|
|
562
|
+
const label = "Local server port";
|
|
313
563
|
const target = `${endpoint.host}:${endpoint.port}`;
|
|
314
564
|
const result = await deps.probePort(endpoint.host, endpoint.port, PLANE_PORT_PROBE_TIMEOUT_MS);
|
|
315
|
-
if (result.kind === "refused")
|
|
316
|
-
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.
|
|
317
570
|
if (result.kind === "connected") {
|
|
318
|
-
return {
|
|
571
|
+
return planeCheckFinding({
|
|
319
572
|
check: "server-port",
|
|
320
573
|
severity: "blocking",
|
|
321
574
|
message: `${target} is already accepting connections. ` +
|
|
322
575
|
"That port may belong to a SIBLING WORKTREE's server — check before you kill it. " +
|
|
323
576
|
`Stop the existing server (or wind down its plane) and retry, or set ${PLANE_SERVER_PORT_ENV_VAR} ` +
|
|
324
577
|
"to a free port.",
|
|
325
|
-
|
|
578
|
+
remediation: PLANE_SERVER_PORT_OCCUPIED_REMEDIATION,
|
|
579
|
+
}, label, `${target} is already accepting connections`);
|
|
326
580
|
}
|
|
327
|
-
return {
|
|
581
|
+
return planeCheckFinding({
|
|
328
582
|
check: "server-port",
|
|
329
583
|
severity: "warning",
|
|
330
584
|
message: `could not determine whether ${target} is free ` +
|
|
331
585
|
`(${result.error}); startup continues and uvicorn will fail loudly if the port is taken.`,
|
|
332
|
-
|
|
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`);
|
|
333
591
|
}
|
|
334
592
|
/**
|
|
335
593
|
* Refuse to start over a plane that is still alive.
|
|
@@ -339,17 +597,23 @@ export async function checkServerPort(deps, endpoint) {
|
|
|
339
597
|
* where the classification can be re-checked. Preflight never mutates disk.
|
|
340
598
|
*/
|
|
341
599
|
export async function checkExistingPlane(repoRoot, deps) {
|
|
600
|
+
const label = "Existing plane";
|
|
342
601
|
const read = await readPlaneManifest(repoRoot, deps.fs);
|
|
343
|
-
|
|
344
|
-
|
|
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
|
+
}
|
|
345
608
|
if (read.kind !== "valid") {
|
|
346
|
-
return {
|
|
609
|
+
return planeCheckFinding({
|
|
347
610
|
check: "existing-plane",
|
|
348
611
|
severity: "blocking",
|
|
349
612
|
message: `an existing .bridge/plane/plane.json could not be validated (${read.error}). ` +
|
|
350
613
|
"No process was signalled and the file was left untouched — inspect it, then remove " +
|
|
351
614
|
"it by hand if no plane is running.",
|
|
352
|
-
|
|
615
|
+
remediation: PLANE_MANIFEST_UNREADABLE_REMEDIATION,
|
|
616
|
+
}, label, "an existing plane manifest could not be validated");
|
|
353
617
|
}
|
|
354
618
|
// BAPI-882: a manifest carrying THIS process's own plane id is its own claim,
|
|
355
619
|
// not a competitor. Checked after validation and BEFORE the liveness probe,
|
|
@@ -368,24 +632,40 @@ export async function checkExistingPlane(repoRoot, deps) {
|
|
|
368
632
|
// A live manifest with a DIFFERENT plane id still blocks, unchanged — that is
|
|
369
633
|
// a genuine second plane and the whole point of the check.
|
|
370
634
|
if (deps.ownPlaneId !== undefined && read.manifest.planeId === deps.ownPlaneId) {
|
|
371
|
-
return
|
|
635
|
+
return planeCheckPassed("existing-plane", label, "the existing manifest carries this launch's own plane id");
|
|
372
636
|
}
|
|
373
637
|
if (!manifestHasLiveProcess(read.manifest, deps.proc)) {
|
|
374
638
|
// Stale. Reported as information so the operator understands why an old
|
|
375
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.
|
|
376
641
|
return {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
"
|
|
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
|
+
],
|
|
381
657
|
};
|
|
382
658
|
}
|
|
383
|
-
return {
|
|
659
|
+
return planeCheckFinding({
|
|
384
660
|
check: "existing-plane",
|
|
385
661
|
severity: "blocking",
|
|
386
662
|
message: `a plane is already running (supervisor pid ${read.manifest.supervisorPid}). ` +
|
|
387
663
|
"Run `plane status` to inspect it, or `plane down` to wind it down first.",
|
|
388
|
-
|
|
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");
|
|
389
669
|
}
|
|
390
670
|
async function fileExists(filePath, fs) {
|
|
391
671
|
try {
|
|
@@ -396,3 +676,81 @@ async function fileExists(filePath, fs) {
|
|
|
396
676
|
return false;
|
|
397
677
|
}
|
|
398
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
|
+
}
|