@bridge_gpt/mcp-server 0.2.16 → 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 (103) 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 +3 -3
  7. package/build/backend-warnings.js +44 -0
  8. package/build/claude-settings.js +129 -0
  9. package/build/commands.generated.js +7 -6
  10. package/build/conductor/bridge-api-client.js +198 -18
  11. package/build/conductor/claude-hook.js +22 -4
  12. package/build/conductor/cli.js +76 -25
  13. package/build/conductor/deny-enforcement-preflight.js +96 -0
  14. package/build/conductor/doctor.js +183 -2
  15. package/build/conductor/done-gate.js +5 -0
  16. package/build/conductor/epic-reconcile.js +71 -14
  17. package/build/conductor/epic-runtime.js +839 -67
  18. package/build/conductor/epic-state.js +524 -63
  19. package/build/conductor/errors.js +156 -3
  20. package/build/conductor/event-accessors.js +252 -0
  21. package/build/conductor/file-scope-guard.js +201 -0
  22. package/build/conductor/github-mergeability.js +85 -0
  23. package/build/conductor/local-merge.js +47 -1
  24. package/build/conductor/merge-identity.js +41 -0
  25. package/build/conductor/merge-ledger.js +19 -72
  26. package/build/conductor/plan.js +12 -2
  27. package/build/conductor/pr-ci-producer.js +17 -2
  28. package/build/conductor/pr-discovery.js +11 -1
  29. package/build/conductor/producer-ledger.js +1 -1
  30. package/build/conductor/store.js +161 -18
  31. package/build/conductor/supervisor-config.js +4 -39
  32. package/build/conductor/supervisor-escalation.js +10 -26
  33. package/build/conductor/supervisor-ledger.js +5 -12
  34. package/build/conductor/supervisor-merge.js +32 -5
  35. package/build/conductor/supervisor-message-relay.js +2 -5
  36. package/build/conductor/supervisor-notification.js +1 -1
  37. package/build/conductor/supervisor-runtime.js +12 -54
  38. package/build/conductor/supervisor-state.js +4 -18
  39. package/build/conductor/supervisor-types.js +2 -2
  40. package/build/conductor/taxonomy.js +12 -0
  41. package/build/conductor/tools.js +28 -6
  42. package/build/conductor/worker-ledger-cli.js +244 -0
  43. package/build/conductor-bin.js +1800 -5166
  44. package/build/conductor-claude-hook-bin.js +4 -2
  45. package/build/doctor.js +40 -0
  46. package/build/executor/cli.js +229 -0
  47. package/build/executor/credentials.js +65 -0
  48. package/build/executor/deps.js +117 -0
  49. package/build/executor/env.js +79 -0
  50. package/build/executor/heartbeat.js +59 -0
  51. package/build/executor/http-client.js +131 -0
  52. package/build/executor/index.js +10 -0
  53. package/build/executor/job-errors.js +55 -0
  54. package/build/executor/job-log-registry.js +110 -0
  55. package/build/executor/job-runner.js +688 -0
  56. package/build/executor/job-types.js +60 -0
  57. package/build/executor/merge-job.js +155 -0
  58. package/build/executor/observation.js +123 -0
  59. package/build/executor/permissions.js +79 -0
  60. package/build/executor/preflight.js +144 -0
  61. package/build/executor/process.js +81 -0
  62. package/build/executor/prompt-spec.js +235 -0
  63. package/build/executor/results.js +134 -0
  64. package/build/executor/resume-pre-spawn.js +179 -0
  65. package/build/executor/runner.js +98 -0
  66. package/build/executor/terminal-mutation.js +34 -0
  67. package/build/executor/test-clock.js +109 -0
  68. package/build/executor/types.js +18 -0
  69. package/build/executor/verdict-artifact.js +53 -0
  70. package/build/executor/viewer-tabs.js +78 -0
  71. package/build/executor/watch-cli.js +113 -0
  72. package/build/executor/worker-command.js +106 -0
  73. package/build/executor/worker-finalization.js +97 -0
  74. package/build/executor/worker-log.js +92 -0
  75. package/build/executor/worktree-gc.js +134 -0
  76. package/build/executor/worktree-inspection.js +86 -0
  77. package/build/executor/worktree.js +103 -0
  78. package/build/index.js +13950 -9669
  79. package/build/install-bridge.js +25 -8
  80. package/build/install-doctor.js +387 -0
  81. package/build/mcp-invoke.js +19 -3
  82. package/build/mcp-provisioning.js +31 -25
  83. package/build/mcp-registration-doctor.js +27 -7
  84. package/build/mcp-server-invocation.js +152 -0
  85. package/build/pipelines.generated.js +31 -6
  86. package/build/readme.generated.js +1 -1
  87. package/build/regression-check.js +53 -1
  88. package/build/review-tickets.js +175 -21
  89. package/build/sfcc/reads-site-preference.js +52 -19
  90. package/build/start-tickets-conductor.js +47 -99
  91. package/build/start-tickets-prereqs.js +185 -4
  92. package/build/start-tickets.js +218 -180
  93. package/build/version.generated.js +1 -1
  94. package/build/visual-diff-worker.js +313 -0
  95. package/build/visual-diff.js +632 -0
  96. package/build/worktree-core.js +202 -0
  97. package/package.json +10 -6
  98. package/pipelines/review-ticket.json +24 -2
  99. package/public/css/main.min.css +3311 -1
  100. package/public/css/main.min.css.map +1 -1
  101. package/public/js/main.min.js +7924 -1
  102. package/public/js/main.min.js.map +1 -1
  103. package/smoke-test/SMOKE-TEST.md +5 -2
@@ -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
@@ -405,7 +400,10 @@ export function getDefaultSpawnTerminalTabForPlatform(platform) {
405
400
  * are always honored). Returns a structured error for unsupported platforms;
406
401
  * never throws.
407
402
  */
408
- export function resolveStartTicketsPlatformConfig(deps, agent, autoApprove = false, conductorEnabled = false, repoName = null) {
403
+ export function resolveStartTicketsPlatformConfig(deps, agent, autoApprove = false, conductorEnabled = false, repoName = null,
404
+ // BAPI-494: a conductor remediation re-dispatch. Appends the full-suite finalize
405
+ // instruction to the resume-mode worker's prompt.
406
+ resumeMode = false) {
409
407
  if (!isSupportedStartTicketsPlatform(deps.platform)) {
410
408
  return { ok: false, error: unsupportedPlatformMessage(deps.platform) };
411
409
  }
@@ -417,7 +415,7 @@ export function resolveStartTicketsPlatformConfig(deps, agent, autoApprove = fal
417
415
  worktrunkBinary: resolveWorktrunkBinary(platform, deps.env),
418
416
  // Inject the resolved repo identity so the spawned worktree session never
419
417
  // falls back to the basename-derived repo name (the 403 root cause).
420
- buildAgentShellCommand: (key, worktreePath, modelAlias) => prependRepoNameEnvAssignment(buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove, modelAlias, conductorEnabled), repoName, platform),
418
+ buildAgentShellCommand: (key, worktreePath, modelAlias) => prependRepoNameEnvAssignment(buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove, modelAlias, conductorEnabled, resumeMode), repoName, platform),
421
419
  spawnTerminalTab: deps.spawnTerminalTab,
422
420
  },
423
421
  };
@@ -577,16 +575,29 @@ export async function requireAnyCommandOnPath(deps, candidates, errorMessage) {
577
575
  * read-only `doctor`; an unsupported-platform failure is returned as-is (doctor
578
576
  * cannot fix it, and orchestration/CLI tests assert the bare message). `uv` and
579
577
  * the selected agent are NOT enforced here — they are doctor-only. Never throws.
580
- */
581
- 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)) {
582
587
  if (options.dryRun)
583
588
  return { ok: true };
584
- const result = await enforcePreflightPrerequisites(deps);
585
- 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);
586
595
  return { ok: true };
596
+ }
587
597
  if (result.reason === "unsupported-platform") {
588
598
  return { ok: false, error: result.error };
589
599
  }
600
+ // Both missing-prerequisite and live-source-collision are doctor-diagnosable.
590
601
  return { ok: false, error: appendDoctorHint(result.error) };
591
602
  }
592
603
  // ---------------------------------------------------------------------------
@@ -714,6 +725,42 @@ export async function refreshBaseBranch(deps, options) {
714
725
  export async function refreshMainBranch(deps, options) {
715
726
  return refreshBaseBranch(deps, { refreshMain: options.refreshMain, baseBranch: "main" });
716
727
  }
728
+ /**
729
+ * Non-mutating counterpart to {@link refreshBaseBranch}: fetches
730
+ * `origin/<baseBranch>` and resolves the fetched tip to an immutable commit
731
+ * SHA, WITHOUT ever fast-forwarding or force-moving any local branch ref (no
732
+ * `git merge --ff-only`, no `git branch --force`). Used by review-grounding
733
+ * (BAPI-474), which must read a pinned base tree without touching the user's
734
+ * working tree, index, stash, or local branch refs.
735
+ *
736
+ * Validates `baseBranch` with {@link validateBranchName} first so an
737
+ * injection-shaped ref name is rejected before any git invocation. Every git
738
+ * call uses an argv array (never a shell string), consistent with
739
+ * {@link refreshBaseBranch}.
740
+ */
741
+ export async function fetchAndResolveBaseSha(deps, baseBranch) {
742
+ const validationError = validateBranchName(baseBranch);
743
+ if (validationError) {
744
+ return { ok: false, error: `Invalid base branch '${baseBranch}': ${validationError}` };
745
+ }
746
+ const fetch = await deps.runCommand("git", ["fetch", "origin", baseBranch], {
747
+ cwd: deps.cwd,
748
+ });
749
+ if (!commandSucceeded(fetch)) {
750
+ return {
751
+ ok: false,
752
+ error: `git fetch origin ${baseBranch} failed. Check your network and 'git remote get-url origin', or pass --no-refresh-base to skip.`,
753
+ };
754
+ }
755
+ const resolve = await deps.runCommand("git", ["rev-parse", "--verify", `origin/${baseBranch}^{commit}`], { cwd: deps.cwd });
756
+ if (!commandSucceeded(resolve)) {
757
+ return {
758
+ ok: false,
759
+ error: `Failed to resolve origin/${baseBranch} to a commit SHA after fetch (git rev-parse --verify failed).`,
760
+ };
761
+ }
762
+ return { ok: true, base_sha: resolve.stdout.trim() };
763
+ }
717
764
  // ---------------------------------------------------------------------------
718
765
  // Concurrency + worktree creation
719
766
  // ---------------------------------------------------------------------------
@@ -742,146 +789,29 @@ export async function runWithConcurrency(items, limit, worker) {
742
789
  await Promise.all(runners);
743
790
  return results;
744
791
  }
745
- /** True only when `git show-ref --verify --quiet refs/heads/<branch>` exits 0. */
746
- export async function branchExists(deps, branch) {
747
- const result = await deps.runCommand("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], { cwd: deps.cwd });
748
- return commandSucceeded(result);
749
- }
750
- /**
751
- * Build `wt switch` args. Include `--create` only when the branch does not yet
752
- * exist. Always include `-y` (auto-approve): the CLI runs the Worktrunk binary
753
- * via `execFile` with no TTY, so without `-y` the `pre-start` / `post-start`
754
- * hook-approval prompts would have no input source and the call would hang or
755
- * fail — this mirrors the deleted `scripts/start-tickets.sh`, which ran
756
- * `wt switch -c -y`. Never includes `-x` — the CLI owns worktree creation and
757
- * tab spawning separately. The args are platform-agnostic; only the binary
758
- * differs (`wt` vs `git-wt`).
759
- */
760
- export function buildWtSwitchArgs(branch, exists, baseBranch = "main") {
761
- if (exists) {
762
- return ["switch", "-y", branch, "--format=json"];
763
- }
764
- return ["switch", "--create", "-y", branch, "-b", baseBranch, "--format=json"];
765
- }
766
- /** Return the Node `path` API matching a platform (`win32` vs POSIX). */
767
- export function pathApiForPlatform(platform) {
768
- return platform === "win32" ? path.win32 : path.posix;
769
- }
770
- /**
771
- * Extract a worktree path from Worktrunk JSON stdout. Accepts several common
772
- * shapes defensively and resolves relative paths against `cwd` using the
773
- * platform-correct path semantics (so Windows-style paths resolve correctly
774
- * even when the test/host OS differs). Throws when no usable path can be
775
- * extracted.
776
- */
777
- export function extractWorktreePath(stdout, cwd, platform = process.platform) {
778
- let parsed;
779
- try {
780
- parsed = JSON.parse(stdout);
781
- }
782
- catch {
783
- throw new Error(`Could not parse Worktrunk JSON output: ${stdout.slice(0, 200)}`);
784
- }
785
- const candidate = pickWorktreePathField(parsed);
786
- if (!candidate) {
787
- throw new Error(`Worktrunk JSON did not include a worktree path: ${stdout.slice(0, 200)}`);
788
- }
789
- const pathApi = pathApiForPlatform(platform);
790
- return pathApi.isAbsolute(candidate) ? candidate : pathApi.resolve(cwd, candidate);
791
- }
792
- function pickWorktreePathField(parsed) {
793
- if (!parsed || typeof parsed !== "object")
794
- return undefined;
795
- const obj = parsed;
796
- if (typeof obj.path === "string")
797
- return obj.path;
798
- if (typeof obj.worktree_path === "string")
799
- return obj.worktree_path;
800
- if (typeof obj.directory === "string")
801
- return obj.directory;
802
- if (obj.worktree && typeof obj.worktree === "object") {
803
- const nested = obj.worktree;
804
- if (typeof nested.path === "string")
805
- return nested.path;
806
- }
807
- return undefined;
808
- }
809
- /**
810
- * F7: decide whether a PRE-EXISTING branch is safe to reuse as a conductor
811
- * worktree base. A branch whose tip is an ancestor of the resolved base carries
812
- * no commits beyond base (nothing stale to build on) and is safe. A branch with
813
- * commits not on base is a leftover from a prior run — refuse it. Prefers the
814
- * authoritative `origin/<base>` ref when present. Conservative: any inability to
815
- * prove ancestry refuses (when in doubt, refuse).
816
- */
817
- export async function isExistingBranchSafeToReuse(deps, branch, baseBranch) {
818
- let baseRef = baseBranch;
819
- const originRef = `origin/${baseBranch}`;
820
- const originExists = await deps.runCommand("git", ["rev-parse", "--verify", "--quiet", originRef], { cwd: deps.cwd });
821
- if (commandSucceeded(originExists))
822
- baseRef = originRef;
823
- // `merge-base --is-ancestor <branch> <baseRef>` exits 0 iff <branch> is an
824
- // ancestor of <baseRef> (a fresh branch at base counts as an ancestor).
825
- const ancestor = await deps.runCommand("git", ["merge-base", "--is-ancestor", branch, baseRef], { cwd: deps.cwd });
826
- if (commandSucceeded(ancestor))
827
- return { safe: true };
828
- return {
829
- safe: false,
830
- reason: `existing branch '${branch}' is not an ancestor of ${baseRef}; it carries commits not on the ` +
831
- `resolved base (likely a leftover from a prior run). Refusing to reuse a stale worktree — delete it ` +
832
- `(git worktree remove + git branch -D ${branch}) or rebase it onto ${baseRef}, then re-dispatch.`,
833
- };
834
- }
835
- /**
836
- * Create / switch the worktree for a single ticket using the resolved Worktrunk
837
- * binary (`wt` on macOS/Linux, `git-wt` on Windows). Returns a `created` row on
838
- * success (with key, branch, path) or a `create-failed` row on any expected
839
- * failure — never throws for per-ticket problems.
840
- */
841
- export async function createWorktreeForTicket(deps, key, branchOverrides, worktrunkBinary, baseBranch = "main", guardStaleWorktree = false) {
842
- const branch = resolveBranchForTicket(key, branchOverrides);
843
- try {
844
- const exists = await branchExists(deps, branch);
845
- // F7 (conductor dispatch): refuse a stale pre-existing branch rather than
846
- // silently building the worker on leftover code.
847
- if (exists && guardStaleWorktree) {
848
- const safety = await isExistingBranchSafeToReuse(deps, branch, baseBranch);
849
- if (!safety.safe) {
850
- return {
851
- key,
852
- branch,
853
- status: "create-failed",
854
- error: `stale worktree guard: ${safety.reason}`,
855
- };
856
- }
857
- }
858
- const args = buildWtSwitchArgs(branch, exists, baseBranch);
859
- const result = await deps.runCommand(worktrunkBinary, args, { cwd: deps.cwd });
860
- if (!commandSucceeded(result)) {
861
- const reason = (result.stderr || result.stdout || "").trim();
862
- return {
863
- key,
864
- branch,
865
- status: "create-failed",
866
- error: `${worktrunkBinary} ${args.join(" ")} failed${reason ? `: ${reason}` : ""}`,
867
- };
868
- }
869
- const worktreePath = extractWorktreePath(result.stdout, deps.cwd, deps.platform);
870
- return { key, branch, status: "created", path: worktreePath };
871
- }
872
- catch (err) {
873
- const message = err instanceof Error ? err.message : String(err);
874
- return { key, branch, status: "create-failed", error: message };
875
- }
876
- }
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, };
877
801
  /**
878
802
  * Create / switch worktrees for every ticket, throttled to `maxParallel`, using
879
803
  * the resolved Worktrunk binary. Returns one row per ticket in original key
880
804
  * order; per-ticket failures are recorded as `create-failed` rows rather than
881
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.
882
812
  */
883
- export async function createWorktrees(deps, options, worktrunkBinary) {
884
- 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));
885
815
  }
886
816
  /**
887
817
  * Resume-mode worktree resolution (BAPI-441). Instead of creating worktrees,
@@ -956,6 +886,31 @@ export function buildConductorMessageRelayLaunchInstruction() {
956
886
  "emits the gate event; it does not merge). If a tool or the conductor identity is " +
957
887
  "unavailable, continue your task without derailing.");
958
888
  }
889
+ /**
890
+ * BAPI-494: the resume-mode remediation finalize instruction. A conductor
891
+ * re-dispatch (start-tickets resume mode) is spawned to fix a `blocked` ticket —
892
+ * for a `merge.conflict` block the worker rebases + resolves conflicts. Catalog
893
+ * E1/F3b proved a clean TEXTUAL merge can still hide a SEMANTIC break (branch A
894
+ * adds a helper; branch B adds a contract test over all helpers), which only the
895
+ * FULL suite catches — so this instruction requires the worker to run the full
896
+ * project test suite (the same gate the advisory pre-push hook enforces) and only
897
+ * push/complete on green, escalating otherwise.
898
+ *
899
+ * Single line (no newlines, no `;`, no quotes/apostrophes) so it stays safe when
900
+ * single-quoted into a shell command and embedded in terminal-launcher args.
901
+ */
902
+ export function buildResumeModeRemediationFinalizeInstruction() {
903
+ return ("Resume-mode remediation finalize: you were re-dispatched to fix a blocked ticket " +
904
+ "(a merge conflict, a CI failure, or requested review changes). First rebase against " +
905
+ "the current base branch and resolve the merge conflicts. A clean textual merge can " +
906
+ "still break behavior, so inspect for semantic conflicts even when there are no textual " +
907
+ "conflict markers. Before you push or mark the ticket complete, run the full test suite " +
908
+ "for the project (the full unit suite, the same gate enforced by the advisory pre-push " +
909
+ "hook described in CLAUDE.md under the CI cost model and advisory pre-push hook section) " +
910
+ "and do not rely on targeted subsets as your only verification. Push and mark the ticket " +
911
+ "complete only after the full suite is green. If you cannot make the full suite pass, " +
912
+ "report the ticket blocked and escalate rather than pushing a green-looking but broken merge.");
913
+ }
959
914
  /**
960
915
  * The starter prompt handed to the selected agent. Identical for every agent.
961
916
  * When `autoApprove` is set, the implementation agent runs hands-off
@@ -966,15 +921,23 @@ export function buildConductorMessageRelayLaunchInstruction() {
966
921
  * flag). A plain run returns the bare `/implement-ticket <KEY> [--auto]` so the
967
922
  * worker is not told to poll `check_messages` for a conductor that is not
968
923
  * running.
924
+ *
925
+ * BAPI-494: when `resumeMode` is set (a conductor remediation re-dispatch), the
926
+ * full-suite finalize instruction is appended so the rebase/resolve worker proves
927
+ * semantic compatibility before pushing. Scoped to resume mode only — a normal
928
+ * fresh dispatch is unaffected.
969
929
  */
970
930
  export function buildAgentPrompt(key, opts = {}) {
971
931
  // `modelAlias` is accepted for signature consistency only — the model is
972
932
  // injected as a `--model` flag (see buildAgentInvocationArgv), never embedded
973
933
  // in the prompt text.
974
934
  const command = `/implement-ticket ${key}${opts.autoApprove ? " --auto" : ""}`;
975
- return opts.conductorEnabled
976
- ? `${command} ${buildConductorMessageRelayLaunchInstruction()}`
977
- : command;
935
+ const parts = [command];
936
+ if (opts.conductorEnabled)
937
+ parts.push(buildConductorMessageRelayLaunchInstruction());
938
+ if (opts.resumeMode)
939
+ parts.push(buildResumeModeRemediationFinalizeInstruction());
940
+ return parts.join(" ");
978
941
  }
979
942
  /**
980
943
  * Build the ordered argv for an agent invocation:
@@ -1013,13 +976,13 @@ export function buildAgentInvocation(agent, prompt, quote, modelAlias) {
1013
976
  }
1014
977
  }
1015
978
  /** POSIX agent shell command: `cd '<path>' && <agent> [--model '<alias>'] '<prompt>'`. */
1016
- export function buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false) {
1017
- const invocation = buildAgentInvocation(agent, buildAgentPrompt(key, { autoApprove, conductorEnabled }), (p) => `'${shSquoteInner(p)}'`, modelAlias);
979
+ export function buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false) {
980
+ const invocation = buildAgentInvocation(agent, buildAgentPrompt(key, { autoApprove, conductorEnabled, resumeMode }), (p) => `'${shSquoteInner(p)}'`, modelAlias);
1018
981
  return `cd '${shSquoteInner(worktreePath)}' && ${invocation}`;
1019
982
  }
1020
983
  /** PowerShell agent shell command: `Set-Location -LiteralPath '<path>'; <agent> [--model '<alias>'] '<prompt>'`. */
1021
- export function buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false) {
1022
- const invocation = buildAgentInvocation(agent, buildAgentPrompt(key, { autoApprove, conductorEnabled }), powershellSquote, modelAlias);
984
+ export function buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false) {
985
+ const invocation = buildAgentInvocation(agent, buildAgentPrompt(key, { autoApprove, conductorEnabled, resumeMode }), powershellSquote, modelAlias);
1023
986
  return `Set-Location -LiteralPath ${powershellSquote(worktreePath)}; ${invocation}`;
1024
987
  }
1025
988
  /**
@@ -1030,10 +993,10 @@ export function buildPowerShellAgentShellCommand(agent, key, worktreePath, autoA
1030
993
  * injected as `--model` at the spawn boundary. `conductorEnabled` appends the
1031
994
  * BAPI-397 message-relay instruction to the prompt (opt-in via `--conductor`).
1032
995
  */
1033
- export function buildAgentShellCommand(agent, key, worktreePath, platform = "darwin", autoApprove = false, modelAlias, conductorEnabled = false) {
996
+ export function buildAgentShellCommand(agent, key, worktreePath, platform = "darwin", autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false) {
1034
997
  if (platform === "win32")
1035
- return buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled);
1036
- return buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled);
998
+ return buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled, resumeMode);
999
+ return buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled, resumeMode);
1037
1000
  }
1038
1001
  /**
1039
1002
  * Build the shell command run inside a spawned tab/session for an ARBITRARY
@@ -1090,15 +1053,36 @@ export function buildTerminalAppleScript(shellCommand, title) {
1090
1053
  "end tell",
1091
1054
  ].join("\n");
1092
1055
  }
1056
+ /**
1057
+ * Build the shell command that sets an iTerm2 session **badge** to `badgeText`
1058
+ * via the proprietary `OSC 1337 ; SetBadgeFormat` escape (base64 payload, per
1059
+ * the iTerm2 spec). The badge is a large translucent per-pane label drawn in
1060
+ * the session corner.
1061
+ *
1062
+ * Why a badge and not just the session `name`: iTerm2 lets a running program
1063
+ * overwrite the session name via ordinary title escapes (`OSC 0/1/2`), and
1064
+ * Claude Code re-sets its terminal title continuously while it works — so the
1065
+ * `set name` label is clobbered within a fraction of a second (empirically the
1066
+ * tab ends up reading "claude working", never "<KEY> Implementation"). The
1067
+ * badge lives in a separate namespace those title escapes never touch, so it
1068
+ * sticks for the life of the pane. `printf` emits the escape from inside the
1069
+ * spawned shell, before the agent command runs.
1070
+ */
1071
+ export function itermBadgeShellCommand(badgeText) {
1072
+ const b64 = Buffer.from(badgeText, "utf8").toString("base64");
1073
+ return `printf '\\033]1337;SetBadgeFormat=%s\\007' '${b64}'`;
1074
+ }
1093
1075
  /**
1094
1076
  * Generate AppleScript that runs `shellCommand` in an iTerm2 tab. We set the
1095
- * session `name` (which drives the tab title) so the label sticks rather than
1096
- * being overwritten by the running agent's program title.
1077
+ * session `name` (labels the tab until the agent starts re-titling) and, when a
1078
+ * `badgeText` is supplied, pin an agent-proof iTerm badge so the pane stays
1079
+ * identifiable even after Claude Code overrides the title — see
1080
+ * {@link itermBadgeShellCommand}.
1097
1081
  */
1098
- export function buildITermAppleScript(shellCommand, title) {
1082
+ export function buildITermAppleScript(shellCommand, title, badgeText) {
1099
1083
  const esc = applescriptDquoteInner(shellCommand);
1100
1084
  const titleEsc = applescriptDquoteInner(title);
1101
- return [
1085
+ const lines = [
1102
1086
  'tell application "iTerm"',
1103
1087
  " activate",
1104
1088
  " if (count of windows) = 0 then",
@@ -1108,10 +1092,15 @@ export function buildITermAppleScript(shellCommand, title) {
1108
1092
  " end if",
1109
1093
  " tell spawnedSession",
1110
1094
  ` set name to "${titleEsc}"`,
1111
- ` write text "${esc}"`,
1112
- " end tell",
1113
- "end tell",
1114
- ].join("\n");
1095
+ ];
1096
+ if (badgeText) {
1097
+ const badgeEsc = applescriptDquoteInner(itermBadgeShellCommand(badgeText));
1098
+ lines.push(` write text "${badgeEsc}"`);
1099
+ }
1100
+ lines.push(` write text "${esc}"`);
1101
+ lines.push(" end tell");
1102
+ lines.push("end tell");
1103
+ return lines.join("\n");
1115
1104
  }
1116
1105
  /**
1117
1106
  * Spawn a single macOS terminal tab running `shellCommand`. Selects the
@@ -1123,7 +1112,7 @@ export function buildITermAppleScript(shellCommand, title) {
1123
1112
  export async function spawnMacOSTerminalTab(deps, terminal, shellCommand, context) {
1124
1113
  const title = terminalTitleForTicket(context?.key ?? "");
1125
1114
  const script = terminal === "iterm"
1126
- ? buildITermAppleScript(shellCommand, title)
1115
+ ? buildITermAppleScript(shellCommand, title, context?.key || undefined)
1127
1116
  : buildTerminalAppleScript(shellCommand, title);
1128
1117
  const result = await deps.runCommand("osascript", ["-e", script]);
1129
1118
  if (commandSucceeded(result))
@@ -1545,11 +1534,20 @@ export function getDryRunPlatformDetails(agent, platform = process.platform, env
1545
1534
  * the `bapi` target. Lists both registration files and the version-pinned shim
1546
1535
  * command. Pure formatting — implies no credentials and writes no files.
1547
1536
  */
1548
- export function buildDryRunMcpProvisioningLines(worktreePath, platform = process.platform) {
1537
+ export function buildDryRunMcpProvisioningLines(worktreePath, platform = process.platform, mcpServerInvocation) {
1549
1538
  const api = platform === "win32" ? path.win32 : path.posix;
1550
1539
  const mcpJson = api.join(worktreePath, ".mcp.json");
1551
1540
  const cursorJson = api.join(worktreePath, ".cursor", "mcp.json");
1552
- 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(" ")}`;
1553
1551
  return [
1554
1552
  "DRY-RUN: MCP provisioning (target-driven from .bridge/config — bapi plus any",
1555
1553
  "DRY-RUN: supported Tier-2 target such as sfcc): would write a secret-free shim",
@@ -1565,7 +1563,7 @@ export function buildDryRunMcpProvisioningLines(worktreePath, platform = process
1565
1563
  * the secret-free MCP provisioning preview. Pure platform formatting only — no
1566
1564
  * preflight, no routing failures.
1567
1565
  */
1568
- 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) {
1569
1567
  const { worktrunkBinary, buildAgentShellCommand: build } = getDryRunPlatformDetails(agent, platform, env, autoApprove, conductorEnabled, repoName);
1570
1568
  const wtArgs = buildWtSwitchArgs(branch, false, baseBranch);
1571
1569
  const agentInvocation = build(key, "<worktree-path>", modelAlias);
@@ -1573,7 +1571,7 @@ export function buildDryRunDetailLines(agent, key, branch, platform = process.pl
1573
1571
  `DRY-RUN: ${key} -> branch=${branch}`,
1574
1572
  `DRY-RUN: ${worktrunkBinary} ${wtArgs.join(" ")}`,
1575
1573
  `DRY-RUN: ${agentInvocation}`,
1576
- ...buildDryRunMcpProvisioningLines("<worktree-path>", platform),
1574
+ ...buildDryRunMcpProvisioningLines("<worktree-path>", platform, mcpServerInvocation),
1577
1575
  ];
1578
1576
  }
1579
1577
  /**
@@ -1602,6 +1600,9 @@ export function formatSummaryReport(rows) {
1602
1600
  line += ` path=${row.path}`;
1603
1601
  if (row.workerId)
1604
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}`;
1605
1606
  lines.push(line);
1606
1607
  }
1607
1608
  // Warnings section: create/spawn-failed row errors AND any non-fatal
@@ -1651,6 +1652,17 @@ export function buildMcpProvisioningDeps(deps) {
1651
1652
  mkdir: (dirPath, options) => mkdir(dirPath, options),
1652
1653
  platform: deps.platform,
1653
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
+ }),
1654
1666
  };
1655
1667
  }
1656
1668
  /**
@@ -2474,7 +2486,7 @@ export async function orchestrateStartTickets(deps, options, overrides = {}) {
2474
2486
  error: `Unknown agent: '${options.agentName}'. Valid agents: ${formatValidAgentNames()}.`,
2475
2487
  };
2476
2488
  }
2477
- const preflight = await runPreflight(deps, options);
2489
+ const preflight = await runPreflight(deps, options, overrides.liveSourceWarningLog);
2478
2490
  if (!preflight.ok)
2479
2491
  return { ok: false, error: preflight.error };
2480
2492
  // Resolve the run-level repo identity ONCE (BAPI_REPO_NAME, else .bridge/config)
@@ -2490,15 +2502,38 @@ export async function orchestrateStartTickets(deps, options, overrides = {}) {
2490
2502
  ".bridge/config). Spawned worktrees may fall back to the directory name and " +
2491
2503
  "hit 403 'repository not registered' if it differs from the Bridge API project name.");
2492
2504
  }
2493
- const platformConfig = resolveStartTicketsPlatformConfig(deps, agent, options.autoApprove, options.conductorEnabled ?? false, resolvedRepoName);
2505
+ const platformConfig = resolveStartTicketsPlatformConfig(deps, agent, options.autoApprove, options.conductorEnabled ?? false, resolvedRepoName,
2506
+ // BAPI-494: resume-mode dispatches get the full-suite remediation finalize prompt.
2507
+ options.resumeMode ?? false);
2494
2508
  if (!platformConfig.ok)
2495
2509
  return { ok: false, error: platformConfig.error };
2496
- const refresh = await refreshBaseBranch(deps, {
2497
- refreshMain: options.refreshMain,
2498
- baseBranch: options.baseBranch,
2499
- });
2500
- if (!refresh.ok)
2501
- 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
+ }
2502
2537
  const createWorktreesFn = overrides.createWorktrees ?? createWorktrees;
2503
2538
  const provisionFn = overrides.provisionMcpRegistrations ??
2504
2539
  ((rows, d) => provisionMcpRegistrationsForCreatedWorktrees(rows, buildMcpProvisioningDeps(d)));
@@ -2512,7 +2547,7 @@ export async function orchestrateStartTickets(deps, options, overrides = {}) {
2512
2547
  const resumeWorktreesFn = overrides.resumeWorktrees ?? resumeWorktrees;
2513
2548
  const created = options.resumeMode
2514
2549
  ? await resumeWorktreesFn(deps, options)
2515
- : await createWorktreesFn(deps, options, platformConfig.config.worktrunkBinary);
2550
+ : await createWorktreesFn(deps, options, platformConfig.config.worktrunkBinary, effectiveBaseStartPoint);
2516
2551
  // Synchronously provision secret-free worktree MCP registrations after
2517
2552
  // worktree creation and before launching the agent tab. Per-worktree
2518
2553
  // provisioning failures mark only that row `spawn-failed` (skipped by the
@@ -2665,11 +2700,14 @@ export async function runStartTicketsCli(argv, overrides = {}) {
2665
2700
  const dryRunRows = buildDryRunResults(options.keys, options.branchOverrides);
2666
2701
  const routedDryRunRows = await resolveDryRunRoutingFn(deps, dryRunRows, options, agent);
2667
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;
2668
2706
  for (const key of options.keys) {
2669
2707
  const branch = resolveBranchForTicket(key, options.branchOverrides);
2670
2708
  const routedRow = routedByKey.get(key);
2671
2709
  const modelAlias = routedRow?.modelAlias ?? null;
2672
- 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)) {
2673
2711
  log(line);
2674
2712
  }
2675
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.16";
2
+ export const VERSION = "0.2.19";