@bridge_gpt/mcp-server 0.2.18 → 0.2.19
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 +75 -0
- package/README.md +2 -2
- package/build/agent-capabilities/probe-context.js +13 -3
- package/build/agent-capabilities/probes.js +262 -11
- package/build/agent-capabilities/reporter.js +1 -0
- package/build/agents.generated.js +1 -1
- package/build/backend-warnings.js +44 -0
- package/build/claude-settings.js +129 -0
- package/build/commands.generated.js +1 -0
- package/build/conductor/bridge-api-client.js +7 -7
- package/build/conductor/cli.js +65 -12
- package/build/conductor/deny-enforcement-preflight.js +96 -0
- package/build/conductor/doctor.js +183 -2
- package/build/conductor/epic-reconcile.js +9 -1
- package/build/conductor/epic-runtime.js +403 -43
- package/build/conductor/epic-state.js +7 -0
- package/build/conductor/errors.js +115 -3
- package/build/conductor/event-accessors.js +28 -10
- package/build/conductor/merge-ledger.js +6 -4
- package/build/conductor/pr-ci-producer.js +17 -2
- package/build/conductor/producer-ledger.js +1 -1
- package/build/conductor/store.js +161 -18
- package/build/conductor/supervisor-merge.js +32 -5
- package/build/conductor/taxonomy.js +8 -0
- package/build/conductor/tools.js +28 -6
- package/build/conductor/worker-ledger-cli.js +244 -0
- package/build/conductor-bin.js +1884 -6917
- package/build/doctor.js +8 -0
- package/build/executor/cli.js +229 -0
- package/build/executor/credentials.js +65 -0
- package/build/executor/deps.js +117 -0
- package/build/executor/env.js +79 -0
- package/build/executor/heartbeat.js +59 -0
- package/build/executor/http-client.js +131 -0
- package/build/executor/index.js +10 -0
- package/build/executor/job-errors.js +55 -0
- package/build/executor/job-log-registry.js +110 -0
- package/build/executor/job-runner.js +688 -0
- package/build/executor/job-types.js +60 -0
- package/build/executor/merge-job.js +155 -0
- package/build/executor/observation.js +123 -0
- package/build/executor/permissions.js +79 -0
- package/build/executor/preflight.js +144 -0
- package/build/executor/process.js +81 -0
- package/build/executor/prompt-spec.js +235 -0
- package/build/executor/results.js +134 -0
- package/build/executor/resume-pre-spawn.js +179 -0
- package/build/executor/runner.js +98 -0
- package/build/executor/terminal-mutation.js +34 -0
- package/build/executor/test-clock.js +109 -0
- package/build/executor/types.js +18 -0
- package/build/executor/verdict-artifact.js +53 -0
- package/build/executor/viewer-tabs.js +78 -0
- package/build/executor/watch-cli.js +113 -0
- package/build/executor/worker-command.js +106 -0
- package/build/executor/worker-finalization.js +97 -0
- package/build/executor/worker-log.js +92 -0
- package/build/executor/worktree-gc.js +134 -0
- package/build/executor/worktree-inspection.js +86 -0
- package/build/executor/worktree.js +103 -0
- package/build/index.js +11222 -8544
- package/build/mcp-invoke.js +19 -3
- package/build/mcp-provisioning.js +31 -25
- package/build/mcp-registration-doctor.js +27 -7
- package/build/mcp-server-invocation.js +152 -0
- package/build/pipelines.generated.js +1 -1
- package/build/readme.generated.js +1 -1
- package/build/sfcc/reads-site-preference.js +52 -19
- package/build/start-tickets-conductor.js +25 -93
- package/build/start-tickets-prereqs.js +152 -1
- package/build/start-tickets.js +96 -158
- package/build/version.generated.js +1 -1
- package/build/visual-diff-worker.js +313 -0
- package/build/visual-diff.js +632 -0
- package/build/worktree-core.js +202 -0
- package/package.json +8 -4
- package/public/css/main.min.css +39 -0
- package/public/css/main.min.css.map +1 -1
- package/public/js/main.min.js +7924 -1
- package/public/js/main.min.js.map +1 -1
- package/smoke-test/SMOKE-TEST.md +2 -1
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conservative executor worktree GC sweep (BAPI-535, TDD §7 teardown).
|
|
3
|
+
*
|
|
4
|
+
* Removes ONLY executor/conductor-managed worktrees whose branch is merged into
|
|
5
|
+
* the base branch (`git merge-base --is-ancestor <branch> origin/<base>`) and
|
|
6
|
+
* idle for more than 24h. CONSERVATIVE by construction: a worktree that is the
|
|
7
|
+
* base branch, unmanaged, dirty, recently active, on an unknown/detached branch,
|
|
8
|
+
* unmerged, or whose merged/idle status cannot be PROVEN is always retained. Any
|
|
9
|
+
* inability to prove eligibility retains the worktree; per-worktree failures
|
|
10
|
+
* never abort the whole sweep. All I/O is injected for offline unit testing.
|
|
11
|
+
*/
|
|
12
|
+
import { listGitWorktrees, pathApiForExecutorPlatform, } from "./worktree-inspection.js";
|
|
13
|
+
/** Idle cutoff: a worktree must be idle strictly longer than 24h to be eligible. */
|
|
14
|
+
export const EXECUTOR_WORKTREE_GC_IDLE_MS = 24 * 60 * 60 * 1000;
|
|
15
|
+
function conductorMarkerPaths(worktreePath, platform) {
|
|
16
|
+
const pathApi = pathApiForExecutorPlatform(platform);
|
|
17
|
+
const dir = pathApi.join(worktreePath, ".conductor");
|
|
18
|
+
return {
|
|
19
|
+
dir,
|
|
20
|
+
workerLog: pathApi.join(dir, "worker.log"),
|
|
21
|
+
resultJson: pathApi.join(dir, "result.json"),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Conservatively identify an executor/conductor-managed worktree by the presence
|
|
26
|
+
* of a `.conductor` directory, `.conductor/worker.log`, or `.conductor/result.json`.
|
|
27
|
+
*/
|
|
28
|
+
export async function isExecutorManagedWorktree(worktreePath, deps) {
|
|
29
|
+
const markers = conductorMarkerPaths(worktreePath, deps.platform ?? process.platform);
|
|
30
|
+
for (const marker of [markers.workerLog, markers.resultJson, markers.dir]) {
|
|
31
|
+
if ((await deps.statMtimeMs(marker)) !== null)
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Compute last activity as the NEWEST mtime among `.conductor/worker.log`,
|
|
38
|
+
* `.conductor/result.json`, and the worktree directory itself. Returns `null`
|
|
39
|
+
* when none can be stat'd (activity cannot be proven → the caller retains).
|
|
40
|
+
*/
|
|
41
|
+
export async function getWorktreeLastActivityMs(worktreePath, deps) {
|
|
42
|
+
const markers = conductorMarkerPaths(worktreePath, deps.platform ?? process.platform);
|
|
43
|
+
const candidates = [markers.workerLog, markers.resultJson, worktreePath];
|
|
44
|
+
let newest = null;
|
|
45
|
+
for (const candidate of candidates) {
|
|
46
|
+
const mtime = await deps.statMtimeMs(candidate);
|
|
47
|
+
if (mtime !== null && (newest === null || mtime > newest))
|
|
48
|
+
newest = mtime;
|
|
49
|
+
}
|
|
50
|
+
return newest;
|
|
51
|
+
}
|
|
52
|
+
/** Idle when the last activity is STRICTLY more than 24h before `now`. */
|
|
53
|
+
export function isWorktreeIdle(lastActivityMs, nowMs) {
|
|
54
|
+
return nowMs - lastActivityMs > EXECUTOR_WORKTREE_GC_IDLE_MS;
|
|
55
|
+
}
|
|
56
|
+
/** True only when `git merge-base --is-ancestor <branch> origin/<base>` exits 0. */
|
|
57
|
+
export async function isBranchMergedIntoBase(runCommand, cwd, branch, baseBranch) {
|
|
58
|
+
const result = await runCommand("git", ["merge-base", "--is-ancestor", branch, `origin/${baseBranch}`], { cwd });
|
|
59
|
+
return result.exitCode === 0;
|
|
60
|
+
}
|
|
61
|
+
/** True when `git status --porcelain` inside the worktree is non-empty. */
|
|
62
|
+
export async function isWorktreeDirty(runCommand, worktreePath) {
|
|
63
|
+
const result = await runCommand("git", ["status", "--porcelain"], { cwd: worktreePath });
|
|
64
|
+
return result.stdout.trim().length > 0;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Sweep worktrees, removing ONLY managed, clean, idle, non-base, merged
|
|
68
|
+
* worktrees. Everything else — including any worktree whose eligibility cannot be
|
|
69
|
+
* proven — is retained with a structured reason. Per-worktree failures are
|
|
70
|
+
* recorded and the sweep continues; the sweep never throws globally.
|
|
71
|
+
*/
|
|
72
|
+
export async function sweepExecutorWorktrees(deps) {
|
|
73
|
+
const summary = { removed: [], retained: [], errors: [] };
|
|
74
|
+
const listWorktrees = deps.listWorktrees ?? listGitWorktrees;
|
|
75
|
+
const nowMs = deps.now();
|
|
76
|
+
let entries;
|
|
77
|
+
try {
|
|
78
|
+
entries = await listWorktrees(deps.runCommand, deps.cwd);
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
summary.errors.push({ path: deps.cwd, error: err instanceof Error ? err.message : String(err) });
|
|
82
|
+
return summary;
|
|
83
|
+
}
|
|
84
|
+
for (const entry of entries) {
|
|
85
|
+
try {
|
|
86
|
+
if (!entry.branch) {
|
|
87
|
+
summary.retained.push({ path: entry.path, reason: "detached/unknown branch" });
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (entry.branch === deps.baseBranch) {
|
|
91
|
+
summary.retained.push({ path: entry.path, reason: "base branch" });
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (!(await isExecutorManagedWorktree(entry.path, deps))) {
|
|
95
|
+
summary.retained.push({ path: entry.path, reason: "unmanaged (no .conductor marker)" });
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (await isWorktreeDirty(deps.runCommand, entry.path)) {
|
|
99
|
+
summary.retained.push({ path: entry.path, reason: "dirty" });
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const lastActivity = await getWorktreeLastActivityMs(entry.path, deps);
|
|
103
|
+
if (lastActivity === null) {
|
|
104
|
+
summary.retained.push({ path: entry.path, reason: "activity unprovable" });
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (!isWorktreeIdle(lastActivity, nowMs)) {
|
|
108
|
+
summary.retained.push({ path: entry.path, reason: "recent activity" });
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (!(await isBranchMergedIntoBase(deps.runCommand, deps.cwd, entry.branch, deps.baseBranch))) {
|
|
112
|
+
summary.retained.push({ path: entry.path, reason: "not merged into base" });
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const removeResult = await deps.runCommand("git", ["worktree", "remove", entry.path], {
|
|
116
|
+
cwd: deps.cwd,
|
|
117
|
+
});
|
|
118
|
+
if (removeResult.exitCode === 0) {
|
|
119
|
+
summary.removed.push(entry.path);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
summary.errors.push({
|
|
123
|
+
path: entry.path,
|
|
124
|
+
error: (removeResult.stderr || removeResult.stdout || "git worktree remove failed").trim(),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
// Any failure to prove eligibility retains the worktree.
|
|
130
|
+
summary.errors.push({ path: entry.path, error: err instanceof Error ? err.message : String(err) });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return summary;
|
|
134
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local worktree inspection utilities for executor job behaviors (BAPI-535).
|
|
3
|
+
*
|
|
4
|
+
* Safe, read-only Git worktree lookup, branch verification, and remote-branch
|
|
5
|
+
* probes used by the `resume` pre-spawn protocol and the worktree GC sweep. All
|
|
6
|
+
* subprocess access is behind the injected `RunCommand` boundary so every helper
|
|
7
|
+
* is unit-testable on Linux CI with no real `git`. NONE of these helpers ever
|
|
8
|
+
* mutates repository state (no reset/checkout/clean/branch writes).
|
|
9
|
+
*/
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { BranchMismatch, ExecutorNamedError } from "./job-errors.js";
|
|
12
|
+
/** Return the Node `path` API matching a platform (`win32` vs POSIX). */
|
|
13
|
+
export function pathApiForExecutorPlatform(platform) {
|
|
14
|
+
return platform === "win32" ? path.win32 : path.posix;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Parse `git worktree list --porcelain` output into entries. Each record is a
|
|
18
|
+
* blank-line-separated block whose first line is `worktree <path>`; a `branch
|
|
19
|
+
* refs/heads/<name>` line carries the local branch, which is normalized to the
|
|
20
|
+
* short branch name. Detached/no-branch worktrees are represented without a
|
|
21
|
+
* `branch` (never an invented one).
|
|
22
|
+
*/
|
|
23
|
+
export function parseGitWorktreePorcelain(porcelain) {
|
|
24
|
+
const entries = [];
|
|
25
|
+
let current = null;
|
|
26
|
+
for (const rawLine of porcelain.split("\n")) {
|
|
27
|
+
const line = rawLine.replace(/\r$/, "");
|
|
28
|
+
if (line.startsWith("worktree ")) {
|
|
29
|
+
if (current)
|
|
30
|
+
entries.push(current);
|
|
31
|
+
current = { path: line.slice("worktree ".length).trim() };
|
|
32
|
+
}
|
|
33
|
+
else if (line.startsWith("branch ") && current) {
|
|
34
|
+
const ref = line.slice("branch ".length).trim();
|
|
35
|
+
current.branch = ref.startsWith("refs/heads/")
|
|
36
|
+
? ref.slice("refs/heads/".length)
|
|
37
|
+
: ref;
|
|
38
|
+
}
|
|
39
|
+
// `detached`, `bare`, `HEAD <sha>`, and blank lines carry no branch.
|
|
40
|
+
}
|
|
41
|
+
if (current)
|
|
42
|
+
entries.push(current);
|
|
43
|
+
return entries;
|
|
44
|
+
}
|
|
45
|
+
/** Run `git worktree list --porcelain` from the repo root and parse the result. */
|
|
46
|
+
export async function listGitWorktrees(runCommand, cwd) {
|
|
47
|
+
const result = await runCommand("git", ["worktree", "list", "--porcelain"], { cwd });
|
|
48
|
+
if (result.exitCode !== 0)
|
|
49
|
+
return [];
|
|
50
|
+
return parseGitWorktreePorcelain(result.stdout);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Return the worktree path whose local branch EXACTLY equals `branch` (not a
|
|
54
|
+
* substring / prefix match), or `undefined` when none matches.
|
|
55
|
+
*/
|
|
56
|
+
export function findWorktreeByBranch(entries, branch) {
|
|
57
|
+
const match = entries.find((e) => e.branch === branch);
|
|
58
|
+
return match?.path;
|
|
59
|
+
}
|
|
60
|
+
/** Run `git rev-parse --abbrev-ref HEAD` inside a worktree and return the branch. */
|
|
61
|
+
export async function getCurrentBranch(runCommand, worktreePath) {
|
|
62
|
+
const result = await runCommand("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
63
|
+
cwd: worktreePath,
|
|
64
|
+
});
|
|
65
|
+
return result.stdout.trim();
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Verify a worktree is on the expected branch; throw {@link ExecutorNamedError}
|
|
69
|
+
* with kind {@link BranchMismatch} otherwise. Working on the wrong branch would
|
|
70
|
+
* corrupt an unrelated branch, so this aborts rather than proceeding (TDD §7).
|
|
71
|
+
*/
|
|
72
|
+
export async function assertWorktreeOnBranch(runCommand, worktreePath, expectedBranch) {
|
|
73
|
+
const actual = await getCurrentBranch(runCommand, worktreePath);
|
|
74
|
+
if (actual !== expectedBranch) {
|
|
75
|
+
throw new ExecutorNamedError(BranchMismatch, `worktree '${worktreePath}' is on '${actual}', expected '${expectedBranch}'`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Non-mutating probe for whether `origin/<branch>` exists. Uses
|
|
80
|
+
* `git rev-parse --verify --quiet origin/<branch>` (exit 0 ⇒ exists). Never
|
|
81
|
+
* fetches, never writes a ref.
|
|
82
|
+
*/
|
|
83
|
+
export async function remoteBranchExists(runCommand, cwd, branch) {
|
|
84
|
+
const result = await runCommand("git", ["rev-parse", "--verify", "--quiet", `origin/${branch}`], { cwd });
|
|
85
|
+
return result.exitCode === 0;
|
|
86
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Executor worktree ensure (BAPI-534, TDD §7, §12).
|
|
3
|
+
*
|
|
4
|
+
* Real worker jobs run in a Worktrunk-created worktree via the SHARED primitive
|
|
5
|
+
* (`worktree-core.createWorktreeForTicket`) — no duplicated `wt switch` argument
|
|
6
|
+
* construction or Worktrunk JSON path parsing here. Per-job create failures are
|
|
7
|
+
* returned as structured job failures (→ `/fail`), never thrown.
|
|
8
|
+
*/
|
|
9
|
+
import { commandSucceeded } from "../start-tickets-prereqs.js";
|
|
10
|
+
import { createWorktreeForTicket } from "../worktree-core.js";
|
|
11
|
+
/**
|
|
12
|
+
* Resolve the branch for a real spawn job: `expected_branch` wins; else
|
|
13
|
+
* `feature/<ticket_key>`; else a structured contract failure.
|
|
14
|
+
*/
|
|
15
|
+
export function resolveExecutorBranch(job) {
|
|
16
|
+
const expected = typeof job.expected_branch === "string" && job.expected_branch.trim().length > 0
|
|
17
|
+
? job.expected_branch.trim()
|
|
18
|
+
: null;
|
|
19
|
+
if (expected)
|
|
20
|
+
return { ok: true, branch: expected };
|
|
21
|
+
const ticketKey = typeof job.ticket_key === "string" && job.ticket_key.trim().length > 0
|
|
22
|
+
? job.ticket_key.trim()
|
|
23
|
+
: null;
|
|
24
|
+
if (ticketKey)
|
|
25
|
+
return { ok: true, branch: `feature/${ticketKey}` };
|
|
26
|
+
return { ok: false, error: "no branch could be resolved (no expected_branch and no ticket_key)" };
|
|
27
|
+
}
|
|
28
|
+
/** Build the lean shared-worktree deps from the executor deps. */
|
|
29
|
+
function toWorktreeCoreDeps(deps) {
|
|
30
|
+
return {
|
|
31
|
+
runCommand: deps.runCommand,
|
|
32
|
+
platform: deps.platform,
|
|
33
|
+
env: deps.env,
|
|
34
|
+
cwd: deps.cwd,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Ensure the worktree for a job exists, delegating to the shared Worktrunk
|
|
39
|
+
* primitive. A create failure is returned as a structured failure that the job
|
|
40
|
+
* runner can send to `/fail`.
|
|
41
|
+
*
|
|
42
|
+
* `policy.reuseExistingBranch` selects between the two supported modes (see
|
|
43
|
+
* {@link EnsureExecutorWorktreePolicy}); it defaults to the fresh-off-base,
|
|
44
|
+
* F7-guarded behavior so callers that omit it are unchanged.
|
|
45
|
+
*/
|
|
46
|
+
export async function ensureExecutorWorktree(job, options, deps, policy = {}) {
|
|
47
|
+
const branchResult = resolveExecutorBranch(job);
|
|
48
|
+
if (!branchResult.ok)
|
|
49
|
+
return { ok: false, error: branchResult.error };
|
|
50
|
+
const branch = branchResult.branch;
|
|
51
|
+
// The shared primitive resolves the branch from `key` via a per-key override,
|
|
52
|
+
// so we pin the exact resolved branch through the override map.
|
|
53
|
+
const key = typeof job.ticket_key === "string" && job.ticket_key.trim().length > 0
|
|
54
|
+
? job.ticket_key.trim()
|
|
55
|
+
: branch;
|
|
56
|
+
const reuseExistingBranch = policy.reuseExistingBranch === true;
|
|
57
|
+
// The F7 guard applies to FRESH implementation-style jobs — refuse to reuse a
|
|
58
|
+
// stale pre-existing branch carrying commits not on base (mirrors the v1
|
|
59
|
+
// conductor's `!isResume` guard). RECOVERY jobs intentionally continue from the
|
|
60
|
+
// ticket's bound branch (BAPI-542): they cut from `origin/<branch>` — the pushed
|
|
61
|
+
// PR branch, so a missing local branch is recreated from it rather than from
|
|
62
|
+
// base — with the guard OFF, because that branch legitimately carries the
|
|
63
|
+
// implement commit and later correction rounds.
|
|
64
|
+
const baseStartPoint = reuseExistingBranch ? `origin/${branch}` : options.baseBranch;
|
|
65
|
+
const guardStaleWorktree = !reuseExistingBranch;
|
|
66
|
+
// BAPI-528: recovery correction is ALWAYS post-push (the PR exists), so the
|
|
67
|
+
// pushed head is the source of truth. Two gaps this closes:
|
|
68
|
+
// 1. A multi-consumer executor that only cloned/fetched main has NEITHER the
|
|
69
|
+
// local branch NOR the `origin/<branch>` tracking ref, so cutting from
|
|
70
|
+
// `origin/<branch>` cannot resolve its start point → WorktreeError → the
|
|
71
|
+
// run strands at code_review. Fetch the pushed head first so it can.
|
|
72
|
+
// 2. When a local branch already exists but has DIVERGED from origin, reusing
|
|
73
|
+
// it as-is would address review comments on stale code and force-push over
|
|
74
|
+
// newer origin commits. Freshen it onto `origin/<branch>` (below).
|
|
75
|
+
// A failed fetch surfaces as a structured job failure — never a silent stale
|
|
76
|
+
// reuse. (§16's "no fetch && reset --hard" rule is scoped to the RESUME path,
|
|
77
|
+
// which must preserve unpushed local work; it does not apply here.)
|
|
78
|
+
if (reuseExistingBranch) {
|
|
79
|
+
const originRef = `origin/${branch}`;
|
|
80
|
+
const fetch = await deps.runCommand("git", ["fetch", "origin", branch], { cwd: deps.cwd });
|
|
81
|
+
if (!commandSucceeded(fetch)) {
|
|
82
|
+
const reason = (fetch.stderr || fetch.stdout || "").trim();
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
error: `git fetch origin ${branch} failed${reason ? `: ${reason}` : ""}`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const row = await createWorktreeForTicket(toWorktreeCoreDeps(deps), key, { [key]: branch }, options.worktrunkBinary, baseStartPoint, guardStaleWorktree,
|
|
89
|
+
// Hard-reset a reused existing local branch onto the freshly fetched
|
|
90
|
+
// pushed head; a missing local branch is instead cut from `baseStartPoint`
|
|
91
|
+
// (= `origin/<branch>`) inside the primitive, so both land at origin.
|
|
92
|
+
{ freshenFromOrigin: originRef });
|
|
93
|
+
if (row.status === "created" && typeof row.path === "string") {
|
|
94
|
+
return { ok: true, worktreePath: row.path, branch };
|
|
95
|
+
}
|
|
96
|
+
return { ok: false, error: row.error ?? `worktree creation failed for branch '${branch}'` };
|
|
97
|
+
}
|
|
98
|
+
const row = await createWorktreeForTicket(toWorktreeCoreDeps(deps), key, { [key]: branch }, options.worktrunkBinary, baseStartPoint, guardStaleWorktree);
|
|
99
|
+
if (row.status === "created" && typeof row.path === "string") {
|
|
100
|
+
return { ok: true, worktreePath: row.path, branch };
|
|
101
|
+
}
|
|
102
|
+
return { ok: false, error: row.error ?? `worktree creation failed for branch '${branch}'` };
|
|
103
|
+
}
|