@bridge_gpt/mcp-server 0.2.41 → 0.2.42
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 +10 -10
- package/build/agent-capabilities/cli.js +2 -1
- package/build/agent-launchers/claude-executor-adapter.js +17 -4
- package/build/claude-user-config-doctor.js +42 -11
- package/build/cli-release.js +2 -1
- package/build/commands.generated.js +4 -4
- package/build/conduct-epic/bridge-client.js +354 -113
- package/build/conduct-epic/checkpoint-store.js +17 -0
- package/build/conduct-epic/cli.js +752 -99
- package/build/conduct-epic/cut-protocol.js +327 -0
- package/build/conduct-epic/spawn.js +14 -2
- package/build/conductor/bridge-api-client.js +27 -1
- package/build/conductor/cli.js +46 -1
- package/build/conductor/doctor.js +101 -16
- package/build/conductor/epic-reconcile.js +72 -19
- package/build/conductor/epic-runtime.js +15 -3
- package/build/conductor/errors.js +47 -0
- package/build/conductor/git-hooks.js +205 -11
- package/build/conductor/install-doctor.js +230 -1
- package/build/conductor/local-merge.js +130 -28
- package/build/conductor/tools.js +32 -3
- package/build/conductor/worker-ledger-cli.js +27 -1
- package/build/conductor-bin.js +15 -15
- package/build/credentials-cli.js +3 -2
- package/build/doctor.js +107 -41
- package/build/executor/cli.js +48 -1
- package/build/executor/env.js +21 -0
- package/build/executor/index-scope.js +39 -0
- package/build/executor/job-log-registry.js +69 -0
- package/build/executor/job-runner.js +148 -26
- package/build/executor/live-worker-registry.js +83 -0
- package/build/executor/observation.js +167 -6
- package/build/executor/platform.js +147 -3
- package/build/executor/process.js +58 -14
- package/build/executor/runner.js +235 -48
- package/build/executor/test-clock.js +3 -2
- package/build/index-scope-contract.js +96 -0
- package/build/index.js +153 -204
- package/build/init.js +83 -22
- package/build/install-bridge-conductor.js +323 -14
- package/build/install-bridge.js +202 -38
- package/build/install-doctor.js +23 -9
- package/build/install-reexec.js +2 -1
- package/build/launcher-config-inspection.js +83 -22
- package/build/mcp-host-config.js +331 -67
- package/build/mcp-host-targets.js +45 -21
- package/build/mcp-identity.js +92 -0
- package/build/mcp-install-state.js +94 -1
- package/build/mcp-invoke.js +2 -1
- package/build/mcp-provisioning.js +45 -12
- package/build/mcp-registration-doctor.js +35 -13
- package/build/mcp-server-invocation.js +4 -2
- package/build/merge-pull-request.js +208 -9
- package/build/pipelines.generated.js +3 -3
- package/build/plane/defaults.js +4 -1
- package/build/plane/preflight.js +81 -10
- package/build/plane/test-fakes.js +9 -1
- package/build/readme.generated.js +1 -1
- package/build/regression-check.js +3 -2
- package/build/review-tickets.js +8 -7
- package/build/run-unit-tests-launcher.js +74 -1
- package/build/schedule-run.js +3 -2
- package/build/setup-epic.js +453 -78
- package/build/sfcc/tool-wrapper.js +15 -0
- package/build/start-tickets-prereqs.js +11 -6
- package/build/start-tickets.js +91 -85
- package/build/update-check.js +3 -2
- package/build/upgrade-advice.js +2 -1
- package/build/upgrade-cli.js +50 -18
- package/build/version.generated.js +1 -1
- package/docs/CONDUCTOR.md +22 -0
- package/docs/install/mcp-tool-integrations.md +19 -3
- package/package.json +2 -2
|
@@ -368,6 +368,21 @@ function notConfigured(failureClass, message) {
|
|
|
368
368
|
*/
|
|
369
369
|
export function withSfccGate(deps, handler) {
|
|
370
370
|
return async (args, target) => {
|
|
371
|
+
// (0) Refuse a handler that was registered WITHOUT the target boundary.
|
|
372
|
+
//
|
|
373
|
+
// `withSfccGate` returns a two-arity handler and `register.ts` is the only
|
|
374
|
+
// thing that supplies the second argument, via `withSfccTargetBoundary`. A
|
|
375
|
+
// caller that skips that wrapper reaches (c) below with `target` undefined
|
|
376
|
+
// and throws a bare `TypeError`, which the read-path error formatter then
|
|
377
|
+
// masks into an opaque, deliberately detail-free INTERNAL_ERROR 500 — a
|
|
378
|
+
// registration defect wearing the costume of an upstream failure. Refusing
|
|
379
|
+
// here names it instead, and does so BEFORE the version fetch so a
|
|
380
|
+
// misregistered tool can never cause a network call.
|
|
381
|
+
if (target === undefined || target === null) {
|
|
382
|
+
return notConfigured("target-not-supplied", "This SFCC tool was invoked without a resolved target, which means it was " +
|
|
383
|
+
"registered without withSfccTargetBoundary. Refusing rather than treating " +
|
|
384
|
+
"an unresolved target as approved.");
|
|
385
|
+
}
|
|
371
386
|
// (a) Read /config-field/version
|
|
372
387
|
const version = await getSfccVersionConfig(deps.buildGetUrl, deps.getGetHeaders, deps.repoName);
|
|
373
388
|
if (version === null) {
|
|
@@ -18,7 +18,8 @@ import { resolveBapiCredentials, getPrimaryCredentialStorePath, } from "./creden
|
|
|
18
18
|
import { resolveStartTicketsRepoName } from "./start-tickets-repo.js";
|
|
19
19
|
import { probeWorktreeMcpRegistration } from "./mcp-registration-doctor.js";
|
|
20
20
|
import { probeWorktreeCommandAssets } from "./command-assets-doctor.js";
|
|
21
|
-
import {
|
|
21
|
+
import { MCP_SERVER_NAME, MCP_PACKAGE_NAME } from "./mcp-identity.js";
|
|
22
|
+
import { CLAUDE_MCP_SHADOWING_REMEDIATION_COMMAND, claudeMcpShadowingRemediationCommand, formatClaudeMcpShadowFinding, formatClaudeUserConfigDiagnostic, inspectClaudeUserConfigForMcpShadowing, resolveClaudeUserConfigPath, } from "./claude-user-config-doctor.js";
|
|
22
23
|
// ---------------------------------------------------------------------------
|
|
23
24
|
// Constants (moved here from start-tickets.ts so both consumers share them)
|
|
24
25
|
// ---------------------------------------------------------------------------
|
|
@@ -42,7 +43,7 @@ export const TMUX_COMMAND = "tmux";
|
|
|
42
43
|
/** Actionable hint emitted when Git Bash is missing on Windows. */
|
|
43
44
|
export const GIT_FOR_WINDOWS_BASH_HINT = "Install Git for Windows / Git Bash — Worktrunk runs its pre-start / post-start hooks via Git Bash.";
|
|
44
45
|
/** The read-only doctor invocation surfaced from preflight failures and elsewhere. */
|
|
45
|
-
export const START_TICKETS_DOCTOR_COMMAND =
|
|
46
|
+
export const START_TICKETS_DOCTOR_COMMAND = `npx -y ${MCP_PACKAGE_NAME} doctor`;
|
|
46
47
|
/**
|
|
47
48
|
* BAPI-527 live-source checkout guard (conductor durable execution).
|
|
48
49
|
*
|
|
@@ -352,7 +353,7 @@ function ripgrepDescriptor() {
|
|
|
352
353
|
const CREDENTIAL_RESOLUTION_HINT = "Rerun /install-bridge to persist the routing credential, set BAPI_API_KEY in the " +
|
|
353
354
|
'environment, or add it under "bapi:<repo_name>" in ~/.config/bridge/credentials.json. ' +
|
|
354
355
|
"To migrate a key that only lives in .mcp.json / .cursor/mcp.json, run: " +
|
|
355
|
-
|
|
356
|
+
`npx -y ${MCP_PACKAGE_NAME} credentials migrate-agent-config --write-credentials.`;
|
|
356
357
|
const CREDENTIAL_RESOLUTION_INSTALL_HINTS = {
|
|
357
358
|
darwin: CREDENTIAL_RESOLUTION_HINT,
|
|
358
359
|
linux: CREDENTIAL_RESOLUTION_HINT,
|
|
@@ -531,7 +532,7 @@ export async function resolveMainWorktreePathForDoctor(deps) {
|
|
|
531
532
|
* which is why the advice below is phrased as "delete, or move aside". Keep every
|
|
532
533
|
* such identifier out of this file, even inside a comment like this one.
|
|
533
534
|
*/
|
|
534
|
-
const CLAUDE_MCP_SHADOWING_HINT = `Run \`${CLAUDE_MCP_SHADOWING_REMEDIATION_COMMAND}\` to clear the '
|
|
535
|
+
const CLAUDE_MCP_SHADOWING_HINT = `Run \`${CLAUDE_MCP_SHADOWING_REMEDIATION_COMMAND}\` to clear the '${MCP_SERVER_NAME}' MCP ` +
|
|
535
536
|
"registration in ~/.claude.json (use the matching scope if it is registered at user scope, " +
|
|
536
537
|
"or under the projects entry for this repository/worktree). This is ADVISORY: workers load " +
|
|
537
538
|
"MCP servers with --strict-mcp-config from their own worktree registration, so this entry " +
|
|
@@ -598,15 +599,19 @@ export function claudeMcpShadowingDescriptor() {
|
|
|
598
599
|
if (inspection.findings.length > 0) {
|
|
599
600
|
// Advisory (BAPI-790): reported in full, with the remediation command,
|
|
600
601
|
// but `found: true` so it never fails the prerequisite or the exit code.
|
|
602
|
+
// BAPI-807: the remediation names the key this finding was ACTUALLY
|
|
603
|
+
// found under. A fixed canonical command suggested against a legacy
|
|
604
|
+
// `bridge-api` registration silently succeeds as a no-op, which reads to
|
|
605
|
+
// the operator as "the advisory is wrong".
|
|
601
606
|
const findings = inspection.findings.map((finding) => `advisory: ${formatClaudeMcpShadowFinding(finding)} — not loaded by a worker under ` +
|
|
602
|
-
`strict MCP loading; clear it with \`${
|
|
607
|
+
`strict MCP loading; clear it with \`${claudeMcpShadowingRemediationCommand(finding.registrationKey)}\``);
|
|
603
608
|
return { found: true, detail: [...findings, ...degraded].join("; ") };
|
|
604
609
|
}
|
|
605
610
|
return {
|
|
606
611
|
found: true,
|
|
607
612
|
detail: degraded.length > 0
|
|
608
613
|
? degraded.join("; ")
|
|
609
|
-
:
|
|
614
|
+
: `no foreign ${MCP_SERVER_NAME} registration in the Claude user config`,
|
|
610
615
|
};
|
|
611
616
|
},
|
|
612
617
|
};
|
package/build/start-tickets.js
CHANGED
|
@@ -59,10 +59,7 @@ import os from "node:os";
|
|
|
59
59
|
import path from "path";
|
|
60
60
|
import { VERSION } from "./version.generated.js";
|
|
61
61
|
import { resolveBapiCredentials, getPrimaryCredentialStorePath, } from "./credential-store.js";
|
|
62
|
-
|
|
63
|
-
// reused from the conduct-epic client rather than re-wrapped, so both conductor
|
|
64
|
-
// paths read the one repository-wide override through the same client.
|
|
65
|
-
import { getIndexBranch } from "./conduct-epic/bridge-client.js";
|
|
62
|
+
import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
|
|
66
63
|
import { resolveStartTicketsRepoName as resolveSharedStartTicketsRepoName, resolveRequiredStartTicketsRepoName, } from "./start-tickets-repo.js";
|
|
67
64
|
import { provisionMcpRegistrationsForCreatedWorktrees, } from "./mcp-provisioning.js";
|
|
68
65
|
import { provisionCommandsForCreatedWorktrees, } from "./command-provisioning.js";
|
|
@@ -78,6 +75,7 @@ import { DEFAULT_AGENT_NAME, resolveAgentSpec, isAgentName, formatValidAgentName
|
|
|
78
75
|
import { createStartTicketsConductorContext, provisionConductorHooksForRows, emitStartTicketsRunStarted, injectConductorEnvIntoShellCommand, buildSupervisorTabCommand, isSupervisorLaunchEnabled, supervisorSpawnKey, } from "./start-tickets-conductor.js";
|
|
79
76
|
import { transitionEpicDispatch, resolveConductorBridgeApiAccess, } from "./conductor/bridge-api-client.js";
|
|
80
77
|
import { PR_BASE_BRANCH_ENV_VAR, buildPrBaseContractLaunchInstruction, } from "./pr-base-contract.js";
|
|
78
|
+
import { INDEX_SCOPE_CONFIGURATION_ERROR, INDEX_SCOPE_ENV_VAR, validateOptionalIndexScope, } from "./index-scope-contract.js";
|
|
81
79
|
// Re-export the shared prereq surface (constants, platform helpers, command
|
|
82
80
|
// probes) so existing import sites that read them from "./start-tickets.js"
|
|
83
81
|
// keep working unchanged.
|
|
@@ -121,7 +119,7 @@ export function appendSummaryRowWarning(row, warning) {
|
|
|
121
119
|
export function getStartTicketsUsage() {
|
|
122
120
|
return [
|
|
123
121
|
"Usage:",
|
|
124
|
-
|
|
122
|
+
` npx -y ${MCP_PACKAGE_NAME} start-tickets [flags] KEY [KEY ...]`,
|
|
125
123
|
"",
|
|
126
124
|
"Flags:",
|
|
127
125
|
" --agent claude|cursor-agent Agent command to launch in each worktree (default: claude)",
|
|
@@ -177,7 +175,6 @@ export function parseStartTicketsArgs(argv) {
|
|
|
177
175
|
let maxParallelRaw;
|
|
178
176
|
let agentName = DEFAULT_AGENT_NAME;
|
|
179
177
|
let baseBranch = "main";
|
|
180
|
-
let baseBranchExplicit = false;
|
|
181
178
|
let conductorEnabled = false;
|
|
182
179
|
let workflow = "implement";
|
|
183
180
|
let reviewRoundsRaw;
|
|
@@ -347,7 +344,6 @@ export function parseStartTicketsArgs(argv) {
|
|
|
347
344
|
return { status: "error", message: `Invalid --base-branch value: ${error}` };
|
|
348
345
|
}
|
|
349
346
|
baseBranch = trimmed;
|
|
350
|
-
baseBranchExplicit = true;
|
|
351
347
|
continue;
|
|
352
348
|
}
|
|
353
349
|
if (arg === "--dry-run") {
|
|
@@ -455,7 +451,6 @@ export function parseStartTicketsArgs(argv) {
|
|
|
455
451
|
branchOverrides,
|
|
456
452
|
agentName,
|
|
457
453
|
baseBranch,
|
|
458
|
-
...(baseBranchExplicit ? { baseBranchExplicit: true } : {}),
|
|
459
454
|
conductorEnabled,
|
|
460
455
|
workflow,
|
|
461
456
|
reviewRounds,
|
|
@@ -514,17 +509,19 @@ export function resolveStartTicketsPlatformConfig(deps, agent, autoApprove = fal
|
|
|
514
509
|
resumeMode = false, workflow = "implement", reviewRounds,
|
|
515
510
|
// BAPI-586: the effective run base branch (incl. epic.base_branch override).
|
|
516
511
|
// Injected as BAPI_BASE_BRANCH so the spawned worker's PR targets it — for
|
|
517
|
-
// conductor runs always, and (BAPI-801) for plain
|
|
518
|
-
//
|
|
512
|
+
// conductor runs always, and (BAPI-801/BAPI-847) for every plain run whose base
|
|
513
|
+
// resolved, `main` included — and threaded (BAPI-593) into the spawned workflow
|
|
514
|
+
// command.
|
|
519
515
|
baseBranch) {
|
|
520
516
|
if (!isSupportedStartTicketsPlatform(deps.platform)) {
|
|
521
517
|
return { ok: false, error: unsupportedPlatformMessage(deps.platform) };
|
|
522
518
|
}
|
|
523
519
|
const platform = deps.platform;
|
|
524
|
-
// BAPI-801: both conductor and plain dispatch carry the PR-base
|
|
525
|
-
// contract
|
|
526
|
-
//
|
|
527
|
-
//
|
|
520
|
+
// BAPI-801/BAPI-847: both conductor and plain dispatch carry the PR-base
|
|
521
|
+
// ENVIRONMENT contract for EVERY resolved base, `main` included (see
|
|
522
|
+
// resolvePrBaseBranchEnvValue). Only the conductor launch INSTRUCTION remains
|
|
523
|
+
// conductor-only — a plain worker's prompt stays bare and learns the base from
|
|
524
|
+
// the env plus the create-PR instruction files.
|
|
528
525
|
const prBaseBranch = resolvePrBaseBranchEnvValue(conductorEnabled, baseBranch);
|
|
529
526
|
return {
|
|
530
527
|
ok: true,
|
|
@@ -566,26 +563,33 @@ export function prependRepoNameEnvAssignment(command, repoName, platform = "darw
|
|
|
566
563
|
return `export BAPI_REPO_NAME='${shSquoteInner(repoName)}' && ${command}`;
|
|
567
564
|
}
|
|
568
565
|
/**
|
|
569
|
-
* BAPI-801: decide the `BAPI_BASE_BRANCH` value a spawned worker's shell
|
|
570
|
-
* carry — the single rule shared by the real spawn builder
|
|
566
|
+
* BAPI-801/BAPI-847: decide the `BAPI_BASE_BRANCH` value a spawned worker's shell
|
|
567
|
+
* should carry — the single rule shared by the real spawn builder
|
|
571
568
|
* ({@link resolveStartTicketsPlatformConfig}) and the dry-run preview builder
|
|
572
569
|
* ({@link getDryRunPlatformDetails}), so the preview can never drift from what
|
|
573
570
|
* is actually spawned.
|
|
574
571
|
*
|
|
575
572
|
* - Conductor runs export whatever base was resolved, `main` included — BAPI-586
|
|
576
573
|
* behavior, unchanged.
|
|
577
|
-
* - Plain runs export
|
|
578
|
-
* This mirrors the `--base-branch` threading rule in {@link buildAgentPrompt},
|
|
579
|
-
* keeps today's plain spawn strings byte-stable on the default base, and costs
|
|
580
|
-
* nothing: the create-PR fallback tier is `main` anyway.
|
|
574
|
+
* - Plain runs export EVERY resolved base, `main` included (BAPI-847).
|
|
581
575
|
*
|
|
582
|
-
*
|
|
576
|
+
* The plain rule used to omit `main` on the reasoning that "the create-PR
|
|
577
|
+
* fallback tier is `main` anyway". That reasoning was false, and it is the defect
|
|
578
|
+
* BAPI-847 removes: create-PR falls back to the `base_branch` CONFIG FIELD before
|
|
579
|
+
* it falls back to `main`, so a run that exported nothing resolved through
|
|
580
|
+
* whatever that repository-wide field happened to hold — `epic/<EPIC>` while an
|
|
581
|
+
* epic held the repository's index. An unrelated ticket's PR was opened against
|
|
582
|
+
* an epic branch exactly that way. Exporting the real base always makes a
|
|
583
|
+
* worker's PR target independent of repository-wide configuration.
|
|
584
|
+
*
|
|
585
|
+
* Returns `null` only when no base was resolved at all (unset, or blank after
|
|
586
|
+
* normalization) — a blank value is never converted into an exported branch name.
|
|
583
587
|
* {@link prependBaseBranchEnvAssignment} is fail-open on that value.
|
|
584
588
|
*/
|
|
585
589
|
function resolvePrBaseBranchEnvValue(conductorEnabled, baseBranch) {
|
|
586
590
|
if (conductorEnabled)
|
|
587
591
|
return baseBranch ?? null;
|
|
588
|
-
if (!baseBranch || baseBranch
|
|
592
|
+
if (!baseBranch || !baseBranch.trim())
|
|
589
593
|
return null;
|
|
590
594
|
return baseBranch;
|
|
591
595
|
}
|
|
@@ -1698,6 +1702,41 @@ export async function materializeWorkerLaunchCommand(deps, key, fullCommand) {
|
|
|
1698
1702
|
// ---------------------------------------------------------------------------
|
|
1699
1703
|
// Tab spawning across created worktrees
|
|
1700
1704
|
// ---------------------------------------------------------------------------
|
|
1705
|
+
/**
|
|
1706
|
+
* Build the per-row environment injected into ONE worker's launch command
|
|
1707
|
+
* (BAPI-394 conductor identity + BAPI-844 index scope).
|
|
1708
|
+
*
|
|
1709
|
+
* Returns `undefined` when the row carries neither, so an ordinary worker's
|
|
1710
|
+
* command passes through {@link injectConductorEnvIntoShellCommand} byte-for-byte
|
|
1711
|
+
* unchanged — the pre-feature bare command, with no empty assignment and no
|
|
1712
|
+
* altered ordering.
|
|
1713
|
+
*
|
|
1714
|
+
* The scope is applied LAST so it is emitted from the row's own value and can
|
|
1715
|
+
* never be supplied by a shared conductor env object. Reuses the existing
|
|
1716
|
+
* injection path rather than adding a second shell-prefix mechanism: one escaper,
|
|
1717
|
+
* one platform rule, one place a quoting bug could live.
|
|
1718
|
+
*/
|
|
1719
|
+
export function buildRowSpawnEnv(row) {
|
|
1720
|
+
if (!row.conductorEnv && !row.indexScope)
|
|
1721
|
+
return undefined;
|
|
1722
|
+
const env = { ...row.conductorEnv };
|
|
1723
|
+
if (row.indexScope)
|
|
1724
|
+
env[INDEX_SCOPE_ENV_VAR] = row.indexScope;
|
|
1725
|
+
return env;
|
|
1726
|
+
}
|
|
1727
|
+
/**
|
|
1728
|
+
* Stamp the run's validated index scope onto every SPAWNABLE row (BAPI-844).
|
|
1729
|
+
*
|
|
1730
|
+
* A no-op that returns the input array unchanged when the run declared no scope,
|
|
1731
|
+
* so an ordinary run's rows are the exact objects they were. Non-spawnable rows
|
|
1732
|
+
* (`create-failed`, `dry-run`, `skipped`) are never stamped: nothing launches for
|
|
1733
|
+
* them, and stamping would put a routing token into a summary row.
|
|
1734
|
+
*/
|
|
1735
|
+
export function stampIndexScopeOnRows(rows, indexScope) {
|
|
1736
|
+
if (!indexScope)
|
|
1737
|
+
return rows;
|
|
1738
|
+
return rows.map((row) => row.status === "created" && row.path ? { ...row, indexScope } : row);
|
|
1739
|
+
}
|
|
1701
1740
|
/**
|
|
1702
1741
|
* Open one tab/session per successfully-created worktree, building each shell
|
|
1703
1742
|
* command with the platform-correct `buildShellCommand` and passing per-ticket
|
|
@@ -1716,7 +1755,7 @@ export async function spawnTabsForCreatedWorktrees(deps, rows, terminal, buildSh
|
|
|
1716
1755
|
// Scope the row's conductor identity env to this terminal/tab/session only
|
|
1717
1756
|
// (BAPI-394). No-op when the row carries no conductorEnv (dry-run, non-Claude
|
|
1718
1757
|
// agents, or conductor disabled). Never mutates process/global env.
|
|
1719
|
-
const shellCommand = injectConductorEnvIntoShellCommand(deps.platform, baseShellCommand, row
|
|
1758
|
+
const shellCommand = injectConductorEnvIntoShellCommand(deps.platform, baseShellCommand, buildRowSpawnEnv(row));
|
|
1720
1759
|
// Deliver the (potentially multi-KB) command via a launch-script file so the
|
|
1721
1760
|
// terminal spawn payload stays tiny and escaping-immune. No-op when no writer
|
|
1722
1761
|
// seam is configured; a failed write falls back inline only while the command
|
|
@@ -1764,10 +1803,10 @@ export function getDryRunPlatformDetails(agent, platform = process.platform, env
|
|
|
1764
1803
|
// The builder accepts an optional resolved modelAlias; the dry-run caller
|
|
1765
1804
|
// now passes the previewed tier's alias so `--model` shows in the preview.
|
|
1766
1805
|
// The resolved repo name (when known) is injected as a BAPI_REPO_NAME prefix,
|
|
1767
|
-
// and (BAPI-801) the PR base as a BAPI_BASE_BRANCH prefix through the
|
|
1768
|
-
// resolvePrBaseBranchEnvValue rule
|
|
1769
|
-
// spawn builder uses
|
|
1770
|
-
// spawning. Reuses the same buildAgentShellCommand/buildAgentPrompt path as a
|
|
1806
|
+
// and (BAPI-801/BAPI-847) the PR base as a BAPI_BASE_BRANCH prefix through the
|
|
1807
|
+
// SAME resolvePrBaseBranchEnvValue rule — including the ordinary `main` case —
|
|
1808
|
+
// and the same outermost ordering the real spawn builder uses; dry-run shares
|
|
1809
|
+
// both environment renderings with real spawning. Reuses the same buildAgentShellCommand/buildAgentPrompt path as a
|
|
1771
1810
|
// real spawn — dry-run is never special-cased — so the preview is exact.
|
|
1772
1811
|
buildAgentShellCommand: (key, worktreePath, modelAlias) => prependBaseBranchEnvAssignment(prependRepoNameEnvAssignment(buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove, modelAlias, conductorEnabled, false, workflow, reviewRounds, baseBranch), repoName, platform), resolvePrBaseBranchEnvValue(conductorEnabled, baseBranch), platform),
|
|
1773
1812
|
};
|
|
@@ -1788,7 +1827,7 @@ export function buildDryRunMcpProvisioningLines(worktreePath, platform = process
|
|
|
1788
1827
|
const invocation = mcpServerInvocation ?? {
|
|
1789
1828
|
form: "npm-channel",
|
|
1790
1829
|
command: "npx",
|
|
1791
|
-
packageSpec:
|
|
1830
|
+
packageSpec: `${MCP_PACKAGE_NAME}@latest`,
|
|
1792
1831
|
};
|
|
1793
1832
|
const built = buildMcpShimCommand(invocation, "<target>", worktreePath);
|
|
1794
1833
|
const shim = `${built.command} ${built.args.join(" ")}`;
|
|
@@ -1811,11 +1850,12 @@ export function buildDryRunDetailLines(agent, key, branch, platform = process.pl
|
|
|
1811
1850
|
const { worktrunkBinary, buildAgentShellCommand: build } = getDryRunPlatformDetails(agent, platform, env, autoApprove, conductorEnabled, repoName, workflow, reviewRounds, baseBranch);
|
|
1812
1851
|
const wtArgs = buildWtSwitchArgs(branch, false, baseBranch);
|
|
1813
1852
|
const agentInvocation = build(key, "<worktree-path>", modelAlias);
|
|
1814
|
-
// BAPI-801: one concise secondary detail naming the PR base, emitted
|
|
1815
|
-
// the shared rule
|
|
1816
|
-
//
|
|
1817
|
-
//
|
|
1818
|
-
//
|
|
1853
|
+
// BAPI-801/BAPI-847: one concise secondary detail naming the PR base, emitted
|
|
1854
|
+
// whenever the shared rule exports BAPI_BASE_BRANCH — which is now every run
|
|
1855
|
+
// with a resolved base, the ordinary `main` case included, because that is what
|
|
1856
|
+
// the real spawn exports. It sits AFTER the rendered spawn command so the
|
|
1857
|
+
// copyable command stays the primary scan target, and it is ordinary routing
|
|
1858
|
+
// detail — never warning-like.
|
|
1819
1859
|
const prBaseBranch = resolvePrBaseBranchEnvValue(conductorEnabled, baseBranch);
|
|
1820
1860
|
return [
|
|
1821
1861
|
`DRY-RUN: ${key} -> branch=${branch}`,
|
|
@@ -2800,6 +2840,19 @@ async function defaultClaimEpicDispatch(dispatchKey, runId, deps) {
|
|
|
2800
2840
|
});
|
|
2801
2841
|
}
|
|
2802
2842
|
export async function orchestrateStartTickets(deps, options, overrides = {}) {
|
|
2843
|
+
// BAPI-844: validate the declared index scope FIRST, before any side effect at
|
|
2844
|
+
// all — including the temp-dir prune below. A malformed declaration is a launch
|
|
2845
|
+
// configuration failure, not something to spawn canonical workers through: the
|
|
2846
|
+
// whole point of declaring a scope is that these workers must not research the
|
|
2847
|
+
// canonical index. The message is the shared fixed text, so the submitted value
|
|
2848
|
+
// never reaches the summary, a warning, or the operator's terminal.
|
|
2849
|
+
let indexScope;
|
|
2850
|
+
try {
|
|
2851
|
+
indexScope = validateOptionalIndexScope(options.epic?.index_scope_id);
|
|
2852
|
+
}
|
|
2853
|
+
catch {
|
|
2854
|
+
return { ok: false, error: INDEX_SCOPE_CONFIGURATION_ERROR };
|
|
2855
|
+
}
|
|
2803
2856
|
// IH-2 (PR #552 review): best-effort prune of stale launch-script temp dirs
|
|
2804
2857
|
// before doing anything else. Awaited (to avoid concurrent cleanup races) but
|
|
2805
2858
|
// fully fail-open — it never throws, so a dry-run also benefits from cleanup
|
|
@@ -2988,57 +3041,13 @@ export async function orchestrateStartTickets(deps, options, overrides = {}) {
|
|
|
2988
3041
|
if (overrides.modelRoutingWarningLog) {
|
|
2989
3042
|
emitInvocationModelRoutingDiagnosticForRows(routed, overrides.modelRoutingWarningLog);
|
|
2990
3043
|
}
|
|
2991
|
-
|
|
3044
|
+
// BAPI-844: stamp the validated scope on the spawnable rows immediately before
|
|
3045
|
+
// the spawn, so the value the tab is launched with is the one this run
|
|
3046
|
+
// declared. A run with no scope gets the identical `routed` array back.
|
|
3047
|
+
const scoped = stampIndexScopeOnRows(routed, indexScope);
|
|
3048
|
+
const rows = await spawnTabsFn(deps, scoped, terminal, platformConfig.config.buildAgentShellCommand);
|
|
2992
3049
|
return { ok: true, rows };
|
|
2993
3050
|
}
|
|
2994
|
-
/**
|
|
2995
|
-
* Warn when an implicit base branch could be resolved through an active
|
|
2996
|
-
* repository-wide index-branch override (BAPI-814/E1b).
|
|
2997
|
-
*
|
|
2998
|
-
* Informational only, and deliberately narrow:
|
|
2999
|
-
*
|
|
3000
|
-
* - **Only for an IMPLICIT base.** An explicit `--base-branch` is the caller's
|
|
3001
|
-
* own decision and is unaffected by the override, so the probe is skipped
|
|
3002
|
-
* entirely — no request is made and no warning is printed.
|
|
3003
|
-
* - **Only with resolvable credentials.** `start-tickets` creates worktrees and
|
|
3004
|
-
* opens tabs; it does not require Bridge access to do its job. "No credential"
|
|
3005
|
-
* is therefore an ordinary state for this command, not a diagnostic, and
|
|
3006
|
-
* staying silent keeps an unrelated command from growing a credential
|
|
3007
|
-
* complaint. A lookup that FAILS after credentials resolved is different, and
|
|
3008
|
-
* does produce one bounded, sanitized line.
|
|
3009
|
-
* - **Never a refusal.** Nothing here changes the exit code, the base branch, or
|
|
3010
|
-
* any spawn behavior.
|
|
3011
|
-
*
|
|
3012
|
-
* Warnings go to `errorLog` (stderr), so machine-readable stdout stays clean.
|
|
3013
|
-
*/
|
|
3014
|
-
async function warnOnIndexOverrideForImplicitBase(deps, options, errorLog, fetchImpl) {
|
|
3015
|
-
if (options.baseBranchExplicit)
|
|
3016
|
-
return;
|
|
3017
|
-
const access = await resolveConductorBridgeApiAccess({
|
|
3018
|
-
env: deps.env,
|
|
3019
|
-
cwd: deps.cwd,
|
|
3020
|
-
homedir: os.homedir,
|
|
3021
|
-
platform: deps.platform,
|
|
3022
|
-
readFile: deps.readFile ?? ((filePath) => readFile(filePath, "utf-8")),
|
|
3023
|
-
stat: (filePath) => stat(filePath).then((info) => ({ mode: info.mode })),
|
|
3024
|
-
});
|
|
3025
|
-
if (!access.ok)
|
|
3026
|
-
return;
|
|
3027
|
-
const result = await getIndexBranch(access.access, fetchImpl);
|
|
3028
|
-
if (!result.ok) {
|
|
3029
|
-
errorLog(`Warning: the repository's indexed-branch override could not be read: ${result.error}. ` +
|
|
3030
|
-
"Continuing — ticket startup is unaffected.");
|
|
3031
|
-
return;
|
|
3032
|
-
}
|
|
3033
|
-
const override = result.value.override;
|
|
3034
|
-
if (override === null)
|
|
3035
|
-
return;
|
|
3036
|
-
errorLog(`Warning: ${access.access.repoName} has an ACTIVE index-branch override: ` +
|
|
3037
|
-
`'${override.override_branch}'. No --base-branch was given, so the implicit base — ` +
|
|
3038
|
-
"the base-branch config field and feature-branch provisioning — may resolve through " +
|
|
3039
|
-
"that override rather than the repository's usual base. Ticket startup is ALLOWED and " +
|
|
3040
|
-
"continues; pass --base-branch to choose explicitly.");
|
|
3041
|
-
}
|
|
3042
3051
|
/** Platform-specific guidance printed when one or more tabs fail to spawn. */
|
|
3043
3052
|
function spawnFailureHintForPlatform(platform) {
|
|
3044
3053
|
if (platform === "darwin") {
|
|
@@ -3094,9 +3103,6 @@ export async function runStartTicketsCli(argv, overrides = {}) {
|
|
|
3094
3103
|
errorLog("Warning: --tier value was not one of cheap|basic|premium; ignoring the override and " +
|
|
3095
3104
|
"using premium model routing for every ticket.");
|
|
3096
3105
|
}
|
|
3097
|
-
// BAPI-814/E1b: informational only, ahead of the dry-run branch so both a
|
|
3098
|
-
// preview and a real run report the same repository-wide condition.
|
|
3099
|
-
await warnOnIndexOverrideForImplicitBase(deps, options, errorLog, overrides.fetchImpl ?? globalThis.fetch);
|
|
3100
3106
|
if (options.dryRun) {
|
|
3101
3107
|
// Resolve the repo identity for the preview so the dry-run command matches
|
|
3102
3108
|
// what the real spawn injects (see prependRepoNameEnvAssignment). The real
|
package/build/update-check.js
CHANGED
|
@@ -6,11 +6,12 @@ import { writeFile, mkdir, readFile } from "fs/promises";
|
|
|
6
6
|
import path from "path";
|
|
7
7
|
import os from "os";
|
|
8
8
|
import { VERSION } from "./version.generated.js";
|
|
9
|
+
import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
|
|
9
10
|
const CACHE_TTL = 86400000; // 24 hours
|
|
10
11
|
const FETCH_TIMEOUT = 3000;
|
|
11
|
-
const REGISTRY_URL =
|
|
12
|
+
const REGISTRY_URL = `https://registry.npmjs.org/${MCP_PACKAGE_NAME}/latest`;
|
|
12
13
|
export function getCachePath() {
|
|
13
|
-
return path.join(os.homedir(), ".config", "
|
|
14
|
+
return path.join(os.homedir(), ".config", ...MCP_PACKAGE_NAME.split("/"), "update-check.json");
|
|
14
15
|
}
|
|
15
16
|
/**
|
|
16
17
|
* Compare two semver strings. Returns true if `latest` is strictly greater.
|
package/build/upgrade-advice.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* caller can interpolate arbitrary (possibly secret-bearing) text into advice
|
|
13
13
|
* that is surfaced to users and models.
|
|
14
14
|
*/
|
|
15
|
+
import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
|
|
15
16
|
/**
|
|
16
17
|
* The canonical, always-fresh upgrade invocation.
|
|
17
18
|
*
|
|
@@ -20,7 +21,7 @@
|
|
|
20
21
|
* pin that upgrader then writes into each host config: the upgrader must be the
|
|
21
22
|
* newest published code, while a project launcher must be reproducible.
|
|
22
23
|
*/
|
|
23
|
-
export const CANONICAL_UPGRADE_COMMAND =
|
|
24
|
+
export const CANONICAL_UPGRADE_COMMAND = `npx -y ${MCP_PACKAGE_NAME}@latest --upgrade`;
|
|
24
25
|
/** Accept only plain release strings into user-facing advice. */
|
|
25
26
|
const SAFE_VERSION_RE = /^[0-9A-Za-z.\-+]{1,64}$/;
|
|
26
27
|
/** Replace anything that is not a plausible version string with a fixed marker. */
|
package/build/upgrade-cli.js
CHANGED
|
@@ -43,6 +43,7 @@ import { CANONICAL_UPGRADE_COMMAND } from "./upgrade-advice.js";
|
|
|
43
43
|
import { describeLauncherReason, inspectLauncherConfigs, isApplicableStandardLauncher, isPrerequisiteFailure, LAUNCHER_CONFIG_TARGETS, } from "./launcher-config-inspection.js";
|
|
44
44
|
import { buildGenericAgentShellCommand, detectTerminal, getDefaultSpawnTerminalTabForPlatform, createDefaultStartTicketsDeps, } from "./start-tickets.js";
|
|
45
45
|
import { AGENT_REGISTRY } from "./agent-registry.js";
|
|
46
|
+
import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
|
|
46
47
|
/** The sentinel this CLI passes to the child it re-execs from `@latest`. */
|
|
47
48
|
const INTERNAL_REEXEC_FLAG = "--internal-reexec";
|
|
48
49
|
// ---------------------------------------------------------------------------
|
|
@@ -53,7 +54,7 @@ function defaultReexec(cwd, oldVersion) {
|
|
|
53
54
|
return new Promise((resolve) => {
|
|
54
55
|
const child = spawn(npxCmd, [
|
|
55
56
|
"-y",
|
|
56
|
-
|
|
57
|
+
`${MCP_PACKAGE_NAME}@latest`,
|
|
57
58
|
"upgrade",
|
|
58
59
|
INTERNAL_REEXEC_FLAG,
|
|
59
60
|
"--old-version",
|
|
@@ -67,7 +68,7 @@ function defaultReexec(cwd, oldVersion) {
|
|
|
67
68
|
});
|
|
68
69
|
}
|
|
69
70
|
function localInstallPath(cwd) {
|
|
70
|
-
return path.join(cwd, "node_modules", "
|
|
71
|
+
return path.join(cwd, "node_modules", ...MCP_PACKAGE_NAME.split("/"));
|
|
71
72
|
}
|
|
72
73
|
async function defaultHasCompetingLocalInstall(cwd) {
|
|
73
74
|
try {
|
|
@@ -81,7 +82,7 @@ async function defaultHasCompetingLocalInstall(cwd) {
|
|
|
81
82
|
function defaultRemoveCompetingLocalInstall(cwd) {
|
|
82
83
|
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
83
84
|
return new Promise((resolve) => {
|
|
84
|
-
const child = spawn(npmCmd, ["uninstall",
|
|
85
|
+
const child = spawn(npmCmd, ["uninstall", MCP_PACKAGE_NAME], {
|
|
85
86
|
stdio: "inherit",
|
|
86
87
|
cwd,
|
|
87
88
|
});
|
|
@@ -153,20 +154,33 @@ export async function resolveUpgradeTarget(argv, deps) {
|
|
|
153
154
|
// ---------------------------------------------------------------------------
|
|
154
155
|
// Reporting helpers (pure)
|
|
155
156
|
// ---------------------------------------------------------------------------
|
|
156
|
-
/**
|
|
157
|
+
/**
|
|
158
|
+
* Render the dry-run preview line for one inspected target.
|
|
159
|
+
*
|
|
160
|
+
* BAPI-807: an update against a legacy registration is described AS an update to
|
|
161
|
+
* that key — never as a rename. `upgrade` moves the package pin; it has no
|
|
162
|
+
* opinion about the registration name, and a preview implying otherwise would
|
|
163
|
+
* advertise a migration this command deliberately does not perform.
|
|
164
|
+
*/
|
|
157
165
|
export function formatDryRunLine(found) {
|
|
158
166
|
const { relPath, targetVersion } = found;
|
|
167
|
+
// Suffix, not a rewrite of the line: the existing pin transitions are pinned
|
|
168
|
+
// by tests and the key is additional context, not a replacement for them.
|
|
169
|
+
const keyNote = found.legacyRegistration ? ` [\`${found.registrationKey}\` entry]` : "";
|
|
159
170
|
switch (found.action) {
|
|
160
171
|
case "repin":
|
|
161
|
-
return ` ${relPath}: ${found.currentVersion} -> ${targetVersion}`;
|
|
172
|
+
return ` ${relPath}: ${found.currentVersion} -> ${targetVersion}${keyNote}`;
|
|
162
173
|
case "already-current":
|
|
163
|
-
return ` ${relPath}: already ${targetVersion}`;
|
|
174
|
+
return ` ${relPath}: already ${targetVersion}${keyNote}`;
|
|
164
175
|
case "normalize-unpinned":
|
|
165
|
-
return ` ${relPath}: would normalize ${found.spec} ->
|
|
176
|
+
return ` ${relPath}: would normalize ${found.spec} -> ${MCP_PACKAGE_NAME}@${targetVersion}${keyNote}`;
|
|
166
177
|
case "add":
|
|
167
|
-
return ` ${relPath}: would add
|
|
178
|
+
return ` ${relPath}: would add ${MCP_PACKAGE_NAME}@${targetVersion}`;
|
|
179
|
+
case "duplicate-registration":
|
|
180
|
+
// Reachable only in the dry-run preview: a real run refuses in preflight.
|
|
181
|
+
return ` ${relPath}: blocked — ${describeLauncherReason("duplicate-registration")}`;
|
|
168
182
|
case "create":
|
|
169
|
-
return ` ${relPath}: would create with
|
|
183
|
+
return ` ${relPath}: would create with ${MCP_PACKAGE_NAME}@${targetVersion}`;
|
|
170
184
|
case "skip-worktree-shim":
|
|
171
185
|
return ` ${relPath}: skipped — worktree mcp-invoke shim (no published-package pin applies)`;
|
|
172
186
|
case "skip-inactive":
|
|
@@ -184,11 +198,11 @@ export function formatResultLine(action) {
|
|
|
184
198
|
case "already-current":
|
|
185
199
|
return ` ${relPath}: already ${action.toVersion}`;
|
|
186
200
|
case "normalized":
|
|
187
|
-
return ` ${relPath}: normalized ${action.fromSpec} ->
|
|
201
|
+
return ` ${relPath}: normalized ${action.fromSpec} -> ${MCP_PACKAGE_NAME}@${action.toVersion}`;
|
|
188
202
|
case "added":
|
|
189
|
-
return ` ${relPath}: added
|
|
203
|
+
return ` ${relPath}: added ${MCP_PACKAGE_NAME}@${action.toVersion}`;
|
|
190
204
|
case "created":
|
|
191
|
-
return ` ${relPath}: created with
|
|
205
|
+
return ` ${relPath}: created with ${MCP_PACKAGE_NAME}@${action.toVersion}`;
|
|
192
206
|
case "skipped-worktree-shim":
|
|
193
207
|
return ` ${relPath}: skipped — worktree mcp-invoke shim preserved`;
|
|
194
208
|
case "skipped-ahead":
|
|
@@ -278,8 +292,8 @@ export async function runUpgradeCli(argv, injected) {
|
|
|
278
292
|
!isInternalReexec) {
|
|
279
293
|
stdout(`- Would re-exec from @latest to upgrade the CLI ${VERSION} -> ${target.version}`);
|
|
280
294
|
}
|
|
281
|
-
stdout(`- Would detect and remove competing local install in node_modules
|
|
282
|
-
stdout(`- Launcher pins (target
|
|
295
|
+
stdout(`- Would detect and remove competing local install in node_modules/${MCP_PACKAGE_NAME} if present.`);
|
|
296
|
+
stdout(`- Launcher pins (target ${MCP_PACKAGE_NAME}@${target.version}):`);
|
|
283
297
|
const lines = inspections
|
|
284
298
|
.map(formatDryRunLine)
|
|
285
299
|
.filter((l) => l !== null);
|
|
@@ -304,8 +318,8 @@ export async function runUpgradeCli(argv, injected) {
|
|
|
304
318
|
if (!removed) {
|
|
305
319
|
// Leaving it in place means npx keeps preferring the stale local copy, so
|
|
306
320
|
// the repin below would be cosmetic. That is not a completed upgrade.
|
|
307
|
-
stderr(
|
|
308
|
-
stderr(
|
|
321
|
+
stderr(`Bridge API upgrade failed: could not remove node_modules/${MCP_PACKAGE_NAME}.`);
|
|
322
|
+
stderr(` Remove it manually (rm -rf node_modules/${MCP_PACKAGE_NAME}), then re-run the upgrade.`);
|
|
309
323
|
return 1;
|
|
310
324
|
}
|
|
311
325
|
stdout(" Removed competing local install.");
|
|
@@ -313,7 +327,7 @@ export async function runUpgradeCli(argv, injected) {
|
|
|
313
327
|
// ---- Phase 5: reconcile ----
|
|
314
328
|
let initResult;
|
|
315
329
|
try {
|
|
316
|
-
stdout(`\nReconciling launcher pins to
|
|
330
|
+
stdout(`\nReconciling launcher pins to ${MCP_PACKAGE_NAME}@${target.version}...\n`);
|
|
317
331
|
initResult = await deps.runInit(deps.cwd, {
|
|
318
332
|
targetVersion: target.version,
|
|
319
333
|
// Upgrade renders its own truthful per-config report below; the generic
|
|
@@ -367,10 +381,28 @@ export async function runUpgradeCli(argv, injected) {
|
|
|
367
381
|
verificationFailures.push(`${action.relPath}: config disappeared after the write`);
|
|
368
382
|
continue;
|
|
369
383
|
}
|
|
384
|
+
// BAPI-807: a reconciliation that ADDED a canonical entry beside an existing
|
|
385
|
+
// legacy one leaves both keys present, which the re-inspection reports as
|
|
386
|
+
// `duplicate-registration`. Check it before the pin comparison — the pin
|
|
387
|
+
// question is meaningless once there are two entries, and this is the exact
|
|
388
|
+
// regression the resolve-then-write rule exists to prevent.
|
|
389
|
+
if (after.action === "duplicate-registration") {
|
|
390
|
+
verificationFailures.push(`${action.relPath}: reconciliation left TWO Bridge registrations in this config ` +
|
|
391
|
+
"(a canonical entry was written beside a legacy one)");
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
370
394
|
if (after.action !== "already-current" || after.currentVersion !== target.version) {
|
|
371
|
-
verificationFailures.push(`${action.relPath}: expected
|
|
395
|
+
verificationFailures.push(`${action.relPath}: expected ${MCP_PACKAGE_NAME}@${target.version} after the write, ` +
|
|
372
396
|
`found ${after.spec ?? "no recognizable package token"}`);
|
|
373
397
|
}
|
|
398
|
+
// The pin landing is not sufficient: a legacy registration must still be
|
|
399
|
+
// under the SAME key it started under. `runInit` reports the key it actually
|
|
400
|
+
// wrote, so this compares two independent observations rather than trusting
|
|
401
|
+
// either one alone.
|
|
402
|
+
if (action.registrationKey && after.registrationKey !== action.registrationKey) {
|
|
403
|
+
verificationFailures.push(`${action.relPath}: expected the Bridge entry to remain under \`${action.registrationKey}\` ` +
|
|
404
|
+
`after the write, found ${after.registrationKey ? `\`${after.registrationKey}\`` : "no recognized registration"}`);
|
|
405
|
+
}
|
|
374
406
|
}
|
|
375
407
|
// ---- Phase 7: report what actually happened ----
|
|
376
408
|
const resultLines = initResult.configActions
|
|
@@ -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.42";
|
package/docs/CONDUCTOR.md
CHANGED
|
@@ -315,6 +315,28 @@ redeems the approval token and the server returns `merge.succeeded`.
|
|
|
315
315
|
store uses schema version 5 (BAPI-413) to accommodate the `merge.pending_approval`
|
|
316
316
|
type in the `events.type` CHECK constraint.
|
|
317
317
|
|
|
318
|
+
**Who performs the write (BAPI-832).** "Never holds VCS write credentials" above
|
|
319
|
+
describes the *credential*, not the *typing*, and it remains true everywhere. Two
|
|
320
|
+
paths now perform the provider write with the operator's local `gh` rather than
|
|
321
|
+
with the GitHub App installation token, because GitHub's merge endpoint requires
|
|
322
|
+
`contents: write` and the Bridge GPT App installation is `contents: read`:
|
|
323
|
+
|
|
324
|
+
- the executor's `merge` job, via `makeLocalMergeExecutor` — the arrangement
|
|
325
|
+
BAPI-572 locked in; and
|
|
326
|
+
- the `merge_pull_request` MCP tool, which since BAPI-832 sends
|
|
327
|
+
`execution: "local"`, receives a non-terminal `approved_for_local_execution`
|
|
328
|
+
from the route above, merges through that same `conductor/local-merge.ts` seam,
|
|
329
|
+
and reports the outcome to
|
|
330
|
+
`POST /vcs/pull-requests/{pr_number}/merge/complete`.
|
|
331
|
+
|
|
332
|
+
In both, the server still owns every decision — the guards, the lease, and the
|
|
333
|
+
terminal ledger row — and neither path holds a credential of its own; they borrow
|
|
334
|
+
the operator's `gh` session. Mode is `merge_execution` on `epic_supervisor_config`
|
|
335
|
+
(`local` by default, `server` for installations whose App holds
|
|
336
|
+
`contents: write`). A caller that dies mid-merge is recovered by lease expiry, not
|
|
337
|
+
by a new mechanism: the next call re-runs the whole tail, and the provider re-read
|
|
338
|
+
reports `already_merged` if the merge landed.
|
|
339
|
+
|
|
318
340
|
## Durable execution environment hardening (BAPI-527)
|
|
319
341
|
|
|
320
342
|
Worker ledger tools are made **reliable again** — this is a robustness fix for the
|