@bridge_gpt/mcp-server 0.2.21 → 0.2.23
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/CONDUCTOR.md +86 -27
- package/README.md +80 -6
- package/build/base-ref.js +151 -0
- package/build/commands.generated.js +5 -3
- 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/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 +1979 -423
- package/build/install-bridge.js +627 -69
- 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/start-tickets.js +164 -75
- package/build/version.generated.js +1 -1
- package/build/worktree-core.js +62 -10
- package/package.json +3 -3
- package/public/js/main.min.js +9 -9
- package/public/js/main.min.js.map +1 -1
|
@@ -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
|
}
|