@bridge_gpt/mcp-server 0.2.21 → 0.2.24

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 (41) hide show
  1. package/README.md +144 -18
  2. package/build/base-ref.js +151 -0
  3. package/build/commands.generated.js +6 -4
  4. package/build/conductor/bridge-api-client.js +44 -3
  5. package/build/conductor/doctor.js +33 -22
  6. package/build/conductor/epic-runtime.js +101 -5
  7. package/build/conductor/pr-ci-producer.js +21 -2
  8. package/build/conductor/pr-discovery.js +12 -2
  9. package/build/conductor-bin.js +50 -20
  10. package/build/credential-store.js +564 -64
  11. package/build/decision-page-template.js +9 -4
  12. package/build/docs.generated.js +5 -0
  13. package/build/executor/base-branch.js +50 -0
  14. package/build/executor/env.js +12 -1
  15. package/build/executor/job-errors.js +1 -0
  16. package/build/executor/job-runner.js +38 -7
  17. package/build/executor/test-clock.js +6 -1
  18. package/build/executor/worker-finalization.js +88 -1
  19. package/build/executor/worktree.js +21 -1
  20. package/build/index.js +2741 -702
  21. package/build/init.js +29 -0
  22. package/build/install-bridge.js +1076 -114
  23. package/build/pipelines.generated.js +2 -2
  24. package/build/pr-base-contract.js +36 -0
  25. package/build/readme.generated.js +1 -1
  26. package/build/setup-epic.js +483 -0
  27. package/build/sfcc/log-gate.js +85 -0
  28. package/build/sfcc/log-query.js +170 -0
  29. package/build/sfcc/register.js +10 -0
  30. package/build/sfcc/setup-status.js +33 -3
  31. package/build/start-tickets.js +164 -75
  32. package/build/version.generated.js +1 -1
  33. package/build/worktree-core.js +62 -10
  34. package/{CONDUCTOR.md → docs/CONDUCTOR.md} +88 -29
  35. package/docs/install/github-app.md +189 -0
  36. package/docs/install/mcp-tool-integrations.md +305 -0
  37. package/docs/install/sfcc-integration.md +140 -0
  38. package/package.json +5 -5
  39. package/public/js/main.min.js +55 -10
  40. package/public/js/main.min.js.map +1 -1
  41. package/smoke-test/SMOKE-TEST.md +3 -2
@@ -0,0 +1,5 @@
1
+ // AUTO-GENERATED — do not edit manually. Regenerate with: npm run build
2
+ // This file is produced by scripts/bundle-docs.js
3
+ export const DOCS = {
4
+ "docs/mcp-tool-integrations.md": "# MCP tool integrations — the human \"why\" behind the capability report\n\nThis catalog is **explanatory prose only**. It exists so the `/install-bridge`\ncapability report can cite a human-readable \"why\" for each gate. It is **not** a\nsource of truth for gating: the server computes every `locked_tools` /\n`unlocked_tools` membership decision itself and the agent must never recompute a\ntool's dependencies from this document.\n\n**Authoritative source of gating.** The enforced rules — which tools are blocked,\nwhich are degraded, and what each requires — live in\n`api/library/vcs/vcs_route_operations.py`:\n\n- `VCS_ROUTE_REQUIREMENTS` — routes that **BLOCK** (are unavailable) without a\n VCS connection.\n- `VCS_ROUTE_WARNINGS` — routes that **DEGRADE** (stay usable, but without\n codebase context) without a VCS connection.\n- `NEVER_GATED_ROUTE_KEYS` — routes that are never gated on any integration.\n- `INDEX_REQUIRED_ROUTE_KEYS`, `INDEX_REQUIRED_BRAINSTORM_MODES`,\n `CREATE_DOC_CODEBASE_CONTEXT_DOC_TYPES`, `CREATE_DOC_WARN_DOC_TYPES` — the\n conditional \"requires a successful code index\" dimension.\n- The resolver helpers `get_required_vcs_operation()`, `get_warn_vcs_operation()`,\n and `requires_successful_index()` are the authoritative functions that decide a\n case. The capability report is derived from these; this catalog explains them.\n\n## Reading the capability report\n\nEach tool entry the server returns has the exact shape\n`{tool, effect, missing, semantics}`:\n\n- **`effect`**\n - **`BLOCK`** — the tool is **unavailable** until every listed dependency is\n met. It will refuse to run without them.\n - **`DEGRADE`** — the tool is **usable right now**, but **without codebase\n context** (it cannot ground its output in your repository). Connecting the\n listed dependency upgrades it from \"works blind\" to \"works with full context\".\n A `DEGRADE` tool is never \"failed\".\n- **`missing`** — the server-computed dependency identifiers still needed:\n integration ids such as `github_app` / `vcs_access_token`, and the synthetic\n `code_index` (a successful repository index).\n- **`semantics`**\n - **`all_of`** — every id in `missing` is required.\n - **`any_of`** — the VCS-provider candidates in `missing` are alternatives:\n **either** `github_app` **or** `vcs_access_token` satisfies the VCS\n requirement (this is the \"provider unknown\" case). When `code_index` also\n appears, it remains separately required — `semantics` describes only the VCS\n provider candidates, and a code index is always mandatory in addition.\n\nThe three readiness dimensions `configured` / `learned` / `indexed` are reported\nindependently. `indexed` may be `true`, `false`, or `null` — a `null` means the\nindex status could not be confirmed and must **not** be read as \"indexed\".\n\n## The integrations\n\n| Integration id | What it is | What it unlocks |\n| --- | --- | --- |\n| `jira` | Jira API access | Ticket reads/writes, estimation and review automations, status transitions. |\n| `github_app` | GitHub App installation | Pull requests, code review, and private-repo parsing on GitHub projects. |\n| `vcs_access_token` | VCS access token | Pull requests, code review, and private-repo parsing on Bitbucket projects. |\n| `vcs_webhook` | VCS webhook secret | Merge webhooks and CI follow-up triggers. |\n| `code_index` | A successful repository index | Codebase-grounded planning, architecture, reimplementation, and technical/discovery brainstorms. Produced by `/parse-repository`. |\n\nA project's `github_app` **or** `vcs_access_token` provides the VCS connection;\nwhich one applies depends on the project's version-control system. When the\nproject's provider is unknown, either credential satisfies the requirement — the\nreport expresses that as `semantics: any_of`.\n\n## The gates, by capability\n\n### Pull requests and CI (BLOCK on VCS)\n\nTools like `create_pull_request`, `resolve_ci_checks`, `poll_ci_checks`, and\n`materialize_fresh_base` are **unavailable** (`BLOCK`) until a VCS connection is\nconfigured. They act directly on the version-control host, so without a\nconnection there is nothing for them to talk to.\n\n### Repository indexing and maps (BLOCK on VCS)\n\n`parse_repository` and `regenerate_directory_map` need a VCS connection to read\nthe repository. They **BLOCK** until VCS is connected.\n\n### Codebase-grounded generation (BLOCK on VCS **and** a code index)\n\nPlanning and architecture tools — `generate_plan_direct`,\n`generate_architecture_direct`, `request_reimplement_context`,\n`code_writer_generate_plan`, `code_writer_generate_architecture`, and\n`create_doc` for **TDD** / **architecture** documents — ground their output in\nyour indexed codebase. They **BLOCK** until BOTH a VCS connection AND a\nsuccessful code index exist (`all_of`, with `code_index` in `missing`).\n\n### Brainstorms (BLOCK on a code index, mode-dependent)\n\n`request_brainstorm` in **technical** or **discovery** mode searches your indexed\ncodebase, so it **BLOCK**s on `code_index`. **Design**-mode brainstorming never\nqueries the index and is never gated.\n\n### Document generation that DEGRADEs (usable without codebase context)\n\nTools like `generate_prd_direct`, `generate_fsd_direct`,\n`code_writer_generate_fsd`, `generate_clarifying_questions_direct`,\n`generate_ticket_critique_direct`, `generate_ticket_review_direct`, and\n`create_doc` for **PRD** / **FSD** documents **DEGRADE** rather than block: they\nrun today from the ticket alone, and connecting VCS simply lets them ground their\noutput in your codebase. They always appear under \"Tools you can use now\", with a\nreduced-context caveat when the VCS connection is missing.\n\n### Never gated\n\nSetup and bootstrap tools (`ping`, `config_field`, `get_install_manifest`,\n`apply_install_manifest`, `get_my_role`, `persist_routing_credential`,\n`get_docs_dir`, and the bootstrap-invite exchange) are always available — they\nare how you configure everything else.\n"
5
+ };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Per-job base-branch resolution (BAPI-586).
3
+ *
4
+ * The executor has one global `--base-branch` (`ExecutorOptions.baseBranch`,
5
+ * default `main`), but Epic Conductor runs can be based on a custom branch
6
+ * (e.g. `develop`, `release/2026.07`) and multi-repository executors may serve
7
+ * repos with different bases. This helper makes the persisted RUN base
8
+ * (`job.payload.base_branch`) authoritative for each job while keeping the CLI
9
+ * value as a legacy fallback.
10
+ *
11
+ * A malformed present value fails CLOSED (contract error) rather than silently
12
+ * falling back — a wrong base is exactly the BAPI-586 defect. An OMITTED value
13
+ * (legacy / older-Conductor jobs) degrades gracefully to the CLI base.
14
+ */
15
+ import { validateBranchName } from "../base-ref.js";
16
+ /**
17
+ * Resolve the effective logical base branch for one executor job.
18
+ *
19
+ * - `payload.base_branch` ABSENT (`undefined`) → legacy job → `fallbackBaseBranch`.
20
+ * - present and a valid branch name → authoritative for this job, even when it
21
+ * differs from the executor-wide `--base-branch`.
22
+ * - present but malformed (`null`, non-string, empty/whitespace, `..`, `.lock`,
23
+ * control chars, leading `-`) → contract failure. The diagnostic names the
24
+ * base-branch contract WITHOUT echoing arbitrary payload contents.
25
+ */
26
+ export function resolveExecutorJobBaseBranch(job, fallbackBaseBranch) {
27
+ const payload = job.payload;
28
+ const raw = payload && typeof payload === "object"
29
+ ? payload.base_branch
30
+ : undefined;
31
+ // Omitted field: legacy / older-Conductor job → CLI fallback (backward compat).
32
+ if (raw === undefined) {
33
+ return { ok: true, baseBranch: fallbackBaseBranch };
34
+ }
35
+ // Present but not a string (null, number, object, …): fail closed, no echo.
36
+ if (typeof raw !== "string") {
37
+ return {
38
+ ok: false,
39
+ error: "job payload base_branch is present but is not a string branch name.",
40
+ };
41
+ }
42
+ const validationError = validateBranchName(raw);
43
+ if (validationError) {
44
+ return {
45
+ ok: false,
46
+ error: `job payload base_branch is not a valid branch name: ${validationError}`,
47
+ };
48
+ }
49
+ return { ok: true, baseBranch: raw };
50
+ }
@@ -8,6 +8,7 @@
8
8
  * keys — it never copies arbitrary `process.env`, so credentials/tokens/headers
9
9
  * cannot leak into the spawned `claude` process.
10
10
  */
11
+ import { PR_BASE_BRANCH_ENV_VAR } from "../pr-base-contract.js";
11
12
  /** Non-secret operational keys forwarded to the worker when present. */
12
13
  const ALLOWED_ENV_KEYS = [
13
14
  "PATH",
@@ -63,8 +64,15 @@ export function isExecutorEnvKeyAllowed(key) {
63
64
  * anyway, and the delay was observed causing a worker to background the push and
64
65
  * exit before it landed. `BAPI_CONDUCTOR_*` identity keys remain intentionally
65
66
  * absent from the worker env (TDD §7) — this literal does not change that.
67
+ *
68
+ * BAPI-586: when `effectiveBaseBranch` is provided (the validated run base for
69
+ * this job), it is injected as `BAPI_BASE_BRANCH` so the worker can run
70
+ * `gh pr create --base "$BAPI_BASE_BRANCH"` deterministically. It is set from the
71
+ * EXPLICIT job value, NOT copied from `parentEnv` (`BAPI_BASE_BRANCH` is not in
72
+ * the allowlist), so any inherited value cannot override the run base and no
73
+ * unrelated `BAPI_*` / secret value can leak in via this key.
66
74
  */
67
- export function buildExecutorWorkerEnv(parentEnv) {
75
+ export function buildExecutorWorkerEnv(parentEnv, effectiveBaseBranch) {
68
76
  const env = {};
69
77
  for (const key of ALLOWED_ENV_KEYS) {
70
78
  if (!isExecutorEnvKeyAllowed(key))
@@ -75,5 +83,8 @@ export function buildExecutorWorkerEnv(parentEnv) {
75
83
  }
76
84
  }
77
85
  env.BRIDGE_SKIP_PREPUSH = "1";
86
+ if (typeof effectiveBaseBranch === "string" && effectiveBaseBranch.length > 0) {
87
+ env[PR_BASE_BRANCH_ENV_VAR] = effectiveBaseBranch;
88
+ }
78
89
  return env;
79
90
  }
@@ -3,6 +3,7 @@ export const MissingVerdictArtifact = "MissingVerdictArtifact";
3
3
  export const WorktreeLostBeforePush = "WorktreeLostBeforePush";
4
4
  export const BranchMismatch = "BranchMismatch";
5
5
  export const WorkerFinalizationMissingRemoteBranchAndPr = "WorkerFinalizationMissingRemoteBranchAndPr";
6
+ export const WorkerFinalizationPrBaseMismatch = "WorkerFinalizationPrBaseMismatch";
6
7
  /** Bound a failure message so no unbounded/secret-bearing text is posted. */
7
8
  const ERROR_MESSAGE_MAX_CHARS = 300;
8
9
  /**
@@ -30,9 +30,11 @@ import { isVerdictJobType, readVerdictArtifact } from "./verdict-artifact.js";
30
30
  import { createWorkerLogTee, closeWorkerLogTee, teeAsyncIterable, } from "./worker-log.js";
31
31
  import { registerExecutorJobLog, markExecutorJobLogFinished, } from "./job-log-registry.js";
32
32
  import { executorViewerTabsEnabled, openExecutorViewerTab } from "./viewer-tabs.js";
33
- import { isRecoveryJobType, isSpawnJobType } from "./job-types.js";
33
+ import { isImplementationStyleJobType, isRecoveryJobType, isSpawnJobType } from "./job-types.js";
34
+ import { resolveExecutorJobBaseBranch } from "./base-branch.js";
34
35
  import { validateWorkerFinalization } from "./worker-finalization.js";
35
36
  import { ensureExecutorWorktree } from "./worktree.js";
37
+ import { buildPrBaseContractLaunchInstruction } from "../pr-base-contract.js";
36
38
  import { buildClaudeExecutorArgv, CLAUDE_EXECUTABLE, resolveExecutorModelAlias, resolveExecutorPrompt, } from "./worker-command.js";
37
39
  import { collectGitTelemetry } from "./observation.js";
38
40
  /** Default runtime for the no-op smoke process (ms). */
@@ -553,11 +555,37 @@ async function prepareSpawn(job, httpClient, options, deps, seams) {
553
555
  }
554
556
  }
555
557
  async function runSpawnJob(job, httpClient, options, deps, ownership, observation, seams) {
558
+ // --- Per-job base branch (BAPI-586) ----------------------------------
559
+ // Resolve the effective logical base BEFORE any side effect: the persisted
560
+ // run base (`payload.base_branch`) is authoritative, falling back to the
561
+ // executor CLI base for legacy jobs. A present-but-malformed value fails
562
+ // CLOSED with `ContractError.BaseBranch` and never spawns. The resulting
563
+ // job-scoped options carry that base through worktree seeding, prompt
564
+ // rendering, deny layer, telemetry, env, and finalization so they cannot
565
+ // disagree about the base.
566
+ const baseResolution = resolveExecutorJobBaseBranch(job, options.baseBranch);
567
+ if (!baseResolution.ok) {
568
+ await httpClient.fail(job, {
569
+ error_kind: "ContractError.BaseBranch",
570
+ error_message: baseResolution.error,
571
+ classification: "crashed",
572
+ });
573
+ return { status: "failed", reason: "base_branch_contract" };
574
+ }
575
+ const effectiveBaseBranch = baseResolution.baseBranch;
576
+ const jobOptions = { ...options, baseBranch: effectiveBaseBranch };
556
577
  // --- Worktree + prompt (resume pre-spawn protocol or standard) -------
557
- const prep = await prepareSpawn(job, httpClient, options, deps, seams);
578
+ const prep = await prepareSpawn(job, httpClient, jobOptions, deps, seams);
558
579
  if (!prep.ok)
559
580
  return prep.result;
560
- const { worktreePath, branch, prompt } = prep;
581
+ const { worktreePath, branch } = prep;
582
+ // BAPI-586: PR-producing spawn jobs (implement/resume/remediate/ci_fix/rebase)
583
+ // are told to open the PR against the injected run base. Verdict-only
584
+ // `spec_review` produces no PR, so it is excluded from the instruction while
585
+ // still using the pinned base for its fresh worktree.
586
+ const prompt = isImplementationStyleJobType(job.job_type)
587
+ ? `${prep.prompt} ${buildPrBaseContractLaunchInstruction()}`
588
+ : prep.prompt;
561
589
  // --- Timeout contract ------------------------------------------------
562
590
  const timeout = resolveJobTimeoutSeconds(job, options.defaultJobTimeoutSeconds);
563
591
  if (!timeout.ok) {
@@ -569,7 +597,7 @@ async function runSpawnJob(job, httpClient, options, deps, ownership, observatio
569
597
  return { status: "failed", reason: "timeout_contract" };
570
598
  }
571
599
  // --- Deny layer (fail-open) ------------------------------------------
572
- const deny = await provisionExecutorDenyLayer(worktreePath, { baseBranch: options.baseBranch }, {
600
+ const deny = await provisionExecutorDenyLayer(worktreePath, { baseBranch: effectiveBaseBranch }, {
573
601
  readFile: deps.readFile,
574
602
  writeFile: deps.writeFile,
575
603
  mkdir: deps.mkdir,
@@ -643,7 +671,9 @@ async function runSpawnJob(job, httpClient, options, deps, ownership, observatio
643
671
  // --- Spawn -----------------------------------------------------------
644
672
  const alias = resolveExecutorModelAlias(job.payload);
645
673
  const argv = buildClaudeExecutorArgv(prompt, alias);
646
- const env = buildExecutorWorkerEnv(deps.env);
674
+ // BAPI-586: inject the validated run base as BAPI_BASE_BRANCH so the worker
675
+ // can target it via `gh pr create --base "$BAPI_BASE_BRANCH"`.
676
+ const env = buildExecutorWorkerEnv(deps.env, effectiveBaseBranch);
647
677
  let proc;
648
678
  try {
649
679
  proc = deps.spawnProcess(CLAUDE_EXECUTABLE, argv, { cwd: worktreePath, env });
@@ -666,11 +696,11 @@ async function runSpawnJob(job, httpClient, options, deps, ownership, observatio
666
696
  stderr: proc.stderr ? teeAsyncIterable(proc.stderr, tee, "stderr") : null,
667
697
  }
668
698
  : proc;
669
- const collectTelemetry = () => collectGitTelemetry({ runCommand: deps.runCommand, now: deps.now }, worktreePath, options.baseBranch);
699
+ const collectTelemetry = () => collectGitTelemetry({ runCommand: deps.runCommand, now: deps.now }, worktreePath, effectiveBaseBranch);
670
700
  const procResult = await superviseProcess({
671
701
  job,
672
702
  httpClient,
673
- options,
703
+ options: jobOptions,
674
704
  deps,
675
705
  ownership,
676
706
  observation,
@@ -765,6 +795,7 @@ async function runSpawnJob(job, httpClient, options, deps, ownership, observatio
765
795
  result,
766
796
  runCommand: deps.runCommand,
767
797
  headSha: git.last_commit_sha,
798
+ expectedBaseBranch: effectiveBaseBranch,
768
799
  });
769
800
  if (!finalization.ok) {
770
801
  await finalizeRegistry();
@@ -28,7 +28,12 @@ export class VirtualClock {
28
28
  }
29
29
  }
30
30
  async function flushMicrotasks() {
31
- for (let i = 0; i < 12; i++)
31
+ // BAPI-586: the fresh executor worktree path now awaits a `git fetch` +
32
+ // `git rev-parse` (base pin) and a post-create head verification BEFORE the
33
+ // worker spawn, adding several microtask turns. Drain generously so a
34
+ // `tickUntil` flush still reaches the spawn/exit in one pass (draining an
35
+ // already-empty queue is a harmless no-op).
36
+ for (let i = 0; i < 64; i++)
32
37
  await Promise.resolve();
33
38
  }
34
39
  /** A controllable fake owned process for `implement` spawn tests. */
@@ -23,7 +23,7 @@
23
23
  * branch tip SHA against the worker's own HEAD commit (`headSha`, from git
24
24
  * telemetry) whenever that comparison is available.
25
25
  */
26
- import { secretFreeErrorMessage, WorkerFinalizationMissingRemoteBranchAndPr } from "./job-errors.js";
26
+ import { secretFreeErrorMessage, WorkerFinalizationMissingRemoteBranchAndPr, WorkerFinalizationPrBaseMismatch, } from "./job-errors.js";
27
27
  import { isImplementationStyleJobType } from "./job-types.js";
28
28
  /** Bounded settling re-check defaults for the authoritative origin-tip lookup. */
29
29
  const DEFAULT_ORIGIN_FINALIZATION_ATTEMPTS = 3;
@@ -111,6 +111,66 @@ async function resolveOriginBranchShaForFinalization(runCommand, worktreePath, b
111
111
  }
112
112
  return remoteSha;
113
113
  }
114
+ /**
115
+ * BAPI-586: resolve the base branch of the PR for `branch` via `gh pr view`.
116
+ * Fail-open on any unavailability (gh missing, non-zero exit, unparseable JSON)
117
+ * → `{ found: false }`, so a wrong-base guard never blocks a job on transport
118
+ * failure; the reconciliation observation (epic-runtime) is the second layer.
119
+ * When a PR IS visible, `baseRef` is the trimmed `baseRefName`, or null if that
120
+ * field is absent/non-string (which the caller treats as a fail-closed mismatch —
121
+ * missing base evidence is never a match). Never copies raw stderr.
122
+ */
123
+ async function resolvePrBaseRef(runCommand, worktreePath, branch) {
124
+ const args = ["pr", "view"];
125
+ if (branch)
126
+ args.push(branch);
127
+ args.push("--json", "number,baseRefName");
128
+ let result;
129
+ try {
130
+ result = await runCommand("gh", args, { cwd: worktreePath });
131
+ }
132
+ catch {
133
+ return { found: false, baseRef: null };
134
+ }
135
+ if (result.exitCode !== 0)
136
+ return { found: false, baseRef: null };
137
+ let parsed;
138
+ try {
139
+ parsed = JSON.parse(result.stdout);
140
+ }
141
+ catch {
142
+ return { found: false, baseRef: null };
143
+ }
144
+ if (!parsed || typeof parsed !== "object")
145
+ return { found: false, baseRef: null };
146
+ const obj = parsed;
147
+ const prNumber = typeof obj.number === "number" ? obj.number : undefined;
148
+ const rawBase = obj.baseRefName;
149
+ const baseRef = typeof rawBase === "string" && rawBase.trim().length > 0 ? rawBase.trim() : null;
150
+ return { found: true, prNumber, baseRef };
151
+ }
152
+ /**
153
+ * BAPI-586: bounded, secret-free failure for a PR that targets a branch other
154
+ * than the run base. Details are limited to ticket key, PR number, expected base,
155
+ * and actual base (all public identifiers). The operator guidance names cherry-pick
156
+ * rebuild as the remedy and explicitly warns against a GitHub-UI retarget (a branch
157
+ * built on a squash-merged dependency goes CONFLICTING when retargeted). This guard
158
+ * is OBSERVATIONAL: it performs no `gh pr edit`, reset, force-push, or branch
159
+ * reconstruction.
160
+ */
161
+ function prBaseMismatchFailure(job, prNumber, expectedBase, actualBase) {
162
+ const label = job.ticket_key ? `${job.ticket_key} (job ${job.id})` : `job ${job.id}`;
163
+ const prLabel = typeof prNumber === "number" ? `PR #${prNumber}` : "its PR";
164
+ return {
165
+ error_kind: WorkerFinalizationPrBaseMismatch,
166
+ error_message: `${label} opened ${prLabel} against base '${actualBase}', but the run base is '${expectedBase}'. ` +
167
+ `A PR that does not target '${expectedBase}' never triggers code review and would strand the ticket. ` +
168
+ `Recovery: rebuild the feature branch from a fresh origin/${expectedBase} and cherry-pick only this ` +
169
+ `ticket's own commits, then force-push. Do NOT merely retarget the PR base in the GitHub UI — a branch ` +
170
+ `built on a squash-merged dependency goes CONFLICTING when retargeted.`,
171
+ classification: "crashed",
172
+ };
173
+ }
114
174
  function missingBranchAndPrFailure(job, detail) {
115
175
  const label = job.ticket_key ? `${job.ticket_key} (job ${job.id})` : `job ${job.id}`;
116
176
  return {
@@ -129,6 +189,33 @@ export async function validateWorkerFinalization(input) {
129
189
  if (!isImplementationStyleJobType(job.job_type)) {
130
190
  return { ok: true };
131
191
  }
192
+ // BAPI-586: a wrong-base PR (opened against a dependency's feature branch or the
193
+ // repo default instead of the run base) never fires `claude-review.yml` and
194
+ // strands the ticket at `code_review`. When the run base is known, query the
195
+ // PR's `baseRefName` and fail loud on any mismatch BEFORE accepting the clean
196
+ // exit — even when a `pr_url` is present (a PR existing is not proof of a
197
+ // correct base). Fail-open when no PR is visible (gh unavailable / no PR):
198
+ // reconciliation is the backstop. A PR with absent/malformed base data is a
199
+ // fail-CLOSED mismatch — missing base evidence is never treated as a match.
200
+ const expectedBase = typeof input.expectedBaseBranch === "string" ? input.expectedBaseBranch.trim() : "";
201
+ if (expectedBase) {
202
+ const branchForPr = typeof branch === "string" ? branch.trim() : "";
203
+ const prBase = await resolvePrBaseRef(runCommand, worktreePath, branchForPr);
204
+ if (prBase.found) {
205
+ if (prBase.baseRef === null) {
206
+ return {
207
+ ok: false,
208
+ failure: prBaseMismatchFailure(job, prBase.prNumber, expectedBase, "(unresolved)"),
209
+ };
210
+ }
211
+ if (prBase.baseRef !== expectedBase) {
212
+ return {
213
+ ok: false,
214
+ failure: prBaseMismatchFailure(job, prBase.prNumber, expectedBase, prBase.baseRef),
215
+ };
216
+ }
217
+ }
218
+ }
132
219
  if (extractPrUrl(result)) {
133
220
  return { ok: true };
134
221
  }
@@ -6,6 +6,7 @@
6
6
  * construction or Worktrunk JSON path parsing here. Per-job create failures are
7
7
  * returned as structured job failures (→ `/fail`), never thrown.
8
8
  */
9
+ import { fetchAndResolveBaseSha } from "../base-ref.js";
9
10
  import { commandSucceeded } from "../start-tickets-prereqs.js";
10
11
  import { createWorktreeForTicket } from "../worktree-core.js";
11
12
  /**
@@ -95,7 +96,26 @@ export async function ensureExecutorWorktree(job, options, deps, policy = {}) {
95
96
  }
96
97
  return { ok: false, error: row.error ?? `worktree creation failed for branch '${branch}'` };
97
98
  }
98
- const row = await createWorktreeForTicket(toWorktreeCoreDeps(deps), key, { [key]: branch }, options.worktrunkBinary, baseStartPoint, guardStaleWorktree);
99
+ // BAPI-586 (fresh dispatch): a fresh implementation-style job must start from
100
+ // the CURRENT remote base — never a stale local `main`, another local base
101
+ // branch, or a sibling/dependency feature branch. Fetch `origin/<base>` and
102
+ // pin its immutable SHA (serialized per repo by the shared helper so
103
+ // concurrent fresh jobs don't collide on git lock files), then:
104
+ // - cut an absent branch directly from that SHA (Worktrunk `-b <sha>`),
105
+ // - align a guard-approved pre-existing branch exactly to that SHA, and
106
+ // - verify the created worktree head equals that SHA before spawning.
107
+ // If the refresh fails, fail CLOSED with a structured, secret-free error —
108
+ // continuing from a cached local base would silently omit a merged dependency
109
+ // and reproduce the BAPI-586 wrong-base strand.
110
+ const resolvedBase = await fetchAndResolveBaseSha(deps, options.baseBranch);
111
+ if (!resolvedBase.ok) {
112
+ return {
113
+ ok: false,
114
+ error: `failed to resolve remote base '${options.baseBranch}' for a fresh worktree: ${resolvedBase.error}`,
115
+ };
116
+ }
117
+ const baseSha = resolvedBase.base_sha;
118
+ const row = await createWorktreeForTicket(toWorktreeCoreDeps(deps), key, { [key]: branch }, options.worktrunkBinary, baseSha, guardStaleWorktree, { alignExistingBranchTo: baseSha, verifyHeadMatches: baseSha });
99
119
  if (row.status === "created" && typeof row.path === "string") {
100
120
  return { ok: true, worktreePath: row.path, branch };
101
121
  }