@bridge_gpt/mcp-server 0.2.33 → 0.2.36

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.
Files changed (49) hide show
  1. package/README.md +456 -340
  2. package/build/agent-capabilities/probe-context.js +8 -1
  3. package/build/agent-capabilities/probes.js +7 -1
  4. package/build/agents.generated.js +1 -1
  5. package/build/claude-review-workflow.js +264 -0
  6. package/build/cli-release.js +53 -0
  7. package/build/commands.generated.js +4 -4
  8. package/build/conductor/bridge-api-client.js +252 -3
  9. package/build/conductor/deny-enforcement-preflight.js +1 -0
  10. package/build/conductor/done-gate.js +44 -5
  11. package/build/conductor/epic-reconcile.js +6 -0
  12. package/build/conductor/install-doctor.js +462 -0
  13. package/build/conductor-bin.js +3 -3
  14. package/build/conductor-bundle-artifacts.js +30 -9
  15. package/build/doctor.js +234 -1
  16. package/build/executor/cli.js +32 -5
  17. package/build/executor/credentials.js +45 -11
  18. package/build/executor/deps.js +14 -0
  19. package/build/executor/env.js +23 -6
  20. package/build/executor/index.js +4 -0
  21. package/build/executor/job-runner.js +119 -9
  22. package/build/executor/permissions.js +12 -2
  23. package/build/executor/preflight.js +95 -8
  24. package/build/executor/prompt-spec.js +51 -0
  25. package/build/executor/runner.js +15 -2
  26. package/build/executor/service-unit.js +876 -0
  27. package/build/executor/test-clock.js +8 -0
  28. package/build/executor/types.js +0 -17
  29. package/build/executor/worker-command.js +62 -9
  30. package/build/index.js +575 -143
  31. package/build/init.js +153 -51
  32. package/build/install-bridge-conductor.js +491 -0
  33. package/build/install-bridge.js +884 -176
  34. package/build/install-reexec.js +233 -0
  35. package/build/mcp-host-config.js +11 -1
  36. package/build/mcp-install-state.js +32 -0
  37. package/build/mcp-provisioning.js +22 -6
  38. package/build/pipelines.generated.js +14 -8
  39. package/build/readme.generated.js +1 -1
  40. package/build/run-unit-tests-launcher.js +257 -0
  41. package/build/setup-epic.js +117 -8
  42. package/build/upgrade-cli.js +1 -15
  43. package/build/version.generated.js +1 -1
  44. package/docs/CONDUCTOR.md +115 -4
  45. package/docs/install/mcp-tool-integrations.md +29 -21
  46. package/package.json +9 -6
  47. package/pipelines/implement-ticket.json +6 -1
  48. package/build/conductor/supervisor-judgment-python.js +0 -141
  49. package/build/conductor/supervisor-judgment.js +0 -215
@@ -0,0 +1,462 @@
1
+ /**
2
+ * Unified conductor install doctor (BAPI-679).
3
+ *
4
+ * Three read-only diagnostics already exist and each answers part of "is the
5
+ * conductor installed correctly": `buildConductorDoctorReport` (local ledger,
6
+ * hooks, schedule), `collectExecutorPreflight` (local executor tooling), and
7
+ * `collectInstallStatusChecks` (Bridge install status). None of them can see
8
+ * SERVER-side conductor state, because the CLI holds neither Postgres access nor
9
+ * the GitHub App token. This module composes all three with the server readiness
10
+ * endpoint — plus the read-only BAPI-688 executor service-unit diagnostics
11
+ * (`collectExecutorServiceDiagnostics`) — so one command reports every gap.
12
+ *
13
+ * COMPOSITION, NOT MERGING. Each existing builder's report object is stored
14
+ * verbatim in its own field and rendered by its own formatter. Nothing here
15
+ * mutates, rewrites, reclassifies, or drops a legacy finding — three reports plus
16
+ * a set of new named sections, never one flattened schema. That keeps this module
17
+ * additive: a change to any legacy builder cannot silently alter what the unified
18
+ * doctor reports about it.
19
+ *
20
+ * STRICTLY READ-ONLY. No POST, no filesystem write, no install-state write, no
21
+ * workflow scaffold, no schema migration, no hook installation. It is invoked
22
+ * from a write-capable installer, so that boundary is pinned by tests.
23
+ */
24
+ import { readdir as fsReaddir, readFile as fsReadFile, stat as fsStat } from "node:fs/promises";
25
+ import os from "node:os";
26
+ import { buildConductorDoctorReport, formatConductorDoctorReport, } from "./doctor.js";
27
+ import { collectExecutorServiceDiagnostics, formatExecutorServiceDiagnosticsReport, } from "../doctor.js";
28
+ import { collectInstallStatusChecks, formatInstallStatusReport, } from "../install-doctor.js";
29
+ import { ConductorBridgeApiError, fetchConductorReadiness, } from "./bridge-api-client.js";
30
+ /** Operator runbook pointer surfaced beside the executor gap. */
31
+ export const CONDUCTOR_OPERATOR_RUNBOOK_POINTER = "docs/claude/epic-conductor-v2-operator-runbook.md";
32
+ /**
33
+ * Capability-matrix guidance for the executor leg (BAPI-688 shipped in the same
34
+ * build as BAPI-679): a persistent unit is *generated* by `executor
35
+ * install-service`, but starting/stopping it stays operator-managed, so the
36
+ * executor is never claimed as provisioned by a bootstrap alone.
37
+ */
38
+ export const EXECUTOR_PROVISIONING_GUIDANCE = "generate a unit with `executor install-service`; start/stop stays operator-managed";
39
+ /** Remediation surfaced whenever no healthy generated service unit is detected. */
40
+ export const EXECUTOR_INSTALL_SERVICE_REMEDIATION = "generate a persistent unit with `executor install-service` (or run the executor by hand) — " +
41
+ `see ${CONDUCTOR_OPERATOR_RUNBOOK_POINTER}.`;
42
+ // ---------------------------------------------------------------------------
43
+ // Section builders
44
+ // ---------------------------------------------------------------------------
45
+ function supervisorSection(readiness) {
46
+ const s = readiness.supervisor;
47
+ const sections = [];
48
+ if (!s.setup_present || !s.config_present) {
49
+ const missing = [
50
+ !s.setup_present ? "setup" : null,
51
+ !s.config_present ? "config" : null,
52
+ ].filter(Boolean).join(" and ");
53
+ sections.push({
54
+ id: "supervisor-configuration",
55
+ label: "Supervisor configuration",
56
+ status: "degraded",
57
+ detail: `project-default supervisor ${missing} absent`,
58
+ remediation: "bootstrap writes the safe project-default posture.",
59
+ });
60
+ }
61
+ else {
62
+ sections.push({
63
+ id: "supervisor-configuration",
64
+ label: "Supervisor configuration",
65
+ status: "ok",
66
+ detail: `setup=${s.setup_source}, config=${s.config_source}`,
67
+ });
68
+ }
69
+ // Deliberately its OWN section, not folded into the one above: "configured but
70
+ // unsafe" and "not configured" drive different operator actions, and an empty
71
+ // required-check list makes the CI gate pass unconditionally rather than fail.
72
+ sections.push(s.required_checks_empty
73
+ ? {
74
+ id: "supervisor-required-checks",
75
+ label: "Supervisor required CI checks",
76
+ status: "degraded",
77
+ detail: "required-check list is EMPTY — the done gate would pass unconditionally",
78
+ remediation: "bootstrap writes a non-empty required-check list into done_gate_config.",
79
+ }
80
+ : {
81
+ id: "supervisor-required-checks",
82
+ label: "Supervisor required CI checks",
83
+ status: "ok",
84
+ detail: `${s.required_checks_count} required check(s) configured`,
85
+ });
86
+ return sections;
87
+ }
88
+ function githubCredentialsSection(readiness) {
89
+ const g = readiness.github;
90
+ if (g.credentials_complete) {
91
+ return {
92
+ id: "github-credentials",
93
+ label: "GitHub App credentials",
94
+ status: "ok",
95
+ detail: "owner, repository id, and installation id all resolved",
96
+ };
97
+ }
98
+ const missing = [
99
+ !g.owner_resolved ? "owner" : null,
100
+ !g.repo_id_resolved ? "repository id" : null,
101
+ !g.installation_id_resolved ? "installation id" : null,
102
+ ].filter(Boolean).join(", ");
103
+ return {
104
+ id: "github-credentials",
105
+ label: "GitHub App credentials",
106
+ status: "degraded",
107
+ detail: g.credentials_readable
108
+ ? `incomplete: ${missing} unresolved`
109
+ : "no GitHub credential row for this repository",
110
+ remediation: "connect GitHub from the Bridge setup UI (`install-bridge connect-github`).",
111
+ };
112
+ }
113
+ function githubActionsSection(readiness) {
114
+ const g = readiness.github;
115
+ if (g.actions_write) {
116
+ return {
117
+ id: "github-actions-permission",
118
+ label: "GitHub App actions permission",
119
+ status: "ok",
120
+ detail: "actions: write",
121
+ };
122
+ }
123
+ if (!g.actions_probe_succeeded) {
124
+ // "Could not be checked" is NOT the same finding as "confirmedly missing" —
125
+ // the first needs a retry, the second needs a permission grant.
126
+ return {
127
+ id: "github-actions-permission",
128
+ label: "GitHub App actions permission",
129
+ status: "degraded",
130
+ detail: "permission could not be checked",
131
+ remediation: "re-run once credentials resolve; the workflow rerun lane fails open on 403 until this reads `write`.",
132
+ };
133
+ }
134
+ return {
135
+ id: "github-actions-permission",
136
+ label: "GitHub App actions permission",
137
+ status: "degraded",
138
+ detail: `actions: ${g.actions_permission_level}`,
139
+ remediation: "grant the GitHub App `actions: write`; the conductor's workflow rerun lane fails open on 403 without it.",
140
+ };
141
+ }
142
+ function reconcilerSection(readiness) {
143
+ const r = readiness.reconciler;
144
+ if (!r.liveness_readable) {
145
+ return {
146
+ id: "reconciler-liveness",
147
+ label: "Reconciler tick liveness",
148
+ status: "degraded",
149
+ detail: "liveness could not be read",
150
+ };
151
+ }
152
+ if (!r.stale) {
153
+ return {
154
+ id: "reconciler-liveness",
155
+ label: "Reconciler tick liveness",
156
+ status: "ok",
157
+ detail: `last tick ${r.last_tick_age_seconds}s ago across ${r.active_run_count} active run(s)`,
158
+ };
159
+ }
160
+ return {
161
+ id: "reconciler-liveness",
162
+ label: "Reconciler tick liveness",
163
+ status: "degraded",
164
+ detail: r.last_tick_at === null
165
+ ? "no reconciler tick recorded for this repository"
166
+ : `last tick ${r.last_tick_age_seconds}s ago (threshold ${readiness.thresholds.reconciler_stale_after_seconds}s)`,
167
+ remediation: "start the reconciler (`conductor epic-tick` schedule) for this repository.",
168
+ };
169
+ }
170
+ function workflowSection(presence, reviewPolicySource) {
171
+ if (presence === "present") {
172
+ return {
173
+ id: "claude-review-workflow",
174
+ label: "claude-review workflow",
175
+ status: "ok",
176
+ detail: ".github/workflows/claude-review.yml present",
177
+ };
178
+ }
179
+ // Applicability is policy-dependent: a run whose review signal is GitHub's own
180
+ // review decision does not need the Claude workflow at all, so its absence is
181
+ // not a gap. Reporting it as degraded there would train operators to ignore
182
+ // this section.
183
+ if (reviewPolicySource === "native_review_decision" || reviewPolicySource === "none") {
184
+ return {
185
+ id: "claude-review-workflow",
186
+ label: "claude-review workflow",
187
+ status: "ok",
188
+ detail: `not applicable for review policy '${reviewPolicySource}'`,
189
+ };
190
+ }
191
+ return {
192
+ id: "claude-review-workflow",
193
+ label: "claude-review workflow",
194
+ status: "degraded",
195
+ detail: presence === "unreadable"
196
+ ? ".github/workflows/claude-review.yml could not be read"
197
+ : ".github/workflows/claude-review.yml absent",
198
+ remediation: "the selected review policy consumes the sticky verdict this workflow emits; install the template.",
199
+ };
200
+ }
201
+ /**
202
+ * Real detection of BAPI-688 generated service units, composed from
203
+ * `collectExecutorServiceDiagnostics`. Never `fatal` (a missing unit is a
204
+ * repairable gap, and blocking every install on it would be wrong) and never
205
+ * omitted (the flow must not read as fully provisioned by silence). A healthy
206
+ * unit reports `ok` for *provisioning* only — whether the service is running is
207
+ * the capability matrix's `executor_ready`, observed server-side.
208
+ */
209
+ function executorProvisioningSection(diagnostics) {
210
+ const base = { id: "executor-provisioning", label: "Executor provisioning" };
211
+ if (diagnostics === null) {
212
+ return {
213
+ ...base,
214
+ status: "degraded",
215
+ detail: "executor service-unit diagnostics could not be collected",
216
+ remediation: EXECUTOR_INSTALL_SERVICE_REMEDIATION,
217
+ };
218
+ }
219
+ if (diagnostics.status === "skipped") {
220
+ // The collector's `reason` is already sanitized and secret-free (fixed
221
+ // convention paths only): "no units found", unsupported platform, or the
222
+ // Windows manual-Task-Scheduler case.
223
+ return {
224
+ ...base,
225
+ status: "degraded",
226
+ detail: diagnostics.reason,
227
+ remediation: EXECUTOR_INSTALL_SERVICE_REMEDIATION,
228
+ };
229
+ }
230
+ const unhealthy = diagnostics.units.filter((unit) => unit.warnings.length > 0 || unit.credentials.some((cred) => !cred.resolved));
231
+ const ids = diagnostics.units.map((unit) => unit.executorId).join(", ");
232
+ if (unhealthy.length === 0 && diagnostics.warnings.length === 0) {
233
+ return {
234
+ ...base,
235
+ status: "ok",
236
+ detail: `${diagnostics.units.length} generated service unit(s) found (${ids}); ` +
237
+ "lifecycle stays operator-managed",
238
+ };
239
+ }
240
+ return {
241
+ ...base,
242
+ status: "degraded",
243
+ detail: `${diagnostics.units.length} generated service unit(s) found (${ids}), but ` +
244
+ `${unhealthy.length} unit(s) carry warnings or unresolved credentials — see the ` +
245
+ "embedded executor-provisioning report",
246
+ remediation: EXECUTOR_INSTALL_SERVICE_REMEDIATION,
247
+ };
248
+ }
249
+ // ---------------------------------------------------------------------------
250
+ // Runner
251
+ // ---------------------------------------------------------------------------
252
+ async function inspectWorkflowPresence(readWorkflowFile) {
253
+ try {
254
+ await readWorkflowFile();
255
+ return "present";
256
+ }
257
+ catch (err) {
258
+ // ENOENT is the ordinary "absent" case; anything else is "unreadable". The
259
+ // error's message is never surfaced — it can echo an absolute path.
260
+ const code = err?.code;
261
+ return code === "ENOENT" ? "absent" : "unreadable";
262
+ }
263
+ }
264
+ /**
265
+ * Build the unified report. Never throws: every probe failure becomes a section.
266
+ */
267
+ export async function runConductorInstallDoctor(deps) {
268
+ const sections = [];
269
+ // --- Legacy builders, composed verbatim ---------------------------------
270
+ const buildConductor = deps.buildConductorReport ?? buildConductorDoctorReport;
271
+ let legacyConductor = null;
272
+ try {
273
+ legacyConductor = await buildConductor(deps.conductorDoctorDeps);
274
+ }
275
+ catch {
276
+ sections.push({
277
+ id: "conductor-ledger",
278
+ label: "Conductor ledger doctor",
279
+ status: "degraded",
280
+ detail: "the local conductor doctor could not be built",
281
+ });
282
+ }
283
+ let legacyExecutorPreflight = null;
284
+ if (deps.collectExecutorPreflight) {
285
+ try {
286
+ legacyExecutorPreflight = await deps.collectExecutorPreflight();
287
+ }
288
+ catch {
289
+ sections.push({
290
+ id: "executor-preflight",
291
+ label: "Executor preflight",
292
+ status: "degraded",
293
+ detail: "the executor preflight could not be collected",
294
+ });
295
+ }
296
+ }
297
+ let legacyInstallChecks = null;
298
+ const collectInstall = deps.collectInstallChecks ??
299
+ (deps.installDoctorDeps
300
+ ? () => collectInstallStatusChecks(deps.installDoctorDeps)
301
+ : null);
302
+ if (collectInstall) {
303
+ try {
304
+ legacyInstallChecks = await collectInstall();
305
+ }
306
+ catch {
307
+ sections.push({
308
+ id: "install-status",
309
+ label: "Bridge install status",
310
+ status: "degraded",
311
+ detail: "the install-status checklist could not be collected",
312
+ });
313
+ }
314
+ }
315
+ // --- Server readiness ---------------------------------------------------
316
+ let readiness = null;
317
+ if (!deps.access) {
318
+ // No resolvable identity/credential means every server-side section below is
319
+ // unknowable — the operator must fix this before any of it can be judged.
320
+ sections.push({
321
+ id: "bridge-access",
322
+ label: "Bridge API access",
323
+ status: "fatal",
324
+ detail: deps.accessError ?? "Bridge API repository/credential could not be resolved",
325
+ remediation: "run `install-bridge` (or set BAPI_API_KEY) for this repository.",
326
+ });
327
+ }
328
+ else {
329
+ try {
330
+ readiness = await fetchConductorReadiness(deps.access, deps.fetch);
331
+ }
332
+ catch (err) {
333
+ const kind = err instanceof ConductorBridgeApiError ? err.kind : "network";
334
+ // Authentication failure and a malformed 200 are FATAL: in both cases the
335
+ // report cannot be trusted, and continuing would present unknown server
336
+ // state as healthy. A transient transport failure is degraded — the local
337
+ // sections are still meaningful and a retry may succeed.
338
+ const fatalKind = kind === "unauthorized" || kind === "invalid-input";
339
+ sections.push({
340
+ id: "server-readiness",
341
+ label: "Server readiness",
342
+ status: fatalKind ? "fatal" : "degraded",
343
+ detail: kind === "unauthorized"
344
+ ? "Bridge API rejected the credential for this repository"
345
+ : kind === "invalid-input"
346
+ ? "the readiness response failed shape validation"
347
+ : "the readiness endpoint could not be reached",
348
+ remediation: fatalKind
349
+ ? "re-check the API key and repository, then re-run."
350
+ : "retry; the local sections above are unaffected.",
351
+ });
352
+ }
353
+ }
354
+ if (readiness) {
355
+ sections.push(...supervisorSection(readiness));
356
+ sections.push(githubCredentialsSection(readiness));
357
+ sections.push(githubActionsSection(readiness));
358
+ }
359
+ const presence = await inspectWorkflowPresence(deps.readWorkflowFile);
360
+ sections.push(workflowSection(presence, deps.reviewPolicySource));
361
+ if (readiness) {
362
+ sections.push(reconcilerSection(readiness));
363
+ }
364
+ // --- Executor provisioning (BAPI-688): real, read-only unit detection -----
365
+ const collectServiceUnits = deps.collectExecutorServiceUnits ??
366
+ (() => collectExecutorServiceDiagnostics({
367
+ platform: process.platform,
368
+ env: process.env,
369
+ homedir: os.homedir,
370
+ readdir: (dirPath) => fsReaddir(dirPath),
371
+ readFile: (filePath) => fsReadFile(filePath, "utf-8"),
372
+ stat: (filePath) => fsStat(filePath),
373
+ }));
374
+ let executorService = null;
375
+ try {
376
+ executorService = await collectServiceUnits();
377
+ }
378
+ catch {
379
+ // The collector itself never throws; this guards an injected fake. The
380
+ // section reports the collection gap rather than losing the section.
381
+ }
382
+ sections.push(executorProvisioningSection(executorService));
383
+ return {
384
+ legacyConductor,
385
+ legacyExecutorPreflight,
386
+ legacyInstallChecks,
387
+ executorService,
388
+ readiness,
389
+ sections,
390
+ };
391
+ }
392
+ // ---------------------------------------------------------------------------
393
+ // Formatting + exit code
394
+ // ---------------------------------------------------------------------------
395
+ /** Status tag, matching `formatConductorDoctorReport`'s visual hierarchy. */
396
+ function statusTag(status) {
397
+ if (status === "fatal")
398
+ return "✗ FATAL";
399
+ if (status === "degraded")
400
+ return "! DEGRADED";
401
+ return "✓ OK";
402
+ }
403
+ /**
404
+ * Render the unified report. The three legacy reports are EMBEDDED unchanged via
405
+ * their own formatters — never re-rendered from a merged schema.
406
+ */
407
+ export function formatConductorInstallDoctorReport(report) {
408
+ const lines = [
409
+ "Conductor install doctor",
410
+ "════════════════════════",
411
+ "",
412
+ ];
413
+ for (const section of report.sections) {
414
+ lines.push(`${statusTag(section.status)} ${section.label}`);
415
+ lines.push(` ${section.detail}`);
416
+ if (section.remediation)
417
+ lines.push(` → ${section.remediation}`);
418
+ }
419
+ // Each legacy formatter is called defensively. They are written for the exact
420
+ // shape their own builder returns, so a builder that degrades (or an injected
421
+ // fake) can make one throw — and this renderer runs inside the installer right
422
+ // before its write decisions, where an unhandled throw would abort the run with
423
+ // a raw stack instead of the report the operator needs to act on.
424
+ const embed = (label, render) => {
425
+ lines.push("");
426
+ try {
427
+ lines.push(render());
428
+ }
429
+ catch {
430
+ lines.push(`${label}: report could not be rendered.`);
431
+ }
432
+ };
433
+ if (report.legacyInstallChecks) {
434
+ embed("Bridge install status", () => formatInstallStatusReport(report.legacyInstallChecks));
435
+ }
436
+ if (report.legacyConductor) {
437
+ embed("Conductor ledger doctor", () => formatConductorDoctorReport(report.legacyConductor));
438
+ }
439
+ if (report.legacyExecutorPreflight) {
440
+ const p = report.legacyExecutorPreflight;
441
+ lines.push("");
442
+ lines.push("executor preflight (local)");
443
+ lines.push("──────────────────────────");
444
+ lines.push(`ok: ${p.ok}`);
445
+ for (const finding of p.fatalFindings)
446
+ lines.push(` fatal: ${finding}`);
447
+ for (const warning of p.warnings)
448
+ lines.push(` warn: ${warning}`);
449
+ }
450
+ if (report.executorService) {
451
+ embed("Executor provisioning", () => formatExecutorServiceDiagnosticsReport(report.executorService));
452
+ }
453
+ return lines.join("\n");
454
+ }
455
+ /** `1` when any section is fatal; `0` for ok/degraded-only reports. */
456
+ export function conductorInstallDoctorExitCode(report) {
457
+ return report.sections.some((s) => s.status === "fatal") ? 1 : 0;
458
+ }
459
+ /** True when the report has a fatal section (the pre-write abort predicate). */
460
+ export function conductorInstallDoctorHasFatal(report) {
461
+ return report.sections.some((s) => s.status === "fatal");
462
+ }