@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.
- package/README.md +144 -18
- package/build/base-ref.js +151 -0
- package/build/commands.generated.js +6 -4
- package/build/conductor/bridge-api-client.js +44 -3
- package/build/conductor/doctor.js +33 -22
- package/build/conductor/epic-runtime.js +101 -5
- package/build/conductor/pr-ci-producer.js +21 -2
- package/build/conductor/pr-discovery.js +12 -2
- package/build/conductor-bin.js +50 -20
- package/build/credential-store.js +564 -64
- package/build/decision-page-template.js +9 -4
- package/build/docs.generated.js +5 -0
- package/build/executor/base-branch.js +50 -0
- package/build/executor/env.js +12 -1
- package/build/executor/job-errors.js +1 -0
- package/build/executor/job-runner.js +38 -7
- package/build/executor/test-clock.js +6 -1
- package/build/executor/worker-finalization.js +88 -1
- package/build/executor/worktree.js +21 -1
- package/build/index.js +2741 -702
- package/build/init.js +29 -0
- package/build/install-bridge.js +1076 -114
- package/build/pipelines.generated.js +2 -2
- package/build/pr-base-contract.js +36 -0
- package/build/readme.generated.js +1 -1
- package/build/setup-epic.js +483 -0
- package/build/sfcc/log-gate.js +85 -0
- package/build/sfcc/log-query.js +170 -0
- package/build/sfcc/register.js +10 -0
- package/build/sfcc/setup-status.js +33 -3
- package/build/start-tickets.js +164 -75
- package/build/version.generated.js +1 -1
- package/build/worktree-core.js +62 -10
- package/{CONDUCTOR.md → docs/CONDUCTOR.md} +88 -29
- package/docs/install/github-app.md +189 -0
- package/docs/install/mcp-tool-integrations.md +305 -0
- package/docs/install/sfcc-integration.md +140 -0
- package/package.json +5 -5
- package/public/js/main.min.js +55 -10
- package/public/js/main.min.js.map +1 -1
- 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
|
+
}
|
package/build/executor/env.js
CHANGED
|
@@ -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,
|
|
578
|
+
const prep = await prepareSpawn(job, httpClient, jobOptions, deps, seams);
|
|
558
579
|
if (!prep.ok)
|
|
559
580
|
return prep.result;
|
|
560
|
-
const { worktreePath, branch
|
|
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:
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|