@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.
Files changed (79) hide show
  1. package/README.md +59 -13
  2. package/build/agent-capabilities/probe-context.js +15 -7
  3. package/build/agent-capabilities/probes.js +42 -6
  4. package/build/agent-launchers/claude-executor-adapter.js +98 -14
  5. package/build/commands.generated.js +7 -5
  6. package/build/conduct-epic/cut-protocol.js +17 -3
  7. package/build/conductor/bridge-api-client.js +232 -5
  8. package/build/conductor/cli.js +23 -0
  9. package/build/conductor/deny-enforcement-preflight.js +107 -10
  10. package/build/conductor/doctor.js +428 -5
  11. package/build/conductor/install-doctor.js +65 -656
  12. package/build/conductor/local-merge.js +170 -11
  13. package/build/conductor/readiness-cli.js +152 -0
  14. package/build/conductor/readiness-sections.js +666 -0
  15. package/build/conductor/readiness.js +710 -0
  16. package/build/conductor/tools.js +56 -3
  17. package/build/conductor-bin.js +21 -17
  18. package/build/connect-bitbucket-api.js +370 -0
  19. package/build/connect-bitbucket.js +437 -0
  20. package/build/docs.generated.js +1 -1
  21. package/build/doctor.js +40 -1
  22. package/build/drive-epic.js +423 -11
  23. package/build/env-file-link.js +164 -0
  24. package/build/epic-integration-pr.js +10 -0
  25. package/build/executor/cli.js +41 -6
  26. package/build/executor/deps.js +5 -1
  27. package/build/executor/env-file-guard.js +113 -0
  28. package/build/executor/env.js +78 -1
  29. package/build/executor/heartbeat.js +9 -0
  30. package/build/executor/http-client.js +90 -22
  31. package/build/executor/job-errors.js +43 -2
  32. package/build/executor/job-runner.js +130 -28
  33. package/build/executor/merge-job.js +67 -16
  34. package/build/executor/permissions.js +106 -0
  35. package/build/executor/preflight.js +38 -13
  36. package/build/executor/resume-pre-spawn.js +2 -1
  37. package/build/executor/runner.js +175 -4
  38. package/build/executor/service-unit.js +15 -0
  39. package/build/executor/terminal-mutation.js +22 -1
  40. package/build/executor/types.js +86 -0
  41. package/build/executor/worker-command.js +21 -5
  42. package/build/executor/worker-guard-hook.js +939 -0
  43. package/build/executor/worker-log.js +56 -0
  44. package/build/executor/worktree.js +11 -0
  45. package/build/git-reachability.js +147 -0
  46. package/build/index.js +4734 -4270
  47. package/build/install-bridge.js +95 -0
  48. package/build/install-doctor.js +154 -2
  49. package/build/pipelines.generated.js +6 -4
  50. package/build/plan-epic-conductor-eligibility.js +37 -7
  51. package/build/plane/alembic-head.js +40 -11
  52. package/build/plane/build-freshness.js +22 -11
  53. package/build/plane/cli.js +78 -15
  54. package/build/plane/defaults.js +165 -0
  55. package/build/plane/manifest.js +63 -8
  56. package/build/plane/member-logs.js +6 -0
  57. package/build/plane/member-roster.js +195 -11
  58. package/build/plane/preflight.js +402 -44
  59. package/build/plane/shutdown.js +25 -3
  60. package/build/plane/status.js +11 -0
  61. package/build/plane/supervisor.js +343 -14
  62. package/build/plane/test-fakes.js +43 -0
  63. package/build/plane/types.js +118 -11
  64. package/build/pr-base-contract.js +20 -0
  65. package/build/readiness-check.js +412 -0
  66. package/build/readme.generated.js +1 -1
  67. package/build/review-synthesis-config.js +60 -0
  68. package/build/scripts/executor-protocol-contract-driver.js +311 -0
  69. package/build/setup-epic.js +560 -139
  70. package/build/sfcc/log-query.js +2 -1
  71. package/build/start-tickets-conductor.js +11 -2
  72. package/build/start-tickets.js +69 -2
  73. package/build/version.generated.js +3 -3
  74. package/build/worker-containment-diagnostic.js +97 -0
  75. package/build/worker-guard-hook-bin.js +6 -0
  76. package/docs/CONDUCTOR.md +27 -0
  77. package/docs/install/mcp-tool-integrations.md +3 -2
  78. package/package.json +4 -3
  79. package/pipelines/{full-automation.json → idea-to-pr.json} +1 -1
@@ -0,0 +1,666 @@
1
+ /**
2
+ * The shared conductor readiness SECTION layer (BAPI-1055).
3
+ *
4
+ * Extracted verbatim from `conductor/install-doctor.ts`, where these builders
5
+ * had already composed four of the five prerequisite loci. Nothing about a
6
+ * section changed in the move: same ids, same labels, same statuses, same
7
+ * detail text, same installer remediations. What changed is who may consume
8
+ * them — the install-time doctor and the advisory `conductor readiness` gate now
9
+ * read ONE adapter set instead of interpreting the same probes twice.
10
+ *
11
+ * ## Why extraction rather than a second aggregator
12
+ *
13
+ * The obvious alternative was to build the consolidated gate beside the install
14
+ * doctor. That would have left two independent gates over the same probes, which
15
+ * is precisely the duplication AC-6 exists to remove — and they would drift, as
16
+ * two implementations of one classification always do. Evolving the install
17
+ * doctor in place was the other alternative, and it would have mixed advisory
18
+ * reporting into an install-time, fatal-capable contract.
19
+ *
20
+ * ## The fatal boundary stays with the installer
21
+ *
22
+ * `ConductorInstallSectionStatus` keeps its `fatal` state, because the installer
23
+ * genuinely refuses to write on one (`conductorInstallDoctorHasFatal` gates the
24
+ * pre-write abort in `install-bridge-conductor.ts`). That refusal is unchanged.
25
+ * The advisory gate maps `fatal` onto the canonical `fail` status and never
26
+ * inherits the exit-code consequence: an advisory command that could exit
27
+ * non-zero would not be advisory.
28
+ *
29
+ * ## Purity
30
+ *
31
+ * Every builder here is PURE — it reads an already-collected report and returns
32
+ * sections. No probe, no network call, no filesystem write, no process spawn.
33
+ * The one exception is {@link inspectWorkflowPresence}, which awaits the
34
+ * READ-ONLY reader seams its caller injects and never opens a path of its own.
35
+ */
36
+ import { MANAGED_HOOK_NAMES } from "./git-hooks.js";
37
+ import { MCP_PACKAGE_NAME } from "../mcp-identity.js";
38
+ import { CLAUDE_REVIEW_WORKFLOW_DRIFT_REMEDIATION, summarizeClaudeReviewWorkflowDrift, } from "../claude-review-workflow-drift.js";
39
+ import { describeNativeLedgerAvailability } from "./doctor.js";
40
+ /** Operator runbook pointer surfaced beside the executor gap. */
41
+ export const CONDUCTOR_OPERATOR_RUNBOOK_POINTER = "docs/claude/epic-conductor-v2-operator-runbook.md";
42
+ /**
43
+ * Capability-matrix guidance for the executor leg.
44
+ *
45
+ * Amended by BAPI-779 (R-1): `install conductor` can now write the unit AND
46
+ * start it, each behind its own consent. What did NOT change is the readiness
47
+ * rule — `executor_ready` still comes only from a live server observation, so
48
+ * neither a written unit nor a successful local start may produce that claim.
49
+ */
50
+ export const EXECUTOR_PROVISIONING_GUIDANCE = "`install conductor` writes the unit and starts it, each behind its own consent; " +
51
+ "readiness still comes only from a live server observation";
52
+ /**
53
+ * The `BRIDGE_MCP_PROFILE` token that makes the 8 conductor MCP tools visible.
54
+ *
55
+ * The tools are profile-gated, never core, and this is the only token that
56
+ * un-gates them — so the doctor names it explicitly rather than describing the
57
+ * gap abstractly (BAPI-775).
58
+ */
59
+ export const CONDUCTOR_PROFILE_TOKEN = "conductor";
60
+ /** Remediation for a host MCP config that carries no Bridge entry at all. */
61
+ export const CONDUCTOR_PROFILE_TOKEN_NO_ENTRY_REMEDIATION = "no supported host MCP config carries a Bridge entry — run " +
62
+ `\`npx ${MCP_PACKAGE_NAME} install\` to register one, then re-run \`install conductor\`.`;
63
+ /** Remediation for an entry that exists but lacks the conductor token. */
64
+ export const CONDUCTOR_PROFILE_TOKEN_REMEDIATION = "re-run `install conductor` and accept the tool-visibility consent; the conductor " +
65
+ "tools become visible after the MCP client is restarted.";
66
+ /** Remediation surfaced whenever the managed conductor hooks are not usable. */
67
+ export const CONDUCTOR_HOOK_TARGET_REMEDIATION = "re-run `install conductor` and accept the local-observability consent, or run " +
68
+ "`conductor install-git-hooks` directly.";
69
+ /** Remediation surfaced when the ledger's native binding does not load. */
70
+ export const CONDUCTOR_LEDGER_LOADABILITY_REMEDIATION = "`better-sqlite3` is an optionalDependency npm silently skips on a build failure — " +
71
+ "reinstall it for this Node runtime to restore local conductor observability.";
72
+ /** Remediation surfaced whenever no healthy generated service unit is detected. */
73
+ export const EXECUTOR_INSTALL_SERVICE_REMEDIATION = "generate a persistent unit with `install conductor --executor-id <id>` or " +
74
+ "`executor install-service` (or run the executor by hand) — " +
75
+ `see ${CONDUCTOR_OPERATOR_RUNBOOK_POINTER}.`;
76
+ /** Remediation for a unit that exists but whose service is not running. */
77
+ export const EXECUTOR_SERVICE_START_REMEDIATION = "start the service (re-run `install conductor` and accept the start consent, or run the " +
78
+ `printed launchctl/systemctl command by hand) — see ${CONDUCTOR_OPERATOR_RUNBOOK_POINTER}.`;
79
+ // ---------------------------------------------------------------------------
80
+ // Section builders
81
+ // ---------------------------------------------------------------------------
82
+ export function supervisorSection(readiness) {
83
+ const s = readiness.supervisor;
84
+ const sections = [];
85
+ if (!s.setup_present || !s.config_present) {
86
+ const missing = [
87
+ !s.setup_present ? "setup" : null,
88
+ !s.config_present ? "config" : null,
89
+ ].filter(Boolean).join(" and ");
90
+ sections.push({
91
+ id: "supervisor-configuration",
92
+ label: "Supervisor configuration",
93
+ status: "degraded",
94
+ detail: `project-default supervisor ${missing} absent`,
95
+ remediation: "bootstrap writes the safe project-default posture.",
96
+ });
97
+ }
98
+ else {
99
+ sections.push({
100
+ id: "supervisor-configuration",
101
+ label: "Supervisor configuration",
102
+ status: "ok",
103
+ detail: `setup=${s.setup_source}, config=${s.config_source}`,
104
+ });
105
+ }
106
+ // Deliberately its OWN section, not folded into the one above: "configured but
107
+ // unsafe" and "not configured" drive different operator actions, and an empty
108
+ // required-check list makes the CI gate pass unconditionally rather than fail.
109
+ sections.push(s.required_checks_empty
110
+ ? {
111
+ id: "supervisor-required-checks",
112
+ label: "Supervisor required CI checks",
113
+ status: "degraded",
114
+ detail: "required-check list is EMPTY — the done gate would pass unconditionally",
115
+ remediation: "bootstrap writes a non-empty required-check list into done_gate_config.",
116
+ }
117
+ : {
118
+ id: "supervisor-required-checks",
119
+ label: "Supervisor required CI checks",
120
+ status: "ok",
121
+ detail: `${s.required_checks_count} required check(s) configured`,
122
+ });
123
+ // BAPI-694 (R5) — its OWN section, and WARN-ONLY.
124
+ //
125
+ // `degraded`, never `fatal`: a divergence between the code_review gate's
126
+ // signal and the merge path's may be exactly what the operator intended, and
127
+ // blocking an install over a legitimate posture would be worse than the
128
+ // silence this replaces. It also changes no gate: both settings are reported,
129
+ // neither is coerced into the other's vocabulary.
130
+ //
131
+ // Absent (`null`) means the server predates BAPI-694 and cannot report
132
+ // alignment, so no section is emitted at all — an advisory addition must not
133
+ // manufacture a finding out of a server that never answered.
134
+ const alignment = s.review_policy_alignment;
135
+ if (alignment !== null) {
136
+ if (alignment.status === "divergent" || alignment.status === "invalid") {
137
+ sections.push({
138
+ id: "supervisor-review-alignment",
139
+ label: "Supervisor review-policy alignment",
140
+ status: "degraded",
141
+ detail: alignment.explanation,
142
+ remediation: alignment.status === "invalid"
143
+ ? "fix the done gate's review_state condition — review configuration " +
144
+ "exists but cannot be read, so the merge path refuses rather than merges."
145
+ : "set the repository review-policy default and the done gate's " +
146
+ "review_state condition to the same signal, or confirm the difference " +
147
+ `is intended — see ${CONDUCTOR_OPERATOR_RUNBOOK_POINTER}.`,
148
+ });
149
+ }
150
+ else {
151
+ // `aligned` and `not_configured` are both non-warning states, and are
152
+ // reported separately: "they agree" and "neither is set" are different
153
+ // facts, and collapsing them would imply a stored default that is absent.
154
+ sections.push({
155
+ id: "supervisor-review-alignment",
156
+ label: "Supervisor review-policy alignment",
157
+ status: "ok",
158
+ detail: alignment.explanation,
159
+ });
160
+ }
161
+ }
162
+ return sections;
163
+ }
164
+ export function githubCredentialsSection(readiness) {
165
+ const g = readiness.github;
166
+ if (g.credentials_complete) {
167
+ return {
168
+ id: "github-credentials",
169
+ label: "GitHub App credentials",
170
+ status: "ok",
171
+ detail: "owner, repository id, and installation id all resolved",
172
+ };
173
+ }
174
+ const missing = [
175
+ !g.owner_resolved ? "owner" : null,
176
+ !g.repo_id_resolved ? "repository id" : null,
177
+ !g.installation_id_resolved ? "installation id" : null,
178
+ ].filter(Boolean).join(", ");
179
+ return {
180
+ id: "github-credentials",
181
+ label: "GitHub App credentials",
182
+ status: "degraded",
183
+ detail: g.credentials_readable
184
+ ? `incomplete: ${missing} unresolved`
185
+ : "no GitHub credential row for this repository",
186
+ remediation: "connect GitHub from the Bridge setup UI (`install-bridge connect-github`).",
187
+ };
188
+ }
189
+ export function githubActionsSection(readiness) {
190
+ const g = readiness.github;
191
+ if (g.actions_write) {
192
+ return {
193
+ id: "github-actions-permission",
194
+ label: "GitHub App actions permission",
195
+ status: "ok",
196
+ detail: "actions: write",
197
+ };
198
+ }
199
+ if (!g.actions_probe_succeeded) {
200
+ // "Could not be checked" is NOT the same finding as "confirmedly missing" —
201
+ // the first needs a retry, the second needs a permission grant.
202
+ return {
203
+ id: "github-actions-permission",
204
+ label: "GitHub App actions permission",
205
+ status: "degraded",
206
+ detail: "permission could not be checked",
207
+ remediation: "re-run once credentials resolve; the workflow rerun lane fails open on 403 until this reads `write`.",
208
+ };
209
+ }
210
+ return {
211
+ id: "github-actions-permission",
212
+ label: "GitHub App actions permission",
213
+ status: "degraded",
214
+ detail: `actions: ${g.actions_permission_level}`,
215
+ remediation: "grant the GitHub App `actions: write`; the conductor's workflow rerun lane fails open on 403 without it.",
216
+ };
217
+ }
218
+ export function reconcilerSection(readiness) {
219
+ const r = readiness.reconciler;
220
+ if (!r.liveness_readable) {
221
+ // BAPI-1055: unreadable is NOT the same finding as stale, and it had no
222
+ // remediation at all — so a report could name a gap with no next step.
223
+ return {
224
+ id: "reconciler-liveness",
225
+ label: "Reconciler tick liveness",
226
+ status: "degraded",
227
+ detail: "liveness could not be read",
228
+ remediation: "retry once the Bridge API is reachable; reconciler liveness is unknown here, " +
229
+ "which is not the same as confirmed-stale.",
230
+ };
231
+ }
232
+ if (!r.stale) {
233
+ return {
234
+ id: "reconciler-liveness",
235
+ label: "Reconciler tick liveness",
236
+ status: "ok",
237
+ detail: `last tick ${r.last_tick_age_seconds}s ago across ${r.active_run_count} active run(s)`,
238
+ };
239
+ }
240
+ return {
241
+ id: "reconciler-liveness",
242
+ label: "Reconciler tick liveness",
243
+ status: "degraded",
244
+ detail: r.last_tick_at === null
245
+ ? "no reconciler tick recorded for this repository"
246
+ : `last tick ${r.last_tick_age_seconds}s ago (threshold ${readiness.thresholds.reconciler_stale_after_seconds}s)`,
247
+ remediation: "start the reconciler (`conductor epic-tick` schedule) for this repository.",
248
+ };
249
+ }
250
+ export function workflowSection(presence, reviewPolicySource, driftDetail) {
251
+ if (presence === "present") {
252
+ return {
253
+ id: "claude-review-workflow",
254
+ label: "claude-review workflow",
255
+ status: "ok",
256
+ // BAPI-941: when the lineage comparison ran but could not be completed,
257
+ // that uncertainty rides on the SAME informational detail line rather than
258
+ // becoming a status of its own. The section stays `ok`: the workflow is
259
+ // installed and readable, which is all this section has ever asserted.
260
+ detail: driftDetail
261
+ ? `.github/workflows/claude-review.yml present — ${driftDetail}`
262
+ : ".github/workflows/claude-review.yml present",
263
+ };
264
+ }
265
+ // BAPI-941: present but stale. Reported through the SAME section, status
266
+ // vocabulary, and detail/remediation shape as every other degraded state — no
267
+ // new health component, no bespoke styling, and no color dependence: the
268
+ // detail's own wording carries the warning. The file IS installed, so this is
269
+ // never `absent`, and the policy-applicability branch below does not apply
270
+ // (a stale workflow only matters to a policy that consumes its verdict, and
271
+ // the caller only classifies drift when it does).
272
+ if (presence === "drifted") {
273
+ return {
274
+ id: "claude-review-workflow",
275
+ label: "claude-review workflow",
276
+ status: "degraded",
277
+ detail: driftDetail ??
278
+ ".github/workflows/claude-review.yml differs from the repository default branch",
279
+ remediation: CLAUDE_REVIEW_WORKFLOW_DRIFT_REMEDIATION,
280
+ };
281
+ }
282
+ // Applicability is policy-dependent: a run whose review signal is GitHub's own
283
+ // review decision does not need the Claude workflow at all, so its absence is
284
+ // not a gap. Reporting it as degraded there would train operators to ignore
285
+ // this section.
286
+ if (reviewPolicySource === "native_review_decision" || reviewPolicySource === "none") {
287
+ return {
288
+ id: "claude-review-workflow",
289
+ label: "claude-review workflow",
290
+ status: "ok",
291
+ detail: `not applicable for review policy '${reviewPolicySource}'`,
292
+ };
293
+ }
294
+ return {
295
+ id: "claude-review-workflow",
296
+ label: "claude-review workflow",
297
+ status: "degraded",
298
+ detail: presence === "unreadable"
299
+ ? ".github/workflows/claude-review.yml could not be read"
300
+ : ".github/workflows/claude-review.yml absent",
301
+ remediation: "the selected review policy consumes the sticky verdict this workflow emits; install the template.",
302
+ };
303
+ }
304
+ /**
305
+ * Real detection of BAPI-688 generated service units, composed from
306
+ * `collectExecutorServiceDiagnostics`. Never `fatal` (a missing unit is a
307
+ * repairable gap, and blocking every install on it would be wrong) and never
308
+ * omitted (the flow must not read as fully provisioned by silence).
309
+ *
310
+ * This section reports UNIT PRESENCE and nothing else (BAPI-779). Whether the
311
+ * service manager is actually running the unit is
312
+ * {@link executorServiceStateSection}, and whether the SERVER has observed an
313
+ * executor is {@link executorObservationSection}. Three separate facts, three
314
+ * separate sections — a healthy unit here proves only that a file exists.
315
+ */
316
+ export function executorProvisioningSection(diagnostics) {
317
+ const base = { id: "executor-provisioning", label: "Executor provisioning" };
318
+ if (diagnostics === null) {
319
+ return {
320
+ ...base,
321
+ status: "degraded",
322
+ detail: "executor service-unit diagnostics could not be collected",
323
+ remediation: EXECUTOR_INSTALL_SERVICE_REMEDIATION,
324
+ };
325
+ }
326
+ if (diagnostics.status === "skipped") {
327
+ // The collector's `reason` is already sanitized and secret-free (fixed
328
+ // convention paths only): "no units found", unsupported platform, or the
329
+ // Windows manual-Task-Scheduler case.
330
+ return {
331
+ ...base,
332
+ status: "degraded",
333
+ detail: diagnostics.reason,
334
+ remediation: EXECUTOR_INSTALL_SERVICE_REMEDIATION,
335
+ };
336
+ }
337
+ const unhealthy = diagnostics.units.filter((unit) => unit.warnings.length > 0 || unit.credentials.some((cred) => !cred.resolved));
338
+ const ids = diagnostics.units.map((unit) => unit.executorId).join(", ");
339
+ if (unhealthy.length === 0 && diagnostics.warnings.length === 0) {
340
+ return {
341
+ ...base,
342
+ status: "ok",
343
+ detail: `${diagnostics.units.length} generated service unit(s) found (${ids}); ` +
344
+ "unit presence only — see the service-state and observation sections",
345
+ };
346
+ }
347
+ return {
348
+ ...base,
349
+ status: "degraded",
350
+ detail: `${diagnostics.units.length} generated service unit(s) found (${ids}), but ` +
351
+ `${unhealthy.length} unit(s) carry warnings or unresolved credentials — see the ` +
352
+ "embedded executor-provisioning report",
353
+ remediation: EXECUTOR_INSTALL_SERVICE_REMEDIATION,
354
+ };
355
+ }
356
+ /**
357
+ * Local service-manager state for the units that were discovered (BAPI-779).
358
+ *
359
+ * Never `fatal`: this is a diagnostic, and an unreadable service manager is a
360
+ * gap in the *report*, not a broken install. `unknown` is deliberately not
361
+ * collapsed into `not-found` — "the probe could not answer" and "the service
362
+ * manager has no such unit" lead to different operator actions.
363
+ */
364
+ export function executorServiceStateSection(observations, unitsDiscovered, inspectorAvailable) {
365
+ const base = { id: "executor-service-state", label: "Executor service state" };
366
+ if (!unitsDiscovered) {
367
+ return {
368
+ ...base,
369
+ status: "degraded",
370
+ detail: "no generated unit to inspect, so no service state was collected",
371
+ remediation: EXECUTOR_INSTALL_SERVICE_REMEDIATION,
372
+ };
373
+ }
374
+ if (!inspectorAvailable || observations.length === 0) {
375
+ return {
376
+ ...base,
377
+ status: "degraded",
378
+ detail: "local service state could not be collected on this host",
379
+ remediation: EXECUTOR_SERVICE_START_REMEDIATION,
380
+ };
381
+ }
382
+ // Deterministic ordering by executor id so two runs of the same host render
383
+ // identically regardless of enumeration order.
384
+ const rendered = [...observations]
385
+ .sort((a, b) => (a.executorId < b.executorId ? -1 : a.executorId > b.executorId ? 1 : 0))
386
+ .map((o) => `${o.executorId}=${o.state}`)
387
+ .join(", ");
388
+ const allActive = observations.every((o) => o.state === "active");
389
+ return allActive
390
+ ? { ...base, status: "ok", detail: `service state: ${rendered}` }
391
+ : {
392
+ ...base,
393
+ status: "degraded",
394
+ detail: `service state: ${rendered}`,
395
+ remediation: EXECUTOR_SERVICE_START_REMEDIATION,
396
+ };
397
+ }
398
+ /**
399
+ * The AUTHORITATIVE executor fact: what the server has observed (BAPI-779).
400
+ *
401
+ * This is the only section permitted to say `executor_ready`, and only for
402
+ * `readiness.executor.ready === true`. The three not-ready shapes are kept
403
+ * distinct because they mean different things: `null` is "never observed" (the
404
+ * executor has not claimed yet), `false` is "observed but stale" (it claimed
405
+ * once and went quiet), and an unreadable readiness payload is "unknown" (the
406
+ * doctor cannot say). Collapsing them would let a stale executor read like a
407
+ * fresh install, or vice versa.
408
+ */
409
+ export function executorObservationSection(readiness) {
410
+ const base = { id: "executor-observation", label: "Executor server observation" };
411
+ if (readiness === null || !readiness.executor.liveness_readable) {
412
+ return {
413
+ ...base,
414
+ status: "degraded",
415
+ detail: "server executor observation is unknown (readiness could not be read)",
416
+ remediation: "retry once the Bridge API is reachable; no readiness claim is made from local state.",
417
+ };
418
+ }
419
+ const e = readiness.executor;
420
+ const seen = e.last_seen_at === null
421
+ ? "never seen"
422
+ : `last seen ${e.last_seen_at} (${e.last_seen_age_seconds}s ago)`;
423
+ if (e.ready === true) {
424
+ return { ...base, status: "ok", detail: `executor_ready — ${seen}` };
425
+ }
426
+ if (e.ready === false) {
427
+ return {
428
+ ...base,
429
+ status: "degraded",
430
+ detail: `observed but stale/not ready — ${seen}`,
431
+ remediation: EXECUTOR_SERVICE_START_REMEDIATION,
432
+ };
433
+ }
434
+ return {
435
+ ...base,
436
+ status: "degraded",
437
+ detail: `never observed by the server — ${seen}`,
438
+ remediation: EXECUTOR_SERVICE_START_REMEDIATION,
439
+ };
440
+ }
441
+ // ---------------------------------------------------------------------------
442
+ // Local conductor capability sections (BAPI-775)
443
+ //
444
+ // Three INDEPENDENT read-only checks — host profile token, managed hook targets,
445
+ // native ledger loadability. All three are non-fatal by construction: a missing
446
+ // local conductor capability is a repairable gap, and letting one flip
447
+ // `conductorInstallDoctorExitCode` to 1 would make every ordinary install fail
448
+ // on a capability it never asked for.
449
+ // ---------------------------------------------------------------------------
450
+ /**
451
+ * Whether this host looks like a conductor context.
452
+ *
453
+ * Composed from the two facts already collected: the resolved MCP-profile
454
+ * inspection (`BAPI_CONDUCTOR_ENABLED` or a registered epic-tick schedule) and
455
+ * the read-only host token inspection. Outside such a context a missing local
456
+ * capability is ADVISORY — reporting it as a gap would train operators of
457
+ * ordinary repositories to ignore this whole part of the report.
458
+ */
459
+ export function detectConductorContext(legacyConductor, profile) {
460
+ // Read defensively: an INJECTED report sentinel (and the installer's own
461
+ // degraded-doctor fallback) may populate only the fields a test cares about,
462
+ // and a doctor that throws on a partially-built report would abort the run it
463
+ // exists to diagnose. Same posture as the defensive legacy formatters below.
464
+ return (legacyConductor?.mcp_profile?.conductor_context_detected === true ||
465
+ profile?.tokenPresent === true);
466
+ }
467
+ /**
468
+ * Host MCP profile-token presence.
469
+ *
470
+ * Reports CONFIG IDENTIFIERS AND BOOLEANS ONLY — never an entry, an `env`
471
+ * object, a command line, an API key, or an unrelated profile token — and names
472
+ * only the three configs the merge path can write, so nothing here can imply
473
+ * that Codex or Copilot configuration was inspected or changed.
474
+ */
475
+ export function conductorProfileTokenSection(profile, conductorContext) {
476
+ const base = { id: "conductor-profile-token", label: "MCP conductor profile token" };
477
+ if (profile === null) {
478
+ return {
479
+ ...base,
480
+ status: "degraded",
481
+ detail: "the host MCP profile token could not be inspected",
482
+ remediation: CONDUCTOR_PROFILE_TOKEN_REMEDIATION,
483
+ };
484
+ }
485
+ const carrying = profile.configs
486
+ .filter((c) => c.state === "token-present")
487
+ .map((c) => c.path);
488
+ const scope = "inspected .mcp.json, .vscode/mcp.json, .cursor/mcp.json only " +
489
+ "(Codex and Copilot host configuration is not inspected)";
490
+ if (carrying.length > 0) {
491
+ return {
492
+ ...base,
493
+ status: "ok",
494
+ detail: `\`${profile.token}\` present in ${carrying.join(", ")}; ${scope}`,
495
+ };
496
+ }
497
+ // "No entry anywhere" and "an entry without the token" are different findings
498
+ // with different next steps, so they never collapse into one remediation.
499
+ const detail = profile.bridgeEntryPresent
500
+ ? `no supported host config carries the \`${profile.token}\` token; ${scope}`
501
+ : `no supported host config carries a Bridge entry; ${scope}`;
502
+ const remediation = profile.bridgeEntryPresent
503
+ ? CONDUCTOR_PROFILE_TOKEN_REMEDIATION
504
+ : CONDUCTOR_PROFILE_TOKEN_NO_ENTRY_REMEDIATION;
505
+ return conductorContext
506
+ ? { ...base, status: "degraded", detail, remediation }
507
+ : { ...base, status: "ok", detail: `advisory — ${detail}`, remediation };
508
+ }
509
+ /**
510
+ * Managed conductor hook targets, DERIVED from `legacyConductor.git_hooks`.
511
+ *
512
+ * Derived, never re-probed: a second parse of the same hook files is a second
513
+ * chance to disagree with the report embedded right below this section. Healthy
514
+ * requires each managed hook to exist, carry the managed block, and point at an
515
+ * embedded binary that stats as a regular file — a hook whose target vanished is
516
+ * a dead hook, and reporting it as installed is the exact failure BAPI-772 made
517
+ * structurally impossible at INSTALL time but which can still arise later.
518
+ *
519
+ * Renders hook NAME and boolean state only. The embedded absolute binary path is
520
+ * read for the file check and never copied into this section.
521
+ */
522
+ export function conductorHookTargetsSection(legacyConductor, conductorContext) {
523
+ const base = { id: "conductor-hook-targets", label: "Conductor hook targets" };
524
+ // Nullish, not `=== null`: an injected builder may resolve `undefined`, and a
525
+ // doctor that throws on it would abort the run it exists to diagnose.
526
+ if (legacyConductor === null || legacyConductor === undefined) {
527
+ return {
528
+ ...base,
529
+ status: "degraded",
530
+ detail: "hook targets could not be derived (the conductor doctor was not collected)",
531
+ remediation: CONDUCTOR_HOOK_TARGET_REMEDIATION,
532
+ };
533
+ }
534
+ // Defensive for the same reason as {@link detectConductorContext}: an injected
535
+ // sentinel need not carry every legacy field.
536
+ const hooks = legacyConductor.git_hooks;
537
+ if (hooks === undefined) {
538
+ return {
539
+ ...base,
540
+ status: "degraded",
541
+ detail: "hook targets could not be derived (the conductor doctor reported no hook state)",
542
+ remediation: CONDUCTOR_HOOK_TARGET_REMEDIATION,
543
+ };
544
+ }
545
+ if (!hooks.is_worktree) {
546
+ return {
547
+ ...base,
548
+ status: "ok",
549
+ detail: "advisory — not a git worktree, so no managed conductor hooks apply",
550
+ };
551
+ }
552
+ const usable = (name) => {
553
+ const hook = (hooks.hooks ?? []).find((h) => h.name === name);
554
+ return (hook !== undefined &&
555
+ hook.exists &&
556
+ hook.managed_block_present &&
557
+ hook.embedded_bin_is_file);
558
+ };
559
+ const rendered = MANAGED_HOOK_NAMES.map((name) => `${name}=${usable(name)}`).join(", ");
560
+ const allUsable = MANAGED_HOOK_NAMES.every((name) => usable(name));
561
+ if (allUsable) {
562
+ return { ...base, status: "ok", detail: `managed hooks usable: ${rendered}` };
563
+ }
564
+ const detail = `managed hooks usable: ${rendered}`;
565
+ return conductorContext
566
+ ? { ...base, status: "degraded", detail, remediation: CONDUCTOR_HOOK_TARGET_REMEDIATION }
567
+ : {
568
+ ...base,
569
+ status: "ok",
570
+ detail: `advisory — ${detail}`,
571
+ remediation: CONDUCTOR_HOOK_TARGET_REMEDIATION,
572
+ };
573
+ }
574
+ /**
575
+ * Native ledger loadability, DERIVED from `legacyConductor.native_ledger`.
576
+ *
577
+ * No parallel `better-sqlite3` check: the mapping from an inspection to
578
+ * available/degraded lives in one place ({@link describeNativeLedgerAvailability}),
579
+ * so this section and the conductor doctor can never disagree about whether the
580
+ * binding loads. A degraded result carries the module name, the ABI, and the
581
+ * sanitized failure kind, because the common cause is npm silently skipping an
582
+ * optionalDependency build and that is undiagnosable without the ABI.
583
+ */
584
+ export function conductorLedgerLoadabilitySection(legacyConductor) {
585
+ const base = {
586
+ id: "conductor-ledger-loadability",
587
+ label: "Conductor ledger loadability",
588
+ };
589
+ if (legacyConductor === null || legacyConductor === undefined) {
590
+ return {
591
+ ...base,
592
+ status: "degraded",
593
+ detail: "ledger loadability could not be derived (the conductor doctor was not collected)",
594
+ remediation: CONDUCTOR_LEDGER_LOADABILITY_REMEDIATION,
595
+ };
596
+ }
597
+ const inspection = legacyConductor.native_ledger;
598
+ if (inspection === undefined) {
599
+ return {
600
+ ...base,
601
+ status: "degraded",
602
+ detail: "ledger loadability could not be derived (the conductor doctor reported no native-ledger state)",
603
+ remediation: CONDUCTOR_LEDGER_LOADABILITY_REMEDIATION,
604
+ };
605
+ }
606
+ const availability = describeNativeLedgerAvailability(inspection);
607
+ if (availability.status === "available") {
608
+ return { ...base, status: "ok", detail: "ledger available — the native binding loads" };
609
+ }
610
+ return {
611
+ ...base,
612
+ status: "degraded",
613
+ detail: `ledger degraded — ${availability.module} did not load ` +
614
+ `(NODE_MODULE_VERSION ${availability.nodeModulesAbi}, failure: ${availability.failureKind})`,
615
+ remediation: CONDUCTOR_LEDGER_LOADABILITY_REMEDIATION,
616
+ };
617
+ }
618
+ // ---------------------------------------------------------------------------
619
+ // Runner
620
+ // ---------------------------------------------------------------------------
621
+ /**
622
+ * BAPI-941: the workflow's local state AND, when the seam is supplied and the
623
+ * file is genuinely present, its lineage relative to the default branch. The
624
+ * drift detail is bounded text from the shared classifier — never git stderr and
625
+ * never file content.
626
+ */
627
+ export async function inspectWorkflowPresence(readWorkflowFile, classifyWorkflowDrift) {
628
+ const local = await inspectWorkflowLocalPresence(readWorkflowFile);
629
+ if (local !== "present" || !classifyWorkflowDrift)
630
+ return { presence: local };
631
+ try {
632
+ const classification = await classifyWorkflowDrift();
633
+ if (classification.state === "drifted") {
634
+ return {
635
+ presence: "drifted",
636
+ driftDetail: summarizeClaudeReviewWorkflowDrift(classification),
637
+ };
638
+ }
639
+ if (classification.state === "unverified") {
640
+ // Inconclusive, so the presence state stays `present` and non-failing. The
641
+ // uncertainty is still reported, through the existing detail line rather
642
+ // than by inventing a failure the probe did not establish.
643
+ return {
644
+ presence: "present",
645
+ driftDetail: summarizeClaudeReviewWorkflowDrift(classification),
646
+ };
647
+ }
648
+ return { presence: "present" };
649
+ }
650
+ catch {
651
+ // A thrown probe is an unavailable comparison, not a fault in the workflow.
652
+ return { presence: "present" };
653
+ }
654
+ }
655
+ export async function inspectWorkflowLocalPresence(readWorkflowFile) {
656
+ try {
657
+ await readWorkflowFile();
658
+ return "present";
659
+ }
660
+ catch (err) {
661
+ // ENOENT is the ordinary "absent" case; anything else is "unreadable". The
662
+ // error's message is never surfaced — it can echo an absolute path.
663
+ const code = err?.code;
664
+ return code === "ENOENT" ? "absent" : "unreadable";
665
+ }
666
+ }