@bridge_gpt/mcp-server 0.1.16 → 0.2.0

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 (50) hide show
  1. package/README.md +333 -162
  2. package/build/agent-capabilities/cli.js +152 -0
  3. package/build/agent-capabilities/default-deps.js +45 -0
  4. package/build/agent-capabilities/probe-context.js +111 -0
  5. package/build/agent-capabilities/probes.js +278 -0
  6. package/build/agent-capabilities/reporter.js +50 -0
  7. package/build/agent-capabilities/runner.js +56 -0
  8. package/build/agent-capabilities/types.js +10 -0
  9. package/build/agent-launchers/claude.js +85 -0
  10. package/build/agent-launchers/index.js +17 -0
  11. package/build/agent-launchers/types.js +1 -0
  12. package/build/agents.generated.js +1 -1
  13. package/build/brainstorm-files.js +89 -0
  14. package/build/bridge-config.js +404 -0
  15. package/build/chain-orchestrator.js +1364 -0
  16. package/build/chain-utils.js +68 -0
  17. package/build/commands.generated.js +5 -3
  18. package/build/credential-materialization.js +128 -0
  19. package/build/credential-store.js +232 -0
  20. package/build/decision-page-schema.js +39 -6
  21. package/build/decision-page-template.js +54 -18
  22. package/build/doctor.js +18 -2
  23. package/build/fetch-stub.js +139 -0
  24. package/build/git-ignore-utils.js +63 -0
  25. package/build/index.js +1623 -546
  26. package/build/mcp-invoke.js +417 -0
  27. package/build/mcp-provisioning.js +249 -0
  28. package/build/mcp-registration-doctor.js +96 -0
  29. package/build/pipeline-orchestrator.js +66 -1
  30. package/build/pipeline-utils.js +33 -0
  31. package/build/pipelines.generated.js +165 -5
  32. package/build/schedule-run.js +951 -0
  33. package/build/schedule-store.js +132 -0
  34. package/build/scheduler-backends/at-fallback.js +144 -0
  35. package/build/scheduler-backends/escaping.js +113 -0
  36. package/build/scheduler-backends/index.js +72 -0
  37. package/build/scheduler-backends/launchd.js +216 -0
  38. package/build/scheduler-backends/systemd-user.js +237 -0
  39. package/build/scheduler-backends/task-scheduler.js +219 -0
  40. package/build/scheduler-backends/types.js +23 -0
  41. package/build/start-tickets-prereqs.js +90 -1
  42. package/build/start-tickets.js +222 -70
  43. package/build/third-party-mcp-targets.js +75 -0
  44. package/build/version.generated.js +1 -1
  45. package/package.json +8 -8
  46. package/pipelines/full-automation.json +49 -0
  47. package/pipelines/idea-to-ticket.json +71 -0
  48. package/pipelines/implement-ticket.json +28 -2
  49. package/smoke-test/SMOKE-TEST.md +511 -0
  50. package/smoke-test/smoke-test-mcp.md +23 -0
@@ -53,7 +53,10 @@
53
53
  * unit-testable on Linux CI without spawning real commands or terminals.
54
54
  */
55
55
  import { execFile } from "child_process";
56
+ import { readFile, writeFile, mkdir } from "fs/promises";
56
57
  import path from "path";
58
+ import { VERSION } from "./version.generated.js";
59
+ import { provisionMcpRegistrationsForCreatedWorktrees, } from "./mcp-provisioning.js";
57
60
  // Per-OS prerequisite knowledge + low-level command probes live in the shared
58
61
  // prereqs module so `runPreflight` (enforce) and the read-only `doctor` (render)
59
62
  // can never drift. `start-tickets.ts` imports VALUES from there; the prereqs
@@ -75,6 +78,10 @@ export const DEFAULT_MAX_PARALLEL = 3;
75
78
  export const DEFAULT_TMUX_SESSION_PREFIX = "bridge-start-tickets";
76
79
  /** Environment variable overriding the tmux session-name prefix. */
77
80
  export const TMUX_SESSION_OVERRIDE_ENV = "BAPI_TMUX_SESSION";
81
+ /** Return a copy of `row` with `warning` appended; never mutates the input. */
82
+ export function appendSummaryRowWarning(row, warning) {
83
+ return { ...row, warnings: [...(row.warnings ?? []), warning] };
84
+ }
78
85
  // ---------------------------------------------------------------------------
79
86
  // Usage / argument parsing
80
87
  // ---------------------------------------------------------------------------
@@ -89,7 +96,8 @@ export function getStartTicketsUsage() {
89
96
  " --terminal terminal|iterm Override the macOS terminal app (default: auto-detect via $TERM_PROGRAM); honored on macOS only",
90
97
  " --dry-run Print intended actions; create no worktrees, open no tabs",
91
98
  " --branch KEY=BRANCH Use BRANCH instead of feature/KEY for that ticket (repeatable)",
92
- " --no-refresh-main Skip 'git fetch origin main' + fast-forward of local main",
99
+ " --base-branch BRANCH Cut new worktrees from BRANCH and refresh origin/BRANCH (default: main)",
100
+ " --no-refresh-main Skip refresh of the configured base branch (default main); historical name retained for backward compatibility",
93
101
  " --max-parallel N Max worktrees to create concurrently (default: 3)",
94
102
  " -h, --help Show this help",
95
103
  "",
@@ -116,9 +124,11 @@ export function parseStartTicketsArgs(argv) {
116
124
  }
117
125
  let terminal;
118
126
  let dryRun = false;
127
+ let autoApprove = false;
119
128
  let refreshMain = true;
120
129
  let maxParallelRaw;
121
130
  let agentName = DEFAULT_AGENT_NAME;
131
+ let baseBranch = "main";
122
132
  const branchEntries = [];
123
133
  const keys = [];
124
134
  for (let i = 0; i < argv.length; i++) {
@@ -198,10 +208,36 @@ export function parseStartTicketsArgs(argv) {
198
208
  branchEntries.push(value);
199
209
  continue;
200
210
  }
211
+ if (arg === "--base-branch" || arg.startsWith("--base-branch=")) {
212
+ let value;
213
+ if (arg.startsWith("--base-branch=")) {
214
+ value = arg.slice("--base-branch=".length);
215
+ }
216
+ else {
217
+ // For the space-separated form, reject a following option token so we
218
+ // don't silently consume "--dry-run" (etc.) as the branch value.
219
+ const next = i + 1 < argv.length ? argv[i + 1] : undefined;
220
+ if (next === undefined || next.startsWith("-")) {
221
+ return { status: "error", message: "--base-branch requires a value (a branch name)." };
222
+ }
223
+ value = takeValue();
224
+ }
225
+ const trimmed = (value ?? "").trim();
226
+ const error = validateBranchName(trimmed);
227
+ if (error) {
228
+ return { status: "error", message: `Invalid --base-branch value: ${error}` };
229
+ }
230
+ baseBranch = trimmed;
231
+ continue;
232
+ }
201
233
  if (arg === "--dry-run") {
202
234
  dryRun = true;
203
235
  continue;
204
236
  }
237
+ if (arg === "--auto") {
238
+ autoApprove = true;
239
+ continue;
240
+ }
205
241
  if (arg === "--no-refresh-main") {
206
242
  refreshMain = false;
207
243
  continue;
@@ -277,13 +313,15 @@ export function parseStartTicketsArgs(argv) {
277
313
  }
278
314
  return {
279
315
  status: "ok",
280
- options: { keys, terminal, dryRun, refreshMain, maxParallel, branchOverrides, agentName },
316
+ options: { keys, terminal, dryRun, autoApprove, refreshMain, maxParallel, branchOverrides, agentName, baseBranch },
281
317
  };
282
318
  }
283
319
  /** Returns an error string for an unsafe branch name, or null when valid. */
284
- function validateBranchName(branch) {
285
- if (branch.length === 0)
320
+ export function validateBranchName(branch) {
321
+ if (branch.trim().length === 0)
286
322
  return "branch name must not be empty.";
323
+ if (branch.length > 255)
324
+ return "branch name must be 255 characters or fewer.";
287
325
  if (branch.startsWith("-"))
288
326
  return "branch name must not start with '-'.";
289
327
  // Reject ASCII control characters (0x00-0x1F and 0x7F) without embedding
@@ -343,7 +381,7 @@ export function getDefaultSpawnTerminalTabForPlatform(platform) {
343
381
  * are always honored). Returns a structured error for unsupported platforms;
344
382
  * never throws.
345
383
  */
346
- export function resolveStartTicketsPlatformConfig(deps, agent) {
384
+ export function resolveStartTicketsPlatformConfig(deps, agent, autoApprove = false) {
347
385
  if (!isSupportedStartTicketsPlatform(deps.platform)) {
348
386
  return { ok: false, error: unsupportedPlatformMessage(deps.platform) };
349
387
  }
@@ -353,7 +391,7 @@ export function resolveStartTicketsPlatformConfig(deps, agent) {
353
391
  config: {
354
392
  platform,
355
393
  worktrunkBinary: resolveWorktrunkBinary(platform, deps.env),
356
- buildAgentShellCommand: (key, worktreePath) => buildAgentShellCommand(agent, key, worktreePath, platform),
394
+ buildAgentShellCommand: (key, worktreePath) => buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove),
357
395
  spawnTerminalTab: deps.spawnTerminalTab,
358
396
  },
359
397
  };
@@ -522,84 +560,106 @@ export function parseGitWorktreeList(output) {
522
560
  entries.push(current);
523
561
  return entries;
524
562
  }
525
- /** Return the worktree path owning branch `main`, or null. */
526
- export function findMainWorktreePath(entries) {
563
+ /** Return the worktree path owning branch `baseBranch`, or null. */
564
+ export function findBaseWorktreePath(entries, baseBranch) {
527
565
  for (const entry of entries) {
528
- if (entry.branch === "main")
566
+ if (entry.branch === baseBranch)
529
567
  return entry.path;
530
568
  }
531
569
  return null;
532
570
  }
533
571
  /**
534
- * Fetch origin/main and fast-forward local `main`. No-op when `refreshMain` is
535
- * false. Handles both checkout states:
572
+ * Compatibility wrapper: returns the worktree path owning branch `main`, or
573
+ * null. Defers to {@link findBaseWorktreePath} so the `main` and non-main paths
574
+ * stay identical.
575
+ */
576
+ export function findMainWorktreePath(entries) {
577
+ return findBaseWorktreePath(entries, "main");
578
+ }
579
+ /**
580
+ * Fetch origin/<baseBranch> and fast-forward the local base branch. No-op when
581
+ * `refreshMain` is false. (The option key keeps its historical name so the
582
+ * `--no-refresh-main` user-facing flag remains backward-compatible; the
583
+ * behavior now follows whatever `baseBranch` resolves to, default `"main"`.)
584
+ *
585
+ * Handles both checkout states:
536
586
  *
537
- * - `main` IS checked out in a worktree → `git merge --ff-only` inside that
538
- * worktree, so its index/working tree stay consistent (git forbids
587
+ * - `<baseBranch>` IS checked out in a worktree → `git merge --ff-only` inside
588
+ * that worktree, so its index/working tree stay consistent (git forbids
539
589
  * force-moving a checked-out branch ref).
540
- * - `main` is NOT checked out anywhere (e.g. the primary checkout is on a
541
- * feature/chore branch) → fast-forward the ref directly with
590
+ * - `<baseBranch>` is NOT checked out anywhere (e.g. the primary checkout is
591
+ * on a feature/chore branch) → fast-forward the ref directly with
542
592
  * `git branch --force`, since no working tree depends on it. This is guarded
543
- * by an ancestry check so a diverged local `main` is never clobbered, and it
544
- * creates `main` from origin/main when no local ref exists yet.
593
+ * by an ancestry check so a diverged local base is never clobbered, and it
594
+ * creates the local base ref from origin when no local ref exists yet.
545
595
  *
546
596
  * Returns a structured failure for any expected problem (fetch failure, diverged
547
- * main, or a failed ref update).
597
+ * base branch, or a failed ref update).
548
598
  */
549
- export async function refreshMainBranch(deps, options) {
599
+ export async function refreshBaseBranch(deps, options) {
550
600
  if (!options.refreshMain)
551
601
  return { ok: true };
552
- const fetch = await deps.runCommand("git", ["fetch", "origin", "main"], {
602
+ const baseBranch = options.baseBranch;
603
+ const originRef = `origin/${baseBranch}`;
604
+ const fetch = await deps.runCommand("git", ["fetch", "origin", baseBranch], {
553
605
  cwd: deps.cwd,
554
606
  });
555
607
  if (!commandSucceeded(fetch)) {
556
608
  return {
557
609
  ok: false,
558
- error: "git fetch origin main failed. Check your network and 'git remote get-url origin', or pass --no-refresh-main to skip.",
610
+ error: `git fetch origin ${baseBranch} failed. Check your network and 'git remote get-url origin', or pass --no-refresh-main to skip.`,
559
611
  };
560
612
  }
561
613
  const list = await deps.runCommand("git", ["worktree", "list", "--porcelain"], { cwd: deps.cwd });
562
614
  if (!commandSucceeded(list)) {
563
615
  return {
564
616
  ok: false,
565
- error: "git worktree list --porcelain failed; cannot locate the main worktree.",
617
+ error: `git worktree list --porcelain failed; cannot locate the ${baseBranch} worktree.`,
566
618
  };
567
619
  }
568
- const mainPath = findMainWorktreePath(parseGitWorktreeList(list.stdout));
569
- // `main` is checked out somewhere: fast-forward it in place. git refuses to
620
+ const basePath = findBaseWorktreePath(parseGitWorktreeList(list.stdout), baseBranch);
621
+ // `<baseBranch>` is checked out somewhere: fast-forward it in place. git refuses to
570
622
  // force-move a checked-out branch ref, so we must go through merge.
571
- if (mainPath) {
572
- const merge = await deps.runCommand("git", ["merge", "--ff-only", "origin/main"], { cwd: mainPath });
623
+ if (basePath) {
624
+ const merge = await deps.runCommand("git", ["merge", "--ff-only", originRef], { cwd: basePath });
573
625
  if (!commandSucceeded(merge)) {
574
626
  return {
575
627
  ok: false,
576
- error: `Local main has diverged from origin/main (checked out at ${mainPath}). Resolve the divergence manually, or rerun with --no-refresh-main.`,
628
+ error: `Local ${baseBranch} has diverged from ${originRef} (checked out at ${basePath}). Resolve the divergence manually, or rerun with --no-refresh-main.`,
577
629
  };
578
630
  }
579
631
  return { ok: true };
580
632
  }
581
- // `main` is not checked out in any worktree. No working tree depends on the
582
- // ref, so fast-forward it directly — but only when it is a true fast-forward,
583
- // never clobbering local-only commits. When local `main` does not exist yet,
584
- // create it from origin/main.
585
- if (await branchExists(deps, "main")) {
586
- const ancestor = await deps.runCommand("git", ["merge-base", "--is-ancestor", "main", "origin/main"], { cwd: deps.cwd });
633
+ // `<baseBranch>` is not checked out in any worktree. No working tree depends
634
+ // on the ref, so fast-forward it directly — but only when it is a true
635
+ // fast-forward, never clobbering local-only commits. When the local base ref
636
+ // does not exist yet, create it from origin.
637
+ if (await branchExists(deps, baseBranch)) {
638
+ const ancestor = await deps.runCommand("git", ["merge-base", "--is-ancestor", baseBranch, originRef], { cwd: deps.cwd });
587
639
  if (!commandSucceeded(ancestor)) {
588
640
  return {
589
641
  ok: false,
590
- error: "Local main has diverged from origin/main. Resolve the divergence manually, or rerun with --no-refresh-main.",
642
+ error: `Local ${baseBranch} has diverged from ${originRef}. Resolve the divergence manually, or rerun with --no-refresh-main.`,
591
643
  };
592
644
  }
593
645
  }
594
- const update = await deps.runCommand("git", ["branch", "--force", "main", "origin/main"], { cwd: deps.cwd });
646
+ const update = await deps.runCommand("git", ["branch", "--force", baseBranch, originRef], { cwd: deps.cwd });
595
647
  if (!commandSucceeded(update)) {
596
648
  return {
597
649
  ok: false,
598
- error: "Failed to fast-forward local main to origin/main. Resolve manually, or rerun with --no-refresh-main.",
650
+ error: `Failed to fast-forward local ${baseBranch} to ${originRef}. Resolve manually, or rerun with --no-refresh-main.`,
599
651
  };
600
652
  }
601
653
  return { ok: true };
602
654
  }
655
+ /**
656
+ * Compatibility wrapper around {@link refreshBaseBranch} that preserves the
657
+ * historical `refreshMainBranch(deps, { refreshMain })` signature for callers
658
+ * that target `main` specifically.
659
+ */
660
+ export async function refreshMainBranch(deps, options) {
661
+ return refreshBaseBranch(deps, { refreshMain: options.refreshMain, baseBranch: "main" });
662
+ }
603
663
  // ---------------------------------------------------------------------------
604
664
  // Concurrency + worktree creation
605
665
  // ---------------------------------------------------------------------------
@@ -643,11 +703,11 @@ export async function branchExists(deps, branch) {
643
703
  * tab spawning separately. The args are platform-agnostic; only the binary
644
704
  * differs (`wt` vs `git-wt`).
645
705
  */
646
- export function buildWtSwitchArgs(branch, exists) {
706
+ export function buildWtSwitchArgs(branch, exists, baseBranch = "main") {
647
707
  if (exists) {
648
708
  return ["switch", "-y", branch, "--format=json"];
649
709
  }
650
- return ["switch", "--create", "-y", branch, "--format=json"];
710
+ return ["switch", "--create", "-y", branch, "-b", baseBranch, "--format=json"];
651
711
  }
652
712
  /** Return the Node `path` API matching a platform (`win32` vs POSIX). */
653
713
  export function pathApiForPlatform(platform) {
@@ -698,11 +758,11 @@ function pickWorktreePathField(parsed) {
698
758
  * success (with key, branch, path) or a `create-failed` row on any expected
699
759
  * failure — never throws for per-ticket problems.
700
760
  */
701
- export async function createWorktreeForTicket(deps, key, branchOverrides, worktrunkBinary) {
761
+ export async function createWorktreeForTicket(deps, key, branchOverrides, worktrunkBinary, baseBranch = "main") {
702
762
  const branch = resolveBranchForTicket(key, branchOverrides);
703
763
  try {
704
764
  const exists = await branchExists(deps, branch);
705
- const args = buildWtSwitchArgs(branch, exists);
765
+ const args = buildWtSwitchArgs(branch, exists, baseBranch);
706
766
  const result = await deps.runCommand(worktrunkBinary, args, { cwd: deps.cwd });
707
767
  if (!commandSucceeded(result)) {
708
768
  const reason = (result.stderr || result.stdout || "").trim();
@@ -728,14 +788,18 @@ export async function createWorktreeForTicket(deps, key, branchOverrides, worktr
728
788
  * aborting the run.
729
789
  */
730
790
  export async function createWorktrees(deps, options, worktrunkBinary) {
731
- return runWithConcurrency(options.keys, options.maxParallel, (key) => createWorktreeForTicket(deps, key, options.branchOverrides, worktrunkBinary));
791
+ return runWithConcurrency(options.keys, options.maxParallel, (key) => createWorktreeForTicket(deps, key, options.branchOverrides, worktrunkBinary, options.baseBranch));
732
792
  }
733
793
  // ---------------------------------------------------------------------------
734
794
  // Per-platform shell-command construction
735
795
  // ---------------------------------------------------------------------------
736
- /** The starter prompt handed to the selected agent. Identical for every agent. */
737
- export function buildAgentPrompt(key) {
738
- return `/implement-ticket ${key}`;
796
+ /**
797
+ * The starter prompt handed to the selected agent. Identical for every agent.
798
+ * When `autoApprove` is set, the implementation agent runs hands-off
799
+ * (`/implement-ticket <KEY> --auto`) — used by full-automation chains.
800
+ */
801
+ export function buildAgentPrompt(key, opts = {}) {
802
+ return `/implement-ticket ${key}${opts.autoApprove ? " --auto" : ""}`;
739
803
  }
740
804
  /**
741
805
  * Build the agent invocation (`<command> <quotedPrompt>`) for the agent's prompt
@@ -754,13 +818,13 @@ export function buildAgentInvocation(agent, prompt, quote) {
754
818
  }
755
819
  }
756
820
  /** POSIX agent shell command: `cd '<path>' && <agent> '<prompt>'`. */
757
- export function buildPosixAgentShellCommand(agent, key, worktreePath) {
758
- const invocation = buildAgentInvocation(agent, buildAgentPrompt(key), (p) => `'${shSquoteInner(p)}'`);
821
+ export function buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove = false) {
822
+ const invocation = buildAgentInvocation(agent, buildAgentPrompt(key, { autoApprove }), (p) => `'${shSquoteInner(p)}'`);
759
823
  return `cd '${shSquoteInner(worktreePath)}' && ${invocation}`;
760
824
  }
761
825
  /** PowerShell agent shell command: `Set-Location -LiteralPath '<path>'; <agent> '<prompt>'`. */
762
- export function buildPowerShellAgentShellCommand(agent, key, worktreePath) {
763
- const invocation = buildAgentInvocation(agent, buildAgentPrompt(key), powershellSquote);
826
+ export function buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove = false) {
827
+ const invocation = buildAgentInvocation(agent, buildAgentPrompt(key, { autoApprove }), powershellSquote);
764
828
  return `Set-Location -LiteralPath ${powershellSquote(worktreePath)}; ${invocation}`;
765
829
  }
766
830
  /**
@@ -769,10 +833,10 @@ export function buildPowerShellAgentShellCommand(agent, key, worktreePath) {
769
833
  * dry-run fallback). The selected `agent` (never a module-level constant)
770
834
  * determines the launched command.
771
835
  */
772
- export function buildAgentShellCommand(agent, key, worktreePath, platform = "darwin") {
836
+ export function buildAgentShellCommand(agent, key, worktreePath, platform = "darwin", autoApprove = false) {
773
837
  if (platform === "win32")
774
- return buildPowerShellAgentShellCommand(agent, key, worktreePath);
775
- return buildPosixAgentShellCommand(agent, key, worktreePath);
838
+ return buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove);
839
+ return buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove);
776
840
  }
777
841
  // ---------------------------------------------------------------------------
778
842
  // macOS terminal spawning (behind the injected boundary)
@@ -1048,25 +1112,47 @@ export function buildDryRunResults(keys, overrides) {
1048
1112
  * PowerShell on Windows, `wt` + POSIX on macOS/Linux, and a non-throwing `wt` +
1049
1113
  * POSIX fallback for unsupported platforms.
1050
1114
  */
1051
- export function getDryRunPlatformDetails(agent, platform = process.platform, env = process.env) {
1115
+ export function getDryRunPlatformDetails(agent, platform = process.platform, env = process.env, autoApprove = false) {
1052
1116
  return {
1053
1117
  worktrunkBinary: resolveWorktrunkBinary(platform, env),
1054
- buildAgentShellCommand: (key, worktreePath) => buildAgentShellCommand(agent, key, worktreePath, platform),
1118
+ buildAgentShellCommand: (key, worktreePath) => buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove),
1055
1119
  };
1056
1120
  }
1121
+ /**
1122
+ * Build the dry-run preview of the secret-free MCP provisioning that
1123
+ * `start-tickets` would perform for a worktree whose `.bridge/config` includes
1124
+ * the `bapi` target. Lists both registration files and the version-pinned shim
1125
+ * command. Pure formatting — implies no credentials and writes no files.
1126
+ */
1127
+ export function buildDryRunMcpProvisioningLines(worktreePath, platform = process.platform) {
1128
+ const api = platform === "win32" ? path.win32 : path.posix;
1129
+ const mcpJson = api.join(worktreePath, ".mcp.json");
1130
+ const cursorJson = api.join(worktreePath, ".cursor", "mcp.json");
1131
+ const shim = `npx -y @bridge_gpt/mcp-server@${VERSION} mcp-invoke --target <target> --project-root ${worktreePath}`;
1132
+ return [
1133
+ "DRY-RUN: MCP provisioning (target-driven from .bridge/config — bapi plus any",
1134
+ "DRY-RUN: supported Tier-2 target such as sfcc): would write a secret-free shim",
1135
+ "DRY-RUN: entry per target to",
1136
+ `DRY-RUN: ${mcpJson}`,
1137
+ `DRY-RUN: ${cursorJson}`,
1138
+ `DRY-RUN: ${shim}`,
1139
+ ];
1140
+ }
1057
1141
  /**
1058
1142
  * Build the user-facing dry-run detail lines for one ticket, rendering the
1059
- * platform-correct Worktrunk binary and the selected agent's shell command. Pure
1060
- * platform formatting only — no preflight, no routing failures.
1143
+ * platform-correct Worktrunk binary, the selected agent's shell command, and
1144
+ * the secret-free MCP provisioning preview. Pure platform formatting only — no
1145
+ * preflight, no routing failures.
1061
1146
  */
1062
- export function buildDryRunDetailLines(agent, key, branch, platform = process.platform, env = process.env) {
1063
- const { worktrunkBinary, buildAgentShellCommand: build } = getDryRunPlatformDetails(agent, platform, env);
1064
- const wtArgs = buildWtSwitchArgs(branch, false);
1147
+ export function buildDryRunDetailLines(agent, key, branch, platform = process.platform, env = process.env, baseBranch = "main", autoApprove = false) {
1148
+ const { worktrunkBinary, buildAgentShellCommand: build } = getDryRunPlatformDetails(agent, platform, env, autoApprove);
1149
+ const wtArgs = buildWtSwitchArgs(branch, false, baseBranch);
1065
1150
  const agentInvocation = build(key, "<worktree-path>");
1066
1151
  return [
1067
1152
  `DRY-RUN: ${key} -> branch=${branch}`,
1068
1153
  `DRY-RUN: ${worktrunkBinary} ${wtArgs.join(" ")}`,
1069
1154
  `DRY-RUN: ${agentInvocation}`,
1155
+ ...buildDryRunMcpProvisioningLines("<worktree-path>", platform),
1070
1156
  ];
1071
1157
  }
1072
1158
  /**
@@ -1083,13 +1169,30 @@ export function formatSummaryReport(rows) {
1083
1169
  line += ` path=${row.path}`;
1084
1170
  lines.push(line);
1085
1171
  }
1086
- const failures = rows.filter((r) => r.status === "create-failed" || r.status === "spawn-failed");
1087
- if (failures.length > 0) {
1172
+ // Warnings section: create/spawn-failed row errors AND any non-fatal
1173
+ // per-row provisioning warnings. Identical error/warning text for the same
1174
+ // row is emitted only once.
1175
+ const warningLines = [];
1176
+ for (const row of rows) {
1177
+ const messages = [];
1178
+ if (row.status === "create-failed" || row.status === "spawn-failed") {
1179
+ messages.push(row.error ?? row.status);
1180
+ }
1181
+ for (const warning of row.warnings ?? []) {
1182
+ messages.push(warning);
1183
+ }
1184
+ const seen = new Set();
1185
+ for (const message of messages) {
1186
+ if (seen.has(message))
1187
+ continue;
1188
+ seen.add(message);
1189
+ warningLines.push(` ${row.key}: ${message}`);
1190
+ }
1191
+ }
1192
+ if (warningLines.length > 0) {
1088
1193
  lines.push("");
1089
1194
  lines.push("Warnings:");
1090
- for (const row of failures) {
1091
- lines.push(` ${row.key}: ${row.error ?? row.status}`);
1092
- }
1195
+ lines.push(...warningLines);
1093
1196
  }
1094
1197
  return lines.join("\n");
1095
1198
  }
@@ -1101,7 +1204,38 @@ export function formatSummaryReport(rows) {
1101
1204
  * using the platform-correct shell builder. Global preflight/refresh failures
1102
1205
  * are returned as `{ ok: false }`; per-ticket failures stay in the rows.
1103
1206
  */
1104
- export async function orchestrateStartTickets(deps, options) {
1207
+ /**
1208
+ * Build the `mcp-provisioning` filesystem boundary from `StartTicketsDeps`.
1209
+ * Provisioning needs real `fs/promises` ops (not part of the command-runner DI
1210
+ * boundary), but inherits the platform and cwd from the orchestration deps.
1211
+ */
1212
+ export function buildMcpProvisioningDeps(deps) {
1213
+ return {
1214
+ readFile: (filePath) => readFile(filePath, "utf-8"),
1215
+ writeFile: (filePath, data) => writeFile(filePath, data, "utf-8"),
1216
+ mkdir: (dirPath, options) => mkdir(dirPath, options),
1217
+ platform: deps.platform,
1218
+ cwd: deps.cwd,
1219
+ };
1220
+ }
1221
+ /**
1222
+ * Tier-3 file-credential materialization seam.
1223
+ *
1224
+ * The committed `.bridge/config` schema does not (yet) declare file-credential
1225
+ * entries — Tier-3 file materialization is a gated seam (see
1226
+ * `credential-materialization.ts`, whose Windows worktree-visible copy remains
1227
+ * disabled pending the open context-vs-server-only design decision). With no
1228
+ * file-credential configuration there is nothing to materialize, so the default
1229
+ * is a safe pass-through. The seam exists so `orchestrateStartTickets` calls it
1230
+ * in the correct order — AFTER MCP registration provisioning and BEFORE tab
1231
+ * spawning — once the schema and the design decision are resolved. Per-row
1232
+ * failures must mark only the affected row `spawn-failed`; normal symlinks are
1233
+ * never torn down on completion.
1234
+ */
1235
+ export async function materializeFileCredentialsForCreatedWorktrees(rows, _deps) {
1236
+ return rows;
1237
+ }
1238
+ export async function orchestrateStartTickets(deps, options, overrides = {}) {
1105
1239
  if (options.dryRun) {
1106
1240
  return { ok: true, rows: buildDryRunResults(options.keys, options.branchOverrides) };
1107
1241
  }
@@ -1117,15 +1251,33 @@ export async function orchestrateStartTickets(deps, options) {
1117
1251
  const preflight = await runPreflight(deps, options);
1118
1252
  if (!preflight.ok)
1119
1253
  return { ok: false, error: preflight.error };
1120
- const platformConfig = resolveStartTicketsPlatformConfig(deps, agent);
1254
+ const platformConfig = resolveStartTicketsPlatformConfig(deps, agent, options.autoApprove);
1121
1255
  if (!platformConfig.ok)
1122
1256
  return { ok: false, error: platformConfig.error };
1123
- const refresh = await refreshMainBranch(deps, options);
1257
+ const refresh = await refreshBaseBranch(deps, {
1258
+ refreshMain: options.refreshMain,
1259
+ baseBranch: options.baseBranch,
1260
+ });
1124
1261
  if (!refresh.ok)
1125
1262
  return { ok: false, error: refresh.error };
1126
- const created = await createWorktrees(deps, options, platformConfig.config.worktrunkBinary);
1127
- const terminal = detectTerminal(options.terminal, deps.env);
1128
- const rows = await spawnTabsForCreatedWorktrees(deps, created, terminal, platformConfig.config.buildAgentShellCommand);
1263
+ const createWorktreesFn = overrides.createWorktrees ?? createWorktrees;
1264
+ const provisionFn = overrides.provisionMcpRegistrations ??
1265
+ ((rows, d) => provisionMcpRegistrationsForCreatedWorktrees(rows, buildMcpProvisioningDeps(d)));
1266
+ const materializeFn = overrides.materializeFileCredentials ?? materializeFileCredentialsForCreatedWorktrees;
1267
+ const detectTerminalFn = overrides.detectTerminal ?? detectTerminal;
1268
+ const spawnTabsFn = overrides.spawnTabsForCreatedWorktrees ?? spawnTabsForCreatedWorktrees;
1269
+ const created = await createWorktreesFn(deps, options, platformConfig.config.worktrunkBinary);
1270
+ // Synchronously provision secret-free worktree MCP registrations after
1271
+ // worktree creation and before launching the agent tab. Per-worktree
1272
+ // provisioning failures mark only that row `spawn-failed` (skipped by the
1273
+ // spawn step below) — they never abort the whole run.
1274
+ const provisioned = await provisionFn(created, deps);
1275
+ // Materialize any Tier-3 file credentials AFTER MCP registration provisioning
1276
+ // and BEFORE tab spawning (currently a pass-through seam; see
1277
+ // materializeFileCredentialsForCreatedWorktrees).
1278
+ const materialized = await materializeFn(provisioned, deps);
1279
+ const terminal = detectTerminalFn(options.terminal, deps.env);
1280
+ const rows = await spawnTabsFn(deps, materialized, terminal, platformConfig.config.buildAgentShellCommand);
1129
1281
  return { ok: true, rows };
1130
1282
  }
1131
1283
  /** Platform-specific guidance printed when one or more tabs fail to spawn. */
@@ -1179,7 +1331,7 @@ export async function runStartTicketsCli(argv, overrides = {}) {
1179
1331
  if (options.dryRun) {
1180
1332
  for (const key of options.keys) {
1181
1333
  const branch = resolveBranchForTicket(key, options.branchOverrides);
1182
- for (const line of buildDryRunDetailLines(agent, key, branch, deps.platform, deps.env)) {
1334
+ for (const line of buildDryRunDetailLines(agent, key, branch, deps.platform, deps.env, options.baseBranch, options.autoApprove)) {
1183
1335
  log(line);
1184
1336
  }
1185
1337
  }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Tier-2 third-party MCP target metadata + atomic env injection.
3
+ *
4
+ * Known third-party MCP targets (e.g. Salesforce `b2c-dx-mcp` via `sfcc`) are
5
+ * declared here as additive, data-only definitions: each names the secret env
6
+ * keys it needs. Provisioning and `mcp-invoke` consume these definitions without
7
+ * any target-specific branching, so adding a future Tier-2 target is a matter of
8
+ * adding one entry to the registry below.
9
+ *
10
+ * Credential resolution is delegated to the generic
11
+ * {@link resolveCredentialBundle}; the resulting env overlay is ATOMIC — either
12
+ * every required key resolves or the whole overlay fails. A half overlay (one of
13
+ * an `SFCC_CLIENT_ID`/`SFCC_CLIENT_SECRET` pair) is never returned. No secret
14
+ * value ever appears in an error message.
15
+ */
16
+ import { resolveCredentialBundle, } from "./credential-store.js";
17
+ // ---------------------------------------------------------------------------
18
+ // Registry — additive; add a new entry to support a new Tier-2 target.
19
+ // ---------------------------------------------------------------------------
20
+ const THIRD_PARTY_TARGETS = {
21
+ sfcc: {
22
+ target: "sfcc",
23
+ requiredEnvKeys: ["SFCC_CLIENT_ID", "SFCC_CLIENT_SECRET"],
24
+ },
25
+ };
26
+ /** Return the definition for a supported Tier-2 target, or `undefined`. */
27
+ export function getThirdPartyTargetDefinition(target) {
28
+ return THIRD_PARTY_TARGETS[target];
29
+ }
30
+ /** List all supported Tier-2 target identifiers. */
31
+ export function getSupportedThirdPartyTargets() {
32
+ return Object.keys(THIRD_PARTY_TARGETS);
33
+ }
34
+ // ---------------------------------------------------------------------------
35
+ // Manifest entry validation
36
+ // ---------------------------------------------------------------------------
37
+ /**
38
+ * Validate that a parsed manifest entry for a Tier-2 target declares everything
39
+ * needed to launch it: a non-empty `command`, an `args` array, and a non-empty
40
+ * `secret_bundle`. Errors name only the missing field, never any value.
41
+ */
42
+ export function validateThirdPartyTargetManifestEntry(entry) {
43
+ if (entry.command === undefined || entry.command.trim().length === 0) {
44
+ return { ok: false, error: `target '${entry.target}' requires a non-empty command` };
45
+ }
46
+ if (entry.args === undefined) {
47
+ return { ok: false, error: `target '${entry.target}' requires an args array` };
48
+ }
49
+ if (entry.secretBundle === undefined || entry.secretBundle.trim().length === 0) {
50
+ return { ok: false, error: `target '${entry.target}' requires a non-empty secret_bundle` };
51
+ }
52
+ return { ok: true };
53
+ }
54
+ // ---------------------------------------------------------------------------
55
+ // Atomic env resolution
56
+ // ---------------------------------------------------------------------------
57
+ /**
58
+ * Resolve the complete env overlay a Tier-2 target needs from its secret bundle.
59
+ * Delegates to {@link resolveCredentialBundle}; because that resolver fails
60
+ * unless every required key resolves, the returned overlay is atomic — there is
61
+ * no code path that returns a partial set of the target's secrets. Parent env
62
+ * values override store values key-by-key (handled by the generic resolver).
63
+ * Errors are secret-free and name the missing key(s).
64
+ */
65
+ export async function resolveThirdPartyTargetEnv(definition, secretBundle, deps) {
66
+ const result = await resolveCredentialBundle(secretBundle, definition.requiredEnvKeys, deps);
67
+ if (!result.ok) {
68
+ return { ok: false, error: result.error };
69
+ }
70
+ const env = {};
71
+ for (const key of definition.requiredEnvKeys) {
72
+ env[key] = result.values[key];
73
+ }
74
+ return { ok: true, env };
75
+ }
@@ -1,2 +1,2 @@
1
1
  // AUTO-GENERATED — do not edit manually. Regenerate with: npm run build
2
- export const VERSION = "0.1.16";
2
+ export const VERSION = "0.2.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bridge_gpt/mcp-server",
3
- "version": "0.1.16",
3
+ "version": "0.2.0",
4
4
  "description": "Bridge API MCP server — exposes Jira endpoints as MCP tools for Claude Code agents",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -12,6 +12,7 @@
12
12
  "!build/**/*.test.js",
13
13
  "!build/integration/**",
14
14
  "pipelines/",
15
+ "smoke-test/",
15
16
  "README.md",
16
17
  "LICENSE"
17
18
  ],
@@ -19,18 +20,17 @@
19
20
  "build": "node scripts/bundle-version.js && node scripts/bundle-pipelines.js && node scripts/bundle-commands.js && node scripts/bundle-agents.js && tsc",
20
21
  "postbuild": "node scripts/prepend-shebang.cjs",
21
22
  "start": "node build/index.js",
22
- "test": "node --test build/pipeline-utils.test.js build/update-check.test.js build/cli-upgrade.test.js build/decision-page-schema.test.js build/decision-page-template.test.js build/bundle-pipelines.test.js build/instructions-contract.test.js build/pipeline-orchestrator-persistence.test.js build/pipeline-orchestrator-execution.test.js build/pipeline-orchestrator-integration.test.js build/index-static.test.js build/start-tickets.test.js build/agent-registry.test.js build/start-tickets-prereqs.test.js build/doctor.test.js build/package-static.test.js",
23
- "test:integration": "node --test build/integration/refresh-main.integration.test.js build/integration/start-tickets.integration.test.js build/integration/doctor.integration.test.js",
23
+ "test": "node --test build/pipeline-utils.test.js build/update-check.test.js build/cli-upgrade.test.js build/decision-page-schema.test.js build/decision-page-template.test.js build/bundle-pipelines.test.js build/instructions-contract.test.js build/pipeline-orchestrator-persistence.test.js build/pipeline-orchestrator-execution.test.js build/pipeline-orchestrator-integration.test.js build/index-static.test.js build/index-resolvers.test.js build/index-project-root.test.js build/index-pipelines.test.js build/index.test.js build/bridge-config.test.js build/credential-store.test.js build/mcp-invoke.test.js build/mcp-provisioning.test.js build/third-party-mcp-targets.test.js build/git-ignore-utils.test.js build/credential-materialization.test.js build/mcp-registration-doctor.test.js build/secret-safety.test.js build/start-tickets.test.js build/start-tickets-base-branch.test.js build/agent-registry.test.js build/start-tickets-prereqs.test.js build/doctor.test.js build/package-static.test.js build/chain-utils.test.js build/chain-orchestrator.test.js build/scheduler-backends/types.test.js build/scheduler-backends/escaping.test.js build/scheduler-backends/launchd.test.js build/scheduler-backends/task-scheduler.test.js build/scheduler-backends/systemd-user.test.js build/scheduler-backends/at-fallback.test.js build/scheduler-backends/index.test.js build/agent-launchers/claude.test.js build/agent-launchers/index.test.js build/schedule-store.test.js build/schedule-run.test.js build/agent-capabilities/cli.test.js build/agent-capabilities/runner.test.js build/agent-capabilities/probes.test.js build/agent-capabilities/reporter.test.js && node --experimental-test-module-mocks --test build/index-heavy-read-truncation.test.js build/index-artifacts.test.js build/index-brainstorm-filenames.test.js build/index-output-path.test.js build/index-generate-decision-page.test.js build/index-generate-decision-page.integration.test.js",
24
+ "test:integration": "node --test build/integration/refresh-main.integration.test.js build/integration/start-tickets.integration.test.js build/integration/doctor.integration.test.js build/integration/agent-capabilities.integration.test.js",
24
25
  "prepublishOnly": "npm run build && node scripts/verify-shebang.cjs"
25
26
  },
26
27
  "dependencies": {
27
- "@modelcontextprotocol/sdk": "^1.26.0",
28
- "zod": "^3.25.0"
28
+ "@modelcontextprotocol/sdk": "^1.29.0",
29
+ "zod": "^4.4.3"
29
30
  },
30
31
  "devDependencies": {
31
- "@types/node": "^22.0.0",
32
- "typescript": "^5.7.0",
33
- "undici": "^6.25.0"
32
+ "@types/node": "^25.9.1",
33
+ "typescript": "^6.0.3"
34
34
  },
35
35
  "engines": {
36
36
  "node": ">=18.0.0"