@bridge_gpt/mcp-server 0.2.54 → 0.2.56

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 (89) hide show
  1. package/README.md +45 -59
  2. package/build/agent-capabilities/types.js +4 -0
  3. package/build/agent-guidance.generated.js +3 -0
  4. package/build/agent-launchers/claude-executor-adapter.js +3 -0
  5. package/build/agent-notes.js +178 -0
  6. package/build/agent-registry.js +25 -5
  7. package/build/agent-utils.js +58 -0
  8. package/build/agents.generated.js +1 -1
  9. package/build/codex-launch-scope.js +49 -0
  10. package/build/codex-skill-adapter.js +58 -0
  11. package/build/command-host.js +9 -0
  12. package/build/command-provisioning.js +35 -4
  13. package/build/commands.generated.js +9 -8
  14. package/build/conductor/bridge-api-client.js +199 -6
  15. package/build/conductor/bring-up-facts.js +187 -0
  16. package/build/conductor/claude-hook.js +7 -5
  17. package/build/conductor/cli.js +28 -0
  18. package/build/conductor/doctor.js +80 -9
  19. package/build/conductor/epic-implementer-cli.js +1298 -0
  20. package/build/conductor/epic-runtime.js +1 -1
  21. package/build/conductor/errors.js +2 -2
  22. package/build/conductor/git-ci-types.js +1 -1
  23. package/build/conductor/git-hooks.js +28 -14
  24. package/build/conductor/install-doctor.js +11 -5
  25. package/build/conductor/readiness-cli.js +10 -10
  26. package/build/conductor/readiness-sections.js +58 -9
  27. package/build/conductor/readiness.js +37 -6
  28. package/build/conductor/recovery-cli.js +289 -10
  29. package/build/conductor/recovery-operations.js +125 -2
  30. package/build/conductor/repair-contract.js +58 -0
  31. package/build/conductor/store.js +2 -2
  32. package/build/conductor/supervisor-runtime.js +1 -1
  33. package/build/conductor-bin.js +2 -139
  34. package/build/conductor-claude-hook-bin.js +2 -2
  35. package/build/conductor-claude-hook-removed-stub-bin.js +31 -0
  36. package/build/conductor-removed-stub-bin.js +30 -0
  37. package/build/docs.generated.js +1 -1
  38. package/build/doctor.js +81 -18
  39. package/build/drive-epic.js +541 -115
  40. package/build/epic-implementer-bin.js +145 -0
  41. package/build/epic-implementer-bundle-cli.js +264 -0
  42. package/build/epic-implementer-claude-hook-bin.js +3 -0
  43. package/build/epic-integration-pr.js +5 -3
  44. package/build/executor/env.js +6 -0
  45. package/build/executor/interrupted-worktree.js +60 -0
  46. package/build/executor/job-errors.js +45 -0
  47. package/build/executor/job-runner.js +274 -9
  48. package/build/executor/job-types.js +25 -9
  49. package/build/executor/merge-tree-classifier.js +171 -0
  50. package/build/executor/reconcile-overlap-governance.js +129 -0
  51. package/build/executor/reconcile-overlap-job.js +989 -0
  52. package/build/executor/reconcile-overlap-types.js +14 -0
  53. package/build/executor/spawn-job-driver.js +1 -0
  54. package/build/executor/types.js +2 -0
  55. package/build/executor/worker-finalization.js +25 -2
  56. package/build/executor/worker-guard-hook.js +15 -7
  57. package/build/implement-epic/bridge-client.js +773 -0
  58. package/build/implement-epic/checkpoint-store.js +542 -0
  59. package/build/implement-epic/cli.js +3158 -0
  60. package/build/implement-epic/cut-protocol.js +392 -0
  61. package/build/implement-epic/lock.js +302 -0
  62. package/build/implement-epic/pr-state.js +286 -0
  63. package/build/implement-epic/spawn.js +113 -0
  64. package/build/index.js +617 -168
  65. package/build/init.js +74 -9
  66. package/build/install-bridge-conductor.js +5 -5
  67. package/build/install-bridge.js +425 -79
  68. package/build/mcp-host-config.js +22 -60
  69. package/build/mcp-host-entry-adapter.js +18 -0
  70. package/build/mcp-host-targets.js +19 -32
  71. package/build/mcp-provisioning.js +1 -1
  72. package/build/mcp-registration-doctor.js +1 -1
  73. package/build/merge-pull-request.js +1 -1
  74. package/build/pipelines.generated.js +7 -7
  75. package/build/plan-epic-conductor-eligibility.js +1 -1
  76. package/build/plane/cli.js +36 -5
  77. package/build/plane/preflight.js +128 -12
  78. package/build/plane/shutdown.js +4 -4
  79. package/build/readiness-check.js +3 -3
  80. package/build/readme.generated.js +1 -1
  81. package/build/review-tickets.js +50 -35
  82. package/build/run-unit-tests-launcher.js +1 -1
  83. package/build/setup-epic.js +69 -31
  84. package/build/start-tickets-conductor.js +8 -7
  85. package/build/start-tickets.js +266 -48
  86. package/build/version.generated.js +3 -3
  87. package/build/worker-guard-hook-bin.js +1 -1
  88. package/docs/CONDUCTOR.md +8 -6
  89. package/package.json +5 -3
@@ -18,11 +18,21 @@
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.
20
20
  */
21
- import { createDefaultStartTicketsDeps, detectTerminal, runWithConcurrency, buildAgentInvocation, shSquoteInner, powershellSquote, isCommandOnPath, resolveFirstCommandOnPath, tmuxSessionNameForTicket, TICKET_KEY_PATTERN, DEFAULT_MAX_PARALLEL, validateBranchName, fetchAndResolveBaseSha, fetchStartTicketsConfigField, resolveStartTicketsBridgeApiAccess, } from "./start-tickets.js";
21
+ import { createDefaultStartTicketsDeps, detectTerminal, runWithConcurrency, buildAgentInvocation, shSquoteInner, powershellSquote, isCommandOnPath, resolveFirstCommandOnPath, tmuxSessionNameForTicket, TICKET_KEY_PATTERN, DEFAULT_MAX_PARALLEL, validateBranchName, fetchAndResolveBaseSha, fetchStartTicketsConfigField, resolveStartTicketsBridgeApiAccess, buildCommandProvisioningDeps, buildMcpProvisioningDeps, } from "./start-tickets.js";
22
+ import { provisionCommandsForWorktree } from "./command-provisioning.js";
23
+ import { resolveCodexLaunchScope } from "./codex-launch-scope.js";
22
24
  import { WINDOWS_TERMINAL_COMMAND, WINDOWS_POWERSHELL_CANDIDATES, TMUX_COMMAND, isSupportedStartTicketsPlatform, unsupportedPlatformMessage, } from "./start-tickets-prereqs.js";
23
- import { DEFAULT_AGENT_NAME, resolveAgentSpec, isAgentName, formatValidAgentNames, isValidModelAlias, } from "./agent-registry.js";
25
+ import { DEFAULT_AGENT_NAME, resolveAgentSpec, isAgentName, isValidModelAlias, } from "./agent-registry.js";
24
26
  import { injectConductorEnvIntoShellCommand, buildEpicIdentityEnv, mintStartTicketsRunId, } from "./start-tickets-conductor.js";
25
27
  import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
28
+ /** Every registered agent with a review workflow the launcher can provision. */
29
+ const REVIEW_TICKETS_AGENT_NAMES = ["claude", "cursor-agent", "codex"];
30
+ function isReviewTicketsAgentName(value) {
31
+ return REVIEW_TICKETS_AGENT_NAMES.includes(value);
32
+ }
33
+ function formatReviewTicketsAgentNames() {
34
+ return REVIEW_TICKETS_AGENT_NAMES.join(", ");
35
+ }
26
36
  /** Default base context: no flags threaded through (matches pre-BAPI-474 behavior). */
27
37
  const NO_BASE_CONTEXT = { noRefreshBase: false };
28
38
  // ---------------------------------------------------------------------------
@@ -37,7 +47,7 @@ export function getReviewTicketsUsage() {
37
47
  " --auto Auto-approve all review gates (global, applies to all tickets)",
38
48
  " --rounds=1|2 Review depth: 1=single-pass, 2=full two-pass (default: adaptive — backend routes by difficulty)",
39
49
  " --review KEY=auto,rounds=1 Per-ticket review mode override (repeatable)",
40
- " --agent claude|cursor-agent Agent command to launch in each tab (default: claude)",
50
+ " --agent claude|cursor-agent|codex Agent command to launch in each tab (default: claude)",
41
51
  " --model VALUE Model alias to pass as --model to the agent (optional passthrough)",
42
52
  " --max-parallel N Max tabs to spawn concurrently (default: 3)",
43
53
  " --dry-run Print intended commands; creates no tabs and opens no sessions",
@@ -289,10 +299,10 @@ export function parseReviewTicketsArgs(argv) {
289
299
  return { status: "error", message: "--agent requires a value (an agent name)." };
290
300
  }
291
301
  }
292
- if (!isAgentName(value)) {
302
+ if (!isAgentName(value) || !isReviewTicketsAgentName(value)) {
293
303
  return {
294
304
  status: "error",
295
- message: `Invalid --agent value: '${value}' (allowed agents: ${formatValidAgentNames()}).`,
305
+ message: `Invalid --agent value: '${value}' (allowed agents: ${formatReviewTicketsAgentNames()}).`,
296
306
  };
297
307
  }
298
308
  agentName = value;
@@ -416,7 +426,7 @@ export function resolveEffectiveReviewMode(key, options) {
416
426
  // hard-wired to `/implement-ticket`. Review-aware builders are used instead so
417
427
  // the prompt uses `/review-ticket` and no directory change is prepended.
418
428
  // ---------------------------------------------------------------------------
419
- export function buildReviewTicketPrompt(key, mode, base = NO_BASE_CONTEXT) {
429
+ export function buildReviewTicketPrompt(key, mode, base = NO_BASE_CONTEXT, agentName = "claude") {
420
430
  const autoFlag = mode.auto ? " --auto" : "";
421
431
  // Omit --rounds entirely when adaptive (mode.rounds === undefined) so the
422
432
  // backend routes by difficulty; only forward it for an explicit 1/2 override.
@@ -424,22 +434,23 @@ export function buildReviewTicketPrompt(key, mode, base = NO_BASE_CONTEXT) {
424
434
  const baseBranchFlag = base.baseBranch ? ` --base-branch=${base.baseBranch}` : "";
425
435
  const baseShaFlag = base.baseSha ? ` --base-sha=${base.baseSha}` : "";
426
436
  const noRefreshFlag = base.noRefreshBase ? " --no-refresh-base" : "";
427
- return `/review-ticket ${key}${autoFlag}${roundsFlag}${baseBranchFlag}${baseShaFlag}${noRefreshFlag}`;
437
+ const command = agentName === "codex" ? "$bridge-review-ticket" : "/review-ticket";
438
+ return `${command} ${key}${autoFlag}${roundsFlag}${baseBranchFlag}${baseShaFlag}${noRefreshFlag}`;
428
439
  }
429
- export function buildPosixReviewAgentShellCommand(agent, key, mode, cwd, modelAlias, base = NO_BASE_CONTEXT) {
430
- const prompt = buildReviewTicketPrompt(key, mode, base);
431
- const invocation = buildAgentInvocation(agent, prompt, (p) => `'${shSquoteInner(p)}'`, modelAlias);
440
+ export function buildPosixReviewAgentShellCommand(agent, key, mode, cwd, modelAlias, base = NO_BASE_CONTEXT, launchArgs = []) {
441
+ const prompt = buildReviewTicketPrompt(key, mode, base, agent.name);
442
+ const invocation = buildAgentInvocation(agent, prompt, (p) => `'${shSquoteInner(p)}'`, modelAlias, launchArgs);
432
443
  return `cd '${shSquoteInner(cwd)}' && ${invocation}`;
433
444
  }
434
- export function buildPowerShellReviewAgentShellCommand(agent, key, mode, modelAlias, base = NO_BASE_CONTEXT) {
435
- const prompt = buildReviewTicketPrompt(key, mode, base);
436
- return buildAgentInvocation(agent, prompt, powershellSquote, modelAlias);
445
+ export function buildPowerShellReviewAgentShellCommand(agent, key, mode, modelAlias, base = NO_BASE_CONTEXT, launchArgs = []) {
446
+ const prompt = buildReviewTicketPrompt(key, mode, base, agent.name);
447
+ return buildAgentInvocation(agent, prompt, powershellSquote, modelAlias, launchArgs);
437
448
  }
438
- export function buildReviewAgentShellCommand(agent, key, mode, platform, cwd, modelAlias, base = NO_BASE_CONTEXT) {
449
+ export function buildReviewAgentShellCommand(agent, key, mode, platform, cwd, modelAlias, base = NO_BASE_CONTEXT, launchArgs = []) {
439
450
  if (platform === "win32") {
440
- return buildPowerShellReviewAgentShellCommand(agent, key, mode, modelAlias, base);
451
+ return buildPowerShellReviewAgentShellCommand(agent, key, mode, modelAlias, base, launchArgs);
441
452
  }
442
- return buildPosixReviewAgentShellCommand(agent, key, mode, cwd, modelAlias, base);
453
+ return buildPosixReviewAgentShellCommand(agent, key, mode, cwd, modelAlias, base, launchArgs);
443
454
  }
444
455
  // ---------------------------------------------------------------------------
445
456
  // Unsupported platform message for review-tickets
@@ -535,17 +546,18 @@ export async function orchestrateReviewTickets(deps, options, overrides = {}) {
535
546
  if (!agent) {
536
547
  return {
537
548
  ok: false,
538
- error: `Unknown agent: '${options.agentName}'. Valid agents: ${formatValidAgentNames()}.`,
549
+ error: `Unknown agent: '${options.agentName}'. Valid agents: ${formatReviewTicketsAgentNames()}.`,
550
+ };
551
+ }
552
+ // Reject an invalid multi-key claim before any Codex skill/config writes.
553
+ if (options.epic?.dispatch_key && options.keys.length > 1) {
554
+ return {
555
+ ok: false,
556
+ error: "epic.dispatch_key cannot be used with multiple keys: dispatch_key is a single-ticket claim " +
557
+ "and would be re-claimed for each key in the batch. Call orchestrateReviewTickets once per ticket instead.",
539
558
  };
540
559
  }
541
560
  if (options.dryRun) {
542
- if (options.epic?.dispatch_key && options.keys.length > 1) {
543
- return {
544
- ok: false,
545
- error: "epic.dispatch_key cannot be used with multiple keys: dispatch_key is a single-ticket claim " +
546
- "and would be re-claimed for each key in the batch. Call orchestrateReviewTickets once per ticket instead.",
547
- };
548
- }
549
561
  // --dry-run never performs the base-branch fetch (side-effect-free preview);
550
562
  // it reflects only what the user explicitly passed on the CLI.
551
563
  const rows = buildReviewPlanRows(deps, options, agent, "dry-run", overrides, previewBaseContextFromOptions(options));
@@ -554,15 +566,18 @@ export async function orchestrateReviewTickets(deps, options, overrides = {}) {
554
566
  const preflight = await runReviewTicketsPreflight(deps, options);
555
567
  if (!preflight.ok)
556
568
  return { ok: false, error: preflight.error };
557
- // A dispatch_key embeds a single ticket (dispatch:{epic}:{ticket}:{plan_version}).
558
- // Claiming the same key for every iteration of a multi-key batch would silently
559
- // drop all but the first ticket; reject early with a clear error instead.
560
- if (options.epic?.dispatch_key && options.keys.length > 1) {
561
- return {
562
- ok: false,
563
- error: "epic.dispatch_key cannot be used with multiple keys: dispatch_key is a single-ticket claim " +
564
- "and would be re-claimed for each key in the batch. Call orchestrateReviewTickets once per ticket instead.",
565
- };
569
+ let codexLaunchArgs = [];
570
+ if (agent.name === "codex") {
571
+ const prepare = overrides.prepareCodexSession ?? (async (d) => {
572
+ const provision = await provisionCommandsForWorktree(d.cwd, buildCommandProvisioningDeps(d), true);
573
+ if (!provision.ok)
574
+ return { ok: false, error: provision.error };
575
+ return resolveCodexLaunchScope(d.cwd, buildMcpProvisioningDeps(d));
576
+ });
577
+ const scope = await prepare(deps);
578
+ if (!scope.ok)
579
+ return { ok: false, error: scope.error };
580
+ codexLaunchArgs = scope.argv;
566
581
  }
567
582
  // BAPI-474 D-3: parent-fetch-once — resolve ONE base_branch/base_sha pin for
568
583
  // the whole batch before any tab is spawned. D-4: on failure (and no
@@ -578,7 +593,7 @@ export async function orchestrateReviewTickets(deps, options, overrides = {}) {
578
593
  const terminal = detectTerminal(undefined, deps.env);
579
594
  const rows = await runWithConcurrency(options.keys, options.maxParallel, async (key) => {
580
595
  const mode = resolveEffectiveReviewMode(key, options);
581
- const baseShellCommand = buildReviewAgentShellCommand(agent, key, mode, deps.platform, deps.cwd, options.modelAlias, baseContext);
596
+ const baseShellCommand = buildReviewAgentShellCommand(agent, key, mode, deps.platform, deps.cwd, options.modelAlias, baseContext, codexLaunchArgs);
582
597
  let shellCommand = baseShellCommand;
583
598
  let runId;
584
599
  if (options.epic) {
@@ -727,7 +742,7 @@ export async function runReviewTicketsCli(argv, overrides = {}) {
727
742
  if (options.dryRun) {
728
743
  const agent = resolveAgentSpec(options.agentName);
729
744
  if (!agent) {
730
- errorLog(`Error: Unknown agent: '${options.agentName}'. Valid agents: ${formatValidAgentNames()}.`);
745
+ errorLog(`Error: Unknown agent: '${options.agentName}'. Valid agents: ${formatReviewTicketsAgentNames()}.`);
731
746
  return 1;
732
747
  }
733
748
  const rows = buildReviewPlanRows(deps, options, agent, "dry-run", overrides.orchestrateOverrides, previewBaseContextFromOptions(options));
@@ -55,7 +55,7 @@ export const MODULE_MOCK_MANIFEST = [
55
55
  // exist — the failure mode this manifest is for.
56
56
  "build/ci-check-wait.test.js",
57
57
  "build/conductor/cli-git-hooks.test.js",
58
- "build/conductor/cli.test.js",
58
+ "build/conductor/epic-implementer-cli.test.js",
59
59
  "build/conductor/git-inspection.test.js",
60
60
  "build/conductor/paths.test.js",
61
61
  "build/conductor/pr-ci-producer-emit-seam.test.js",
@@ -47,7 +47,7 @@
47
47
  * association is immutable server-side and can never be attached later.
48
48
  * c. Cut or validate `origin/<feature>` at EXACTLY the canonical indexed SHA
49
49
  * with the operator's LOCAL `git`, through the shared cut protocol
50
- * `conduct-epic init` also drives, with the run id so the scope is minted
50
+ * `implement-epic init` also drives, with the run id so the scope is minted
51
51
  * owned by the run. An existing branch at a different commit fails closed.
52
52
  * The GitHub App never pushes — it is `contents: read`.
53
53
  * d. Store and approve (unchanged requests). The server now READ-validates the
@@ -81,7 +81,7 @@ import { createProductionEpicIntegrationGhRunner, ensureEpicIntegrationPullReque
81
81
  import { approveEpicPlan, createEpicRunWithDisposition, fetchConductorReadiness, fetchEpicRunState, replaceEpicRunPolicy, resolveConductorBridgeApiAccess, storeEpicPlan, validateEpicPlan, ConductorBridgeApiError, } from "./conductor/bridge-api-client.js";
82
82
  import { resolveTrackedPaths } from "./git-reachability.js";
83
83
  import { validateBranchName } from "./base-ref.js";
84
- // Reused from the conduct-epic client on purpose: adding a second wrapper under
84
+ // Reused from the implement-epic client on purpose: adding a second wrapper under
85
85
  // `conductor/` would give the v2 path its own copy to drift from.
86
86
  //
87
87
  // BAPI-850 extends that reuse to the canonical parse read, the repository base
@@ -89,9 +89,10 @@ import { validateBranchName } from "./base-ref.js";
89
89
  // typed clients the pilot already has are the v2 path's clients too, so neither
90
90
  // conductor can drift on a field name or an endpoint shape. No MCP tool, no
91
91
  // input-schema field, and no description changes.
92
- import { getConfigFieldBaseBranch, getIndexScopeLifecycle, getParseStatus, } from "./conduct-epic/bridge-client.js";
93
- import { boundScopeLifecycleLabel, createExecFileRunCommand, ensureCommitResolvableLocally, normalizeCommitSha, performExactIndexScopeCut, pollIndexScopeLifecycle, readRemoteBranchHead, runGit, SCOPE_BOOTSTRAP_POLL_INTERVAL_MS, SCOPE_LIFECYCLE_LABELS, } from "./conduct-epic/cut-protocol.js";
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";
94
94
  import { hashPlan } from "./conductor/plan.js";
95
+ import { BRING_UP_REFUSAL_MESSAGES, firstBringUpRefusal, } from "./conductor/bring-up-facts.js";
95
96
  import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
96
97
  // BAPI-872: local plane-manifest binding, so `plane down` can later stop the
97
98
  // run this setup resolved WITHOUT guessing from repository-wide active-run
@@ -176,7 +177,7 @@ export function getSetupEpicUsage() {
176
177
  "single-node plan) is unchanged and composes no posture at all.",
177
178
  "",
178
179
  "Unattended runs have TWO one-time repository prerequisites, refused by name",
179
- "before any run is created: repository consent (unattended_conductor_allowed) and",
180
+ "before any run is created: repository consent (unattended_epic_implementer_allowed) and",
180
181
  "a verified notify webhook default. `drive-epic` readiness lists both with their",
181
182
  "current state.",
182
183
  "",
@@ -1180,7 +1181,7 @@ export function evaluateSetupEpicUnattendedPrerequisites(readiness, requestedPol
1180
1181
  return {
1181
1182
  reason: "target_not_allowed",
1182
1183
  message: "this repository has not consented to unattended conductor runs. Set " +
1183
- "`unattended_conductor_allowed` for it (see " +
1184
+ "`unattended_epic_implementer_allowed` for it (see " +
1184
1185
  "docs/claude/account-settings-operator-runbook.md), or pass --attended to " +
1185
1186
  "create an attended run. No run was created.",
1186
1187
  };
@@ -1218,6 +1219,31 @@ export function evaluateSetupEpicUnattendedPrerequisites(readiness, requestedPol
1218
1219
  }
1219
1220
  return null;
1220
1221
  }
1222
+ /** The closing sentence every pre-create refusal ends with. */
1223
+ const NO_RUN_WAS_CREATED = "No run was created.";
1224
+ /**
1225
+ * Evaluate the bring-up facts against a readiness report.
1226
+ *
1227
+ * Pure and total: `null` is a readiness read that failed, which has proven
1228
+ * nothing and refuses as `readiness_unreadable`. Otherwise at most ONE refusal,
1229
+ * in the fixed {@link BRING_UP_REFUSAL_REASONS} order — each fact's
1230
+ * "unreadable" before its "bad", so an operator is never told to repair a
1231
+ * condition that could not be read.
1232
+ */
1233
+ export function evaluateSetupEpicBringUpPrerequisites(readiness) {
1234
+ if (readiness === null) {
1235
+ return {
1236
+ reason: "readiness_unreadable",
1237
+ message: "the Bridge API conductor readiness report could not be read, so the database " +
1238
+ "migration, worker presence, and single-reconciler facts could not be confirmed. " +
1239
+ `Retry once the server is reachable. ${NO_RUN_WAS_CREATED}`,
1240
+ };
1241
+ }
1242
+ const reason = firstBringUpRefusal(readiness.bring_up ?? null);
1243
+ if (reason === null)
1244
+ return null;
1245
+ return { reason, message: `${BRING_UP_REFUSAL_MESSAGES[reason]} ${NO_RUN_WAS_CREATED}` };
1246
+ }
1221
1247
  /**
1222
1248
  * Does the policy this invocation will actually send request unattended operation?
1223
1249
  *
@@ -1619,13 +1645,13 @@ function scopeFailureGuidance(reason, featureBranch, epicRunId) {
1619
1645
  if (reason === "canonical_index_advanced") {
1620
1646
  return (`${SETUP_EPIC_SCOPE_REDRIVE_RECOVERY} The canonical index advanced before the ` +
1621
1647
  `scope could be seeded: delete origin/${featureBranch}, abandon this run ` +
1622
- `(conductor abandon-run --epic-run-id ${epicRunId}), and re-run setup-epic.`);
1648
+ `(epic-implementer abandon-run --epic-run-id ${epicRunId}), and re-run setup-epic.`);
1623
1649
  }
1624
1650
  if (reason === "head_mismatch") {
1625
1651
  return (`${SETUP_EPIC_SCOPE_REDRIVE_RECOVERY} origin/${featureBranch} does not sit at the ` +
1626
1652
  `commit this scope was cut at, and setup-epic never force-updates an existing ` +
1627
1653
  `ref: delete origin/${featureBranch}, abandon this run ` +
1628
- `(conductor abandon-run --epic-run-id ${epicRunId}), and re-run setup-epic.`);
1654
+ `(epic-implementer abandon-run --epic-run-id ${epicRunId}), and re-run setup-epic.`);
1629
1655
  }
1630
1656
  return `Recovery: ${SETUP_EPIC_SCOPE_RECOVERY_COMMAND}.`;
1631
1657
  }
@@ -1924,7 +1950,7 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
1924
1950
  else if (err instanceof ConductorBridgeApiError && err.status === 409) {
1925
1951
  deps.errorLog(`Epic ${opts.epicKey} has MULTIPLE active runs — it is wedged, and every ` +
1926
1952
  `plan call will keep failing. Abandon the duplicate before retrying:\n` +
1927
- ` conductor abandon-run --epic-run-id <duplicate_epic_run_id>\n` +
1953
+ ` epic-implementer abandon-run --epic-run-id <duplicate_epic_run_id>\n` +
1928
1954
  `Detail: ${errorDetail(err)}`);
1929
1955
  return 1;
1930
1956
  }
@@ -1982,7 +2008,7 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
1982
2008
  `'${existingBaseBranch}', not the repository base '${repositoryBase}', and ` +
1983
2009
  `--into-base asks for the repository base branch. setup-epic will not retarget ` +
1984
2010
  `an existing run. Re-run without --into-base to reuse it unchanged, or abandon ` +
1985
- `the run (conductor abandon-run --epic-run-id ${existingRunId}) and recreate ` +
2011
+ `the run (epic-implementer abandon-run --epic-run-id ${existingRunId}) and recreate ` +
1986
2012
  `it on base.`);
1987
2013
  return 1;
1988
2014
  }
@@ -2112,7 +2138,7 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
2112
2138
  `keep failing. This is TERMINAL: it will not clear on retry, and it is ` +
2113
2139
  `NOT a problem with your plan. Adopt the run you want to keep, or ` +
2114
2140
  `abandon the duplicate:\n` +
2115
- ` conductor abandon-run --epic-run-id <duplicate_epic_run_id>\n` +
2141
+ ` epic-implementer abandon-run --epic-run-id <duplicate_epic_run_id>\n` +
2116
2142
  `No run was created and no automation-start charge occurred.\n` +
2117
2143
  `Detail: ${errorDetail(err)}`);
2118
2144
  return 1;
@@ -2186,7 +2212,7 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
2186
2212
  // used for status instead). A no-feature run performs none of this.
2187
2213
  //
2188
2214
  // The scope's BASE is the repository's configured base branch (the same
2189
- // resolution `conduct-epic init` uses), NOT `policy_json.base_branch` — that
2215
+ // resolution `implement-epic init` uses), NOT `policy_json.base_branch` — that
2190
2216
  // key names the FEATURE branch. A feature branch equal to the configured base
2191
2217
  // needs no scope (dispatch already cuts from it) and matches the server's
2192
2218
  // long-standing no-op for that case.
@@ -2371,9 +2397,9 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
2371
2397
  // run that is already going.
2372
2398
  //
2373
2399
  // Evaluated against the COMPOSED policy, so it covers the default profile and
2374
- // a `--policy-file` alike; skipped entirely for `--attended` and for any
2375
- // policy that is not unattended, which is what keeps an attended run from
2376
- // paying for a readiness read it does not need.
2400
+ // a `--policy-file` alike; the UNATTENDED evaluation is skipped for
2401
+ // `--attended` and for any policy that is not unattended. (The readiness
2402
+ // read itself is no longer skipped — see BAPI-1153 below.)
2377
2403
  //
2378
2404
  // The file branch runs through `applySetupEpicUnattendedDefault` rather than
2379
2405
  // taking the file verbatim: a file bypasses the no-file composer entirely, so
@@ -2381,18 +2407,30 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
2381
2407
  // which made the default profile unreachable by any documented invocation.
2382
2408
  // The composed object is what gets gated AND what gets sent, so the create
2383
2409
  // request carries exactly what was validated.
2410
+ //
2411
+ // BAPI-1153: ONE readiness read now serves both gates, and it happens for
2412
+ // EVERY create — attended included — because the bring-up gate below applies
2413
+ // to every run. Before any create request, manifest binding, or lane.
2384
2414
  {
2415
+ let readiness = null;
2416
+ try {
2417
+ readiness = await fetchConductorReadiness(access, deps.fetch);
2418
+ }
2419
+ catch {
2420
+ // Fail CLOSED, and silently as to WHY: the thrown value can carry a URL
2421
+ // or a response excerpt, and this refusal is rendered to a terminal. The
2422
+ // generic `readiness_unreadable` message says what to do about it.
2423
+ readiness = null;
2424
+ }
2425
+ // Bring-up first: a behind database or a missing worker makes the
2426
+ // unattended questions moot, and refusing on the cheaper, universal fact
2427
+ // keeps the first-named blocker the same for attended and unattended runs.
2428
+ const bringUpRefusal = evaluateSetupEpicBringUpPrerequisites(readiness);
2429
+ if (bringUpRefusal !== null) {
2430
+ deps.errorLog(`setup-epic REFUSED (${bringUpRefusal.reason}): ${bringUpRefusal.message}`);
2431
+ return 1;
2432
+ }
2385
2433
  if (requestedPolicyIsUnattended(composedForCreate, opts.attended)) {
2386
- let readiness = null;
2387
- try {
2388
- readiness = await fetchConductorReadiness(access, deps.fetch);
2389
- }
2390
- catch {
2391
- // Fail CLOSED, and silently as to WHY: the thrown value can carry a URL
2392
- // or a response excerpt, and this refusal is rendered to a terminal. The
2393
- // generic `readiness_unreadable` message says what to do about it.
2394
- readiness = null;
2395
- }
2396
2434
  // The COMPOSED policy, not the flags: a --policy-file declaring
2397
2435
  // notify.webhook_url satisfies the escalation-channel prerequisite, and
2398
2436
  // it is the same object the create request carries.
@@ -2456,7 +2494,7 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
2456
2494
  say(`Run: adopting existing active run ${run.epic_run_id} (status: ${run.status})`);
2457
2495
  say(` No second run was created — one epic has at most one active ` +
2458
2496
  `run. Continue on this one, or abandon it first with ` +
2459
- `\`conductor abandon-run --epic-run-id ${run.epic_run_id}\` to start over.`);
2497
+ `\`epic-implementer abandon-run --epic-run-id ${run.epic_run_id}\` to start over.`);
2460
2498
  if (requestedPolicy !== undefined) {
2461
2499
  const outcome = await applyReusedRunPolicy({
2462
2500
  access,
@@ -2522,7 +2560,7 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
2522
2560
  else {
2523
2561
  const msg = `Could not bind the local plane to run ${result.epic_run_id} (${bound.message}). ` +
2524
2562
  "A later `plane down` cannot be guaranteed to stop it automatically — if you need " +
2525
- `to stop this run, run \`conductor stop-run --epic-run-id ${result.epic_run_id}\`.`;
2563
+ `to stop this run, run \`epic-implementer stop-run --epic-run-id ${result.epic_run_id}\`.`;
2526
2564
  warnings.push(msg);
2527
2565
  say(`Plane: [warn] ${msg}`);
2528
2566
  }
@@ -2542,7 +2580,7 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
2542
2580
  const msg = `A local plane manifest exists but could not be validated (${manifestRead.error}), so ` +
2543
2581
  `run ${result.epic_run_id} was NOT bound to it. If a plane is running, \`plane down\` ` +
2544
2582
  "will not stop this run automatically — use " +
2545
- `\`conductor stop-run --epic-run-id ${result.epic_run_id}\`. A manifest written by a ` +
2583
+ `\`epic-implementer stop-run --epic-run-id ${result.epic_run_id}\`. A manifest written by a ` +
2546
2584
  "NEWER build must be wound down with that build's `plane down`.";
2547
2585
  warnings.push(msg);
2548
2586
  say(`Plane: [warn] ${msg}`);
@@ -2553,7 +2591,7 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
2553
2591
  // an unexpected error here is reported as a warning, never thrown.
2554
2592
  const msg = `Could not check for a local plane to bind run ${result.epic_run_id} to (${errorDetail(err)}). ` +
2555
2593
  "A later `plane down` cannot be guaranteed to stop it automatically — if you need to stop " +
2556
- `this run, run \`conductor stop-run --epic-run-id ${result.epic_run_id}\`.`;
2594
+ `this run, run \`epic-implementer stop-run --epic-run-id ${result.epic_run_id}\`.`;
2557
2595
  warnings.push(msg);
2558
2596
  say(`Plane: [warn] ${msg}`);
2559
2597
  }
@@ -2562,7 +2600,7 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
2562
2600
  // AFTER the run exists and BEFORE the plan is stored/approved: the scope's run
2563
2601
  // association is immutable server-side, so the cut is driven with this run's
2564
2602
  // id and the scope is minted owned by it. Everything from `cut/begin` to the
2565
- // hold's release is the SHARED protocol `conduct-epic init` drives — local git
2603
+ // hold's release is the SHARED protocol `implement-epic init` drives — local git
2566
2604
  // only, an exact-SHA create-only push, never the GitHub App, never a force
2567
2605
  // update. A failure leaves the run non-active (never discoverable by the
2568
2606
  // reconciler) and re-running setup-epic reuses it and re-drives the cut.
@@ -2683,7 +2721,7 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
2683
2721
  deps.errorLog(`The plan was NOT approved: this repository is no longer authorized for ` +
2684
2722
  `unattended runs. The run WAS created and is left in 'planning' with no ` +
2685
2723
  `activation and no executor dispatch — nothing was half-applied.\n` +
2686
- `Restore \`unattended_conductor_allowed\` for this repository (see ` +
2724
+ `Restore \`unattended_epic_implementer_allowed\` for this repository (see ` +
2687
2725
  `docs/claude/account-settings-operator-runbook.md) and re-run setup-epic, ` +
2688
2726
  `or deliberately convert this run to an attended posture through the ` +
2689
2727
  `supported operator flow.\nDetail: ${errorDetail(err)}`);
@@ -83,9 +83,10 @@ export const DEFAULT_CONDUCTOR_GATE_NAME = "implement-ticket";
83
83
  * Exported since BAPI-1020 so the executor resolves the worker-guard bin through
84
84
  * the SAME resolver rather than growing a second one. Correct in both build
85
85
  * shapes because this module compiles to the build ROOT: the esbuild bundle puts
86
- * `import.meta.url` at `build/index.js` / `build/conductor-bin.js`, and `tsc` puts
87
- * it at `build/start-tickets-conductor.js`. A resolver living in a subdirectory
88
- * module would resolve to `build/<subdir>/<filename>` under `tsc` and be wrong.
86
+ * `import.meta.url` at `build/index.js` / `build/epic-implementer-bin.js`, and
87
+ * `tsc` puts it at `build/start-tickets-conductor.js`. A resolver living in a
88
+ * subdirectory module would resolve to `build/<subdir>/<filename>` under `tsc`
89
+ * and be wrong.
89
90
  */
90
91
  export function defaultResolveBinPath(filename) {
91
92
  return fileURLToPath(new URL(`./${filename}`, import.meta.url));
@@ -124,12 +125,12 @@ export async function createStartTicketsConductorContext(options, agent, deps) {
124
125
  gateName,
125
126
  supervisorMode,
126
127
  agentName: agent.name,
127
- cliFile: resolveBinPath("conductor-bin.js"),
128
+ cliFile: resolveBinPath("epic-implementer-bin.js"),
128
129
  // Capture the conductor process's Node executable so dispatched workers run
129
- // conductor-bin.js under THIS runtime (which owns the ledger), never the
130
- // worker's own Node (BAPI-527).
130
+ // epic-implementer-bin.js under THIS runtime (which owns the ledger), never
131
+ // the worker's own Node (BAPI-527).
131
132
  conductorNodePath: deps.execPath ?? process.execPath,
132
- hookBinPath: resolveBinPath("conductor-claude-hook-bin.js"),
133
+ hookBinPath: resolveBinPath("epic-implementer-claude-hook-bin.js"),
133
134
  };
134
135
  if (options.epic) {
135
136
  context.epic = options.epic;