@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
@@ -8,7 +8,9 @@
8
8
  * written BEFORE spawn — see `../mcp-provisioning.js` wiring in
9
9
  * `job-runner.ts`), never via env vars. This builder still omits
10
10
  * `BRIDGE_MCP_PROFILE` (an absent profile selects the intended `core` tool
11
- * group default), every `BAPI_CONDUCTOR_*` key, `CONDUCTOR_NODE_PATH` (the
11
+ * group default), every Epic Implementer env key under BOTH the supported
12
+ * `BAPI_EPIC_IMPLEMENTER_*` prefix and the legacy Conductor prefix (BAPI-1190),
13
+ * the worker Node-path key under both its supported and legacy spellings (the
12
14
  * shim's node executable is resolved into the registration's command/args at
13
15
  * provisioning time instead), and all secret-bearing keys. It constructs a
14
16
  * FRESH object from a strict allowlist of non-secret operational keys — it
@@ -55,6 +57,7 @@
55
57
  import { PR_BASE_BRANCH_ENV_VAR, WORKER_BRANCH_ENV_VAR } from "../pr-base-contract.js";
56
58
  import { DECLARED_TOUCHED_FILES_ENV, normalizeDeclaredTouchedFiles, } from "../conductor/file-scope-guard.js";
57
59
  import { INDEX_SCOPE_ENV_VAR, validateOptionalIndexScope } from "../index-scope-contract.js";
60
+ import { EPIC_IMPLEMENTER_ENV_PREFIX, EPIC_IMPLEMENTER_NODE_PATH_ENV, LEGACY_EPIC_IMPLEMENTER_ENV_PREFIX, LEGACY_EPIC_IMPLEMENTER_NODE_PATH_ENV, } from "../epic-implementer-env.js";
58
61
  /** Non-secret operational keys forwarded to the worker when present. */
59
62
  const ALLOWED_ENV_KEYS = [
60
63
  "PATH",
@@ -116,7 +119,10 @@ const SECRET_NAME_FRAGMENTS = ["TOKEN", "SECRET", "PASSWORD", "API_KEY"];
116
119
  */
117
120
  export const EXPLICIT_DENY_KEYS = [
118
121
  "BRIDGE_MCP_PROFILE",
119
- "CONDUCTOR_NODE_PATH",
122
+ EPIC_IMPLEMENTER_NODE_PATH_ENV,
123
+ // BAPI-1190: the legacy spelling stays denied so a stale ambient value from an
124
+ // older package can never reach a worker during a version-skew window.
125
+ LEGACY_EPIC_IMPLEMENTER_NODE_PATH_ENV,
120
126
  "BAPI_API_KEY",
121
127
  // ANTHROPIC_API_KEY has NO passthrough exception (BAPI-791): it stays denied
122
128
  // absolutely, unlike CLAUDE_CODE_OAUTH_TOKEN below, which is denied here too
@@ -164,15 +170,78 @@ export const EXPLICIT_DENY_KEYS = [
164
170
  // the allowlist.
165
171
  ...WORKER_SENTINEL_DATABASE_ENV_KEYS,
166
172
  ];
173
+ // ---------------------------------------------------------------------------
174
+ // Executor-PROCESS GitHub credential deny-list (BAPI-1137, R-4)
175
+ //
176
+ // Everything above this line governs what reaches a spawned WORKER. This block
177
+ // governs the executor process itself, and it exists because the worker
178
+ // allowlist cannot reach far enough: a manager-level environment injection — a
179
+ // `launchctl setenv`, a systemd drop-in, a `~/.zprofile` that ran before the
180
+ // operator started the executor by hand — puts a GitHub write token in the
181
+ // executor's own environment, where a subprocess that builds its own environment
182
+ // (or any future code path that reads one) could still find it.
183
+ //
184
+ // The durable-host claim is that the host holds NO STANDING GitHub write
185
+ // credential. A unit that declares none (see `service-unit.ts`) and a process
186
+ // that scrubs any it inherited are the two halves of that claim; without the
187
+ // second, the first only proves the token did not come from the file Bridge
188
+ // generated.
189
+ // ---------------------------------------------------------------------------
190
+ /**
191
+ * The GitHub write-credential variable names the executor process refuses to
192
+ * carry. Exactly the two names this repository's environment-safety conventions
193
+ * already recognize (`EXPLICIT_DENY_KEYS` above denies the same pair for a
194
+ * worker), so there is ONE vocabulary for "a GitHub write credential" rather
195
+ * than a second, drifting list.
196
+ */
197
+ export const EXECUTOR_PROCESS_GITHUB_CREDENTIAL_DENY_KEYS = [
198
+ "GH_TOKEN",
199
+ "GITHUB_TOKEN",
200
+ ];
201
+ /**
202
+ * Delete every prohibited GitHub credential variable from the executor's own
203
+ * environment, and report only HOW MANY were removed.
204
+ *
205
+ * Three properties are load-bearing and are what the return type is shaped
206
+ * around:
207
+ *
208
+ * - **Names are compared case-insensitively.** `GH_TOKEN`, `gh_token`, and
209
+ * `Gh_Token` are the same variable to a shell on a case-preserving platform,
210
+ * and a scrubber that matched only the canonical spelling would leave the
211
+ * other two in place.
212
+ * - **Values are never read.** Nothing here dereferences a prohibited key
213
+ * before deleting it, so no credential value can be logged, returned,
214
+ * compared, or classified — not even as "looks like a PAT". A count is the
215
+ * most a caller can learn.
216
+ * - **It mutates in place.** The executor spawns children that inherit
217
+ * `process.env`; returning a filtered COPY would leave the real environment
218
+ * untouched and the guarantee imaginary.
219
+ */
220
+ export function scrubExecutorProcessGithubCredentials(env = process.env) {
221
+ const prohibited = new Set(EXECUTOR_PROCESS_GITHUB_CREDENTIAL_DENY_KEYS.map((key) => key.toUpperCase()));
222
+ let removedCount = 0;
223
+ // A snapshot of the key names, because deleting while iterating the live
224
+ // object is exactly the kind of thing that silently skips an entry.
225
+ for (const key of Object.keys(env)) {
226
+ if (!prohibited.has(key.toUpperCase()))
227
+ continue;
228
+ delete env[key];
229
+ removedCount++;
230
+ }
231
+ return { removedCount };
232
+ }
167
233
  /**
168
234
  * True only when `key` is a safe, allowlisted operational key. Exported so the
169
235
  * allowlist is locally auditable/testable. A key is allowed iff it is in the
170
- * allowlist AND is not explicitly denied, does not begin with `BAPI_CONDUCTOR_`,
171
- * and does not contain a secret-name fragment.
236
+ * allowlist AND is not explicitly denied, does not begin with the supported
237
+ * `BAPI_EPIC_IMPLEMENTER_` prefix or the legacy Conductor prefix, and does not
238
+ * contain a secret-name fragment.
172
239
  *
173
- * The `BAPI_CONDUCTOR_` rejection is a WHOLE-PREFIX rule, not an enumerated list,
174
- * which is what makes it hold for keys added later. BAPI-722's
175
- * `BAPI_CONDUCTOR_DENY_PROBE_TIMEOUT_MS` is rejected twice over — it is absent from
240
+ * Both prefix rejections are WHOLE-PREFIX rules, not enumerated lists, which is
241
+ * what makes them hold for keys added later. BAPI-1190 keeps the legacy prefix
242
+ * rejected alongside the renamed one, so a stale name inherited during a
243
+ * version-skew window is filtered too. BAPI-722's
244
+ * `BAPI_EPIC_IMPLEMENTER_DENY_PROBE_TIMEOUT_MS` is rejected twice over — it is absent from
176
245
  * `ALLOWED_ENV_KEYS` and it carries the prefix — so it stays executor-process
177
246
  * configuration and can never reach a spawned worker. That is deliberate: a worker
178
247
  * runs no deny probe, and letting job-side state influence the executor's own
@@ -183,7 +252,9 @@ export function isExecutorEnvKeyAllowed(key) {
183
252
  return false;
184
253
  if (EXPLICIT_DENY_KEYS.includes(key))
185
254
  return false;
186
- if (key.startsWith("BAPI_CONDUCTOR_"))
255
+ if (key.startsWith(EPIC_IMPLEMENTER_ENV_PREFIX))
256
+ return false;
257
+ if (key.startsWith(LEGACY_EPIC_IMPLEMENTER_ENV_PREFIX))
187
258
  return false;
188
259
  const upper = key.toUpperCase();
189
260
  if (SECRET_NAME_FRAGMENTS.some((fragment) => upper.includes(fragment)))
@@ -22,7 +22,7 @@
22
22
  * interval. This is TELEMETRY ONLY (R14): the marker is never persisted, never
23
23
  * reported as `last_commit_sha`, and never drives an executor decision — the
24
24
  * reconciler remains the sole consumer and the sole transition authority
25
- * (`docs/claude/conductor-standing-rules.md`).
25
+ * (`docs/claude/epic-implementer-standing-rules.md`).
26
26
  *
27
27
  * - SERVER STOP (WS-B executor half). A heartbeat response carrying the
28
28
  * server's stop request kills the owned worker like `stale_claim` does, but
@@ -7,16 +7,33 @@
7
7
  */
8
8
  export { runExecutorCli, getExecutorUsage, parseExecutorArgs } from "./cli.js";
9
9
  export { runExecutor } from "./runner.js";
10
- // Persistent-service packaging (BAPI-688). The CLI entry plus the PURE helpers
11
- // doctor needs to enumerate and inspect generated units — doctor must share this
10
+ // Persistent-service packaging (BAPI-688), the file-generation leg of
11
+ // durable-host mode (BAPI-1137). The CLI entry plus the PURE helpers doctor
12
+ // needs to enumerate and inspect generated units — doctor must share this
12
13
  // module's naming/format contract instead of maintaining a second parser.
14
+ //
15
+ // This group grants GENERATION only. It confers no lifecycle capability, which
16
+ // is why the lifecycle exports stay in their own group below: a caller that
17
+ // imports the generator still cannot start a persistent daemon.
13
18
  export { runExecutorInstallServiceCli, getExecutorInstallServiceUsage, buildExecutorServiceArtifact, writeExecutorServiceArtifact, inspectExecutorServiceArtifact, executorLaunchdDirForHome, executorSystemdDirForHome, executorIdFromLaunchdFilename, executorIdFromSystemdFilename,
14
19
  // Programmatic, lifecycle-free plan/write seam (BAPI-779) — how
15
20
  // `install conductor` reaches the generator without shelling out to the CLI.
16
- planExecutorServiceInstall, writeExecutorServicePlan, resolvePackagedExecutorInvocation, EXECUTOR_SERVICE_BASE_URL_ENV, EXECUTOR_SERVICE_PATH_ENV, } from "./service-unit.js";
21
+ planExecutorServiceInstall, writeExecutorServicePlan, resolvePackagedExecutorInvocation,
22
+ // Durable-host credential declaration contract (BAPI-1137, R-4). doctor needs
23
+ // the shared inspector and the postcondition helper so its diagnostic and the
24
+ // generator's write check are literally the same rule; exporting the allowlist
25
+ // and the argument-contract predicate keeps the contract locally auditable
26
+ // rather than discoverable only by reading the renderer.
27
+ inspectExecutorServiceCredentialDeclaration, assertExecutorServiceCredentialPostcondition, checkExecutorServiceArgumentContract, isAllowedExecutorServiceUnitEnvKey, executorLaunchdLogPathsForId, EXECUTOR_SERVICE_BASE_URL_ENV, EXECUTOR_SERVICE_PATH_ENV, EXECUTOR_SERVICE_UNIT_ENV_ALLOWLIST, EXECUTOR_SERVICE_UNIT_FILE_MODE, } from "./service-unit.js";
17
28
  // Consented lifecycle (BAPI-779, R-1). Deliberately a SEPARATE module from the
18
29
  // generator above: importing the writer never grants the ability to start a
19
30
  // persistent daemon.
20
- export { buildExecutorServiceStartPlan, buildExecutorServiceStateProbe, formatExecutorServiceStartFailure, inspectExecutorServiceState, startExecutorService, } from "./service-lifecycle.js";
31
+ export { buildExecutorServiceStartPlan, buildExecutorServiceStateProbe, formatExecutorServiceStartFailure, inspectExecutorServiceState, startExecutorService,
32
+ // Durability prerequisites (BAPI-1137). Exported SEPARATELY from the
33
+ // generation group above, and alongside — not inside — the start group: these
34
+ // are strictly read-only probes, so importing them grants the ability to ASK
35
+ // whether this host survives logout, never the ability to change it. Nothing
36
+ // here runs `loginctl enable-linger`.
37
+ buildExecutorServiceDurabilityProbe, inspectExecutorServiceDurability, EXECUTOR_LINGER_REMEDIATION, EXECUTOR_MACOS_SESSION_REMEDIATION, } from "./service-lifecycle.js";
21
38
  export { collectExecutorInstallPreflight, formatExecutorInstallPreflight, EXECUTOR_INSTALL_REQUIRED_TOOLS, EXECUTOR_INSTALL_CLAUDE_LOGIN_REMEDIATION, } from "./install-preflight.js";
22
39
  export { runExecutorWatchCli, getExecutorWatchUsage, parseExecutorWatchArgs, } from "./watch-cli.js";
@@ -143,12 +143,12 @@ export const WorkerEnvFilePresent = "ContractError.WorkerEnvFilePresent";
143
143
  * BAPI-1152 — `reconcile_overlap` failure kinds. Each names ONE way the
144
144
  * wrapper's merge-into-child could not reach a verified push, so an operator
145
145
  * reading the row knows which fact to go look at. None of them is a verdict: a
146
- * refused governance change and a moved head are NOT failures — they complete
147
- * with a fact envelope — so they have no kind here.
146
+ * moved head is NOT a failure — it completes with a fact envelope — so it has
147
+ * no kind here.
148
148
  *
149
149
  * - `ContractError.ReconcileOverlapPayload` — the payload failed validation
150
150
  * before any subprocess ran (missing/blank input SHA, unknown schema version,
151
- * malformed command or region record).
151
+ * malformed command record).
152
152
  * - `ContractError.ReconcileOverlapBranchMismatch` — the prepared worktree's
153
153
  * branch is not the job's top-level `expected_branch`, so the push destination
154
154
  * cannot be trusted.
@@ -19,6 +19,7 @@ import os from "node:os";
19
19
  import { runHeartbeatLoop } from "./heartbeat.js";
20
20
  import { isExecutorNamedError, toExecutorFailure, secretFreeErrorMessage, MissingVerdictArtifact, PreSpawnVerification, RequiredMcpRegistration, McpSurfaceMismatch, WorkerStartupFatal, WORKER_STARTUP_FATAL_MESSAGE, formatMcpSurfaceMismatch, ClaudeNotAuthenticated, CLAUDE_NOT_AUTHENTICATED_MESSAGE, ExecutorAdapterUnavailable, ExecutorNamedError, WorktreeBusy, StaleArtifactCleanupFailed, WORKTREE_BUSY_MESSAGE, WorkerEnvFilePresent, WORKER_ENV_FILE_PRESENT_MESSAGE, } from "./job-errors.js";
21
21
  import { buildConductorMergeAccessForExecutorJob, buildDefaultMergeLocalDeps, runExecutorMergeJob, MERGE_RETRYABLE, MERGE_FAILED, } from "./merge-job.js";
22
+ import { createProductionCiRerunDeps, runCiRerunJob, } from "./ci-rerun-job.js";
22
23
  import { createObservationState } from "./observation.js";
23
24
  import { getWorktreeMcpRegistrationTargets, normalizeWorktreePathForRegistration, provisionMcpRegistrationForWorktree, } from "../mcp-provisioning.js";
24
25
  import { resolveExecutorAgentAdapter } from "../agent-launchers/executor-adapter-registry.js";
@@ -615,6 +616,16 @@ control) {
615
616
  if (job.job_type === "smoke") {
616
617
  return runSmokeJob(job, httpClient, options, deps, ownership, observation, control);
617
618
  }
619
+ // BAPI-1160: `ci_rerun` is a deterministic executor action, dispatched here
620
+ // beside `merge`/`smoke` and BEFORE the generic spawn path. It ensures no
621
+ // worktree and spawns no worker, and it must NEVER fall back to one: spawning
622
+ // an LLM worker to "fix" a failure the server already proved to be
623
+ // infrastructure is exactly the behavior this job type replaces. `job-types.ts`
624
+ // keeps it out of both `SPAWN_JOB_TYPES` and `RECOVERY_JOB_TYPES`, so reaching
625
+ // the spawn path below is structurally impossible rather than merely unlikely.
626
+ if (job.job_type === "ci_rerun") {
627
+ return runCiRerunExecutorJob(job, httpClient, options, deps, ownership, observation, seams, control);
628
+ }
618
629
  // BAPI-1152: `reconcile_overlap` takes its OWN branch, after `merge`/`smoke`
619
630
  // and before the generic spawn path. It needs a prepared, locked worktree like
620
631
  // a spawn job, but its body is the reconcile module's state machine: the
@@ -652,6 +663,121 @@ control) {
652
663
  });
653
664
  return { status: "failed", reason: "unsupported_job_type" };
654
665
  }
666
+ /**
667
+ * Run a deterministic `ci_rerun` job (BAPI-1160).
668
+ *
669
+ * Supervised through the SAME ownership, heartbeat, dead-man, and timeout
670
+ * protocol as {@link runMergeJob} — via {@link createFlowProcess} — so a
671
+ * deterministic job is not a second, weaker lifecycle. The body itself spawns no
672
+ * subprocess of its own beyond the bounded `gh` invocations inside
673
+ * `ci-rerun-job.ts`, and there is NO fallback to a worker on failure.
674
+ *
675
+ * The completion carries only the job's fact envelope. It names no gate,
676
+ * requests no transition, and asserts no verdict: the reconciler re-observes the
677
+ * head independently and remains the sole transition authority (R14 rule 3).
678
+ */
679
+ async function runCiRerunExecutorJob(job, httpClient, options, deps, ownership, observation, seams, control) {
680
+ // Resolve + enforce payload.timeout_seconds BEFORE any provider work, exactly
681
+ // as the merge branch does.
682
+ const timeout = resolveJobTimeoutSeconds(job, options.defaultJobTimeoutSeconds);
683
+ if (!timeout.ok) {
684
+ await httpClient.fail(job, {
685
+ error_kind: "ContractError.Timeout",
686
+ error_message: timeout.error,
687
+ classification: "crashed",
688
+ });
689
+ return { status: "failed", reason: "timeout_contract" };
690
+ }
691
+ const runCiRerun = seams.runCiRerun ??
692
+ ((payload) => runCiRerunJob(payload, createProductionCiRerunDeps()));
693
+ const flow = createFlowProcess(() => runCiRerun(job.payload), (result) => result.ok);
694
+ const procResult = await superviseProcess({
695
+ job,
696
+ httpClient,
697
+ options,
698
+ deps,
699
+ ownership,
700
+ observation,
701
+ proc: flow.proc,
702
+ timeoutSeconds: timeout.timeoutSeconds,
703
+ collectTelemetry: async () => ({}),
704
+ control,
705
+ });
706
+ if (ownership.abandoned) {
707
+ return { status: "abandoned", reason: ownership.abandonReason };
708
+ }
709
+ if (procResult.classification === "timeout") {
710
+ const terminal = await sendTerminalMutationWithAuthReport({
711
+ kind: "fail",
712
+ send: () => httpClient.fail(job, {
713
+ error_kind: "CiRerunFailed",
714
+ error_message: "ci_rerun exceeded its job timeout",
715
+ classification: "timeout",
716
+ telemetry: observation.snapshot(),
717
+ }),
718
+ deps,
719
+ options,
720
+ ownership,
721
+ log: deps.log,
722
+ }, job, control);
723
+ return terminalToRunResult(terminal, "failed");
724
+ }
725
+ const outcome = flow.getOutcome();
726
+ if (outcome === undefined) {
727
+ const terminal = await sendTerminalMutationWithAuthReport({
728
+ kind: "fail",
729
+ send: () => httpClient.fail(job, {
730
+ error_kind: "CiRerunFailed",
731
+ error_message: "ci_rerun completed without an outcome",
732
+ classification: "crashed",
733
+ telemetry: observation.snapshot(),
734
+ }),
735
+ deps,
736
+ options,
737
+ ownership,
738
+ log: deps.log,
739
+ }, job, control);
740
+ return terminalToRunResult(terminal, "failed");
741
+ }
742
+ if (outcome.ok) {
743
+ const completion = {
744
+ job_type: "ci_rerun",
745
+ exit_code: 0,
746
+ classification: "clean_exit",
747
+ // The FACT ENVELOPE, verbatim. No gate, no transition, no verdict — the
748
+ // backend model is `extra="forbid"`, so anything else fails validation
749
+ // rather than being silently ignored.
750
+ result: outcome.result,
751
+ telemetry: observation.snapshot(),
752
+ };
753
+ const terminal = await sendTerminalMutationWithAuthReport({
754
+ kind: "complete",
755
+ send: () => httpClient.complete(job, completion),
756
+ deps,
757
+ options,
758
+ ownership,
759
+ log: deps.log,
760
+ }, job, control);
761
+ return terminalToRunResult(terminal, "completed");
762
+ }
763
+ // A refused or failed re-run. The reserved infrastructure attempt stays
764
+ // CONSUMED — see `ci-rerun-job.ts` — and the reason is a closed token, never
765
+ // provider text.
766
+ const terminal = await sendTerminalMutationWithAuthReport({
767
+ kind: "fail",
768
+ send: () => httpClient.fail(job, {
769
+ error_kind: outcome.failure.errorKind,
770
+ error_message: outcome.failure.reason,
771
+ classification: "crashed",
772
+ telemetry: observation.snapshot(),
773
+ }),
774
+ deps,
775
+ options,
776
+ ownership,
777
+ log: deps.log,
778
+ }, job, control);
779
+ return terminalToRunResult(terminal, "failed");
780
+ }
655
781
  /**
656
782
  * Adapt a `runMerge(...)` promise into an {@link OwnedChildProcess} so the merge
657
783
  * flow is supervised by the SAME heartbeat/deadman/timeout path as spawn/smoke
@@ -2114,7 +2240,7 @@ async function runPreparedSpawn(params) {
2114
2240
  }
2115
2241
  const git = observation.git();
2116
2242
  // BAPI-1152: a driver owns everything after a clean worker exit — staging, the
2117
- // governance scan, the commits, the guarded push, and finalization.
2243
+ // commits, the guarded push, and finalization.
2118
2244
  if (driver && procResult.classification === "clean_exit") {
2119
2245
  await finalizeRegistry();
2120
2246
  const phase = await superviseDriverPhase({
@@ -26,6 +26,15 @@
26
26
  * resolution, but it does NOT re-enter `/implement-ticket`: it always carries an
27
27
  * explicit server-rendered `payload.prompt`, which wins over the synthesis in
28
28
  * `worker-command.ts`, and `job-runner.ts` dispatches it to its own body.
29
+ *
30
+ * BAPI-1160: `ci_rerun` is DELIBERATELY ABSENT from this set and from
31
+ * `SPAWN_JOB_TYPES` below. It is a deterministic actuator — one bounded
32
+ * `gh run rerun --failed` and a bounded poll — with no worktree, no branch to
33
+ * reuse, no prompt, and no worker. It has a dedicated dispatch branch in
34
+ * `job-runner.ts` beside `merge` and `smoke`, ahead of the generic spawn path,
35
+ * and it must NEVER fall back to a worker subprocess: spawning an LLM worker to
36
+ * "fix" a failure the server already PROVED to be infrastructure is the exact
37
+ * behavior this job type exists to replace.
29
38
  */
30
39
  export const RECOVERY_JOB_TYPES = new Set([
31
40
  "remediate",
@@ -37,8 +46,9 @@ export const RECOVERY_JOB_TYPES = new Set([
37
46
  * Real headless-spawn job types (worktree + worker LLM): the first-pass
38
47
  * `implement`, the `resume` continuation (own pre-spawn protocol), the
39
48
  * `spec_review` verdict job, and the recovery jobs. `merge` (deterministic,
40
- * no worktree/worker) and `smoke` (no-op acceptance) are intentionally EXCLUDED —
41
- * they have dedicated dispatch branches BEFORE the generic spawn path.
49
+ * no worktree/worker), `smoke` (no-op acceptance), and BAPI-1160's `ci_rerun`
50
+ * (deterministic, no worktree/worker) are intentionally EXCLUDED — they have
51
+ * dedicated dispatch branches BEFORE the generic spawn path.
42
52
  *
43
53
  * BAPI-1152: `reconcile_overlap` is here because it needs a prepared, locked
44
54
  * worktree and MAY spawn a worker — but only CONDITIONALLY. A clean merge whose
@@ -21,7 +21,7 @@
21
21
  * {@link MERGE_FAILED} `error_kind` (via {@link classifyMergeFailureErrorKind})
22
22
  * so the companion re-fire lane can route retry-vs-rebase without string parsing.
23
23
  */
24
- import { makeLocalMergeExecutor, resolveLocalMergeMethod, } from "../conductor/local-merge.js";
24
+ import { makeLocalMergeExecutor, resolveLocalMergeCredential, resolveLocalMergeMethod, } from "../conductor/local-merge.js";
25
25
  import { resolveConductorBridgeApiAccess, } from "../conductor/bridge-api-client.js";
26
26
  import { createProductionEpicIntegrationGhRunner, ensureEpicIntegrationPullRequest, formatEpicIntegrationPullRequestOutcome, EPIC_BRANCH_PREFIX, } from "../epic-integration-pr.js";
27
27
  import { secretFreeErrorMessage } from "./job-errors.js";
@@ -166,6 +166,30 @@ export const MERGE_RETRYABLE = "MergeRetryable";
166
166
  export const MERGE_CONFLICT = "MergeConflict";
167
167
  export const MERGE_FAILED = "MergeFailed";
168
168
  export const MERGE_REF_MISMATCH = "MergeRefMismatch";
169
+ /**
170
+ * BAPI-1234 — the server's migration-graph merge guard refused the merge. Two
171
+ * kinds, kept distinct because they call for different human responses:
172
+ * - {@link MERGE_MIGRATION_GRAPH_INVALID}: merging this head onto the current
173
+ * base produces a broken Alembic graph. A human renumbers the PR's own
174
+ * never-applied migration.
175
+ * - {@link MERGE_MIGRATION_GRAPH_UNREADABLE}: the merged graph could not be
176
+ * proven either way. An operator checks the provider, then unparks.
177
+ * Like {@link MERGE_REF_MISMATCH}, both are deliberate terminal refusals: never
178
+ * retryable, never a rebase target, and reported as `clean_exit`.
179
+ */
180
+ export const MERGE_MIGRATION_GRAPH_INVALID = "MergeMigrationGraphInvalid";
181
+ export const MERGE_MIGRATION_GRAPH_UNREADABLE = "MergeMigrationGraphUnreadable";
182
+ /** Server refusal reasons mapped to their dedicated terminal error kinds. */
183
+ const MIGRATION_GRAPH_ERROR_KINDS = new Map([
184
+ ["migration_graph_invalid", MERGE_MIGRATION_GRAPH_INVALID],
185
+ ["migration_graph_unreadable", MERGE_MIGRATION_GRAPH_UNREADABLE],
186
+ ]);
187
+ /** Error kinds that are deliberate refusals rather than crashes. */
188
+ const DELIBERATE_REFUSAL_ERROR_KINDS = new Set([
189
+ MERGE_REF_MISMATCH,
190
+ MERGE_MIGRATION_GRAPH_INVALID,
191
+ MERGE_MIGRATION_GRAPH_UNREADABLE,
192
+ ]);
169
193
  /** Reasons whose failure is transient/infrastructure and safe to retry. */
170
194
  const RETRYABLE_MERGE_REASONS = new Set([
171
195
  "gh_pr_view_timeout",
@@ -190,6 +214,9 @@ const RETRYABLE_MERGE_REASONS = new Set([
190
214
  * variants covered so a distinct reason never accidentally downgrades to
191
215
  * {@link MERGE_FAILED}. Conflict classification is checked first so it is never masked.
192
216
  *
217
+ * BAPI-1234: `migration_graph_invalid` / `migration_graph_unreadable` map to
218
+ * their own terminal kinds, in the same early position and for the same reason.
219
+ *
193
220
  * BAPI-1021: `ref_mismatch` is classified BEFORE conflict, retryable-prefix, and
194
221
  * generic-failure handling, and is deliberately NEVER added to
195
222
  * {@link RETRYABLE_MERGE_REASONS} — it is a terminal safety refusal, not a
@@ -201,6 +228,9 @@ export function classifyMergeFailureErrorKind(response) {
201
228
  const reason = asString(response.reason ?? undefined);
202
229
  if (reason === "ref_mismatch")
203
230
  return MERGE_REF_MISMATCH;
231
+ const migrationGraphKind = reason ? MIGRATION_GRAPH_ERROR_KINDS.get(reason) : undefined;
232
+ if (migrationGraphKind)
233
+ return migrationGraphKind;
204
234
  if (hasConflictEvent || reason === "gh_merge_conflict")
205
235
  return MERGE_CONFLICT;
206
236
  if (reason && (reason.startsWith("ci_poll_") || RETRYABLE_MERGE_REASONS.has(reason))) {
@@ -217,8 +247,10 @@ export function classifyMergeFailureErrorKind(response) {
217
247
  *
218
248
  * BAPI-1021: a {@link MERGE_REF_MISMATCH} classifies as `classification:
219
249
  * "clean_exit"` rather than `"crashed"` — it is a deliberate refusal the
220
- * executor made correctly, not a crash. Every other merge outcome keeps its
221
- * existing `"crashed"` classification.
250
+ * executor made correctly, not a crash. BAPI-1234 adds the two migration-graph
251
+ * refusal kinds to that set: a `crashed` classification would make the
252
+ * backend's merge re-fire lane retry a merge the server will refuse identically.
253
+ * Every other merge outcome keeps its existing `"crashed"` classification.
222
254
  */
223
255
  export function buildMergeJobFailure(response) {
224
256
  const reason = asString(response.reason ?? undefined) ?? response.status;
@@ -226,7 +258,7 @@ export function buildMergeJobFailure(response) {
226
258
  return {
227
259
  error_kind: errorKind,
228
260
  error_message: secretFreeErrorMessage(new Error(`local merge ${response.status}: ${reason}`)),
229
- classification: errorKind === MERGE_REF_MISMATCH ? "clean_exit" : "crashed",
261
+ classification: DELIBERATE_REFUSAL_ERROR_KINDS.has(errorKind) ? "clean_exit" : "crashed",
230
262
  };
231
263
  }
232
264
  /**
@@ -321,5 +353,14 @@ export async function runExecutorMergeJob(job, seams) {
321
353
  * unchanged (no App token is ever placed here).
322
354
  */
323
355
  export function buildDefaultMergeLocalDeps(deps, signal) {
324
- return { env: deps.env, signal };
356
+ // BAPI-1136: the executor's merge job runs on the operator's own machine, which
357
+ // is typed `local`, so it reads the local configuration key and no other. The
358
+ // environment is still passed for the allowlisted operational base (PATH, HOME,
359
+ // certificates); it no longer supplies the identity, and an ambient GH_TOKEN in
360
+ // it is dropped rather than inherited.
361
+ return {
362
+ env: deps.env,
363
+ credential: resolveLocalMergeCredential(deps.env, "local"),
364
+ signal,
365
+ };
325
366
  }
@@ -291,7 +291,7 @@ function parseRateLimitEvent(obj) {
291
291
  const RATE_LIMIT_CONSEQUENCE_TEXT = "The executor keeps claiming: a worker that hits the ceiling fails fast and its " +
292
292
  "job classifies as a worker failure under the normal retry budget.";
293
293
  /** Fixed runbook reference appended to every rate-limit diagnostic (BAPI-898). */
294
- const RATE_LIMIT_RUNBOOK_REFERENCE = "See docs/claude/epic-conductor-v2-operator-runbook.md §8.";
294
+ const RATE_LIMIT_RUNBOOK_REFERENCE = "See docs/claude/epic-implementer-v2-operator-runbook.md §8.";
295
295
  /**
296
296
  * Render the operator-facing rate-limit line (BAPI-828, BAPI-898).
297
297
  *
@@ -11,7 +11,7 @@ import { resolveAllExecutorApiAccess, resolveBaseUrl, EXECUTOR_BASE_URL_REQUIRED
11
11
  * time out, and letting a job influence the executor's own safety-probe budget
12
12
  * would be a privilege inversion.
13
13
  */
14
- export const DENY_PROBE_TIMEOUT_ENV_KEY = "BAPI_CONDUCTOR_DENY_PROBE_TIMEOUT_MS";
14
+ export const DENY_PROBE_TIMEOUT_ENV_KEY = "BAPI_EPIC_IMPLEMENTER_DENY_PROBE_TIMEOUT_MS";
15
15
  /**
16
16
  * Accepted bounds for the resolved probe timeout. The lower bound keeps a
17
17
  * fat-fingered `1` from turning every probe into an instant HANG (which is FATAL —
@@ -2,7 +2,7 @@
2
2
  * Executor-side structured prompt-spec renderer (BAPI-550, TDD §6/§7).
3
3
  *
4
4
  * The reconciler ships a server-authored prompt template in a `spec_review` job's
5
- * `payload.prompt_spec` (from `src/python/llms/agents/conductor_jobs/prompt_specs.py`).
5
+ * `payload.prompt_spec` (from `src/python/llms/agents/epic_implementer_jobs/prompt_specs.py`).
6
6
  * This module validates that spec and renders it into the single `claude -p`
7
7
  * prompt string, substituting each declared `{{PLACEHOLDER}}` marker from real
8
8
  * runtime state (ticket spec, base/work branch, git log, prior revise reasons).