@bridge_gpt/mcp-server 0.2.58 → 0.2.60

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 (127) hide show
  1. package/README.md +231 -113
  2. package/build/agent-guidance.generated.js +1 -1
  3. package/build/agent-notes.js +7 -1
  4. package/build/agent-registry.js +15 -0
  5. package/build/agent-utils.js +16 -8
  6. package/build/claude-user-config-doctor.js +1 -35
  7. package/build/codex-bridge-profile.js +1 -1
  8. package/build/commands.generated.js +7 -8
  9. package/build/conductor/bridge-api-client.js +202 -2
  10. package/build/conductor/bring-up-facts.js +1 -1
  11. package/build/conductor/claude-hook.js +16 -16
  12. package/build/conductor/doctor.js +279 -72
  13. package/build/conductor/epic-implementer-cli.js +38 -5
  14. package/build/conductor/epic-reconcile.js +1 -1
  15. package/build/conductor/epic-runtime.js +21 -12
  16. package/build/conductor/errors.js +3 -3
  17. package/build/conductor/file-scope-guard.js +4 -4
  18. package/build/conductor/git-ci-types.js +1 -1
  19. package/build/conductor/git-inspection.js +2 -2
  20. package/build/conductor/install-doctor.js +28 -6
  21. package/build/conductor/local-merge.js +319 -26
  22. package/build/conductor/merge-execution-contract.js +61 -0
  23. package/build/conductor/merge-execution-eligibility.js +129 -0
  24. package/build/conductor/merge-ledger.js +2 -2
  25. package/build/conductor/pr-ci-producer.js +6 -6
  26. package/build/conductor/pr-review-producer.js +2 -2
  27. package/build/conductor/producer-ledger.js +2 -0
  28. package/build/conductor/readiness-sections.js +166 -9
  29. package/build/conductor/readiness.js +97 -16
  30. package/build/conductor/recovery-cli.js +243 -12
  31. package/build/conductor/recovery-operations.js +64 -6
  32. package/build/conductor/repair-contract.js +38 -4
  33. package/build/conductor/spec-review-producer.js +6 -6
  34. package/build/conductor/store.js +10 -10
  35. package/build/conductor/supervisor-config.js +14 -14
  36. package/build/conductor/supervisor-ledger.js +2 -2
  37. package/build/conductor/supervisor-message-relay.js +2 -2
  38. package/build/conductor/worker-ledger-cli.js +19 -19
  39. package/build/conductor-bundle-artifacts.js +2 -2
  40. package/build/connect-bitbucket-api.js +6 -1
  41. package/build/connect-bitbucket.js +398 -88
  42. package/build/credential-store.js +16 -0
  43. package/build/docs.generated.js +2 -2
  44. package/build/doctor.js +199 -303
  45. package/build/drive-epic.js +88 -71
  46. package/build/epic-implementer-bin.js +22 -17
  47. package/build/epic-implementer-claude-hook-bin.js +4 -2
  48. package/build/epic-implementer-env.js +184 -0
  49. package/build/epic-integration-pr.js +2 -2
  50. package/build/executor/ci-rerun-job.js +269 -0
  51. package/build/executor/cli.js +31 -0
  52. package/build/executor/credentials.js +11 -0
  53. package/build/executor/env.js +79 -8
  54. package/build/executor/heartbeat.js +1 -1
  55. package/build/executor/index.js +21 -4
  56. package/build/executor/job-errors.js +3 -3
  57. package/build/executor/job-runner.js +127 -1
  58. package/build/executor/job-types.js +12 -2
  59. package/build/executor/merge-job.js +46 -5
  60. package/build/executor/observation.js +1 -1
  61. package/build/executor/preflight.js +1 -1
  62. package/build/executor/prompt-spec.js +1 -1
  63. package/build/executor/reconcile-overlap-job.js +10 -130
  64. package/build/executor/reconcile-overlap-types.js +1 -1
  65. package/build/executor/runner.js +1 -1
  66. package/build/executor/service-lifecycle.js +136 -0
  67. package/build/executor/service-unit.js +427 -9
  68. package/build/executor/types.js +5 -0
  69. package/build/index.js +320 -342
  70. package/build/init.js +20 -28
  71. package/build/install-bridge-conductor.js +208 -15
  72. package/build/install-bridge.js +555 -37
  73. package/build/mcp-host-entry-adapter.js +6 -0
  74. package/build/mcp-host-targets.js +2 -0
  75. package/build/mcp-install-state.js +42 -4
  76. package/build/mcp-invoke.js +5 -5
  77. package/build/mcp-profile.js +2 -2
  78. package/build/mcp-provisioning.js +2 -2
  79. package/build/mcp-server-invocation.js +3 -3
  80. package/build/merge-pull-request.js +118 -10
  81. package/build/merge-workflow-codeowners.js +224 -0
  82. package/build/merge-workflow-config.js +133 -0
  83. package/build/merge-workflow-drift.js +66 -0
  84. package/build/merge-workflow-install.js +638 -0
  85. package/build/merge-workflow.js +572 -0
  86. package/build/model-routing-host.js +25 -0
  87. package/build/model-routing-preferences.js +99 -0
  88. package/build/pipelines.generated.js +10 -10
  89. package/build/{plan-epic-conductor-eligibility.js → plan-epic-implementer-eligibility.js} +18 -16
  90. package/build/plan-execution-ledger.js +8 -0
  91. package/build/plane/cli.js +24 -2
  92. package/build/plane/member-roster.js +5 -5
  93. package/build/plane/preflight.js +68 -0
  94. package/build/plane/supervisor.js +1 -1
  95. package/build/plane/types.js +5 -5
  96. package/build/readiness-check.js +1 -1
  97. package/build/readme.generated.js +1 -1
  98. package/build/review-tickets.js +4 -4
  99. package/build/run-unit-tests-launcher.js +5 -0
  100. package/build/setup-epic.js +171 -17
  101. package/build/start-tickets-conductor.js +32 -32
  102. package/build/start-tickets-prereqs.js +4 -9
  103. package/build/start-tickets.js +475 -91
  104. package/build/version.generated.js +3 -3
  105. package/docs/CONDUCTOR.md +69 -18
  106. package/docs/install/mcp-tool-integrations.md +2 -1
  107. package/package.json +4 -4
  108. package/pipelines/implement-ticket.json +1 -1
  109. package/pipelines/plan-epic.json +2 -2
  110. package/build/conduct-epic/bridge-client.js +0 -773
  111. package/build/conduct-epic/checkpoint-store.js +0 -542
  112. package/build/conduct-epic/cli.js +0 -3145
  113. package/build/conduct-epic/cut-protocol.js +0 -392
  114. package/build/conduct-epic/lock.js +0 -302
  115. package/build/conduct-epic/pr-state.js +0 -286
  116. package/build/conduct-epic/spawn.js +0 -113
  117. package/build/conductor/cli.js +0 -1278
  118. package/build/conductor-bundle-cli.js +0 -256
  119. package/build/direct-ticket-tools.js +0 -70
  120. package/build/executor/reconcile-overlap-governance.js +0 -129
  121. package/build/implement-epic/checkpoint-store.js +0 -542
  122. package/build/implement-epic/cli.js +0 -3158
  123. package/build/implement-epic/lock.js +0 -302
  124. package/build/implement-epic/pr-state.js +0 -286
  125. package/build/implement-epic/spawn.js +0 -113
  126. /package/build/{implement-epic → epic-shared}/bridge-client.js +0 -0
  127. /package/build/{implement-epic → epic-shared}/cut-protocol.js +0 -0
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Combined merge/review conductor-eligibility assessment for `plan-epic` (BAPI-964).
2
+ * Combined merge/review Epic Implementer eligibility assessment for `plan-epic` (BAPI-964).
3
3
  *
4
4
  * Runs once the epic decomposition's child manifest is frozen and every
5
5
  * sub-task's exploration text is written, so this module can predict — before
6
- * any conductor worker is spawned — which children are likely to trip the
6
+ * any Epic Implementer worker is spawned — which children are likely to trip the
7
7
  * runtime workflow-file merge guard (`api/library/vcs/epic_implementer_merge_service.py`)
8
8
  * and which of those additionally touch `claude-review.yml` itself, where
9
9
  * automated review cannot run at all (BAPI-941's supply-chain preflight refuses
@@ -26,17 +26,19 @@ import { CLAUDE_REVIEW_WORKFLOW_RELPATH } from "./claude-review-workflow-drift.j
26
26
  * Build the real network-backed classification client that POSTs the complete
27
27
  * child set to `/jira/planning/epic-workflow-eligibility` exactly once.
28
28
  */
29
- export function createBridgeEpicConductorEligibilityClient(deps) {
29
+ export function createBridgeEpicImplementerEligibilityClient(deps) {
30
30
  return {
31
31
  async classifyChildren(children) {
32
32
  const fetchImpl = deps.fetchImpl ?? fetch;
33
+ // S3 backend route seam: keep the existing endpoint until its API migration.
33
34
  const resp = await fetchImpl(deps.buildUrl("/planning/epic-workflow-eligibility"), {
34
35
  method: "POST",
35
36
  headers: await deps.getPostHeaders(),
36
37
  body: JSON.stringify({ repo_name: deps.repoName, children }),
37
38
  });
38
39
  if (!resp.ok) {
39
- throw new Error(`epic-workflow-eligibility classification request failed: ${resp.status}`);
40
+ const errorText = await deps.handleResponse(resp);
41
+ throw new Error(`epic-workflow-eligibility classification request failed: ${resp.status}: ${errorText}`);
40
42
  }
41
43
  return (await resp.json());
42
44
  },
@@ -92,7 +94,7 @@ function validateChildren(children) {
92
94
  return validated;
93
95
  }
94
96
  /**
95
- * Assess conductor merge/review eligibility for one frozen epic child set.
97
+ * Assess Epic Implementer merge/review eligibility for one frozen epic child set.
96
98
  *
97
99
  * Sends the complete ordered child set to the injected Bridge client exactly
98
100
  * once. Never throws — every failure path (malformed input, client rejection,
@@ -100,10 +102,10 @@ function validateChildren(children) {
100
102
  * to the explicit `unavailable` result, diagnosed with `console.error` (never
101
103
  * `console.log`, which is reserved for the MCP stdio transport).
102
104
  */
103
- export async function assessEpicConductorEligibility(children, deps) {
105
+ export async function assessEpicImplementerEligibility(children, deps) {
104
106
  const validated = validateChildren(children);
105
107
  if (validated === null) {
106
- console.error("assessEpicConductorEligibility: malformed child collection; unavailable.");
108
+ console.error("assessEpicImplementerEligibility: malformed child collection; unavailable.");
107
109
  return UNAVAILABLE;
108
110
  }
109
111
  let response;
@@ -111,7 +113,7 @@ export async function assessEpicConductorEligibility(children, deps) {
111
113
  response = await deps.client.classifyChildren(validated.map((child) => ({ child_id: child.id, combined_text: child.combinedText })));
112
114
  }
113
115
  catch (err) {
114
- console.error("assessEpicConductorEligibility: Bridge classification call failed:", err);
116
+ console.error("assessEpicImplementerEligibility: Bridge classification call failed:", err);
115
117
  return UNAVAILABLE;
116
118
  }
117
119
  if (response === null ||
@@ -124,11 +126,11 @@ export async function assessEpicConductorEligibility(children, deps) {
124
126
  !Number.isInteger(response.containment_children) ||
125
127
  response.containment_children < 0 ||
126
128
  typeof response.reason !== "string") {
127
- console.error("assessEpicConductorEligibility: malformed backend response shape; unavailable.");
129
+ console.error("assessEpicImplementerEligibility: malformed backend response shape; unavailable.");
128
130
  return UNAVAILABLE;
129
131
  }
130
132
  if (response.predictions.length !== validated.length) {
131
- console.error("assessEpicConductorEligibility: backend prediction count does not match child count; unavailable.");
133
+ console.error("assessEpicImplementerEligibility: backend prediction count does not match child count; unavailable.");
132
134
  return UNAVAILABLE;
133
135
  }
134
136
  // The backend is contractually order-preserving; still verify identity and
@@ -143,16 +145,16 @@ export async function assessEpicConductorEligibility(children, deps) {
143
145
  !isStringArray(prediction.matched_paths) ||
144
146
  typeof prediction.predicted_containment_hazard !== "boolean" ||
145
147
  !isStringArray(prediction.matched_hazards)) {
146
- console.error("assessEpicConductorEligibility: malformed per-child prediction; unavailable.");
148
+ console.error("assessEpicImplementerEligibility: malformed per-child prediction; unavailable.");
147
149
  return UNAVAILABLE;
148
150
  }
149
151
  if (prediction.predicted_containment_hazard !== (prediction.matched_hazards.length > 0)) {
150
- console.error(`assessEpicConductorEligibility: child ${prediction.child_id} has a containment flag that ` +
152
+ console.error(`assessEpicImplementerEligibility: child ${prediction.child_id} has a containment flag that ` +
151
153
  "disagrees with whether matched hazard evidence is present; unavailable.");
152
154
  return UNAVAILABLE;
153
155
  }
154
156
  if (predictionById.has(prediction.child_id)) {
155
- console.error("assessEpicConductorEligibility: duplicate child_id in backend response; unavailable.");
157
+ console.error("assessEpicImplementerEligibility: duplicate child_id in backend response; unavailable.");
156
158
  return UNAVAILABLE;
157
159
  }
158
160
  predictionById.set(prediction.child_id, prediction);
@@ -164,7 +166,7 @@ export async function assessEpicConductorEligibility(children, deps) {
164
166
  for (const child of validated) {
165
167
  const prediction = predictionById.get(child.id);
166
168
  if (!prediction) {
167
- console.error(`assessEpicConductorEligibility: backend response is missing child ${child.id}; unavailable.`);
169
+ console.error(`assessEpicImplementerEligibility: backend response is missing child ${child.id}; unavailable.`);
168
170
  return UNAVAILABLE;
169
171
  }
170
172
  const reviewPathReferenced = child.combinedText.includes(CLAUDE_REVIEW_WORKFLOW_RELPATH);
@@ -172,7 +174,7 @@ export async function assessEpicConductorEligibility(children, deps) {
172
174
  // Subset invariant violated: a child naming claude-review.yml itself must
173
175
  // also be in the broader merge-blocked set. Publishing this as a
174
176
  // "review-only" child would contradict the merge count, so refuse instead.
175
- console.error(`assessEpicConductorEligibility: child ${child.id} references the review workflow ` +
177
+ console.error(`assessEpicImplementerEligibility: child ${child.id} references the review workflow ` +
176
178
  "but the backend did not mark it merge-blocked; refusing contradictory result.");
177
179
  return UNAVAILABLE;
178
180
  }
@@ -197,7 +199,7 @@ export async function assessEpicConductorEligibility(children, deps) {
197
199
  });
198
200
  }
199
201
  if (containmentChildren !== response.containment_children) {
200
- console.error("assessEpicConductorEligibility: containment_children count disagrees with the validated " +
202
+ console.error("assessEpicImplementerEligibility: containment_children count disagrees with the validated " +
201
203
  "per-child containment flags; unavailable.");
202
204
  return UNAVAILABLE;
203
205
  }
@@ -43,6 +43,10 @@ export const PLAN_PROVENANCE_CLASSES = [
43
43
  "rendered_ui_review",
44
44
  "test_gap_review",
45
45
  "final_plan_review",
46
+ // BAPI-1223: the optimized posture's combined review/test stage. ONE class, not
47
+ // a test/review pair, because a single agent authors one contiguous body with
48
+ // interleaved test and review steps plus its deterministic floor repairs.
49
+ "combined_review_test",
46
50
  ];
47
51
  export const PLAN_PHASES = [
48
52
  "produce",
@@ -107,6 +111,10 @@ export const PLAN_CLASS_OWNERSHIP = Object.freeze({
107
111
  rendered_ui_review: "pre_pr_verification",
108
112
  test_gap_review: "pre_pr_verification",
109
113
  final_plan_review: "pre_pr_verification",
114
+ // BAPI-1223: mixed test and review work in one body. Both halves are settled
115
+ // by the phase that RUNS them, which is the same phase that owns the separate
116
+ // classes this one replaces — so the route change moves no ownership.
117
+ combined_review_test: "pre_pr_verification",
110
118
  });
111
119
  export const PRE_PR_VERIFICATION_LIMITS = Object.freeze({
112
120
  maxCorrectionTurns: 3,
@@ -469,6 +469,27 @@ async function runUpAction(options, sinks, overrides) {
469
469
  }
470
470
  return launch.exitCode;
471
471
  }
472
+ /**
473
+ * Format one diagnostic as one or more sink lines (BAPI-1237).
474
+ *
475
+ * `formatDiagnostic` returns a single string, but a diagnostic's `message` can
476
+ * itself be multiline — the shared legacy-environment guard's message is its
477
+ * own fixed header plus one `key -> successor` line per offending key. Writing
478
+ * that whole string through a single sink call would bury every line after the
479
+ * first inside one write, so this splits on newline boundaries and lets each
480
+ * substantive line become its own element — preserving the deterministic order
481
+ * the guard already produced.
482
+ *
483
+ * The final EMPTY element is dropped, never any other: it exists only because
484
+ * `runLegacyEpicImplementerEnvPreflight`'s formatter ends its block with a
485
+ * trailing newline, which is block punctuation, not a line to render.
486
+ */
487
+ function formatDiagnosticLines(diagnostic) {
488
+ const lines = formatDiagnostic(diagnostic).split("\n");
489
+ if (lines.length > 0 && lines[lines.length - 1].length === 0)
490
+ lines.pop();
491
+ return lines;
492
+ }
472
493
  /**
473
494
  * The aggregate preflight refusal, as lines.
474
495
  *
@@ -482,7 +503,7 @@ export function formatPlanePreflightRefusal(preflight) {
482
503
  "plane up REFUSED — preflight found blocking problems:",
483
504
  ...preflight.diagnostics
484
505
  .filter((d) => d.severity === "blocking")
485
- .map((d) => formatDiagnostic(d)),
506
+ .flatMap((d) => formatDiagnosticLines(d)),
486
507
  "",
487
508
  `Fix every item above and retry. ${PLANE_NOTHING_STARTED}.`,
488
509
  ];
@@ -792,7 +813,8 @@ async function runRuntimeAction(options, overrides) {
792
813
  sinks.stderr("");
793
814
  sinks.stderr("plane runtime REFUSED — preflight found blocking problems:");
794
815
  for (const diagnostic of preflight.diagnostics.filter((d) => d.severity === "blocking")) {
795
- sinks.stderr(formatDiagnostic(diagnostic));
816
+ for (const line of formatDiagnosticLines(diagnostic))
817
+ sinks.stderr(line);
796
818
  }
797
819
  sinks.stderr("");
798
820
  sinks.stderr(`${PLANE_NOTHING_STARTED}.`);
@@ -13,7 +13,7 @@
13
13
  * - the dead-man observer is a SEPARATE member in its own OS process (R-1,
14
14
  * BAPI-1029): the plane contains exactly one ordinary worker and exactly one
15
15
  * dead-man-only observer, and only the observer's environment carries
16
- * `CONDUCTOR_DEAD_MAN_ONLY`. The two are built from distinct environment
16
+ * `EPIC_IMPLEMENTER_DEAD_MAN_ONLY`. The two are built from distinct environment
17
17
  * objects so the observer flag can never reach the worker by reference —
18
18
  * a plane whose "worker" is a second observer has nothing reconciling it.
19
19
  * Independence here is process-level on ONE host; separate-host isolation is
@@ -132,7 +132,7 @@ export function resolvePlaneServerEndpoint(env) {
132
132
  * Build a child environment: a copy of the parent plus the resolved values the
133
133
  * existing processes already expect.
134
134
  *
135
- * `CONDUCTOR_DEAD_MAN_ONLY` is explicitly deleted rather than merely left
135
+ * `EPIC_IMPLEMENTER_DEAD_MAN_ONLY` is explicitly deleted rather than merely left
136
136
  * unset. An operator who exported it in this shell to run an observer would
137
137
  * otherwise silently turn the plane's reconciler worker into a second observer,
138
138
  * and the run would sit there with nothing reconciling it.
@@ -146,7 +146,7 @@ export function buildPlaneChildEnv(parentEnv, context) {
146
146
  env.BAPI_REPO_NAME = context.repoName;
147
147
  env.BAPI_BASE_URL = context.endpoint.baseUrl;
148
148
  env.BAPI_API_KEY = context.bridgeApiKey;
149
- delete env.CONDUCTOR_DEAD_MAN_ONLY;
149
+ delete env[PLANE_OBSERVER_MODE_ENV];
150
150
  return env;
151
151
  }
152
152
  /**
@@ -159,7 +159,7 @@ export function buildPlaneChildEnv(parentEnv, context) {
159
159
  * the precise failure `buildPlaneChildEnv`'s `delete` exists to prevent, arrived
160
160
  * at from the other direction.
161
161
  *
162
- * Any `CONDUCTOR_DEADMAN_CHANNEL_TYPE` / `CONDUCTOR_DEADMAN_DESTINATION_REF` the
162
+ * Any `EPIC_IMPLEMENTER_DEADMAN_CHANNEL_TYPE` / `EPIC_IMPLEMENTER_DEADMAN_DESTINATION_REF` the
163
163
  * operator exported is inherited through the copy and is never read, echoed,
164
164
  * placed in argv, or written to the manifest here: the observer resolves its own
165
165
  * alert channel from its environment, exactly as a hand-started observer does.
@@ -251,7 +251,7 @@ export function buildPlaneMemberRoster(params) {
251
251
  * The independent dead-man observer (R-1, BAPI-1029).
252
252
  *
253
253
  * The SAME `worker.py` the ordinary worker runs, in its own OS process, with
254
- * `CONDUCTOR_DEAD_MAN_ONLY=true` in its environment and nowhere else. No shell
254
+ * `EPIC_IMPLEMENTER_DEAD_MAN_ONLY=true` in its environment and nowhere else. No shell
255
255
  * string: `PLANE_OBSERVER_COMMAND` remains operator-facing prose while the
256
256
  * spawn is an executable plus an argument vector, so the mode flag never
257
257
  * passes through a shell that could log it or reorder it.
@@ -13,6 +13,7 @@
13
13
  */
14
14
  import path from "path";
15
15
  import { PLANE_SERVER_PORT_ENV_VAR, planeCheckFinding, planeCheckPassed, } from "./types.js";
16
+ import { runLegacyEpicImplementerEnvPreflight } from "../epic-implementer-env.js";
16
17
  import { checkPlaneBuildFreshness, checkPlaneRuntimeEntrypoint } from "./build-freshness.js";
17
18
  import { checkAlembicHead } from "./alembic-head.js";
18
19
  import { manifestHasLiveProcess, planeManifestLifecycle, probeManifestMembers, readPlaneManifest, } from "./manifest.js";
@@ -163,6 +164,50 @@ const ROOT_DEPENDENT_CHECKS = [
163
164
  ["alembic-head", "alembic-head", "Database migration head"],
164
165
  ["existing-plane", "existing-plane", "Existing plane"],
165
166
  ];
167
+ /** Fixed detail for a prerequisite skipped because the legacy-environment guard refused first. */
168
+ const PLANE_LEGACY_ENVIRONMENT_SKIP_DETAIL = "not run — the legacy-environment guard refused first";
169
+ /**
170
+ * Every prerequisite `runPlanePreflight` short-circuits past when the shared
171
+ * legacy-environment guard finds an old name (BAPI-1237).
172
+ *
173
+ * `repository-root` is included here, unlike {@link ROOT_DEPENDENT_CHECKS}: the
174
+ * legacy-environment guard is the FIRST check, running even before repository
175
+ * root is confirmed, so a hit skips everything else the same way a bad root
176
+ * skips everything after it.
177
+ */
178
+ const LEGACY_ENVIRONMENT_DEPENDENT_CHECKS = [
179
+ ["repository-root", "repository-root", "Repository root"],
180
+ ...ROOT_DEPENDENT_CHECKS,
181
+ ];
182
+ /** Fixed remediation for a legacy Conductor / dead-man environment variable still set. */
183
+ export const PLANE_LEGACY_ENVIRONMENT_REMEDIATION = "rename or remove each legacy variable named above to its EPIC_IMPLEMENTER_* successor, then retry.";
184
+ /**
185
+ * Run the shared legacy Epic Implementer / dead-man environment guard as a
186
+ * plane preflight check (BAPI-1237).
187
+ *
188
+ * Key-only and non-mutating: this adapter never reads a legacy value,
189
+ * translates a key, deletes a key, or constructs a successor value from a
190
+ * supplied value. It only asks the shared guard — the same one `epic-implementer-env.ts`
191
+ * exports for every other guarded entry point — whether any old name is
192
+ * present in the INJECTED preflight environment, and renders its one fixed
193
+ * multiline diagnostic as a blocking finding when it is.
194
+ */
195
+ function checkLegacyEnvironment(env) {
196
+ const label = "Legacy environment";
197
+ let captured = "";
198
+ const hit = runLegacyEpicImplementerEnvPreflight(env, (chunk) => {
199
+ captured += chunk;
200
+ });
201
+ if (hit === 0) {
202
+ return planeCheckPassed("legacy-environment", label, "no legacy Conductor or dead-man environment variable is set");
203
+ }
204
+ return planeCheckFinding({
205
+ check: "legacy-environment",
206
+ severity: "blocking",
207
+ message: captured,
208
+ remediation: PLANE_LEGACY_ENVIRONMENT_REMEDIATION,
209
+ }, label, "a legacy Conductor or dead-man environment variable is still set");
210
+ }
166
211
  /** Bound on the port probe so a black-holed port cannot stall bring-up. */
167
212
  export const PLANE_PORT_PROBE_TIMEOUT_MS = 1_500;
168
213
  /**
@@ -188,6 +233,23 @@ export async function runPlanePreflight(repoRoot, deps) {
188
233
  outcomes.push(...report.outcomes);
189
234
  return report;
190
235
  };
236
+ // BAPI-1237: the shared legacy-environment guard runs FIRST — before
237
+ // repository inspection, filesystem setup, subprocess execution, port
238
+ // probing, or credential resolution — so a legacy Conductor/dead-man name
239
+ // blocks before any of those collaborators run, not merely before launch.
240
+ const legacyEnvironment = record(checkLegacyEnvironment(deps.env));
241
+ if (legacyEnvironment.diagnostic) {
242
+ for (const [check, id, label] of LEGACY_ENVIRONMENT_DEPENDENT_CHECKS) {
243
+ outcomes.push({
244
+ check,
245
+ id,
246
+ label,
247
+ status: "skip",
248
+ detail: PLANE_LEGACY_ENVIRONMENT_SKIP_DETAIL,
249
+ });
250
+ }
251
+ return { ok: false, diagnostics, outcomes };
252
+ }
191
253
  const rootCheck = record(await checkRepositoryRoot(repoRoot, deps));
192
254
  // Every remaining check is relative to the repository root. Without a valid
193
255
  // one there is nothing coherent left to check, so report and stop here rather
@@ -803,6 +865,12 @@ async function fileExists(filePath, fs) {
803
865
  * yield every prerequisite as an explicit unknown.
804
866
  */
805
867
  export const PLANE_READINESS_DESCRIPTORS = [
868
+ // BAPI-1237 — the shared legacy-environment guard is the first check
869
+ // `runPlanePreflight` runs, so it leads this descriptor table too. Omitting
870
+ // it here would make the check invisible to `epic-implementer readiness` /
871
+ // `drive-epic` even though it can block `plane up` — exactly the "silence
872
+ // reads as pass" failure this table exists to rule out.
873
+ { id: "legacy-environment", label: "Legacy environment" },
806
874
  { id: "repository-root", label: "Repository root" },
807
875
  { id: "worktree-presence", label: "Git work tree" },
808
876
  { id: "claude-login", label: "Claude login" },
@@ -106,7 +106,7 @@ export function createLineReader(onLine) {
106
106
  * Rebuild the instance identity a spawned member publishes under.
107
107
  *
108
108
  * Mirrors `_resolve_process_instance_id` in
109
- * `api/library/epic_conductor/reconciler.py` exactly, because the string has to
109
+ * `api/library/epic_implementer/reconciler.py` exactly, because the string has to
110
110
  * MATCH: `DYNO` when that variable is set and non-blank after trimming,
111
111
  * otherwise `<hostname>:<pid>`, truncated to 128 characters. Python resolves it
112
112
  * once at import from the process's own environment and pid; this rebuilds it
@@ -12,7 +12,7 @@
12
12
  *
13
13
  * - **R-1, the dead-man observer is a same-host, separate-process member**
14
14
  * (BAPI-1029, superseding R-3). The observer runs `worker.py` with
15
- * `CONDUCTOR_DEAD_MAN_ONLY=true` in its OWN OS process, owned by the plane
15
+ * `EPIC_IMPLEMENTER_DEAD_MAN_ONLY=true` in its OWN OS process, owned by the plane
16
16
  * lifecycle: it has a member name, a log path, and a manifest record, and
17
17
  * `plane status` / `plane down` treat it like every other member. Its failure
18
18
  * domain is independent at the PROCESS level — killing the reconciler leaves
@@ -143,15 +143,15 @@ export const PLANE_ID_ENV_VAR = "BAPI_PLANE_ID";
143
143
  * session-background FALLBACK still starts the observer by hand, and because an
144
144
  * operator debugging the plane needs to know exactly what the observer member is.
145
145
  */
146
- export const PLANE_OBSERVER_COMMAND = "CONDUCTOR_DEAD_MAN_ONLY=true python worker.py";
146
+ export const PLANE_OBSERVER_COMMAND = "EPIC_IMPLEMENTER_DEAD_MAN_ONLY=true python worker.py";
147
147
  /** Env var that puts `worker.py` into dead-man-only mode. Observer member ONLY. */
148
- export const PLANE_OBSERVER_MODE_ENV = "CONDUCTOR_DEAD_MAN_ONLY";
148
+ export const PLANE_OBSERVER_MODE_ENV = "EPIC_IMPLEMENTER_DEAD_MAN_ONLY";
149
149
  /** The exact value {@link PLANE_OBSERVER_MODE_ENV} is set to for the observer. */
150
150
  export const PLANE_OBSERVER_MODE_VALUE = "true";
151
151
  /** Env var naming the observer's alert channel type. */
152
- export const PLANE_OBSERVER_CHANNEL_TYPE_ENV = "CONDUCTOR_DEADMAN_CHANNEL_TYPE";
152
+ export const PLANE_OBSERVER_CHANNEL_TYPE_ENV = "EPIC_IMPLEMENTER_DEADMAN_CHANNEL_TYPE";
153
153
  /** Env var naming the env var that holds the observer's destination URL. */
154
- export const PLANE_OBSERVER_DESTINATION_ENV = "CONDUCTOR_DEADMAN_DESTINATION_REF";
154
+ export const PLANE_OBSERVER_DESTINATION_ENV = "EPIC_IMPLEMENTER_DEADMAN_DESTINATION_REF";
155
155
  /** Build a {@link PlaneCheckReport} for a check that ran and passed. */
156
156
  export function planeCheckPassed(check, label, detail) {
157
157
  return {
@@ -147,7 +147,7 @@ const POSIX_PATH_ROOTS = [
147
147
  * Deliberately narrow: it catches the obvious leak (`/Users/...`, `/private/tmp/...`,
148
148
  * `C:\\...`) without rejecting the relative, repository-conventional paths
149
149
  * remediations legitimately name (`.github/workflows/claude-review.yml`,
150
- * `docs/claude/epic-conductor-v2-operator-runbook.md`) or the approved setup
150
+ * `docs/claude/epic-implementer-v2-operator-runbook.md`) or the approved setup
151
151
  * URLs (`https://host/setup`) — note the Windows drive alternative requires a
152
152
  * word boundary before the letter, so the `s:/` inside `https://` is not a hit.
153
153
  */