@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
@@ -12,8 +12,8 @@
12
12
  *
13
13
  * Optionally supports a supervisor-driven epic-dispatch path: when an
14
14
  * `EpicDispatchIdentity` is threaded through `ReviewTicketsOptions.epic`, each
15
- * spawned tab receives an epic-identity env block (BAPI_CONDUCTOR_EPIC_KEY /
16
- * BAPI_CONDUCTOR_EPIC_RUN_ID / BAPI_CONDUCTOR_PLAN_VERSION / BAPI_CONDUCTOR_RUN_ID)
15
+ * spawned tab receives an epic-identity env block (BAPI_EPIC_IMPLEMENTER_EPIC_KEY /
16
+ * BAPI_EPIC_IMPLEMENTER_EPIC_RUN_ID / BAPI_EPIC_IMPLEMENTER_PLAN_VERSION / BAPI_EPIC_IMPLEMENTER_RUN_ID)
17
17
  * and dispatch is made idempotent via an injected `claimEpicDispatch` callback.
18
18
  * This path is worktree-free and base-branch-free; the human CLI path leaves
19
19
  * `epic` unset and retains byte-for-byte identical behavior.
@@ -518,7 +518,7 @@ export function buildReviewPlanRows(deps, options, agent, status, overrides = {}
518
518
  if (options.epic) {
519
519
  const mint = overrides.mintRunId ?? mintStartTicketsRunId;
520
520
  runId = mint([key]);
521
- const env = { ...buildEpicIdentityEnv(options.epic), BAPI_CONDUCTOR_RUN_ID: runId };
521
+ const env = { ...buildEpicIdentityEnv(options.epic), BAPI_EPIC_IMPLEMENTER_RUN_ID: runId };
522
522
  command = injectConductorEnvIntoShellCommand(deps.platform, baseCommand, env);
523
523
  }
524
524
  return {
@@ -599,7 +599,7 @@ export async function orchestrateReviewTickets(deps, options, overrides = {}) {
599
599
  if (options.epic) {
600
600
  const mint = overrides.mintRunId ?? mintStartTicketsRunId;
601
601
  runId = mint([key]);
602
- const env = { ...buildEpicIdentityEnv(options.epic), BAPI_CONDUCTOR_RUN_ID: runId };
602
+ const env = { ...buildEpicIdentityEnv(options.epic), BAPI_EPIC_IMPLEMENTER_RUN_ID: runId };
603
603
  shellCommand = injectConductorEnvIntoShellCommand(deps.platform, baseShellCommand, env);
604
604
  if (options.epic.dispatch_key) {
605
605
  if (!overrides.claimEpicDispatch) {
@@ -65,6 +65,11 @@ export const MODULE_MOCK_MANIFEST = [
65
65
  "build/conductor/store-lifecycle.test.js",
66
66
  "build/conductor/store-queries.test.js",
67
67
  "build/conductor/tools.test.js",
68
+ // BAPI-1247: drives offerBitbucketConnection to a chosen structured outcome by
69
+ // mocking the shared connect flow at install-bridge.ts's import site, which needs
70
+ // --experimental-test-module-mocks. Kept apart from install-bridge.test.js so that
71
+ // 346-test file stays in the NORMAL lane.
72
+ "build/connect-bitbucket-handoff.test.js",
68
73
  "build/connect-github-api.test.js",
69
74
  "build/connect-github-dispatch.static.test.js",
70
75
  "build/connect-github-handoff.test.js",
@@ -78,7 +78,14 @@
78
78
  import { readFile as fsReadFile, stat as fsStat } from "node:fs/promises";
79
79
  import os from "node:os";
80
80
  import { createProductionEpicIntegrationGhRunner, ensureEpicIntegrationPullRequest, formatEpicIntegrationPullRequestOutcome, } from "./epic-integration-pr.js";
81
- import { approveEpicPlan, createEpicRunWithDisposition, fetchConductorReadiness, fetchEpicRunState, replaceEpicRunPolicy, resolveConductorBridgeApiAccess, storeEpicPlan, validateEpicPlan, ConductorBridgeApiError, } from "./conductor/bridge-api-client.js";
81
+ import { approveEpicPlan, buildConductorJiraUrl, conductorGetHeaders, createEpicRunWithDisposition, fetchConductorReadiness, fetchEpicRunState, replaceEpicRunPolicy, resolveConductorBridgeApiAccess, storeEpicPlan, validateEpicPlan, ConductorBridgeApiError, } from "./conductor/bridge-api-client.js";
82
+ // BAPI-1133 (A2) — the single legacy-to-typed merge-execution-mode mapping and
83
+ // its total legacy resolver live in `merge-pull-request.ts` (no import cycle:
84
+ // that module never imports from `setup-epic.ts`). Reused here rather than
85
+ // reimplemented so the mapping stays single-defined.
86
+ import { mapLegacyMergeExecutionModeToPolicyMode, resolveMergeExecutionMode, } from "./merge-pull-request.js";
87
+ import { MERGE_EXECUTION_INELIGIBLE, MERGE_EXECUTION_MODE_GITHUB_ACTIONS, MERGE_WORKFLOW_REMEDY_COMMAND, } from "./conductor/merge-execution-contract.js";
88
+ import { classifyMergeExecutionEligibility } from "./conductor/merge-execution-eligibility.js";
82
89
  import { resolveTrackedPaths } from "./git-reachability.js";
83
90
  import { validateBranchName } from "./base-ref.js";
84
91
  // Reused from the implement-epic client on purpose: adding a second wrapper under
@@ -89,8 +96,8 @@ import { validateBranchName } from "./base-ref.js";
89
96
  // typed clients the pilot already has are the v2 path's clients too, so neither
90
97
  // conductor can drift on a field name or an endpoint shape. No MCP tool, no
91
98
  // input-schema field, and no description changes.
92
- import { getConfigFieldBaseBranch, getIndexScopeLifecycle, getParseStatus, } from "./implement-epic/bridge-client.js";
93
- import { boundScopeLifecycleLabel, createExecFileRunCommand, ensureCommitResolvableLocally, normalizeCommitSha, performExactIndexScopeCut, pollIndexScopeLifecycle, readRemoteBranchHead, runGit, SCOPE_BOOTSTRAP_POLL_INTERVAL_MS, SCOPE_LIFECYCLE_LABELS, } from "./implement-epic/cut-protocol.js";
99
+ import { getConfigFieldBaseBranch, getIndexScopeLifecycle, getParseStatus, } from "./epic-shared/bridge-client.js";
100
+ import { boundScopeLifecycleLabel, createExecFileRunCommand, ensureCommitResolvableLocally, normalizeCommitSha, performExactIndexScopeCut, pollIndexScopeLifecycle, readRemoteBranchHead, runGit, SCOPE_BOOTSTRAP_POLL_INTERVAL_MS, SCOPE_LIFECYCLE_LABELS, } from "./epic-shared/cut-protocol.js";
94
101
  import { hashPlan } from "./conductor/plan.js";
95
102
  import { BRING_UP_REFUSAL_MESSAGES, firstBringUpRefusal, } from "./conductor/bring-up-facts.js";
96
103
  import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
@@ -111,7 +118,7 @@ export const SETUP_EPIC_UNATTENDED_POSTURE = "unattended";
111
118
  /**
112
119
  * The server's stable error code for an unattended-posture refusal (BAPI-734).
113
120
  *
114
- * Mirrors `api.library.epic_conductor.unattended_posture.UNATTENDED_POSTURE_REFUSED_CODE`.
121
+ * Mirrors `api.library.epic_implementer.unattended_posture.UNATTENDED_POSTURE_REFUSED_CODE`.
115
122
  * Matched by EQUALITY on the structured `error_code`, never by searching the
116
123
  * message text: the message is prose that may be rewritten, the code is the
117
124
  * contract.
@@ -128,6 +135,19 @@ export const SETUP_EPIC_UNATTENDED_POSTURE_REFUSED_CODE = "UNATTENDED_POSTURE_RE
128
135
  * which is what the observed BAPI-1085 run did.
129
136
  */
130
137
  export const SETUP_EPIC_AUTO_MERGE_POLICY_KEY = "v2_auto_merge_enabled";
138
+ /**
139
+ * The `policy_json` key declaring the typed merge-execution mode (BAPI-1133, A2).
140
+ *
141
+ * Composed into the unattended default alongside `posture` and
142
+ * {@link SETUP_EPIC_AUTO_MERGE_POLICY_KEY}, in the SAME fill-if-absent branch,
143
+ * because the server's `RunPolicy` refuses an unattended auto-merge policy that
144
+ * omits it (`merge_execution_mode_unconfigured`). `setup-epic` is the only
145
+ * IMPLICIT writer of this key — any other client may still submit it explicitly
146
+ * as operator policy, and an explicitly supplied value here is never overwritten.
147
+ * Declaring `github_actions` or `machine_token` is intent only in this slice:
148
+ * neither changes merge execution until a later A2 child.
149
+ */
150
+ export const SETUP_EPIC_MERGE_EXECUTION_MODE_POLICY_KEY = "merge_execution_mode";
131
151
  /** Accepted `policy_json.review_policy.source` values (the `ReviewPolicy` surface). */
132
152
  export const SETUP_EPIC_REVIEW_POLICY_SOURCES = [
133
153
  "verdict_protocol",
@@ -169,12 +189,15 @@ export function getSetupEpicUsage() {
169
189
  "repository base branch, matching plain start-tickets semantics.",
170
190
  "",
171
191
  "Posture (default): a run that selects an epic branch is created UNATTENDED —",
172
- "posture: unattended and v2_auto_merge_enabled: true — and the server stamps its",
173
- "CI gate at first approval from the repository default, or records an explicit",
174
- "no-CI-gate mode when the repository declares none. Child PRs merge into the epic",
175
- "branch; the INTEGRATION PR into the repository base branch stays human-gated.",
176
- "Pass --attended to opt out. A run that selects no epic branch (--into-base, or a",
177
- "single-node plan) is unchanged and composes no posture at all.",
192
+ "posture: unattended, v2_auto_merge_enabled: true, and merge_execution_mode",
193
+ "(declared automatically from the repository's configured merge execution; a",
194
+ "policy declaration only — it does not change merge execution in this release)",
195
+ "— and the server stamps its CI gate at first approval from the repository",
196
+ "default, or records an explicit no-CI-gate mode when the repository declares",
197
+ "none. Child PRs merge into the epic branch; the INTEGRATION PR into the",
198
+ "repository base branch stays human-gated. Pass --attended to opt out. A run",
199
+ "that selects no epic branch (--into-base, or a single-node plan) is unchanged",
200
+ "and composes no posture at all.",
178
201
  "",
179
202
  "Unattended runs have TWO one-time repository prerequisites, refused by name",
180
203
  "before any run is created: repository consent (unattended_epic_implementer_allowed) and",
@@ -239,7 +262,7 @@ export function getSetupEpicUsage() {
239
262
  " 10799; remediate / ci_fix 3599; rebase / smoke 2699;",
240
263
  " merge 1799); the server rejects a higher value with a",
241
264
  " 422 naming the offending field. Full table:",
242
- " docs/claude/epic-conductor-v2-operator-runbook.md §4.",
265
+ " docs/claude/epic-implementer-v2-operator-runbook.md §4.",
243
266
  " --replace-policy Authorize replacing a LIVE run's stored policy with the",
244
267
  " policy file, as a complete replacement. Without it, a",
245
268
  " divergent policy on a reused run is refused with a",
@@ -1130,7 +1153,7 @@ export function resolveFeatureBranchSelection(opts, planNodeCount, policyDeclare
1130
1153
  * refused every unattended run, its own named remedy included.
1131
1154
  *
1132
1155
  * The reason CODES are the server's bounded vocabulary, spelled exactly as
1133
- * `api/library/epic_conductor/unattended_posture.py` spells them. The client adds
1156
+ * `api/library/epic_implementer/unattended_posture.py` spells them. The client adds
1134
1157
  * remediation TEXT around them and invents no aliases — an alias would drift from
1135
1158
  * the server's refusal the first time either side was edited, and an operator
1136
1159
  * comparing a CLI message with an API response would have no way to tell they
@@ -1145,7 +1168,7 @@ export function resolveFeatureBranchSelection(opts, planNodeCount, policyDeclare
1145
1168
  * Does this policy declare a notify webhook the server would probe?
1146
1169
  *
1147
1170
  * BAPI-1102. The shape check mirrors `_extract_webhook_url` in
1148
- * `api/library/epic_conductor/webhook_verification.py` exactly as far as
1171
+ * `api/library/epic_implementer/webhook_verification.py` exactly as far as
1149
1172
  * PRESENCE goes — a `notify` object carrying a non-blank string `webhook_url` —
1150
1173
  * and stops there on purpose. Scheme, host, and reachability are the server's to
1151
1174
  * judge at activation, where the probe actually happens; re-deciding them here
@@ -1244,6 +1267,66 @@ export function evaluateSetupEpicBringUpPrerequisites(readiness) {
1244
1267
  return null;
1245
1268
  return { reason, message: `${BRING_UP_REFUSAL_MESSAGES[reason]} ${NO_RUN_WAS_CREATED}` };
1246
1269
  }
1270
+ /**
1271
+ * Read the composed policy's typed `merge_execution_mode`, if it declares one.
1272
+ *
1273
+ * Strict equality against the mirrored constant, matching the server's own guard
1274
+ * (`preflight._strict_github_actions_mode`). A missing key, a non-string, and
1275
+ * any other mode all answer `false`, so nothing but an explicit declaration
1276
+ * opens the gate.
1277
+ */
1278
+ export function policyDeclaresGitHubActionsMerge(policy) {
1279
+ if (policy === undefined || policy === null)
1280
+ return false;
1281
+ return policy.merge_execution_mode === MERGE_EXECUTION_MODE_GITHUB_ACTIONS;
1282
+ }
1283
+ /**
1284
+ * Evaluate merge-execution eligibility against a readiness report.
1285
+ *
1286
+ * Pure and total. Returns `null` — no refusal — for every policy that does not
1287
+ * declare `github_actions` mode, without reading the report at all.
1288
+ *
1289
+ * Uses {@link classifyMergeExecutionEligibility}, the same classifier readiness
1290
+ * and `drive-epic` read, so all three agree on the status AND on the first
1291
+ * blocker. A client that classified the block itself here would eventually
1292
+ * disagree with the row an operator had just been shown.
1293
+ *
1294
+ * BOTH `not_eligible` and `could_not_verify` return
1295
+ * `merge_execution_ineligible`. One code, because the operator's next action is
1296
+ * the same and splitting it would put the burden of telling "not proven" from
1297
+ * "proven absent" on the person least able to check. The MESSAGE keeps the
1298
+ * capability category explicit, so the distinction survives where it is
1299
+ * actionable.
1300
+ *
1301
+ * The message is built from the shared remedy constant, ends with the exact
1302
+ * standalone sentence `No run was created.`, and carries no GitHub payload, URL,
1303
+ * branch name, workflow source, credential, or exception text.
1304
+ */
1305
+ export function evaluateSetupEpicMergeExecutionPrerequisites(readiness, requestedPolicy) {
1306
+ if (!policyDeclaresGitHubActionsMerge(requestedPolicy))
1307
+ return null;
1308
+ if (readiness === null) {
1309
+ return {
1310
+ reason: MERGE_EXECUTION_INELIGIBLE,
1311
+ message: "this run declares merge_execution_mode: github_actions, and the Bridge API " +
1312
+ "conductor readiness report could not be read, so the repository's merge " +
1313
+ "capability could not be confirmed. Retry once the server is reachable, or " +
1314
+ `resolve it with \`${MERGE_WORKFLOW_REMEDY_COMMAND}\`. ${NO_RUN_WAS_CREATED}`,
1315
+ };
1316
+ }
1317
+ const verdict = classifyMergeExecutionEligibility(readiness);
1318
+ if (verdict.status === "eligible")
1319
+ return null;
1320
+ const category = verdict.status === "could_not_verify"
1321
+ ? "could not be confirmed"
1322
+ : "is not sufficient";
1323
+ return {
1324
+ reason: MERGE_EXECUTION_INELIGIBLE,
1325
+ message: "this run declares merge_execution_mode: github_actions, and this repository's " +
1326
+ `merge capability ${category}: ${verdict.detail}. Resolve it with ` +
1327
+ `\`${MERGE_WORKFLOW_REMEDY_COMMAND}\`, then re-run. ${NO_RUN_WAS_CREATED}`,
1328
+ };
1329
+ }
1247
1330
  /**
1248
1331
  * Does the policy this invocation will actually send request unattended operation?
1249
1332
  *
@@ -1283,8 +1366,10 @@ export function requestedPolicyIsUnattended(policy, attended) {
1283
1366
  * * **Feature branch, `--attended`** → `base_branch` plus an explicitly
1284
1367
  * requested `review_policy`, and nothing else. Byte-identical to the
1285
1368
  * pre-BAPI-1102 composition, which is precisely what the opt-out promises.
1286
- * * **Feature branch, default** → adds `posture: "unattended"` and
1287
- * `v2_auto_merge_enabled: true`.
1369
+ * * **Feature branch, default** → adds `posture: "unattended"`,
1370
+ * `v2_auto_merge_enabled: true`, and (BAPI-1133, A2) `merge_execution_mode`
1371
+ * when the caller resolved one — the server refuses an unattended auto-merge
1372
+ * policy that omits it.
1288
1373
  *
1289
1374
  * Deliberately absent from every shape:
1290
1375
  *
@@ -1306,6 +1391,9 @@ export function composeDefaultSetupEpicPolicy(args) {
1306
1391
  if (!args.attended) {
1307
1392
  policy.posture = SETUP_EPIC_UNATTENDED_POSTURE;
1308
1393
  policy[SETUP_EPIC_AUTO_MERGE_POLICY_KEY] = true;
1394
+ if (args.mergeExecutionMode !== undefined) {
1395
+ policy[SETUP_EPIC_MERGE_EXECUTION_MODE_POLICY_KEY] = args.mergeExecutionMode;
1396
+ }
1309
1397
  }
1310
1398
  }
1311
1399
  if (args.reviewPolicy !== undefined) {
@@ -1346,6 +1434,14 @@ export function composeDefaultSetupEpicPolicy(args) {
1346
1434
  * * **`v2_auto_merge_enabled` already present** → left exactly as the file set
1347
1435
  * it, including `false`. Filling the posture does not overwrite an explicit
1348
1436
  * merge decision.
1437
+ * * **`merge_execution_mode` already present** (BAPI-1133, A2) → left exactly
1438
+ * as the file set it, valid or not. An explicitly present but invalid mode is
1439
+ * NOT silently repaired here; it is forwarded unchanged so the server's
1440
+ * secret-safe typed validation rejects it. Absent, it is filled from the
1441
+ * caller's already-resolved {@link TypedMergeExecutionMode} in the same
1442
+ * fill-if-absent branch that stamps `posture` and
1443
+ * {@link SETUP_EPIC_AUTO_MERGE_POLICY_KEY} — the server refuses an
1444
+ * unattended auto-merge policy that omits it.
1349
1445
  *
1350
1446
  * Pure: it returns a new object and never mutates the input. It is applied on the
1351
1447
  * CREATE path only. A reused live run reconciles the file against its STORED
@@ -1365,6 +1461,10 @@ export function applySetupEpicUnattendedDefault(policy, args) {
1365
1461
  if (!(SETUP_EPIC_AUTO_MERGE_POLICY_KEY in composed)) {
1366
1462
  composed[SETUP_EPIC_AUTO_MERGE_POLICY_KEY] = true;
1367
1463
  }
1464
+ if (!(SETUP_EPIC_MERGE_EXECUTION_MODE_POLICY_KEY in composed) &&
1465
+ args.mergeExecutionMode !== undefined) {
1466
+ composed[SETUP_EPIC_MERGE_EXECUTION_MODE_POLICY_KEY] = args.mergeExecutionMode;
1467
+ }
1368
1468
  return composed;
1369
1469
  }
1370
1470
  /**
@@ -1492,7 +1592,7 @@ export const SETUP_EPIC_SCOPE_RECOVERY_COMMAND = "re-run this same setup-epic co
1492
1592
  * The canonical one-line recovery for a scope collision (BAPI-1009).
1493
1593
  *
1494
1594
  * Repeated VERBATIM as the epic-run gate snapshot's shadow-hold recovery hint
1495
- * (``api/library/epic_conductor/gate_snapshot_service.py``), so an operator who
1595
+ * (``api/library/epic_implementer/gate_snapshot_service.py``), so an operator who
1496
1596
  * reads the diagnosis on the status surface and an operator who reads it from
1497
1597
  * the CLI are told to do the same thing in the same words.
1498
1598
  */
@@ -1873,6 +1973,27 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
1873
1973
  // `base_branch` is honored exactly as `--feature-branch` would be. A malformed
1874
1974
  // or unknown policy field is left for the server's strict validation.
1875
1975
  const effectiveFeatureBranch = resolveEffectiveFeatureBranch(featureBranch, requestedPolicy);
1976
+ // --- Typed merge-execution-mode declaration (BAPI-1133, A2) ---------------
1977
+ // Resolved ONCE, here, before either composer runs, so the composer functions
1978
+ // stay pure and synchronous. Gated on the exact predicate both composers'
1979
+ // OWN fill-if-absent branches use — `!attended`, a feature branch selected,
1980
+ // and (for the file path) no `posture` already declared — so this never
1981
+ // performs the new configuration read for `--attended` or `--into-base` runs,
1982
+ // and never for a file that already answered the posture question itself.
1983
+ const unattendedDefaultEligible = !opts.attended &&
1984
+ effectiveFeatureBranch !== undefined &&
1985
+ (requestedPolicy === undefined || !("posture" in requestedPolicy));
1986
+ let declaredMergeExecutionMode;
1987
+ if (unattendedDefaultEligible) {
1988
+ const mergeExecutionModeResolverDeps = {
1989
+ repoName: access.repoName,
1990
+ buildGetUrl: (path, params) => buildConductorJiraUrl(access.baseUrl, path, params),
1991
+ getHeaders: async () => conductorGetHeaders(access),
1992
+ fetchImpl: deps.fetch,
1993
+ };
1994
+ const effectiveLegacyMode = await resolveMergeExecutionMode(mergeExecutionModeResolverDeps);
1995
+ declaredMergeExecutionMode = mapLegacyMergeExecutionModeToPolicyMode(effectiveLegacyMode);
1996
+ }
1876
1997
  // --- The policy the CREATE path will send (BAPI-1102) ---------------------
1877
1998
  // Composed HERE, as soon as both of its inputs exist, so the one object that
1878
1999
  // gets reported, gated, and sent is the same object throughout. It is consumed
@@ -1883,12 +2004,30 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
1883
2004
  ? applySetupEpicUnattendedDefault(requestedPolicy, {
1884
2005
  featureBranch: effectiveFeatureBranch,
1885
2006
  attended: opts.attended,
2007
+ mergeExecutionMode: declaredMergeExecutionMode,
1886
2008
  })
1887
2009
  : composeDefaultSetupEpicPolicy({
1888
2010
  featureBranch,
1889
2011
  attended: opts.attended,
1890
2012
  reviewPolicy: opts.reviewPolicy,
2013
+ mergeExecutionMode: declaredMergeExecutionMode,
1891
2014
  });
2015
+ // BAPI-1133 (A2). Reported only when THIS command stamped the key — i.e. it
2016
+ // is present in the composed policy and was absent from whatever the
2017
+ // operator supplied — as low-emphasis policy metadata, mirroring the
2018
+ // existing posture/auto-merge reporting rather than adding a new prompt,
2019
+ // wizard, or runtime status. An explicitly declared mode (CLI-composed
2020
+ // policy already carrying the key) is never reported here as if it were
2021
+ // this command's decision.
2022
+ const mergeExecutionModeStamped = declaredMergeExecutionMode !== undefined &&
2023
+ composedForCreate[SETUP_EPIC_MERGE_EXECUTION_MODE_POLICY_KEY] === declaredMergeExecutionMode &&
2024
+ (requestedPolicy === undefined ||
2025
+ !(SETUP_EPIC_MERGE_EXECUTION_MODE_POLICY_KEY in requestedPolicy));
2026
+ if (mergeExecutionModeStamped) {
2027
+ say(`Policy: merge_execution_mode: ${declaredMergeExecutionMode} (declared automatically ` +
2028
+ `for the unattended default; policy metadata only — no dispatch, actuation, eligibility, ` +
2029
+ `containment, or escalation behavior until a later A2 child)`);
2030
+ }
1892
2031
  if (requestedPolicy !== undefined) {
1893
2032
  // The posture is stated for the FILE path too, and read off the composed
1894
2033
  // object rather than re-derived. The `--policy-file` invocation is the one
@@ -2256,7 +2395,7 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
2256
2395
  : ` - feature branch: ${effectiveFeatureBranch} (validated server-side at approval; no request made in dry-run)`);
2257
2396
  if (needsCut && cutCommitSha !== null && scopeBaseBranch !== null) {
2258
2397
  say(` - would open a draft pull request ${effectiveFeatureBranch} → ${scopeBaseBranch} ` +
2259
- "for conductor-ci / gate (BAPI-951; nothing opened in dry-run)");
2398
+ "for epic-implementer-ci / gate (BAPI-951; nothing opened in dry-run)");
2260
2399
  }
2261
2400
  }
2262
2401
  if (opts.reviewPolicy !== undefined) {
@@ -2430,6 +2569,21 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
2430
2569
  deps.errorLog(`setup-epic REFUSED (${bringUpRefusal.reason}): ${bringUpRefusal.message}`);
2431
2570
  return 1;
2432
2571
  }
2572
+ // BAPI-1135 — merge-execution eligibility, AFTER the universal bring-up
2573
+ // blockers (an unreadable readiness report and a behind database make this
2574
+ // question moot) and BEFORE the unattended prerequisites and every
2575
+ // approval check below. That position is what keeps readiness, this gate,
2576
+ // and the approval refusal naming the SAME first blocker.
2577
+ //
2578
+ // Outside the `requestedPolicyIsUnattended` branch on purpose: an
2579
+ // `--attended` run that declares `github_actions` mode is gated too. The
2580
+ // evaluator itself returns null for every policy that does not declare the
2581
+ // mode, so no other run's behavior changes.
2582
+ const mergeExecutionRefusal = evaluateSetupEpicMergeExecutionPrerequisites(readiness, composedForCreate);
2583
+ if (mergeExecutionRefusal !== null) {
2584
+ deps.errorLog(`setup-epic REFUSED (${mergeExecutionRefusal.reason}): ${mergeExecutionRefusal.message}`);
2585
+ return 1;
2586
+ }
2433
2587
  if (requestedPolicyIsUnattended(composedForCreate, opts.attended)) {
2434
2588
  // The COMPOSED policy, not the flags: a --policy-file declaring
2435
2589
  // notify.webhook_url satisfies the escalation-channel prerequisite, and
@@ -62,20 +62,20 @@ export function mintStartTicketsWorkerId(ticketKey, agentName, fragment = random
62
62
  */
63
63
  export function buildEpicIdentityEnv(epic) {
64
64
  const env = {
65
- BAPI_CONDUCTOR_EPIC_KEY: epic.epic_key,
66
- BAPI_CONDUCTOR_EPIC_RUN_ID: epic.epic_run_id,
67
- BAPI_CONDUCTOR_PLAN_VERSION: String(epic.plan_version),
65
+ BAPI_EPIC_IMPLEMENTER_EPIC_KEY: epic.epic_key,
66
+ BAPI_EPIC_IMPLEMENTER_EPIC_RUN_ID: epic.epic_run_id,
67
+ BAPI_EPIC_IMPLEMENTER_PLAN_VERSION: String(epic.plan_version),
68
68
  };
69
69
  const declared = normalizeDeclaredTouchedFiles(epic.declared_touched_files);
70
70
  if (declared.length > 0) {
71
- env.BAPI_CONDUCTOR_DECLARED_TOUCHED_FILES_JSON = JSON.stringify(declared);
71
+ env.BAPI_EPIC_IMPLEMENTER_DECLARED_TOUCHED_FILES_JSON = JSON.stringify(declared);
72
72
  }
73
73
  return env;
74
74
  }
75
75
  // ---------------------------------------------------------------------------
76
76
  // Conductor context
77
77
  // ---------------------------------------------------------------------------
78
- /** Default gate name when no `BAPI_CONDUCTOR_GATE_NAME` override is present. */
78
+ /** Default gate name when no `BAPI_EPIC_IMPLEMENTER_GATE_NAME` override is present. */
79
79
  export const DEFAULT_CONDUCTOR_GATE_NAME = "implement-ticket";
80
80
  /**
81
81
  * Resolve a packaged build artifact path relative to this compiled module.
@@ -110,11 +110,11 @@ export async function createStartTicketsConductorContext(options, agent, deps) {
110
110
  // Repo resolution must never abort conductor setup; fall back to null.
111
111
  repoName = null;
112
112
  }
113
- const gateName = nonEmpty(deps.env.BAPI_CONDUCTOR_GATE_NAME)
114
- ? deps.env.BAPI_CONDUCTOR_GATE_NAME.trim()
113
+ const gateName = nonEmpty(deps.env.BAPI_EPIC_IMPLEMENTER_GATE_NAME)
114
+ ? deps.env.BAPI_EPIC_IMPLEMENTER_GATE_NAME.trim()
115
115
  : DEFAULT_CONDUCTOR_GATE_NAME;
116
- const supervisorMode = nonEmpty(deps.env.BAPI_CONDUCTOR_SUPERVISOR_MODE)
117
- ? deps.env.BAPI_CONDUCTOR_SUPERVISOR_MODE.trim()
116
+ const supervisorMode = nonEmpty(deps.env.BAPI_EPIC_IMPLEMENTER_SUPERVISOR_MODE)
117
+ ? deps.env.BAPI_EPIC_IMPLEMENTER_SUPERVISOR_MODE.trim()
118
118
  : options.autoApprove
119
119
  ? "auto"
120
120
  : "interactive";
@@ -142,12 +142,12 @@ export async function createStartTicketsConductorContext(options, agent, deps) {
142
142
  // ---------------------------------------------------------------------------
143
143
  /** Conductor store-tuning env keys forwarded only when already set upstream. */
144
144
  const CONDUCTOR_TUNING_ENV_KEYS = [
145
- "BAPI_CONDUCTOR_BUSY_TIMEOUT_MS",
146
- "BAPI_CONDUCTOR_RETENTION_DAYS",
147
- "BAPI_CONDUCTOR_RETENTION_MAX_ROWS",
145
+ "BAPI_EPIC_IMPLEMENTER_BUSY_TIMEOUT_MS",
146
+ "BAPI_EPIC_IMPLEMENTER_RETENTION_DAYS",
147
+ "BAPI_EPIC_IMPLEMENTER_RETENTION_MAX_ROWS",
148
148
  // BAPI-397: per-type relay cooldown — forwarded only when explicitly set so
149
149
  // the supervisor's send config is honoured by the conductor store it writes to.
150
- "BAPI_CONDUCTOR_MESSAGE_COOLDOWN_MS",
150
+ "BAPI_EPIC_IMPLEMENTER_MESSAGE_COOLDOWN_MS",
151
151
  ];
152
152
  /** Truthy predicate for opt-in conductor boolean flags (`1` / `true`). */
153
153
  export function isConductorFlagEnabled(value) {
@@ -164,30 +164,30 @@ export function isConductorFlagEnabled(value) {
164
164
  * upstream.
165
165
  */
166
166
  export function buildConductorWorkerEnv(context, worker, parentEnv) {
167
- // Merge "conductor" into the parent project's profile rather than overwriting it.
167
+ // Merge "epic_implementer" into the parent project's profile rather than overwriting it.
168
168
  // This preserves groups like "sfcc" that the project already has, while adding
169
169
  // the conductor-specific tools. Only validated group tokens from resolveProfiles
170
170
  // are threaded — arbitrary parentEnv keys never leak through.
171
171
  const parentActiveGroups = new Set(resolveProfiles(parentEnv.BRIDGE_MCP_PROFILE));
172
- parentActiveGroups.add("conductor");
172
+ parentActiveGroups.add("epic_implementer");
173
173
  const mergedProfile = Array.from(parentActiveGroups).join(",");
174
174
  const env = {
175
- BAPI_CONDUCTOR_ENABLED: "1",
175
+ BAPI_EPIC_IMPLEMENTER_ENABLED: "1",
176
176
  BRIDGE_MCP_PROFILE: mergedProfile,
177
- BAPI_CONDUCTOR_RUN_ID: context.runId,
178
- BAPI_CONDUCTOR_WORKER_ID: worker.workerId,
179
- BAPI_CONDUCTOR_TICKET_KEY: worker.ticketKey,
180
- BAPI_CONDUCTOR_WORKTREE_PATH: worker.worktreePath,
181
- BAPI_CONDUCTOR_GATE_NAME: context.gateName,
182
- BAPI_CONDUCTOR_SUPERVISOR_MODE: context.supervisorMode,
183
- BAPI_CONDUCTOR_CLI_FILE: context.cliFile,
177
+ BAPI_EPIC_IMPLEMENTER_RUN_ID: context.runId,
178
+ BAPI_EPIC_IMPLEMENTER_WORKER_ID: worker.workerId,
179
+ BAPI_EPIC_IMPLEMENTER_TICKET_KEY: worker.ticketKey,
180
+ BAPI_EPIC_IMPLEMENTER_WORKTREE_PATH: worker.worktreePath,
181
+ BAPI_EPIC_IMPLEMENTER_GATE_NAME: context.gateName,
182
+ BAPI_EPIC_IMPLEMENTER_SUPERVISOR_MODE: context.supervisorMode,
183
+ BAPI_EPIC_IMPLEMENTER_CLI_FILE: context.cliFile,
184
184
  // BAPI-527: the CLI file + Node runtime are a PAIR. The worker runs
185
185
  // conductor-bin.js under this captured conductor Node for every worker-side
186
186
  // ledger op, so the worker Node never loads better-sqlite3. Non-secret.
187
- CONDUCTOR_NODE_PATH: context.conductorNodePath,
187
+ EPIC_IMPLEMENTER_NODE_PATH: context.conductorNodePath,
188
188
  };
189
189
  if (context.repoName) {
190
- env.BAPI_CONDUCTOR_REPO_NAME = context.repoName;
190
+ env.BAPI_EPIC_IMPLEMENTER_REPO_NAME = context.repoName;
191
191
  }
192
192
  if (context.epic) {
193
193
  const epicEnv = buildEpicIdentityEnv(context.epic);
@@ -195,8 +195,8 @@ export function buildConductorWorkerEnv(context, worker, parentEnv) {
195
195
  env[k] = v;
196
196
  }
197
197
  }
198
- if (isConductorFlagEnabled(parentEnv.BAPI_CONDUCTOR_ENABLE_PRE_TOOL_USE)) {
199
- env.BAPI_CONDUCTOR_ENABLE_PRE_TOOL_USE = "1";
198
+ if (isConductorFlagEnabled(parentEnv.BAPI_EPIC_IMPLEMENTER_ENABLE_PRE_TOOL_USE)) {
199
+ env.BAPI_EPIC_IMPLEMENTER_ENABLE_PRE_TOOL_USE = "1";
200
200
  }
201
201
  for (const key of CONDUCTOR_TUNING_ENV_KEYS) {
202
202
  if (nonEmpty(parentEnv[key])) {
@@ -234,13 +234,13 @@ function shellQuotePath(value) {
234
234
  return `'${value.replace(/'/g, "'\\''")}'`;
235
235
  }
236
236
  /**
237
- * Resolve the hook command string. Prefers `BAPI_CONDUCTOR_HOOK_COMMAND`
237
+ * Resolve the hook command string. Prefers `BAPI_EPIC_IMPLEMENTER_HOOK_COMMAND`
238
238
  * verbatim; otherwise runs the packaged hook bin via the current Node
239
239
  * executable.
240
240
  */
241
241
  export function resolveConductorHookCommand(env, hookBinPath, execPath = process.execPath) {
242
- if (nonEmpty(env.BAPI_CONDUCTOR_HOOK_COMMAND)) {
243
- return env.BAPI_CONDUCTOR_HOOK_COMMAND;
242
+ if (nonEmpty(env.BAPI_EPIC_IMPLEMENTER_HOOK_COMMAND)) {
243
+ return env.BAPI_EPIC_IMPLEMENTER_HOOK_COMMAND;
244
244
  }
245
245
  return `${shellQuotePath(execPath)} ${shellQuotePath(hookBinPath)}`;
246
246
  }
@@ -279,7 +279,7 @@ export async function provisionConductorHookForWorktree(worktreePath, command, o
279
279
  */
280
280
  export async function provisionConductorHooksForRows(rows, context, deps) {
281
281
  const isClaude = context.agentName === "claude";
282
- const enablePreToolUse = isConductorFlagEnabled(deps.env.BAPI_CONDUCTOR_ENABLE_PRE_TOOL_USE);
282
+ const enablePreToolUse = isConductorFlagEnabled(deps.env.BAPI_EPIC_IMPLEMENTER_ENABLE_PRE_TOOL_USE);
283
283
  const command = resolveConductorHookCommand(deps.env, context.hookBinPath, deps.execPath);
284
284
  const fragment = deps.workerFragment;
285
285
  const out = [];
@@ -451,7 +451,7 @@ export function buildSupervisorTabCommand(context, platform, nodeExecPath = proc
451
451
  /**
452
452
  * Decide whether the supervisor peer tab should launch. Enabled by default for a
453
453
  * normal packaged conductor context; disabled only when the resolved supervisor
454
- * mode is `off` (i.e. `BAPI_CONDUCTOR_SUPERVISOR_MODE=off`).
454
+ * mode is `off` (i.e. `BAPI_EPIC_IMPLEMENTER_SUPERVISOR_MODE=off`).
455
455
  */
456
456
  export function isSupervisorLaunchEnabled(context) {
457
457
  return context.supervisorMode.trim().toLowerCase() !== "off";
@@ -52,7 +52,7 @@ export const START_TICKETS_DOCTOR_COMMAND = `npx -y ${MCP_PACKAGE_NAME} doctor`;
52
52
  /**
53
53
  * BAPI-527 live-source checkout guard (conductor durable execution).
54
54
  *
55
- * `BAPI_CONDUCTOR_LIVE_SOURCE_PATH` — OPTIONAL operator-set path to the checkout
55
+ * `BAPI_EPIC_IMPLEMENTER_LIVE_SOURCE_PATH` — OPTIONAL operator-set path to the checkout
56
56
  * an operator's live dev server (e.g. `uvicorn main:app --reload`) is running
57
57
  * from. When set, unattended conductor/epic/non-mutating start-tickets dispatch
58
58
  * refuses to run if the conductor base repo it would operate on resolves to the
@@ -62,14 +62,14 @@ export const START_TICKETS_DOCTOR_COMMAND = `npx -y ${MCP_PACKAGE_NAME} doctor`;
62
62
  * discovery, so this local env contract is introduced here. Read only from the
63
63
  * injected `deps.env` (never global `process.env`) so it stays mockable.
64
64
  *
65
- * `BAPI_CONDUCTOR_ALLOW_LIVE_SOURCE_DISPATCH` — explicit escape hatch. When set
65
+ * `BAPI_EPIC_IMPLEMENTER_ALLOW_LIVE_SOURCE_DISPATCH` — explicit escape hatch. When set
66
66
  * to a truthy value (`1`/`true`/`yes`/`on`, case-insensitive) a detected
67
67
  * collision is DOWNGRADED from fatal to a loud warning rather than silently
68
68
  * ignored, so an operator who genuinely wants same-checkout dispatch can opt in
69
69
  * with eyes open.
70
70
  */
71
- export const CONDUCTOR_LIVE_SOURCE_PATH_ENV = "BAPI_CONDUCTOR_LIVE_SOURCE_PATH";
72
- export const CONDUCTOR_ALLOW_LIVE_SOURCE_DISPATCH_ENV = "BAPI_CONDUCTOR_ALLOW_LIVE_SOURCE_DISPATCH";
71
+ export const CONDUCTOR_LIVE_SOURCE_PATH_ENV = "BAPI_EPIC_IMPLEMENTER_LIVE_SOURCE_PATH";
72
+ export const CONDUCTOR_ALLOW_LIVE_SOURCE_DISPATCH_ENV = "BAPI_EPIC_IMPLEMENTER_ALLOW_LIVE_SOURCE_DISPATCH";
73
73
  /** Stable descriptor id for the live-source checkout guard (used in messages + doctor). */
74
74
  export const LIVE_SOURCE_GUARD_ID = "conductor-live-source";
75
75
  /** True only for the three first-class supported platforms. */
@@ -566,11 +566,6 @@ const CLAUDE_MCP_SHADOWING_INSTALL_HINTS = {
566
566
  * - a CONFIRMED foreign registration → `found: true` + advisory detail
567
567
  * - unreadable / malformed config, or an unresolved main
568
568
  * worktree path → `found: true` + detail
569
- *
570
- * Reporting stays deliberately independent of the retired
571
- * `BAPI_CONDUCTOR_ALLOW_CLAUDE_MCP_SHADOWING` override: whether that variable is
572
- * exported has never had any bearing on whether the collision exists, and now it
573
- * has no bearing on anything at all.
574
569
  */
575
570
  export function claudeMcpShadowingDescriptor() {
576
571
  return {