@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.
Files changed (81) hide show
  1. package/CONDUCTOR.md +75 -0
  2. package/README.md +2 -2
  3. package/build/agent-capabilities/probe-context.js +13 -3
  4. package/build/agent-capabilities/probes.js +262 -11
  5. package/build/agent-capabilities/reporter.js +1 -0
  6. package/build/agents.generated.js +1 -1
  7. package/build/backend-warnings.js +44 -0
  8. package/build/claude-settings.js +129 -0
  9. package/build/commands.generated.js +1 -0
  10. package/build/conductor/bridge-api-client.js +7 -7
  11. package/build/conductor/cli.js +65 -12
  12. package/build/conductor/deny-enforcement-preflight.js +96 -0
  13. package/build/conductor/doctor.js +183 -2
  14. package/build/conductor/epic-reconcile.js +9 -1
  15. package/build/conductor/epic-runtime.js +403 -43
  16. package/build/conductor/epic-state.js +7 -0
  17. package/build/conductor/errors.js +115 -3
  18. package/build/conductor/event-accessors.js +28 -10
  19. package/build/conductor/merge-ledger.js +6 -4
  20. package/build/conductor/pr-ci-producer.js +17 -2
  21. package/build/conductor/producer-ledger.js +1 -1
  22. package/build/conductor/store.js +161 -18
  23. package/build/conductor/supervisor-merge.js +32 -5
  24. package/build/conductor/taxonomy.js +8 -0
  25. package/build/conductor/tools.js +28 -6
  26. package/build/conductor/worker-ledger-cli.js +244 -0
  27. package/build/conductor-bin.js +1884 -6917
  28. package/build/doctor.js +8 -0
  29. package/build/executor/cli.js +229 -0
  30. package/build/executor/credentials.js +65 -0
  31. package/build/executor/deps.js +117 -0
  32. package/build/executor/env.js +79 -0
  33. package/build/executor/heartbeat.js +59 -0
  34. package/build/executor/http-client.js +131 -0
  35. package/build/executor/index.js +10 -0
  36. package/build/executor/job-errors.js +55 -0
  37. package/build/executor/job-log-registry.js +110 -0
  38. package/build/executor/job-runner.js +688 -0
  39. package/build/executor/job-types.js +60 -0
  40. package/build/executor/merge-job.js +155 -0
  41. package/build/executor/observation.js +123 -0
  42. package/build/executor/permissions.js +79 -0
  43. package/build/executor/preflight.js +144 -0
  44. package/build/executor/process.js +81 -0
  45. package/build/executor/prompt-spec.js +235 -0
  46. package/build/executor/results.js +134 -0
  47. package/build/executor/resume-pre-spawn.js +179 -0
  48. package/build/executor/runner.js +98 -0
  49. package/build/executor/terminal-mutation.js +34 -0
  50. package/build/executor/test-clock.js +109 -0
  51. package/build/executor/types.js +18 -0
  52. package/build/executor/verdict-artifact.js +53 -0
  53. package/build/executor/viewer-tabs.js +78 -0
  54. package/build/executor/watch-cli.js +113 -0
  55. package/build/executor/worker-command.js +106 -0
  56. package/build/executor/worker-finalization.js +97 -0
  57. package/build/executor/worker-log.js +92 -0
  58. package/build/executor/worktree-gc.js +134 -0
  59. package/build/executor/worktree-inspection.js +86 -0
  60. package/build/executor/worktree.js +103 -0
  61. package/build/index.js +11222 -8544
  62. package/build/mcp-invoke.js +19 -3
  63. package/build/mcp-provisioning.js +31 -25
  64. package/build/mcp-registration-doctor.js +27 -7
  65. package/build/mcp-server-invocation.js +152 -0
  66. package/build/pipelines.generated.js +1 -1
  67. package/build/readme.generated.js +1 -1
  68. package/build/sfcc/reads-site-preference.js +52 -19
  69. package/build/start-tickets-conductor.js +25 -93
  70. package/build/start-tickets-prereqs.js +152 -1
  71. package/build/start-tickets.js +96 -158
  72. package/build/version.generated.js +1 -1
  73. package/build/visual-diff-worker.js +313 -0
  74. package/build/visual-diff.js +632 -0
  75. package/build/worktree-core.js +202 -0
  76. package/package.json +8 -4
  77. package/public/css/main.min.css +39 -0
  78. package/public/css/main.min.css.map +1 -1
  79. package/public/js/main.min.js +7924 -1
  80. package/public/js/main.min.js.map +1 -1
  81. package/smoke-test/SMOKE-TEST.md +2 -1
@@ -13,6 +13,7 @@
13
13
  * the runtime graph stays acyclic — `start-tickets.ts` imports values FROM here,
14
14
  * never the reverse.
15
15
  */
16
+ import path from "path";
16
17
  import { resolveBapiCredentials, getPrimaryCredentialStorePath, } from "./credential-store.js";
17
18
  import { resolveStartTicketsRepoName } from "./start-tickets-repo.js";
18
19
  import { probeWorktreeMcpRegistration } from "./mcp-registration-doctor.js";
@@ -40,6 +41,29 @@ export const TMUX_COMMAND = "tmux";
40
41
  export const GIT_FOR_WINDOWS_BASH_HINT = "Install Git for Windows / Git Bash — Worktrunk runs its pre-start / post-start hooks via Git Bash.";
41
42
  /** The read-only doctor invocation surfaced from preflight failures and elsewhere. */
42
43
  export const START_TICKETS_DOCTOR_COMMAND = "npx -y @bridge_gpt/mcp-server doctor";
44
+ /**
45
+ * BAPI-527 live-source checkout guard (conductor durable execution).
46
+ *
47
+ * `BAPI_CONDUCTOR_LIVE_SOURCE_PATH` — OPTIONAL operator-set path to the checkout
48
+ * an operator's live dev server (e.g. `uvicorn main:app --reload`) is running
49
+ * from. When set, unattended conductor/epic/non-mutating start-tickets dispatch
50
+ * refuses to run if the conductor base repo it would operate on resolves to the
51
+ * SAME checkout, because cutting worktrees / touching branches under a live
52
+ * dev-server checkout risks corrupting the operator's working state. No existing
53
+ * "live dev-server source path" convention was found in the codebase during
54
+ * discovery, so this local env contract is introduced here. Read only from the
55
+ * injected `deps.env` (never global `process.env`) so it stays mockable.
56
+ *
57
+ * `BAPI_CONDUCTOR_ALLOW_LIVE_SOURCE_DISPATCH` — explicit escape hatch. When set
58
+ * to a truthy value (`1`/`true`/`yes`/`on`, case-insensitive) a detected
59
+ * collision is DOWNGRADED from fatal to a loud warning rather than silently
60
+ * ignored, so an operator who genuinely wants same-checkout dispatch can opt in
61
+ * with eyes open.
62
+ */
63
+ export const CONDUCTOR_LIVE_SOURCE_PATH_ENV = "BAPI_CONDUCTOR_LIVE_SOURCE_PATH";
64
+ export const CONDUCTOR_ALLOW_LIVE_SOURCE_DISPATCH_ENV = "BAPI_CONDUCTOR_ALLOW_LIVE_SOURCE_DISPATCH";
65
+ /** Stable descriptor id for the live-source checkout guard (used in messages + doctor). */
66
+ export const LIVE_SOURCE_GUARD_ID = "conductor-live-source";
43
67
  /** True only for the three first-class supported platforms. */
44
68
  export function isSupportedStartTicketsPlatform(platform) {
45
69
  return platform === "darwin" || platform === "win32" || platform === "linux";
@@ -412,6 +436,107 @@ export function worktreeMcpReachabilityDescriptor() {
412
436
  },
413
437
  };
414
438
  }
439
+ /**
440
+ * Conservatively normalize a filesystem path for comparison: trim, drop a
441
+ * trailing separator, and resolve `.`/`..` segments via `path.resolve`. Symlink
442
+ * canonicalization is handled UPSTREAM by `git rev-parse --show-toplevel` (which
443
+ * returns the real, absolute repo root); this in-memory step is the fallback for
444
+ * a path that is not a git work tree. Never touches the filesystem.
445
+ */
446
+ export function normalizeCheckoutPath(rawPath) {
447
+ const trimmed = rawPath.trim();
448
+ const resolved = path.resolve(trimmed);
449
+ // Strip a redundant trailing separator (path.resolve already removes most, but
450
+ // a bare root like "/" must be preserved).
451
+ return resolved.length > 1 ? resolved.replace(/[\\/]+$/, "") : resolved;
452
+ }
453
+ /**
454
+ * Resolve the repository ROOT for a path using the authoritative, read-only
455
+ * `git -C <path> rev-parse --show-toplevel`. Falls back to conservative
456
+ * in-memory normalization when the target is not a git work tree (or git is
457
+ * unavailable) so the guard still produces a comparable path. Never mutates.
458
+ */
459
+ export async function resolveRepoRootPath(deps, targetPath) {
460
+ const result = await deps.runCommand("git", ["-C", targetPath, "rev-parse", "--show-toplevel"], { cwd: deps.cwd });
461
+ if (commandSucceeded(result)) {
462
+ const top = result.stdout.trim();
463
+ if (top.length > 0)
464
+ return normalizeCheckoutPath(top);
465
+ }
466
+ return normalizeCheckoutPath(targetPath);
467
+ }
468
+ /** True when the override env is set to a recognized truthy value (case-insensitive). */
469
+ export function isLiveSourceDispatchOverrideEnabled(env) {
470
+ const raw = env[CONDUCTOR_ALLOW_LIVE_SOURCE_DISPATCH_ENV];
471
+ if (raw === undefined)
472
+ return false;
473
+ return ["1", "true", "yes", "on"].includes(raw.trim().toLowerCase());
474
+ }
475
+ /**
476
+ * Evaluate the live-source guard read-only. Resolves the conductor base repo root
477
+ * (from `deps.cwd`) and the configured live-source root, then compares them after
478
+ * conservative normalization. Returns one of three states; never throws, never
479
+ * mutates.
480
+ */
481
+ export async function evaluateLiveSourceGuard(deps) {
482
+ const rawLiveSource = deps.env[CONDUCTOR_LIVE_SOURCE_PATH_ENV];
483
+ if (!rawLiveSource || rawLiveSource.trim().length === 0) {
484
+ return {
485
+ state: "not-configured",
486
+ detail: `no live dev-server source configured (${CONDUCTOR_LIVE_SOURCE_PATH_ENV} unset) — guard inactive`,
487
+ };
488
+ }
489
+ const baseRepoPath = await resolveRepoRootPath(deps, deps.cwd);
490
+ const liveSourcePath = await resolveRepoRootPath(deps, rawLiveSource);
491
+ if (baseRepoPath === liveSourcePath) {
492
+ return {
493
+ state: "collision",
494
+ detail: `COLLISION: the conductor base checkout (${baseRepoPath}) is the SAME checkout as the ` +
495
+ `configured live dev-server source (${CONDUCTOR_LIVE_SOURCE_PATH_ENV}). Unattended dispatch ` +
496
+ `would create worktrees / touch branches under a running dev server.`,
497
+ baseRepoPath,
498
+ liveSourcePath,
499
+ };
500
+ }
501
+ return {
502
+ state: "safe",
503
+ detail: `safe: conductor base checkout (${baseRepoPath}) differs from the configured live ` +
504
+ `dev-server source (${liveSourcePath})`,
505
+ baseRepoPath,
506
+ liveSourcePath,
507
+ };
508
+ }
509
+ /** Per-OS remediation hint for the live-source guard (secret-free). */
510
+ const LIVE_SOURCE_GUARD_HINT = `Point ${CONDUCTOR_LIVE_SOURCE_PATH_ENV} at your live dev server's checkout ONLY when it ` +
511
+ `differs from this conductor base checkout, or dispatch the conductor from a separate clone. ` +
512
+ `To override the guard and dispatch anyway (not recommended), set ` +
513
+ `${CONDUCTOR_ALLOW_LIVE_SOURCE_DISPATCH_ENV}=1.`;
514
+ const LIVE_SOURCE_GUARD_INSTALL_HINTS = {
515
+ darwin: LIVE_SOURCE_GUARD_HINT,
516
+ linux: LIVE_SOURCE_GUARD_HINT,
517
+ win32: LIVE_SOURCE_GUARD_HINT,
518
+ };
519
+ /**
520
+ * Doctor-only, strictly read-only descriptor that surfaces the three live-source
521
+ * guard states through the EXISTING doctor render (found + detail / missing +
522
+ * install hint):
523
+ * - not-configured → `found: true` (guard inactive, neutral)
524
+ * - safe → `found: true` (different checkouts, safe)
525
+ * - collision → `found: false` (renders as a flagged problem + override hint)
526
+ * The runtime path specifics ride in `detail` for programmatic consumers.
527
+ */
528
+ export function liveSourceGuardDescriptor() {
529
+ return {
530
+ id: LIVE_SOURCE_GUARD_ID,
531
+ label: "Conductor live-source checkout guard",
532
+ installHint: LIVE_SOURCE_GUARD_INSTALL_HINTS,
533
+ probe: async (deps) => {
534
+ const outcome = await evaluateLiveSourceGuard(deps);
535
+ // Only a collision reads as a problem; not-configured and safe are neutral.
536
+ return { found: outcome.state !== "collision", detail: outcome.detail };
537
+ },
538
+ };
539
+ }
415
540
  /**
416
541
  * The exact existing live-preflight requirement set per platform, plus the git
417
542
  * work-tree custom check appended after the command checks. This is the ONLY
@@ -457,6 +582,7 @@ export function getDoctorOnlyPrereqDescriptors(_platform, _env, agent) {
457
582
  lizardDescriptor(),
458
583
  ripgrepDescriptor(),
459
584
  reviewTicketsGitDescriptor(),
585
+ liveSourceGuardDescriptor(),
460
586
  ];
461
587
  }
462
588
  /**
@@ -497,8 +623,14 @@ export async function probePrerequisite(deps, descriptor) {
497
623
  * `runPreflight` behaviour). Iterates the shared preflight descriptors in order
498
624
  * and returns on the first missing one. `uv` and the selected agent are NOT
499
625
  * enforced here — they are doctor-only. Never throws.
626
+ *
627
+ * BAPI-527: when `options.enforceLiveSourceGuard` is set (unattended
628
+ * conductor/epic/non-mutating dispatch), the live-source checkout guard is
629
+ * additionally evaluated after the standard loop — a collision is fatal
630
+ * (`live-source-collision`) unless the override env downgrades it to a returned
631
+ * `warning`.
500
632
  */
501
- export async function enforcePreflightPrerequisites(deps) {
633
+ export async function enforcePreflightPrerequisites(deps, options = {}) {
502
634
  const descriptorsResult = getPreflightPrereqDescriptors(deps.platform, deps.env);
503
635
  if (!descriptorsResult.ok) {
504
636
  return { ok: false, reason: "unsupported-platform", error: descriptorsResult.error };
@@ -513,5 +645,24 @@ export async function enforcePreflightPrerequisites(deps) {
513
645
  return { ok: false, reason: "missing-prerequisite", error };
514
646
  }
515
647
  }
648
+ // BAPI-527 live-source checkout guard (unattended dispatch only). Read-only.
649
+ if (options.enforceLiveSourceGuard) {
650
+ const guard = await evaluateLiveSourceGuard(deps);
651
+ if (guard.state === "collision") {
652
+ const base = `Live-source checkout guard (${LIVE_SOURCE_GUARD_ID}): ${guard.detail}`;
653
+ if (isLiveSourceDispatchOverrideEnabled(deps.env)) {
654
+ return {
655
+ ok: true,
656
+ warning: `${base} Proceeding anyway because ${CONDUCTOR_ALLOW_LIVE_SOURCE_DISPATCH_ENV} is set — ` +
657
+ `dispatching under a live dev-server checkout can corrupt the operator's working state.`,
658
+ };
659
+ }
660
+ return {
661
+ ok: false,
662
+ reason: "live-source-collision",
663
+ error: `${base} Refusing unattended dispatch. ${LIVE_SOURCE_GUARD_HINT}`,
664
+ };
665
+ }
666
+ }
516
667
  return { ok: true };
517
668
  }
@@ -61,6 +61,8 @@ import { VERSION } from "./version.generated.js";
61
61
  import { resolveBapiCredentials, getPrimaryCredentialStorePath, } from "./credential-store.js";
62
62
  import { resolveStartTicketsRepoName as resolveSharedStartTicketsRepoName, resolveRequiredStartTicketsRepoName, } from "./start-tickets-repo.js";
63
63
  import { provisionMcpRegistrationsForCreatedWorktrees, } from "./mcp-provisioning.js";
64
+ import { resolveMcpShimInvocationForRuntime, buildMcpShimCommand, } from "./mcp-server-invocation.js";
65
+ import { existsSync } from "node:fs";
64
66
  // Per-OS prerequisite knowledge + low-level command probes live in the shared
65
67
  // prereqs module so `runPreflight` (enforce) and the read-only `doctor` (render)
66
68
  // can never drift. `start-tickets.ts` imports VALUES from there; the prereqs
@@ -358,13 +360,6 @@ export function validateBranchName(branch) {
358
360
  }
359
361
  return null;
360
362
  }
361
- /** Resolve the branch for a ticket: explicit override, else feature/<KEY>. */
362
- export function resolveBranchForTicket(key, overrides) {
363
- if (Object.prototype.hasOwnProperty.call(overrides, key)) {
364
- return overrides[key];
365
- }
366
- return `feature/${key}`;
367
- }
368
363
  /**
369
364
  * Determine which macOS terminal to drive. An explicit choice wins; otherwise
370
365
  * auto-detect iTerm from `$TERM_PROGRAM` (case-insensitive), defaulting to
@@ -580,16 +575,29 @@ export async function requireAnyCommandOnPath(deps, candidates, errorMessage) {
580
575
  * read-only `doctor`; an unsupported-platform failure is returned as-is (doctor
581
576
  * cannot fix it, and orchestration/CLI tests assert the bare message). `uv` and
582
577
  * the selected agent are NOT enforced here — they are doctor-only. Never throws.
583
- */
584
- export async function runPreflight(deps, options) {
578
+ *
579
+ * BAPI-527: for unattended dispatch (`options.nonMutatingBase === true` OR an
580
+ * `options.epic` identity is present), the live-source checkout guard is ALSO
581
+ * enforced. A collision is FATAL unless the operator set the override env, in
582
+ * which case preflight succeeds but emits a loud warning through `warn` (never a
583
+ * silent pass). Interactive non-conductor runs pass neither flag, so the guard is
584
+ * never enforced and behavior stays backward compatible.
585
+ */
586
+ export async function runPreflight(deps, options, warn = (message) => console.warn(message)) {
585
587
  if (options.dryRun)
586
588
  return { ok: true };
587
- const result = await enforcePreflightPrerequisites(deps);
588
- if (result.ok)
589
+ const enforceLiveSourceGuard = options.nonMutatingBase === true || options.epic !== undefined;
590
+ const result = await enforcePreflightPrerequisites(deps, { enforceLiveSourceGuard });
591
+ if (result.ok) {
592
+ // Overridden collision: not silent — surface the warning through the channel.
593
+ if (result.warning)
594
+ warn(result.warning);
589
595
  return { ok: true };
596
+ }
590
597
  if (result.reason === "unsupported-platform") {
591
598
  return { ok: false, error: result.error };
592
599
  }
600
+ // Both missing-prerequisite and live-source-collision are doctor-diagnosable.
593
601
  return { ok: false, error: appendDoctorHint(result.error) };
594
602
  }
595
603
  // ---------------------------------------------------------------------------
@@ -781,146 +789,29 @@ export async function runWithConcurrency(items, limit, worker) {
781
789
  await Promise.all(runners);
782
790
  return results;
783
791
  }
784
- /** True only when `git show-ref --verify --quiet refs/heads/<branch>` exits 0. */
785
- export async function branchExists(deps, branch) {
786
- const result = await deps.runCommand("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], { cwd: deps.cwd });
787
- return commandSucceeded(result);
788
- }
789
- /**
790
- * Build `wt switch` args. Include `--create` only when the branch does not yet
791
- * exist. Always include `-y` (auto-approve): the CLI runs the Worktrunk binary
792
- * via `execFile` with no TTY, so without `-y` the `pre-start` / `post-start`
793
- * hook-approval prompts would have no input source and the call would hang or
794
- * fail — this mirrors the deleted `scripts/start-tickets.sh`, which ran
795
- * `wt switch -c -y`. Never includes `-x` — the CLI owns worktree creation and
796
- * tab spawning separately. The args are platform-agnostic; only the binary
797
- * differs (`wt` vs `git-wt`).
798
- */
799
- export function buildWtSwitchArgs(branch, exists, baseBranch = "main") {
800
- if (exists) {
801
- return ["switch", "-y", branch, "--format=json"];
802
- }
803
- return ["switch", "--create", "-y", branch, "-b", baseBranch, "--format=json"];
804
- }
805
- /** Return the Node `path` API matching a platform (`win32` vs POSIX). */
806
- export function pathApiForPlatform(platform) {
807
- return platform === "win32" ? path.win32 : path.posix;
808
- }
809
- /**
810
- * Extract a worktree path from Worktrunk JSON stdout. Accepts several common
811
- * shapes defensively and resolves relative paths against `cwd` using the
812
- * platform-correct path semantics (so Windows-style paths resolve correctly
813
- * even when the test/host OS differs). Throws when no usable path can be
814
- * extracted.
815
- */
816
- export function extractWorktreePath(stdout, cwd, platform = process.platform) {
817
- let parsed;
818
- try {
819
- parsed = JSON.parse(stdout);
820
- }
821
- catch {
822
- throw new Error(`Could not parse Worktrunk JSON output: ${stdout.slice(0, 200)}`);
823
- }
824
- const candidate = pickWorktreePathField(parsed);
825
- if (!candidate) {
826
- throw new Error(`Worktrunk JSON did not include a worktree path: ${stdout.slice(0, 200)}`);
827
- }
828
- const pathApi = pathApiForPlatform(platform);
829
- return pathApi.isAbsolute(candidate) ? candidate : pathApi.resolve(cwd, candidate);
830
- }
831
- function pickWorktreePathField(parsed) {
832
- if (!parsed || typeof parsed !== "object")
833
- return undefined;
834
- const obj = parsed;
835
- if (typeof obj.path === "string")
836
- return obj.path;
837
- if (typeof obj.worktree_path === "string")
838
- return obj.worktree_path;
839
- if (typeof obj.directory === "string")
840
- return obj.directory;
841
- if (obj.worktree && typeof obj.worktree === "object") {
842
- const nested = obj.worktree;
843
- if (typeof nested.path === "string")
844
- return nested.path;
845
- }
846
- return undefined;
847
- }
848
- /**
849
- * F7: decide whether a PRE-EXISTING branch is safe to reuse as a conductor
850
- * worktree base. A branch whose tip is an ancestor of the resolved base carries
851
- * no commits beyond base (nothing stale to build on) and is safe. A branch with
852
- * commits not on base is a leftover from a prior run — refuse it. Prefers the
853
- * authoritative `origin/<base>` ref when present. Conservative: any inability to
854
- * prove ancestry refuses (when in doubt, refuse).
855
- */
856
- export async function isExistingBranchSafeToReuse(deps, branch, baseBranch) {
857
- let baseRef = baseBranch;
858
- const originRef = `origin/${baseBranch}`;
859
- const originExists = await deps.runCommand("git", ["rev-parse", "--verify", "--quiet", originRef], { cwd: deps.cwd });
860
- if (commandSucceeded(originExists))
861
- baseRef = originRef;
862
- // `merge-base --is-ancestor <branch> <baseRef>` exits 0 iff <branch> is an
863
- // ancestor of <baseRef> (a fresh branch at base counts as an ancestor).
864
- const ancestor = await deps.runCommand("git", ["merge-base", "--is-ancestor", branch, baseRef], { cwd: deps.cwd });
865
- if (commandSucceeded(ancestor))
866
- return { safe: true };
867
- return {
868
- safe: false,
869
- reason: `existing branch '${branch}' is not an ancestor of ${baseRef}; it carries commits not on the ` +
870
- `resolved base (likely a leftover from a prior run). Refusing to reuse a stale worktree — delete it ` +
871
- `(git worktree remove + git branch -D ${branch}) or rebase it onto ${baseRef}, then re-dispatch.`,
872
- };
873
- }
874
- /**
875
- * Create / switch the worktree for a single ticket using the resolved Worktrunk
876
- * binary (`wt` on macOS/Linux, `git-wt` on Windows). Returns a `created` row on
877
- * success (with key, branch, path) or a `create-failed` row on any expected
878
- * failure — never throws for per-ticket problems.
879
- */
880
- export async function createWorktreeForTicket(deps, key, branchOverrides, worktrunkBinary, baseBranch = "main", guardStaleWorktree = false) {
881
- const branch = resolveBranchForTicket(key, branchOverrides);
882
- try {
883
- const exists = await branchExists(deps, branch);
884
- // F7 (conductor dispatch): refuse a stale pre-existing branch rather than
885
- // silently building the worker on leftover code.
886
- if (exists && guardStaleWorktree) {
887
- const safety = await isExistingBranchSafeToReuse(deps, branch, baseBranch);
888
- if (!safety.safe) {
889
- return {
890
- key,
891
- branch,
892
- status: "create-failed",
893
- error: `stale worktree guard: ${safety.reason}`,
894
- };
895
- }
896
- }
897
- const args = buildWtSwitchArgs(branch, exists, baseBranch);
898
- const result = await deps.runCommand(worktrunkBinary, args, { cwd: deps.cwd });
899
- if (!commandSucceeded(result)) {
900
- const reason = (result.stderr || result.stdout || "").trim();
901
- return {
902
- key,
903
- branch,
904
- status: "create-failed",
905
- error: `${worktrunkBinary} ${args.join(" ")} failed${reason ? `: ${reason}` : ""}`,
906
- };
907
- }
908
- const worktreePath = extractWorktreePath(result.stdout, deps.cwd, deps.platform);
909
- return { key, branch, status: "created", path: worktreePath };
910
- }
911
- catch (err) {
912
- const message = err instanceof Error ? err.message : String(err);
913
- return { key, branch, status: "create-failed", error: message };
914
- }
915
- }
792
+ // BAPI-534: the shared Worktrunk primitives (branch inspection, `wt switch` arg
793
+ // construction, Worktrunk JSON path parsing, per-ticket create/switch) live in
794
+ // `worktree-core.ts` so the Epic Conductor v2 executor reuses ONE implementation
795
+ // instead of duplicating `wt switch` logic. They are imported here for local use
796
+ // (by `createWorktrees` / `resumeWorktrees`) and re-exported so existing pinned
797
+ // imports of these names from `./start-tickets.js` keep resolving to the exact
798
+ // same function references.
799
+ import { resolveBranchForTicket, branchExists, buildWtSwitchArgs, pathApiForPlatform, extractWorktreePath, isExistingBranchSafeToReuse, createWorktreeForTicket, } from "./worktree-core.js";
800
+ export { resolveBranchForTicket, branchExists, buildWtSwitchArgs, pathApiForPlatform, extractWorktreePath, isExistingBranchSafeToReuse, createWorktreeForTicket, };
916
801
  /**
917
802
  * Create / switch worktrees for every ticket, throttled to `maxParallel`, using
918
803
  * the resolved Worktrunk binary. Returns one row per ticket in original key
919
804
  * order; per-ticket failures are recorded as `create-failed` rows rather than
920
805
  * aborting the run.
806
+ *
807
+ * BAPI-527: `baseStartPoint` is the effective ref/commit new worktrees are cut
808
+ * from — the immutable SHA resolved by the non-mutating conductor path, or the
809
+ * logical `options.baseBranch` for interactive dispatch. It defaults to
810
+ * `options.baseBranch` so existing callers/tests that omit it keep the
811
+ * historical branch-name behavior unchanged.
921
812
  */
922
- export async function createWorktrees(deps, options, worktrunkBinary) {
923
- return runWithConcurrency(options.keys, options.maxParallel, (key) => createWorktreeForTicket(deps, key, options.branchOverrides, worktrunkBinary, options.baseBranch, options.guardStaleWorktree === true));
813
+ export async function createWorktrees(deps, options, worktrunkBinary, baseStartPoint = options.baseBranch) {
814
+ return runWithConcurrency(options.keys, options.maxParallel, (key) => createWorktreeForTicket(deps, key, options.branchOverrides, worktrunkBinary, baseStartPoint, options.guardStaleWorktree === true));
924
815
  }
925
816
  /**
926
817
  * Resume-mode worktree resolution (BAPI-441). Instead of creating worktrees,
@@ -1643,11 +1534,20 @@ export function getDryRunPlatformDetails(agent, platform = process.platform, env
1643
1534
  * the `bapi` target. Lists both registration files and the version-pinned shim
1644
1535
  * command. Pure formatting — implies no credentials and writes no files.
1645
1536
  */
1646
- export function buildDryRunMcpProvisioningLines(worktreePath, platform = process.platform) {
1537
+ export function buildDryRunMcpProvisioningLines(worktreePath, platform = process.platform, mcpServerInvocation) {
1647
1538
  const api = platform === "win32" ? path.win32 : path.posix;
1648
1539
  const mcpJson = api.join(worktreePath, ".mcp.json");
1649
1540
  const cursorJson = api.join(worktreePath, ".cursor", "mcp.json");
1650
- const shim = `npx -y @bridge_gpt/mcp-server@${VERSION} mcp-invoke --target <target> --project-root ${worktreePath}`;
1541
+ // Render the SAME registration form the code would actually write. When no
1542
+ // invocation is supplied (older callers), fall back to the resolvable
1543
+ // npm-channel spec — never an exact generated version pin.
1544
+ const invocation = mcpServerInvocation ?? {
1545
+ form: "npm-channel",
1546
+ command: "npx",
1547
+ packageSpec: "@bridge_gpt/mcp-server@latest",
1548
+ };
1549
+ const built = buildMcpShimCommand(invocation, "<target>", worktreePath);
1550
+ const shim = `${built.command} ${built.args.join(" ")}`;
1651
1551
  return [
1652
1552
  "DRY-RUN: MCP provisioning (target-driven from .bridge/config — bapi plus any",
1653
1553
  "DRY-RUN: supported Tier-2 target such as sfcc): would write a secret-free shim",
@@ -1663,7 +1563,7 @@ export function buildDryRunMcpProvisioningLines(worktreePath, platform = process
1663
1563
  * the secret-free MCP provisioning preview. Pure platform formatting only — no
1664
1564
  * preflight, no routing failures.
1665
1565
  */
1666
- export function buildDryRunDetailLines(agent, key, branch, platform = process.platform, env = process.env, baseBranch = "main", autoApprove = false, modelAlias = null, conductorEnabled = false, repoName = null) {
1566
+ export function buildDryRunDetailLines(agent, key, branch, platform = process.platform, env = process.env, baseBranch = "main", autoApprove = false, modelAlias = null, conductorEnabled = false, repoName = null, mcpServerInvocation) {
1667
1567
  const { worktrunkBinary, buildAgentShellCommand: build } = getDryRunPlatformDetails(agent, platform, env, autoApprove, conductorEnabled, repoName);
1668
1568
  const wtArgs = buildWtSwitchArgs(branch, false, baseBranch);
1669
1569
  const agentInvocation = build(key, "<worktree-path>", modelAlias);
@@ -1671,7 +1571,7 @@ export function buildDryRunDetailLines(agent, key, branch, platform = process.pl
1671
1571
  `DRY-RUN: ${key} -> branch=${branch}`,
1672
1572
  `DRY-RUN: ${worktrunkBinary} ${wtArgs.join(" ")}`,
1673
1573
  `DRY-RUN: ${agentInvocation}`,
1674
- ...buildDryRunMcpProvisioningLines("<worktree-path>", platform),
1574
+ ...buildDryRunMcpProvisioningLines("<worktree-path>", platform, mcpServerInvocation),
1675
1575
  ];
1676
1576
  }
1677
1577
  /**
@@ -1700,6 +1600,9 @@ export function formatSummaryReport(rows) {
1700
1600
  line += ` path=${row.path}`;
1701
1601
  if (row.workerId)
1702
1602
  line += ` worker_id=${row.workerId}`;
1603
+ // BAPI-526: path-free audit label only (never the absolute server entry path).
1604
+ if (row.mcpRegistrationForm)
1605
+ line += ` mcp_registration=${row.mcpRegistrationForm}`;
1703
1606
  lines.push(line);
1704
1607
  }
1705
1608
  // Warnings section: create/spawn-failed row errors AND any non-fatal
@@ -1749,6 +1652,17 @@ export function buildMcpProvisioningDeps(deps) {
1749
1652
  mkdir: (dirPath, options) => mkdir(dirPath, options),
1750
1653
  platform: deps.platform,
1751
1654
  cwd: deps.cwd,
1655
+ // Resolve how the worker MCP shim launches at dispatch time from the running
1656
+ // conductor's own on-disk build (absolute-build-path primary, npm-channel
1657
+ // fallback) — never from process.cwd(). `nodeExecutable` is kept as "node"
1658
+ // here: Ticket 3 (P2) can later replace this single field with
1659
+ // CONDUCTOR_NODE_PATH without re-plumbing argv construction.
1660
+ mcpServerInvocation: resolveMcpShimInvocationForRuntime({
1661
+ moduleUrl: import.meta.url,
1662
+ nodeExecutable: "node",
1663
+ argv1: process.argv[1],
1664
+ fileExists: existsSync,
1665
+ }),
1752
1666
  };
1753
1667
  }
1754
1668
  /**
@@ -2572,7 +2486,7 @@ export async function orchestrateStartTickets(deps, options, overrides = {}) {
2572
2486
  error: `Unknown agent: '${options.agentName}'. Valid agents: ${formatValidAgentNames()}.`,
2573
2487
  };
2574
2488
  }
2575
- const preflight = await runPreflight(deps, options);
2489
+ const preflight = await runPreflight(deps, options, overrides.liveSourceWarningLog);
2576
2490
  if (!preflight.ok)
2577
2491
  return { ok: false, error: preflight.error };
2578
2492
  // Resolve the run-level repo identity ONCE (BAPI_REPO_NAME, else .bridge/config)
@@ -2593,12 +2507,33 @@ export async function orchestrateStartTickets(deps, options, overrides = {}) {
2593
2507
  options.resumeMode ?? false);
2594
2508
  if (!platformConfig.ok)
2595
2509
  return { ok: false, error: platformConfig.error };
2596
- const refresh = await refreshBaseBranch(deps, {
2597
- refreshMain: options.refreshMain,
2598
- baseBranch: options.baseBranch,
2599
- });
2600
- if (!refresh.ok)
2601
- return { ok: false, error: refresh.error };
2510
+ // BAPI-527: resolve the base start point new worktrees are cut from BEFORE any
2511
+ // worktree side effects. Two mutually-exclusive paths:
2512
+ // - Non-mutating (unattended conductor/epic dispatch): fetch `origin/<base>`
2513
+ // and pin the returned immutable commit SHA. This NEVER fast-forwards or
2514
+ // force-moves a local branch ref (no `refreshBaseBranch`, `git merge
2515
+ // --ff-only`, or `git branch --force`), so it is safe to run against a
2516
+ // checkout whose local branches an operator's live dev server is editing.
2517
+ // - Interactive default: keep the historical `refreshBaseBranch` behavior
2518
+ // (fetch + fast-forward/align the local base branch) and cut worktrees from
2519
+ // the branch name.
2520
+ // A fetch-only resolution failure aborts the run globally before any worktree
2521
+ // is created (matching the historical refresh-failure contract below).
2522
+ let effectiveBaseStartPoint = options.baseBranch;
2523
+ if (options.nonMutatingBase === true) {
2524
+ const resolved = await fetchAndResolveBaseSha(deps, options.baseBranch);
2525
+ if (!resolved.ok)
2526
+ return { ok: false, error: resolved.error };
2527
+ effectiveBaseStartPoint = resolved.base_sha;
2528
+ }
2529
+ else {
2530
+ const refresh = await refreshBaseBranch(deps, {
2531
+ refreshMain: options.refreshMain,
2532
+ baseBranch: options.baseBranch,
2533
+ });
2534
+ if (!refresh.ok)
2535
+ return { ok: false, error: refresh.error };
2536
+ }
2602
2537
  const createWorktreesFn = overrides.createWorktrees ?? createWorktrees;
2603
2538
  const provisionFn = overrides.provisionMcpRegistrations ??
2604
2539
  ((rows, d) => provisionMcpRegistrationsForCreatedWorktrees(rows, buildMcpProvisioningDeps(d)));
@@ -2612,7 +2547,7 @@ export async function orchestrateStartTickets(deps, options, overrides = {}) {
2612
2547
  const resumeWorktreesFn = overrides.resumeWorktrees ?? resumeWorktrees;
2613
2548
  const created = options.resumeMode
2614
2549
  ? await resumeWorktreesFn(deps, options)
2615
- : await createWorktreesFn(deps, options, platformConfig.config.worktrunkBinary);
2550
+ : await createWorktreesFn(deps, options, platformConfig.config.worktrunkBinary, effectiveBaseStartPoint);
2616
2551
  // Synchronously provision secret-free worktree MCP registrations after
2617
2552
  // worktree creation and before launching the agent tab. Per-worktree
2618
2553
  // provisioning failures mark only that row `spawn-failed` (skipped by the
@@ -2765,11 +2700,14 @@ export async function runStartTicketsCli(argv, overrides = {}) {
2765
2700
  const dryRunRows = buildDryRunResults(options.keys, options.branchOverrides);
2766
2701
  const routedDryRunRows = await resolveDryRunRoutingFn(deps, dryRunRows, options, agent);
2767
2702
  const routedByKey = new Map(routedDryRunRows.map((r) => [r.key, r]));
2703
+ // Resolve the SAME worker MCP shim invocation the real provisioning would use
2704
+ // so the dry-run preview matches the `.mcp.json` that would actually be written.
2705
+ const dryRunMcpInvocation = buildMcpProvisioningDeps(deps).mcpServerInvocation;
2768
2706
  for (const key of options.keys) {
2769
2707
  const branch = resolveBranchForTicket(key, options.branchOverrides);
2770
2708
  const routedRow = routedByKey.get(key);
2771
2709
  const modelAlias = routedRow?.modelAlias ?? null;
2772
- for (const line of buildDryRunDetailLines(agent, key, branch, deps.platform, deps.env, options.baseBranch, options.autoApprove, modelAlias, options.conductorEnabled ?? false, dryRunRepoName)) {
2710
+ for (const line of buildDryRunDetailLines(agent, key, branch, deps.platform, deps.env, options.baseBranch, options.autoApprove, modelAlias, options.conductorEnabled ?? false, dryRunRepoName, dryRunMcpInvocation)) {
2773
2711
  log(line);
2774
2712
  }
2775
2713
  log(`DRY-RUN: model routing: ${formatModelRoutingLine(routedRow ?? { key, branch, status: "dry-run" }, agent)}`);
@@ -1,2 +1,2 @@
1
1
  // AUTO-GENERATED — do not edit manually. Regenerate with: npm run build
2
- export const VERSION = "0.2.18";
2
+ export const VERSION = "0.2.19";