@bridge_gpt/mcp-server 0.2.37 → 0.2.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +193 -16
  2. package/build/agent-capabilities/probe-context.js +2 -1
  3. package/build/agent-launchers/claude-executor-adapter.js +392 -0
  4. package/build/agent-launchers/executor-adapter-inspection.js +163 -0
  5. package/build/agent-launchers/executor-adapter-registry.js +90 -0
  6. package/build/agent-launchers/executor-adapter.js +136 -0
  7. package/build/agent-registry.js +28 -0
  8. package/build/agents.generated.js +1 -1
  9. package/build/claude-login.js +85 -0
  10. package/build/claude-user-config-doctor.js +59 -33
  11. package/build/commands.generated.js +12 -11
  12. package/build/conduct-epic/bridge-client.js +345 -0
  13. package/build/conduct-epic/checkpoint-store.js +423 -0
  14. package/build/conduct-epic/cli.js +1732 -0
  15. package/build/conduct-epic/lock.js +302 -0
  16. package/build/conduct-epic/pr-state.js +197 -0
  17. package/build/conduct-epic/spawn.js +101 -0
  18. package/build/conductor/bridge-api-client.js +37 -2
  19. package/build/conductor/doctor.js +11 -1
  20. package/build/conductor/install-doctor.js +184 -10
  21. package/build/conductor-bin.js +7 -7
  22. package/build/credential-store.js +10 -4
  23. package/build/credentials-cli.js +34 -19
  24. package/build/docs.generated.js +1 -1
  25. package/build/doctor.js +579 -88
  26. package/build/executor/agent-identity.js +32 -0
  27. package/build/executor/cli.js +50 -39
  28. package/build/executor/deps.js +15 -1
  29. package/build/executor/env.js +56 -45
  30. package/build/executor/index.js +9 -1
  31. package/build/executor/install-preflight.js +138 -0
  32. package/build/executor/job-errors.js +200 -0
  33. package/build/executor/job-runner.js +619 -268
  34. package/build/executor/observation.js +165 -0
  35. package/build/executor/permissions.js +163 -36
  36. package/build/executor/platform.js +54 -0
  37. package/build/executor/preflight.js +175 -67
  38. package/build/executor/process.js +39 -7
  39. package/build/executor/runner.js +19 -0
  40. package/build/executor/service-lifecycle.js +269 -0
  41. package/build/executor/service-unit.js +121 -12
  42. package/build/executor/stale-artifacts.js +70 -0
  43. package/build/executor/test-clock.js +188 -24
  44. package/build/executor/worker-command.js +22 -58
  45. package/build/executor/worker-log.js +82 -0
  46. package/build/executor/worktree-lock.js +264 -0
  47. package/build/index.js +527 -357
  48. package/build/install-bridge-conductor.js +376 -38
  49. package/build/install-bridge.js +414 -114
  50. package/build/install-doctor.js +13 -0
  51. package/build/install-reexec.js +5 -3
  52. package/build/mcp-install-state.js +130 -0
  53. package/build/mcp-profile.js +11 -2
  54. package/build/mcp-provisioning.js +15 -0
  55. package/build/merge-pull-request.js +562 -0
  56. package/build/phase-result-artifacts.js +450 -0
  57. package/build/pipeline-orchestrator.js +4 -0
  58. package/build/pipeline-utils.js +16 -0
  59. package/build/pipelines.generated.js +7 -7
  60. package/build/plane/preflight.js +18 -14
  61. package/build/plane/supervisor.js +8 -1
  62. package/build/project-root.js +34 -0
  63. package/build/readme.generated.js +1 -1
  64. package/build/run-unit-tests-launcher.js +36 -9
  65. package/build/setup-epic.js +57 -4
  66. package/build/sfcc/ocapi-shape.js +23 -4
  67. package/build/sfcc/permissions.js +25 -6
  68. package/build/sfcc/read-body.js +92 -0
  69. package/build/sfcc/read-projection.js +6 -2
  70. package/build/sfcc/reads-custom-object-def.js +33 -21
  71. package/build/sfcc/reads-site-preference.js +20 -7
  72. package/build/sfcc/reads-system-object.js +11 -5
  73. package/build/sfcc/register.js +61 -23
  74. package/build/sfcc/registration-inventory.js +89 -0
  75. package/build/sfcc/setup-status.js +18 -34
  76. package/build/sfcc/tool-wrapper.js +294 -17
  77. package/build/sfcc/write-grants.js +33 -1
  78. package/build/sfcc/write-guard.js +41 -12
  79. package/build/sfcc/write-result.js +16 -7
  80. package/build/sfcc/writes-custom-object-def.js +12 -4
  81. package/build/sfcc/writes-site-preference.js +6 -1
  82. package/build/sfcc/writes-system-object.js +11 -2
  83. package/build/sfcc/writes.js +13 -8
  84. package/build/start-tickets-prereqs.js +25 -15
  85. package/build/start-tickets.js +123 -21
  86. package/build/version.generated.js +1 -1
  87. package/build/worktree-core.js +9 -3
  88. package/docs/install/mcp-tool-integrations.md +54 -9
  89. package/docs/install/sfcc-integration.md +71 -24
  90. package/package.json +3 -3
  91. package/build/executor/worker-config-isolation.js +0 -287
@@ -27,6 +27,38 @@ export const OCAPI_WRITE_RESOURCE_IDS = [
27
27
  "/custom_object_definitions/**",
28
28
  "/site_preferences/**",
29
29
  ];
30
+ /**
31
+ * The exact HTTP methods each resource family needs, derived from the methods
32
+ * the nine shipped write handlers actually call (BAPI-817).
33
+ *
34
+ * Previously every family was granted `["get","put","patch","delete"]`. **No
35
+ * shipped handler performs a DELETE** — the write surface is create/update only
36
+ * (`ocapiPut`, `ocapiPatch`, `ocapiPatchDirect` in `client.ts`; verified across
37
+ * `writes-system-object.ts`, `writes-custom-object-def.ts`, and
38
+ * `writes-site-preference.ts`). Granting `delete` handed an operator's OCAPI
39
+ * client a destructive capability that Bridge cannot even invoke, on a grant
40
+ * they were told to paste once and forget. The narrower set below is what the
41
+ * code demonstrably needs and nothing more.
42
+ *
43
+ * `get` is retained where a write path genuinely reads: `ocapiPatch` performs a
44
+ * GET-then-If-Match ETag round trip before the PATCH, so removing `get` would
45
+ * break every update tool.
46
+ *
47
+ * /system_object_definitions get — the check_permissions probe.
48
+ * /system_object_definitions/** put, patch (+get for ETag) — attribute and
49
+ * attribute-group create/update, assignment,
50
+ * custom preference create.
51
+ * /custom_object_definitions/** put, patch (+get for ETag) — attribute
52
+ * definition create/update.
53
+ * /site_preferences/** patch (+get) — site_preference_values_set,
54
+ * a direct PATCH with no ETag round trip.
55
+ */
56
+ export const OCAPI_WRITE_RESOURCE_METHODS = {
57
+ "/system_object_definitions": ["get"],
58
+ "/system_object_definitions/**": ["get", "put", "patch"],
59
+ "/custom_object_definitions/**": ["get", "put", "patch"],
60
+ "/site_preferences/**": ["get", "patch"],
61
+ };
30
62
  // ---------------------------------------------------------------------------
31
63
  // Grant JSON builders
32
64
  // ---------------------------------------------------------------------------
@@ -44,7 +76,7 @@ export function buildOcapiWriteGrantSettings(ocapiVersion, clientIdPlaceholder =
44
76
  client_id: clientIdPlaceholder,
45
77
  resources: OCAPI_WRITE_RESOURCE_IDS.map((resource_id) => ({
46
78
  resource_id,
47
- methods: ["get", "put", "patch", "delete"],
79
+ methods: [...(OCAPI_WRITE_RESOURCE_METHODS[resource_id] ?? [])],
48
80
  read_attributes: "(**)",
49
81
  write_attributes: "(**)",
50
82
  })),
@@ -1,11 +1,36 @@
1
1
  /**
2
- * Reusable sandbox-only write guard (BAPI-582 foundation).
2
+ * Secondary OCAPI instance-CONTEXT check for writes (BAPI-582 foundation).
3
3
  *
4
- * Future SFCC write handlers MUST call `rejectIfNotSandboxForWrite`
5
- * immediately after input parsing and BEFORE any payload construction or OCAPI
6
- * mutation. A non-null return value is
7
- * the pre-formatted MCP rejection to return directly; a null return means the
8
- * effective instance is a sandbox and the handler may proceed.
4
+ * ⚠️ **This is NOT the sandbox security boundary.** It validates a
5
+ * caller-supplied *input string*, not the instance the request will reach. The
6
+ * hostname boundary lives in `tool-wrapper.ts`
7
+ * (`withSfccTargetBoundary` / `isApprovedSfccSandboxHost`), runs at
8
+ * registration level for all twenty SFCC tools, and validates the hostname
9
+ * credential resolution actually selected.
10
+ *
11
+ * The distinction is the entire subject of BAPI-817. This guard was previously
12
+ * described — in the README and in its own comments — as the thing that made
13
+ * SFCC writes sandbox-only. It never could be: `instance` is an argument the
14
+ * caller chooses, the target came from `dw.json`/`SFCC_HOSTNAME`, and the two
15
+ * were never compared. Worse, the guard **defaulted an omitted `instance` to
16
+ * `"sandbox"` and passed**, so it approved its single most common call shape
17
+ * while the request went to whatever host was configured. Nine destructive
18
+ * tools sat behind that.
19
+ *
20
+ * The behavior below is deliberately UNCHANGED, and is kept only as
21
+ * defence-in-depth on the OCAPI instance-context input: it rejects a caller who
22
+ * explicitly asks for `"staging"` or `"production"` context, which is still
23
+ * worth refusing. What changed is what it is allowed to *mean*. Its acceptance
24
+ * no longer authorizes anything — a write reaching this guard has already had
25
+ * its target hostname independently approved, and omitting `instance` cannot
26
+ * bypass that approval because the hostname check does not consult `instance`
27
+ * at all.
28
+ *
29
+ * SFCC write handlers still call `rejectIfNotSandboxForWrite` immediately after
30
+ * input parsing and BEFORE any payload construction or OCAPI mutation. A
31
+ * non-null return value is the pre-formatted MCP rejection to return directly;
32
+ * a null return means the effective instance context is `sandbox` and the
33
+ * handler may proceed.
9
34
  *
10
35
  * BAPI-766 moved the rejection's *representation* to the shared SFCC formatter
11
36
  * so the guardrail fails at the protocol level like every other SFCC failure.
@@ -17,13 +42,17 @@ import { formatSfccWriteFailure } from "./sfcc-result.js";
17
42
  // Guard
18
43
  // ---------------------------------------------------------------------------
19
44
  /**
20
- * Reject any write targeting a non-sandbox instance.
45
+ * Reject a write whose OCAPI instance CONTEXT is not `sandbox`.
46
+ *
47
+ * The effective context defaults to `"sandbox"` when `undefined` (some schemas
48
+ * default the value elsewhere), so an omitted instance is allowed here. That
49
+ * permissive default is safe only because it is no longer load-bearing: the
50
+ * resolved-hostname boundary has already approved the target before any handler
51
+ * runs, and it never consults this argument. Returning `null` therefore means
52
+ * "this input is not objectionable", never "this target is a sandbox".
21
53
  *
22
- * SFCC write tools are sandbox-only in this foundation. The effective instance
23
- * defaults to `"sandbox"` when `undefined` (some schemas default the value
24
- * elsewhere), so an omitted instance is allowed. Any other value returns the
25
- * unified validation failure naming the rejected instance; a sandbox returns
26
- * `null` so the caller proceeds.
54
+ * Any other value returns the unified validation failure naming the rejected
55
+ * context.
27
56
  *
28
57
  * `details` carries non-sensitive guard metadata only — the rejected value is
29
58
  * caller-supplied.
@@ -6,15 +6,19 @@
6
6
  * their own 403/fault/success response logic, guaranteeing a consistent surface
7
7
  * across every write tool.
8
8
  *
9
- * Successful results are unchanged and never gain `isError`. Every *failure*
10
- * branch including the 403 write-grant remediation, which used to be prose —
11
- * now returns the universal nested envelope from `sfcc-result.ts`, so
12
- * `result.isError === true` is total across a write tool's post-transport
13
- * failures as well as its pre-transport ones.
9
+ * Successful results never gain `isError`, but their echoed `body` is redacted
10
+ * (BAPI-769): an attribute-definition write's success body can carry the same
11
+ * `default_value` a read can, so it is withheld via `read-body.ts`'s
12
+ * `redactSfccBody` before serialization. Every other *failure* branch
13
+ * including the 403 write-grant remediation, which used to be prose — returns
14
+ * the universal nested envelope from `sfcc-result.ts`, so `result.isError ===
15
+ * true` is total across a write tool's post-transport failures as well as its
16
+ * pre-transport ones.
14
17
  */
15
18
  import { writeGrantForbiddenResult } from "./write-grants.js";
16
19
  import { DEFAULT_OCAPI_VERSION } from "./config.js";
17
20
  import { formatSfccWriteFailure } from "./sfcc-result.js";
21
+ import { redactSfccBody } from "./read-body.js";
18
22
  /** Wrap text in the standard MCP text-result shape (success path only). */
19
23
  function textResult(text) {
20
24
  return { content: [{ type: "text", text }] };
@@ -54,7 +58,12 @@ function ocapiWriteFaultMessage(body, status) {
54
58
  *
55
59
  * - `403` → the unified failure carrying the paste-ready write-grant
56
60
  * remediation as structured data (via write-grants.ts).
57
- * - success → JSON with `status`, `outcome`, and `body`; no `isError`.
61
+ * - success → JSON with `status`, `outcome`, and `body`; no `isError`. `body`
62
+ * is redacted (BAPI-769) before serialization: a successful
63
+ * attribute-definition PUT/PATCH echoes the updated resource in the same
64
+ * shape a GET returns, so it can carry a real `default_value` exactly like a
65
+ * read can — `redactSfccBody` withholds it here the same way `readSfccBody`
66
+ * withholds it on the read path. Every other key survives untouched.
58
67
  * - other failure → the unified `OCAPI_WRITE_ERROR` failure, source `ocapi`,
59
68
  * with the structured fault mapping and the upstream body under
60
69
  * `error.details`.
@@ -76,7 +85,7 @@ export function formatOcapiWriteToolResult(result, operation, path, ocapiVersion
76
85
  return textResult(JSON.stringify({
77
86
  status: result.status,
78
87
  outcome: result.outcome,
79
- body: result.body,
88
+ body: redactSfccBody(result.body),
80
89
  }));
81
90
  }
82
91
  return formatSfccWriteFailure({
@@ -8,7 +8,10 @@
8
8
  * Both tools write ATTRIBUTE DEFINITIONS on an already-known custom object type.
9
9
  * Custom object TYPE creation is intentionally NOT attempted: OCAPI cannot
10
10
  * create custom object types (that is a v2 metadata-import capability), so the
11
- * type must pre-exist. Mirrors reads-custom-object-def.ts.
11
+ * type must pre-exist. OCAPI also cannot enumerate custom object type IDs
12
+ * directly — see reads-custom-object-def.ts for the `system_object_list` +
13
+ * `projection: "full"` discovery path a caller can use to find one before
14
+ * writing to it. Mirrors reads-custom-object-def.ts.
12
15
  *
13
16
  * Every handler is call-time gated by `withSfccGate`, sandbox-guarded via
14
17
  * `rejectIfNotSandboxForWrite`, and routes its OCAPI result through
@@ -25,13 +28,18 @@ import { WRITE_ANNOTATIONS, encodedSegment, preTransportErrorEnvelope, unexpecte
25
28
  // ---------------------------------------------------------------------------
26
29
  // Input schemas
27
30
  // ---------------------------------------------------------------------------
28
- const INSTANCE_DESCRIBE = "OCAPI instance context. SFCC writes are sandbox-only; omit for sandbox. Any " +
29
- "other value is rejected before OCAPI is called.";
31
+ // OCAPI instance CONTEXT, not a target selector: the instance actually
32
+ // contacted is decided by the resolved-hostname boundary (BAPI-817), which this
33
+ // value cannot influence. "omit for sandbox" is gone because omitting it never
34
+ // selected anything.
35
+ const INSTANCE_DESCRIBE = "OCAPI instance context. Omit or pass \"sandbox\"; any other value is " +
36
+ "rejected before OCAPI is called.";
30
37
  const createCustomObjectAttributeDefinitionInput = z.object({
31
38
  object_type: z
32
39
  .string()
33
40
  .describe('Known custom object type identifier (must already exist). OCAPI cannot ' +
34
- "enumerate or create custom object types — only attribute definitions on a known type."),
41
+ "create types, and cannot enumerate type IDs directly — only attribute " +
42
+ "definitions on a known type."),
35
43
  attribute_id: z
36
44
  .string()
37
45
  .describe("URL attribute-definition id. If the body also carries `id`, it must match."),
@@ -28,8 +28,13 @@ import { WRITE_ANNOTATIONS, encodedSegment, preTransportErrorEnvelope, unexpecte
28
28
  // Mirror reads-site-preference.ts so callers see the same instance enum, while
29
29
  // the write remains sandbox-only (enforced by the guard + the literal path).
30
30
  const INSTANCE_ENUM = z.enum(["staging", "development", "sandbox", "production"]);
31
+ // OCAPI instance CONTEXT only. The trailing "Defaults to 'sandbox'" is gone:
32
+ // it read as though the default selected a safe target, when the target is
33
+ // decided by the resolved-hostname boundary (BAPI-817) and this value cannot
34
+ // influence it. The forced `/sandbox` path segment below remains as an extra
35
+ // safeguard on the OCAPI route.
31
36
  const INSTANCE_DESCRIBE = "OCAPI instance context. v1 supports the 'sandbox' context only; any other " +
32
- "value is rejected before OCAPI is called. Defaults to 'sandbox'.";
37
+ "value is rejected before OCAPI is called.";
33
38
  const sitePreferenceValuesSetInput = z.object({
34
39
  group: z.string().describe("Custom site preference group id, e.g. 'LLMIntegration'."),
35
40
  instance: INSTANCE_ENUM.optional().default("sandbox").describe(INSTANCE_DESCRIBE),
@@ -50,11 +50,20 @@ function preferenceObjectTypeForScope(scope) {
50
50
  // ---------------------------------------------------------------------------
51
51
  // Input schemas
52
52
  // ---------------------------------------------------------------------------
53
- /** Optional sandbox instance context. Writes are sandbox-only (see write-guard). */
53
+ /**
54
+ * Optional OCAPI instance CONTEXT — not a target selector.
55
+ *
56
+ * The old description ("Sandbox-only: omit (defaults to sandbox) or pass
57
+ * \"sandbox\"") told the model that omitting this field selected a sandbox. It
58
+ * never did: the host came from `dw.json`/`SFCC_HOSTNAME`, and omitting the
59
+ * field only made `write-guard.ts` default to `"sandbox"` and pass. Which
60
+ * instance is actually contacted is decided by the resolved-hostname boundary in
61
+ * `tool-wrapper.ts` (BAPI-817), which this value cannot influence.
62
+ */
54
63
  const instanceSchema = z
55
64
  .string()
56
65
  .optional()
57
- .describe("Sandbox-only: omit (defaults to sandbox) or pass \"sandbox\".");
66
+ .describe("OCAPI instance context. Omit or pass \"sandbox\".");
58
67
  const createAttributeDefinitionInput = z.object({
59
68
  object_type: z.string().describe("System object type, e.g. \"Product\"."),
60
69
  attribute_id: z.string().describe("Attribute id (URL id); the body id must match it."),
@@ -3,9 +3,10 @@
3
3
  * populated by BAPI-584).
4
4
  *
5
5
  * This is the registration seam that write-surface tickets register through.
6
- * `register.ts` wires this behind the existing `includeReadTools` SFCC gate, so
7
- * the write tools become default-on with the reads without touching
8
- * `register.ts` or `index.ts`.
6
+ * `register.ts` wires it behind its OWN `includeWriteTools` gate, driven by the
7
+ * `sfcc-write` profile group (BAPI-817). The nine tools here are destructive, so
8
+ * they are no longer default-on with the reads: enabling `sfcc` registers the
9
+ * read profile and nothing from this module.
9
10
  *
10
11
  * Each write handler follows the foundation recipe:
11
12
  * 1. Parse inputs.
@@ -25,7 +26,8 @@ import { registerSitePreferenceWriteTools, SITE_PREFERENCE_WRITE_TOOL_NAMES, } f
25
26
  * Write tool names registered by this seam (centralized inventory). Extended by
26
27
  * each write-surface ticket — BAPI-583 adds the six system-object write tools,
27
28
  * BAPI-584 adds the custom-object attribute-definition and site-preference value
28
- * write tools.
29
+ * write tools. These nine have SHIPPED; `registration-inventory.ts` reuses this
30
+ * array as the `sfcc-write` profile inventory rather than duplicating it.
29
31
  */
30
32
  export const SFCC_WRITE_TOOL_NAMES = [
31
33
  ...SYSTEM_OBJECT_WRITE_TOOL_NAMES,
@@ -47,14 +49,17 @@ export const SFCC_WRITE_TOOL_NAMES = [
47
49
  * 4. Call `ocapiPut` or `ocapiPatch` (client.ts) for transport.
48
50
  * 5. Route the result through `formatOcapiWriteToolResult` (write-result.ts).
49
51
  *
50
- * Each handler is additionally call-time gated by `withSfccGate` and
51
- * sandbox-guarded, so registering them here (default-on with the reads) does
52
- * not weaken the per-call safety model.
52
+ * Every handler registered here crosses three checks before it can mutate
53
+ * anything: the resolved-hostname sandbox boundary (registration-level,
54
+ * `tool-wrapper.ts`), the call-time `withSfccGate` version check, and the
55
+ * secondary instance-context guard in `write-guard.ts`. Only the first of those
56
+ * decides which instance is contacted.
53
57
  *
54
58
  * BAPI-583 registers the system-object write surface here; BAPI-584 adds the
55
59
  * custom-object attribute-definition and site-preference value write surfaces
56
60
  * alongside it. Later write-surface tickets add their modules the same way,
57
- * without touching `register.ts`/`index.ts`.
61
+ * without touching `register.ts`/`index.ts` — a new tool added here is
62
+ * automatically inside the `sfcc-write` group and inside the target boundary.
58
63
  */
59
64
  export function registerSfccWriteTools(registerTool, deps) {
60
65
  // System-object attribute/group definition writes (BAPI-583).
@@ -18,7 +18,7 @@ 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 { CONDUCTOR_ALLOW_CLAUDE_MCP_SHADOWING_ENV, formatClaudeMcpShadowFinding, formatClaudeUserConfigDiagnostic, inspectClaudeUserConfigForMcpShadowing, resolveClaudeUserConfigPath, } from "./claude-user-config-doctor.js";
21
+ import { CLAUDE_MCP_SHADOWING_REMEDIATION_COMMAND, formatClaudeMcpShadowFinding, formatClaudeUserConfigDiagnostic, inspectClaudeUserConfigForMcpShadowing, resolveClaudeUserConfigPath, } from "./claude-user-config-doctor.js";
22
22
  // ---------------------------------------------------------------------------
23
23
  // Constants (moved here from start-tickets.ts so both consumers share them)
24
24
  // ---------------------------------------------------------------------------
@@ -531,12 +531,11 @@ export async function resolveMainWorktreePathForDoctor(deps) {
531
531
  * which is why the advice below is phrased as "delete, or move aside". Keep every
532
532
  * such identifier out of this file, even inside a comment like this one.
533
533
  */
534
- const CLAUDE_MCP_SHADOWING_HINT = "Delete, or move aside, the 'bridge-api' MCP registration in ~/.claude.json (at user scope, " +
535
- "or under the projects entry for this repository/worktree) so the registration the " +
536
- "conductor provisions into each worker worktree is the one that takes effect. If the " +
537
- "collision is deliberate, set " +
538
- `${CONDUCTOR_ALLOW_CLAUDE_MCP_SHADOWING_ENV}=1 to override the executor's refusal ` +
539
- "(doctor still reports it).";
534
+ const CLAUDE_MCP_SHADOWING_HINT = `Run \`${CLAUDE_MCP_SHADOWING_REMEDIATION_COMMAND}\` to clear the 'bridge-api' MCP ` +
535
+ "registration in ~/.claude.json (use the matching scope if it is registered at user scope, " +
536
+ "or under the projects entry for this repository/worktree). This is ADVISORY: workers load " +
537
+ "MCP servers with --strict-mcp-config from their own worktree registration, so this entry " +
538
+ "does not reach a worker and does not block a conductor run.";
540
539
  const CLAUDE_MCP_SHADOWING_INSTALL_HINTS = {
541
540
  darwin: CLAUDE_MCP_SHADOWING_HINT,
542
541
  linux: CLAUDE_MCP_SHADOWING_HINT,
@@ -547,17 +546,25 @@ const CLAUDE_MCP_SHADOWING_INSTALL_HINTS = {
547
546
  * configuration register a `bridge-api` MCP server that could shadow a worker's
548
547
  * provisioned registration?
549
548
  *
549
+ * ADVISORY SINCE BAPI-790. Every outcome reports `found: true`, so this never
550
+ * fails the prerequisite section or the exit code; a confirmed collision is
551
+ * surfaced through the DETAIL, which names the scope and carries the exact
552
+ * remediation command. The downgrade is not a softening of the diagnostic — it
553
+ * reflects that strict flag-scoped worker MCP loading means the collision can no
554
+ * longer reach a worker (inventory Finding 6), so reporting it as a broken
555
+ * prerequisite would tell an operator to fix something that is not breaking
556
+ * anything.
557
+ *
550
558
  * Status mapping:
551
559
  * - no registration, or only acceptable provisioned shims → `found: true`
552
- * - a CONFIRMED foreign registration → `found: false`
560
+ * - a CONFIRMED foreign registration → `found: true` + advisory detail
553
561
  * - unreadable / malformed config, or an unresolved main
554
562
  * worktree path → `found: true` + detail
555
563
  *
556
- * The failing case is deliberately independent of
557
- * `BAPI_CONDUCTOR_ALLOW_CLAUDE_MCP_SHADOWING`: the override changes what the
558
- * EXECUTOR does at runtime, not whether the collision exists. A diagnostic that
559
- * went quiet because an override happened to be exported would hide the exact
560
- * condition an operator ran doctor to find.
564
+ * Reporting stays deliberately independent of the retired
565
+ * `BAPI_CONDUCTOR_ALLOW_CLAUDE_MCP_SHADOWING` override: whether that variable is
566
+ * exported has never had any bearing on whether the collision exists, and now it
567
+ * has no bearing on anything at all.
561
568
  */
562
569
  export function claudeMcpShadowingDescriptor() {
563
570
  return {
@@ -589,8 +596,11 @@ export function claudeMcpShadowingDescriptor() {
589
596
  degraded.push("the main worktree path could not be resolved; only user and current-worktree scopes were checked");
590
597
  }
591
598
  if (inspection.findings.length > 0) {
592
- const findings = inspection.findings.map(formatClaudeMcpShadowFinding);
593
- return { found: false, detail: [...findings, ...degraded].join("; ") };
599
+ // Advisory (BAPI-790): reported in full, with the remediation command,
600
+ // but `found: true` so it never fails the prerequisite or the exit code.
601
+ const findings = inspection.findings.map((finding) => `advisory: ${formatClaudeMcpShadowFinding(finding)} — not loaded by a worker under ` +
602
+ `strict MCP loading; clear it with \`${CLAUDE_MCP_SHADOWING_REMEDIATION_COMMAND}\``);
603
+ return { found: true, detail: [...findings, ...degraded].join("; ") };
594
604
  }
595
605
  return {
596
606
  found: true,
@@ -59,6 +59,10 @@ 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
+ // BAPI-814/E1b: the read-only index-branch override probe. `getIndexBranch` is
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
66
  import { resolveStartTicketsRepoName as resolveSharedStartTicketsRepoName, resolveRequiredStartTicketsRepoName, } from "./start-tickets-repo.js";
63
67
  import { provisionMcpRegistrationsForCreatedWorktrees, } from "./mcp-provisioning.js";
64
68
  import { provisionCommandsForCreatedWorktrees, } from "./command-provisioning.js";
@@ -173,6 +177,7 @@ export function parseStartTicketsArgs(argv) {
173
177
  let maxParallelRaw;
174
178
  let agentName = DEFAULT_AGENT_NAME;
175
179
  let baseBranch = "main";
180
+ let baseBranchExplicit = false;
176
181
  let conductorEnabled = false;
177
182
  let workflow = "implement";
178
183
  let reviewRoundsRaw;
@@ -342,6 +347,7 @@ export function parseStartTicketsArgs(argv) {
342
347
  return { status: "error", message: `Invalid --base-branch value: ${error}` };
343
348
  }
344
349
  baseBranch = trimmed;
350
+ baseBranchExplicit = true;
345
351
  continue;
346
352
  }
347
353
  if (arg === "--dry-run") {
@@ -449,6 +455,7 @@ export function parseStartTicketsArgs(argv) {
449
455
  branchOverrides,
450
456
  agentName,
451
457
  baseBranch,
458
+ ...(baseBranchExplicit ? { baseBranchExplicit: true } : {}),
452
459
  conductorEnabled,
453
460
  workflow,
454
461
  reviewRounds,
@@ -506,16 +513,19 @@ export function resolveStartTicketsPlatformConfig(deps, agent, autoApprove = fal
506
513
  // instruction to the resume-mode worker's prompt.
507
514
  resumeMode = false, workflow = "implement", reviewRounds,
508
515
  // BAPI-586: the effective run base branch (incl. epic.base_branch override).
509
- // Injected as BAPI_BASE_BRANCH for conductor workers so their PR targets it,
510
- // and threaded (BAPI-593) into the spawned workflow command.
516
+ // Injected as BAPI_BASE_BRANCH so the spawned worker's PR targets it — for
517
+ // conductor runs always, and (BAPI-801) for plain runs whose base is a
518
+ // non-default branch — and threaded (BAPI-593) into the spawned workflow command.
511
519
  baseBranch) {
512
520
  if (!isSupportedStartTicketsPlatform(deps.platform)) {
513
521
  return { ok: false, error: unsupportedPlatformMessage(deps.platform) };
514
522
  }
515
523
  const platform = deps.platform;
516
- // Only conductor dispatch uses the PR-base contract; interactive dispatch omits
517
- // the BAPI_BASE_BRANCH assignment so its worker launch is unchanged.
518
- const prBaseBranch = conductorEnabled ? baseBranch : null;
524
+ // BAPI-801: both conductor and plain dispatch carry the PR-base ENVIRONMENT
525
+ // contract (see resolvePrBaseBranchEnvValue). Only the conductor launch
526
+ // INSTRUCTION remains conductor-only a plain worker's prompt stays bare and
527
+ // learns the base from the env plus the create-PR instruction files.
528
+ const prBaseBranch = resolvePrBaseBranchEnvValue(conductorEnabled, baseBranch);
519
529
  return {
520
530
  ok: true,
521
531
  config: {
@@ -523,7 +533,8 @@ baseBranch) {
523
533
  worktrunkBinary: resolveWorktrunkBinary(platform, deps.env),
524
534
  // Inject the resolved repo identity so the spawned worktree session never
525
535
  // falls back to the basename-derived repo name (the 403 root cause), and
526
- // (BAPI-586) the run base so the conductor worker opens its PR against it.
536
+ // (BAPI-586/BAPI-801) the run base so the spawned worker opens its PR
537
+ // against it. The base assignment stays outermost.
527
538
  buildAgentShellCommand: (key, worktreePath, modelAlias) => prependBaseBranchEnvAssignment(prependRepoNameEnvAssignment(buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch), repoName, platform), prBaseBranch, platform),
528
539
  spawnTerminalTab: deps.spawnTerminalTab,
529
540
  },
@@ -555,14 +566,40 @@ export function prependRepoNameEnvAssignment(command, repoName, platform = "darw
555
566
  return `export BAPI_REPO_NAME='${shSquoteInner(repoName)}' && ${command}`;
556
567
  }
557
568
  /**
558
- * BAPI-586: prepend a `BAPI_BASE_BRANCH` environment assignment to a spawned
559
- * conductor worker's shell command so the worker can open its PR against the run
560
- * base via `gh pr create --base "$BAPI_BASE_BRANCH"` (paired with the PR-base
561
- * launch instruction), instead of inferring the repo default branch. Platform
562
- * correct `$env:VAR = '…'; …` on PowerShell, `export VAR='…' && …` on POSIX —
563
- * with the value quoted by the same escaper used for the rest of the command.
564
- * Fail-open: a null/empty `baseBranch` returns the command unchanged (interactive
565
- * dispatch does not use the PR-base contract).
569
+ * BAPI-801: decide the `BAPI_BASE_BRANCH` value a spawned worker's shell should
570
+ * carry the single rule shared by the real spawn builder
571
+ * ({@link resolveStartTicketsPlatformConfig}) and the dry-run preview builder
572
+ * ({@link getDryRunPlatformDetails}), so the preview can never drift from what
573
+ * is actually spawned.
574
+ *
575
+ * - Conductor runs export whatever base was resolved, `main` included BAPI-586
576
+ * behavior, unchanged.
577
+ * - Plain runs export only a non-empty base other than the CLI default `main`.
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.
581
+ *
582
+ * Returns `null` when nothing should be exported;
583
+ * {@link prependBaseBranchEnvAssignment} is fail-open on that value.
584
+ */
585
+ function resolvePrBaseBranchEnvValue(conductorEnabled, baseBranch) {
586
+ if (conductorEnabled)
587
+ return baseBranch ?? null;
588
+ if (!baseBranch || baseBranch === "main")
589
+ return null;
590
+ return baseBranch;
591
+ }
592
+ /**
593
+ * BAPI-586/BAPI-801: prepend a `BAPI_BASE_BRANCH` environment assignment to a
594
+ * spawned worker's shell command so the worker opens its PR against the run base
595
+ * instead of inferring the repo default branch. Conductor workers additionally
596
+ * receive the paired launch instruction; plain workers (BAPI-801) carry the same
597
+ * env and learn the contract from the create-PR instruction files, which resolve
598
+ * `$BAPI_BASE_BRANCH` first. Platform correct — `$env:VAR = '…'; …` on
599
+ * PowerShell, `export VAR='…' && …` on POSIX — with the value quoted by the same
600
+ * escaper used for the rest of the command. Fail-open: a null/empty `baseBranch`
601
+ * returns the command unchanged (the caller's rule already decided not to
602
+ * export — see {@link resolvePrBaseBranchEnvValue}).
566
603
  */
567
604
  export function prependBaseBranchEnvAssignment(command, baseBranch, platform = "darwin") {
568
605
  if (!baseBranch)
@@ -1061,6 +1098,9 @@ export function buildAgentPrompt(key, opts = {}) {
1061
1098
  parts.push(buildConductorMessageRelayLaunchInstruction());
1062
1099
  // BAPI-586: conductor implementation workers must open their PR against the
1063
1100
  // run base (injected as BAPI_BASE_BRANCH), never the repo default branch.
1101
+ // The launch INSTRUCTION stays conductor-only. BAPI-801 gave plain runs the
1102
+ // same env, but their prompt stays bare — the create-PR instruction files
1103
+ // read $BAPI_BASE_BRANCH for them.
1064
1104
  parts.push(buildPrBaseContractLaunchInstruction());
1065
1105
  }
1066
1106
  if (opts.resumeMode)
@@ -1723,11 +1763,13 @@ export function getDryRunPlatformDetails(agent, platform = process.platform, env
1723
1763
  worktrunkBinary: resolveWorktrunkBinary(platform, env),
1724
1764
  // The builder accepts an optional resolved modelAlias; the dry-run caller
1725
1765
  // now passes the previewed tier's alias so `--model` shows in the preview.
1726
- // The resolved repo name (when known) is injected as a BAPI_REPO_NAME prefix
1727
- // so the dry-run preview matches the real spawn command exactly. Reuses the
1728
- // same buildAgentShellCommand/buildAgentPrompt path as a real spawn dry-run
1729
- // is never special-casedso the previewed workflow prompt is exact.
1730
- buildAgentShellCommand: (key, worktreePath, modelAlias) => prependRepoNameEnvAssignment(buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove, modelAlias, conductorEnabled, false, workflow, reviewRounds, baseBranch), repoName, platform),
1766
+ // 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 SAME
1768
+ // resolvePrBaseBranchEnvValue rule and the same outermost ordering the real
1769
+ // spawn builder usesdry-run shares both environment renderings with real
1770
+ // spawning. Reuses the same buildAgentShellCommand/buildAgentPrompt path as a
1771
+ // real spawn — dry-run is never special-cased — so the preview is exact.
1772
+ buildAgentShellCommand: (key, worktreePath, modelAlias) => prependBaseBranchEnvAssignment(prependRepoNameEnvAssignment(buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove, modelAlias, conductorEnabled, false, workflow, reviewRounds, baseBranch), repoName, platform), resolvePrBaseBranchEnvValue(conductorEnabled, baseBranch), platform),
1731
1773
  };
1732
1774
  }
1733
1775
  /**
@@ -1769,10 +1811,17 @@ export function buildDryRunDetailLines(agent, key, branch, platform = process.pl
1769
1811
  const { worktrunkBinary, buildAgentShellCommand: build } = getDryRunPlatformDetails(agent, platform, env, autoApprove, conductorEnabled, repoName, workflow, reviewRounds, baseBranch);
1770
1812
  const wtArgs = buildWtSwitchArgs(branch, false, baseBranch);
1771
1813
  const agentInvocation = build(key, "<worktree-path>", modelAlias);
1814
+ // BAPI-801: one concise secondary detail naming the PR base, emitted only when
1815
+ // the shared rule actually exports BAPI_BASE_BRANCH. It sits AFTER the rendered
1816
+ // spawn command so the copyable command stays the primary scan target, and it
1817
+ // is ordinary routing detail — never warning-like. An ordinary plain run on
1818
+ // `main` exports nothing and so keeps its existing output density verbatim.
1819
+ const prBaseBranch = resolvePrBaseBranchEnvValue(conductorEnabled, baseBranch);
1772
1820
  return [
1773
1821
  `DRY-RUN: ${key} -> branch=${branch}`,
1774
1822
  `DRY-RUN: ${worktrunkBinary} ${wtArgs.join(" ")}`,
1775
1823
  `DRY-RUN: ${agentInvocation}`,
1824
+ ...(prBaseBranch ? [`DRY-RUN: PR base: ${prBaseBranch}`] : []),
1776
1825
  ...buildDryRunMcpProvisioningLines("<worktree-path>", platform, mcpServerInvocation),
1777
1826
  ];
1778
1827
  }
@@ -2792,8 +2841,10 @@ export async function orchestrateStartTickets(deps, options, overrides = {}) {
2792
2841
  const platformConfig = resolveStartTicketsPlatformConfig(deps, agent, options.autoApprove, options.conductorEnabled ?? false, resolvedRepoName,
2793
2842
  // BAPI-494: resume-mode dispatches get the full-suite remediation finalize prompt.
2794
2843
  options.resumeMode ?? false, options.workflow, options.reviewRounds,
2795
- // BAPI-586: the effective run base (already carries any epic.base_branch
2796
- // override applied above) so conductor workers get BAPI_BASE_BRANCH.
2844
+ // BAPI-586/BAPI-801: the effective run base (already carries any
2845
+ // epic.base_branch override applied above) so the spawned worker gets
2846
+ // BAPI_BASE_BRANCH — every conductor worker, and a plain worker whose base
2847
+ // is non-default.
2797
2848
  options.baseBranch);
2798
2849
  if (!platformConfig.ok)
2799
2850
  return { ok: false, error: platformConfig.error };
@@ -2940,6 +2991,54 @@ export async function orchestrateStartTickets(deps, options, overrides = {}) {
2940
2991
  const rows = await spawnTabsFn(deps, routed, terminal, platformConfig.config.buildAgentShellCommand);
2941
2992
  return { ok: true, rows };
2942
2993
  }
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
+ }
2943
3042
  /** Platform-specific guidance printed when one or more tabs fail to spawn. */
2944
3043
  function spawnFailureHintForPlatform(platform) {
2945
3044
  if (platform === "darwin") {
@@ -2995,6 +3094,9 @@ export async function runStartTicketsCli(argv, overrides = {}) {
2995
3094
  errorLog("Warning: --tier value was not one of cheap|basic|premium; ignoring the override and " +
2996
3095
  "using premium model routing for every ticket.");
2997
3096
  }
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);
2998
3100
  if (options.dryRun) {
2999
3101
  // Resolve the repo identity for the preview so the dry-run command matches
3000
3102
  // what the real spawn injects (see prependRepoNameEnvAssignment). The real
@@ -1,2 +1,2 @@
1
1
  // AUTO-GENERATED — do not edit manually. Regenerate with: npm run build
2
- export const VERSION = "0.2.37";
2
+ export const VERSION = "0.2.39";