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