@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.
- package/README.md +333 -162
- package/build/agent-capabilities/cli.js +152 -0
- package/build/agent-capabilities/default-deps.js +45 -0
- package/build/agent-capabilities/probe-context.js +111 -0
- package/build/agent-capabilities/probes.js +278 -0
- package/build/agent-capabilities/reporter.js +50 -0
- package/build/agent-capabilities/runner.js +56 -0
- package/build/agent-capabilities/types.js +10 -0
- package/build/agent-launchers/claude.js +85 -0
- package/build/agent-launchers/index.js +17 -0
- package/build/agent-launchers/types.js +1 -0
- package/build/agents.generated.js +1 -1
- package/build/brainstorm-files.js +89 -0
- package/build/bridge-config.js +404 -0
- package/build/chain-orchestrator.js +1364 -0
- package/build/chain-utils.js +68 -0
- package/build/commands.generated.js +5 -3
- package/build/credential-materialization.js +128 -0
- package/build/credential-store.js +232 -0
- package/build/decision-page-schema.js +39 -6
- package/build/decision-page-template.js +54 -18
- package/build/doctor.js +18 -2
- package/build/fetch-stub.js +139 -0
- package/build/git-ignore-utils.js +63 -0
- package/build/index.js +1623 -546
- package/build/mcp-invoke.js +417 -0
- package/build/mcp-provisioning.js +249 -0
- package/build/mcp-registration-doctor.js +96 -0
- package/build/pipeline-orchestrator.js +66 -1
- package/build/pipeline-utils.js +33 -0
- package/build/pipelines.generated.js +165 -5
- package/build/schedule-run.js +951 -0
- package/build/schedule-store.js +132 -0
- package/build/scheduler-backends/at-fallback.js +144 -0
- package/build/scheduler-backends/escaping.js +113 -0
- package/build/scheduler-backends/index.js +72 -0
- package/build/scheduler-backends/launchd.js +216 -0
- package/build/scheduler-backends/systemd-user.js +237 -0
- package/build/scheduler-backends/task-scheduler.js +219 -0
- package/build/scheduler-backends/types.js +23 -0
- package/build/start-tickets-prereqs.js +90 -1
- package/build/start-tickets.js +222 -70
- package/build/third-party-mcp-targets.js +75 -0
- package/build/version.generated.js +1 -1
- package/package.json +8 -8
- package/pipelines/full-automation.json +49 -0
- package/pipelines/idea-to-ticket.json +71 -0
- package/pipelines/implement-ticket.json +28 -2
- package/smoke-test/SMOKE-TEST.md +511 -0
- package/smoke-test/smoke-test-mcp.md +23 -0
package/build/start-tickets.js
CHANGED
|
@@ -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
|
-
" --
|
|
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 `
|
|
526
|
-
export function
|
|
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 ===
|
|
566
|
+
if (entry.branch === baseBranch)
|
|
529
567
|
return entry.path;
|
|
530
568
|
}
|
|
531
569
|
return null;
|
|
532
570
|
}
|
|
533
571
|
/**
|
|
534
|
-
*
|
|
535
|
-
*
|
|
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
|
-
* -
|
|
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
|
-
* -
|
|
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
|
|
544
|
-
* creates
|
|
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
|
-
*
|
|
597
|
+
* base branch, or a failed ref update).
|
|
548
598
|
*/
|
|
549
|
-
export async function
|
|
599
|
+
export async function refreshBaseBranch(deps, options) {
|
|
550
600
|
if (!options.refreshMain)
|
|
551
601
|
return { ok: true };
|
|
552
|
-
const
|
|
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:
|
|
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:
|
|
617
|
+
error: `git worktree list --porcelain failed; cannot locate the ${baseBranch} worktree.`,
|
|
566
618
|
};
|
|
567
619
|
}
|
|
568
|
-
const
|
|
569
|
-
//
|
|
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 (
|
|
572
|
-
const merge = await deps.runCommand("git", ["merge", "--ff-only",
|
|
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
|
|
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
|
-
//
|
|
582
|
-
// ref, so fast-forward it directly — but only when it is a true
|
|
583
|
-
// never clobbering local-only commits. When local
|
|
584
|
-
// create it from origin
|
|
585
|
-
if (await branchExists(deps,
|
|
586
|
-
const ancestor = await deps.runCommand("git", ["merge-base", "--is-ancestor",
|
|
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:
|
|
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",
|
|
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:
|
|
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
|
-
/**
|
|
737
|
-
|
|
738
|
-
|
|
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
|
|
1060
|
-
* platform formatting only — no
|
|
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
|
-
|
|
1087
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1127
|
-
const
|
|
1128
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
28
|
-
"zod": "^
|
|
28
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
29
|
+
"zod": "^4.4.3"
|
|
29
30
|
},
|
|
30
31
|
"devDependencies": {
|
|
31
|
-
"@types/node": "^
|
|
32
|
-
"typescript": "^
|
|
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"
|