@bridge_gpt/mcp-server 0.2.50 → 0.2.52

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 (76) hide show
  1. package/README.md +24 -8
  2. package/build/agent-capabilities/probe-context.js +15 -7
  3. package/build/agent-capabilities/probes.js +42 -6
  4. package/build/agent-launchers/claude-executor-adapter.js +98 -14
  5. package/build/commands.generated.js +1 -1
  6. package/build/conduct-epic/bridge-client.js +115 -1
  7. package/build/conduct-epic/cli.js +351 -33
  8. package/build/conduct-epic/cut-protocol.js +65 -0
  9. package/build/conductor/bridge-api-client.js +171 -5
  10. package/build/conductor/deny-enforcement-preflight.js +107 -10
  11. package/build/conductor/local-merge.js +170 -11
  12. package/build/conductor-bin.js +2 -2
  13. package/build/connect-bitbucket-api.js +370 -0
  14. package/build/connect-bitbucket.js +437 -0
  15. package/build/docs.generated.js +1 -1
  16. package/build/doctor.js +230 -1
  17. package/build/drive-epic.js +423 -11
  18. package/build/env-file-link.js +164 -0
  19. package/build/epic-integration-pr.js +290 -0
  20. package/build/executor/cli.js +41 -6
  21. package/build/executor/deps.js +5 -1
  22. package/build/executor/env-file-guard.js +113 -0
  23. package/build/executor/env.js +78 -1
  24. package/build/executor/heartbeat.js +9 -0
  25. package/build/executor/http-client.js +90 -22
  26. package/build/executor/job-errors.js +43 -2
  27. package/build/executor/job-runner.js +137 -29
  28. package/build/executor/merge-job.js +102 -6
  29. package/build/executor/permissions.js +106 -0
  30. package/build/executor/preflight.js +38 -13
  31. package/build/executor/resume-pre-spawn.js +2 -1
  32. package/build/executor/runner.js +175 -4
  33. package/build/executor/service-unit.js +15 -0
  34. package/build/executor/terminal-mutation.js +22 -1
  35. package/build/executor/types.js +86 -0
  36. package/build/executor/worker-command.js +21 -5
  37. package/build/executor/worker-guard-hook.js +939 -0
  38. package/build/executor/worker-log.js +56 -0
  39. package/build/executor/worktree.js +11 -0
  40. package/build/git-reachability.js +147 -0
  41. package/build/index.js +535 -95
  42. package/build/install-bridge.js +95 -0
  43. package/build/pipelines.generated.js +10 -2
  44. package/build/plan-epic-conductor-eligibility.js +213 -0
  45. package/build/plane/cli.js +78 -15
  46. package/build/plane/defaults.js +165 -0
  47. package/build/plane/manifest.js +63 -8
  48. package/build/plane/member-logs.js +6 -0
  49. package/build/plane/member-roster.js +195 -11
  50. package/build/plane/preflight.js +43 -0
  51. package/build/plane/shutdown.js +25 -3
  52. package/build/plane/status.js +11 -0
  53. package/build/plane/supervisor.js +343 -14
  54. package/build/plane/test-fakes.js +43 -0
  55. package/build/plane/types.js +82 -11
  56. package/build/pr-base-contract.js +20 -0
  57. package/build/readme.generated.js +1 -1
  58. package/build/review-synthesis-config.js +60 -0
  59. package/build/scripts/executor-protocol-contract-driver.js +311 -0
  60. package/build/setup-epic.js +592 -139
  61. package/build/sfcc/log-query.js +2 -1
  62. package/build/sfcc/reads-custom-object-def.js +10 -13
  63. package/build/sfcc/reads-site-preference.js +5 -5
  64. package/build/sfcc/reads-system-object.js +4 -4
  65. package/build/sfcc/writes-custom-object-def.js +7 -7
  66. package/build/sfcc/writes-site-preference.js +4 -3
  67. package/build/sfcc/writes-system-object.js +7 -6
  68. package/build/start-tickets-conductor.js +11 -2
  69. package/build/start-tickets.js +69 -2
  70. package/build/version.generated.js +3 -3
  71. package/build/worker-containment-diagnostic.js +97 -0
  72. package/build/worker-guard-hook-bin.js +6 -0
  73. package/docs/CONDUCTOR.md +27 -0
  74. package/docs/install/mcp-tool-integrations.md +3 -2
  75. package/package.json +5 -3
  76. package/pipelines/plan-epic.json +5 -0
@@ -110,7 +110,8 @@ function semanticCheck(args) {
110
110
  const start = Date.parse(args.time_range.start);
111
111
  const end = Date.parse(args.time_range.end);
112
112
  if (Number.isNaN(start) || Number.isNaN(end)) {
113
- return "time_range.start and time_range.end must be valid ISO-8601 timestamps.";
113
+ return ("time_range.start and time_range.end must be ISO-8601 timestamps that " +
114
+ "include a seconds component and a UTC offset (e.g. 2026-07-15T00:00:00Z).");
114
115
  }
115
116
  if (!(start < end)) {
116
117
  return "time_range.start must be strictly before time_range.end.";
@@ -50,11 +50,10 @@ const READ_ANNOTATIONS = {
50
50
  // ---------------------------------------------------------------------------
51
51
  // Input schemas
52
52
  // ---------------------------------------------------------------------------
53
- const OBJECT_TYPE_DISCOVERY_HINT = 'Known custom object type id (e.g. starting with "c_"). OCAPI cannot enumerate ' +
54
- "type IDs directly, but you can discover a candidate via system_object_list at " +
55
- "projection=\"full\" (distinct display_name + attribute_definition_count per " +
56
- "custom type) and confirm it by checking that this tool's returned attribute " +
57
- "count matches that row's attribute_definition_count.";
53
+ const OBJECT_TYPE_DISCOVERY_HINT = 'Known custom object type id (e.g. starting with "c_"). ' +
54
+ "Type ids aren't enumerable; find via system_object_list projection=full. " +
55
+ "Match by display_name + attribute_definition_count per custom type, then " +
56
+ "confirm this tool's returned attribute count matches that row's count.";
58
57
  const customObjectAttributesGetInput = z.object({
59
58
  object_type: z.string().describe(OBJECT_TYPE_DISCOVERY_HINT),
60
59
  projection: projectionInputFor("attribute_collection"),
@@ -139,20 +138,18 @@ function buildCustomObjectAttributeSearchHandler(gateDeps, getDocsDir) {
139
138
  export function registerSfccCustomObjectDefReadTools(registerTool, deps) {
140
139
  const { gateDeps, getDocsDir } = deps;
141
140
  registerTool("custom_object_definition_attributes_get", {
142
- description: "Retrieve attribute definitions for a KNOWN custom object type via " +
141
+ description: "Retrieve the FULL attribute-definition set for a KNOWN custom object type via " +
143
142
  "GET /custom_object_definitions/{type}/attribute_definitions " +
144
- "(default_value withheld). OCAPI cannot enumerate type IDs directly; " +
145
- "derive one via system_object_list (projection=full, attribute_definition_count), " +
146
- "confirm here. Type creation is v2 metadata-import only.",
143
+ "(default_value withheld). Type creation is v2 metadata-import only. " +
144
+ "Oversized output saved locally.",
147
145
  inputSchema: customObjectAttributesGetInput,
148
146
  annotations: READ_ANNOTATIONS,
149
147
  }, buildCustomObjectAttributesGetHandler(gateDeps, getDocsDir));
150
148
  registerTool("custom_object_definition_attribute_search", {
151
- description: "Search attribute definitions within a KNOWN custom object type via " +
149
+ description: "Filter the attribute-definition set within a KNOWN custom object type via " +
152
150
  "POST /custom_object_definitions/{type}/attribute_definition_search " +
153
- "(default_value withheld). OCAPI cannot enumerate type IDs directly; " +
154
- "derive one via system_object_list (projection=full, attribute_definition_count), " +
155
- "confirm here. Type creation is v2 metadata-import only.",
151
+ "(default_value withheld). Type creation is v2 metadata-import only. " +
152
+ "Oversized output saved locally.",
156
153
  inputSchema: customObjectAttributeSearchInput,
157
154
  annotations: READ_ANNOTATIONS,
158
155
  }, buildCustomObjectAttributeSearchHandler(gateDeps, getDocsDir));
@@ -226,15 +226,15 @@ export function registerSitePreferenceTools(registerTool, deps) {
226
226
  description: "List custom site preference group ids from the developer sandbox via " +
227
227
  "GET /system_object_definitions/SitePreferences/attribute_groups. Read-only; " +
228
228
  "use this to discover group ids before site_preference_get or site_preference_search. " +
229
- "Accepts optional `count` and `start` for paging. Oversized outputs are auto-saved locally.",
229
+ "Accepts optional `count` and `start` for paging. Oversized output saved locally.",
230
230
  inputSchema: sitePreferenceGroupListInput,
231
231
  annotations: READ_ANNOTATIONS,
232
232
  }, buildSitePreferenceGroupListHandler(gateDeps, getDocsDir));
233
233
  registerTool("site_preference_get", {
234
- description: "List preference IDENTIFIERS for a site preference group from the developer sandbox via a match-all " +
234
+ description: "List ALL preference ids in a group (match-all) from the developer sandbox via " +
235
235
  "POST /site_preferences/preference_groups/{group}/sandbox/preference_search. " +
236
- "Returns ids only, NOT values — 'unset' and 'empty string' are indistinguishable. " +
237
- "Read-only, sandbox-only. Optional start/count paging. Oversized payloads auto-saved locally.",
236
+ "Returns identifiers only, NOT values — 'unset' and 'empty string' are indistinguishable. " +
237
+ "Read-only, sandbox-only. Optional start/count paging. Oversized output saved locally.",
238
238
  inputSchema: sitePreferenceGetInput,
239
239
  annotations: READ_ANNOTATIONS,
240
240
  }, buildSitePreferenceGetHandler(gateDeps, getDocsDir));
@@ -243,7 +243,7 @@ export function registerSitePreferenceTools(registerTool, deps) {
243
243
  "POST /site_preferences/preference_groups/{group}/sandbox/preference_search. " +
244
244
  "Returns ids only — this endpoint does not expose values. Read-only; v1 sandbox only. " +
245
245
  "Pass a plain string for id-only text search or a structured OCAPI query. " +
246
- "Oversized results auto-saved locally.",
246
+ "Oversized output saved locally.",
247
247
  inputSchema: sitePreferenceSearchInput,
248
248
  annotations: READ_ANNOTATIONS,
249
249
  }, buildSitePreferenceSearchHandler(gateDeps, getDocsDir));
@@ -149,7 +149,7 @@ export function registerSystemObjectReadTools(registerTool, deps) {
149
149
  "GET /system_object_definitions. Read-only. " +
150
150
  "Accepts optional `count` and `start` for paging (OCAPI default pagination applies when omitted). " +
151
151
  "Returns ids only by default; pass projection=full for all fields. " +
152
- "Oversized outputs are auto-saved locally.",
152
+ "Oversized output saved locally.",
153
153
  inputSchema: systemObjectListInput,
154
154
  annotations: READ_ANNOTATIONS,
155
155
  }, buildSystemObjectListHandler(gateDeps, getDocsDir));
@@ -157,17 +157,17 @@ export function registerSystemObjectReadTools(registerTool, deps) {
157
157
  description: "Retrieve a system object type definition from the developer sandbox. Read-only. " +
158
158
  "GET /system_object_definitions/{type}. " +
159
159
  "Prefer system_object_attribute_search for targeted attribute lookups. " +
160
- "Oversized payloads are auto-saved locally.",
160
+ "Oversized output saved locally.",
161
161
  inputSchema: systemObjectGetInput,
162
162
  annotations: READ_ANNOTATIONS,
163
163
  }, buildSystemObjectGetHandler(gateDeps, getDocsDir));
164
164
  registerTool("system_object_attribute_search", {
165
165
  description: "Search attribute definitions for a system object type. Read-only. " +
166
166
  "POST /system_object_definitions/{type}/attribute_definition_search. " +
167
- "Use for targeted c_ attribute lookups; returns each attribute's schema " +
167
+ "Use for targeted attribute lookups; returns each attribute's schema " +
168
168
  "by default (default_value withheld). " +
169
169
  "Pass a plain string for text search or a structured OCAPI query. " +
170
- "Oversized results auto-saved locally.",
170
+ "Oversized output saved locally.",
171
171
  inputSchema: systemObjectAttributeSearchInput,
172
172
  annotations: READ_ANNOTATIONS,
173
173
  }, buildSystemObjectAttributeSearchHandler(gateDeps, getDocsDir));
@@ -37,9 +37,8 @@ const INSTANCE_DESCRIBE = "OCAPI instance context. Omit or pass \"sandbox\"; any
37
37
  const createCustomObjectAttributeDefinitionInput = z.object({
38
38
  object_type: z
39
39
  .string()
40
- .describe('Known custom object type identifier (must already exist). OCAPI cannot ' +
41
- "create types, and cannot enumerate type IDs directly only attribute " +
42
- "definitions on a known type."),
40
+ .describe('Known custom object type identifier (must already exist; OCAPI cannot create ' +
41
+ "types). Type ids aren't enumerable; find via system_object_list projection=full."),
43
42
  attribute_id: z
44
43
  .string()
45
44
  .describe("URL attribute-definition id. If the body also carries `id`, it must match."),
@@ -49,7 +48,8 @@ const createCustomObjectAttributeDefinitionInput = z.object({
49
48
  const updateCustomObjectAttributeDefinitionInput = z.object({
50
49
  object_type: z
51
50
  .string()
52
- .describe("Known custom object type identifier (must already exist). OCAPI cannot create types."),
51
+ .describe("Known custom object type identifier (must already exist; OCAPI cannot create " +
52
+ "types). Type ids aren't enumerable; find via system_object_list projection=full."),
53
53
  attribute_id: z.string().describe("URL attribute-definition id to update."),
54
54
  patch: objectAttributeDefinitionPatchBodySchema.describe("Partial ObjectAttributeDefinition body; must change at least one field."),
55
55
  instance: z.string().optional().describe(INSTANCE_DESCRIBE),
@@ -134,15 +134,15 @@ export function registerSfccCustomObjectDefWriteTools(registerTool, deps) {
134
134
  registerTool("custom_object_definition_attribute_create", {
135
135
  description: "Create an attribute definition on a KNOWN custom object type via " +
136
136
  "PUT /custom_object_definitions/{type}/attribute_definitions/{id}. Sandbox-only, " +
137
- "destructive; the type must pre-exist (OCAPI cannot create types). Echoes paste-ready " +
138
- "grant JSON on 403.",
137
+ "destructive; the type must pre-exist (OCAPI cannot create types). " +
138
+ "403 → paste-ready grant JSON.",
139
139
  inputSchema: createCustomObjectAttributeDefinitionInput,
140
140
  annotations: WRITE_ANNOTATIONS,
141
141
  }, buildCreateCustomObjectAttributeDefinitionHandler(gateDeps));
142
142
  registerTool("custom_object_definition_attribute_update", {
143
143
  description: "Update an attribute definition on a KNOWN custom object type via an ETag-conditional " +
144
144
  "PATCH /custom_object_definitions/{type}/attribute_definitions/{id}. Sandbox-only, " +
145
- "destructive; surfaces 409/412 conflicts and echoes grant JSON on 403.",
145
+ "destructive; surfaces 409/412 conflicts. 403 paste-ready grant JSON.",
146
146
  inputSchema: updateCustomObjectAttributeDefinitionInput,
147
147
  annotations: WRITE_ANNOTATIONS,
148
148
  }, buildUpdateCustomObjectAttributeDefinitionHandler(gateDeps));
@@ -92,9 +92,10 @@ export const SITE_PREFERENCE_WRITE_TOOL_NAMES = ["site_preference_values_set"];
92
92
  export function registerSitePreferenceWriteTools(registerTool, deps) {
93
93
  const { gateDeps } = deps;
94
94
  registerTool("site_preference_values_set", {
95
- description: "Set custom site-preference VALUES via PATCH /site_preferences/preference_groups/{group}/sandbox. " +
96
- "Sandbox-only, destructive; body is a flat map of c_-prefixed ids to string/number/boolean/string[] " +
97
- "values. A bad group returns 404 CustomPreferenceGroupNotFoundException; echoes grant JSON on 403.",
95
+ description: "Set site-preference VALUES via PATCH /site_preferences/preference_groups/{group}/sandbox. " +
96
+ "Preference must already be defined via custom_preference_definition_create. Sandbox-only, " +
97
+ "destructive; flat c_-prefixed id→value map (string/number/boolean/string[]). " +
98
+ "Bad group → 404 CustomPreferenceGroupNotFoundException. 403 → paste-ready grant JSON.",
98
99
  inputSchema: sitePreferenceValuesSetInput,
99
100
  annotations: WRITE_ANNOTATIONS,
100
101
  }, buildSitePreferenceValuesSetHandler(gateDeps));
@@ -276,42 +276,43 @@ export function registerSystemObjectWriteTools(registerTool, deps) {
276
276
  registerTool("system_object_attribute_definition_create", {
277
277
  description: "Create a system-object attribute definition (sandbox-only, destructive). " +
278
278
  "PUT .../attribute_definitions/{id}; body id must equal URL id and system must be false. " +
279
- "Returns 201. HTTP 403 echoes the OCAPI write-grant JSON.",
279
+ "Returns 201. 403 paste-ready grant JSON.",
280
280
  inputSchema: createAttributeDefinitionInput,
281
281
  annotations: WRITE_ANNOTATIONS,
282
282
  }, buildCreateAttributeDefinitionHandler(gateDeps));
283
283
  registerTool("system_object_attribute_definition_update", {
284
284
  description: "Update a system-object attribute definition (sandbox-only, destructive). " +
285
285
  "PATCH .../attribute_definitions/{id} via the ETag GET-then-If-Match round trip. " +
286
- "Returns 200; surfaces 409/412 on ETag conflicts. HTTP 403 echoes the write-grant JSON.",
286
+ "Returns 200; surfaces 409/412 on ETag conflicts. 403 paste-ready grant JSON.",
287
287
  inputSchema: updateAttributeDefinitionInput,
288
288
  annotations: WRITE_ANNOTATIONS,
289
289
  }, buildUpdateAttributeDefinitionHandler(gateDeps));
290
290
  registerTool("system_object_attribute_group_create", {
291
291
  description: "Create a system-object attribute group (sandbox-only, destructive). " +
292
292
  "PUT .../attribute_groups/{id} with a minimal body (display_name + internal). " +
293
- "HTTP 403 echoes the OCAPI write-grant JSON.",
293
+ "403 paste-ready grant JSON.",
294
294
  inputSchema: createAttributeGroupInput,
295
295
  annotations: WRITE_ANNOTATIONS,
296
296
  }, buildCreateAttributeGroupHandler(gateDeps));
297
297
  registerTool("system_object_attribute_group_update", {
298
298
  description: "Update a system-object attribute group (sandbox-only, destructive). " +
299
299
  "PATCH .../attribute_groups/{id} via the ETag round trip (display_name and/or internal). " +
300
- "HTTP 403 echoes the OCAPI write-grant JSON.",
300
+ "403 paste-ready grant JSON.",
301
301
  inputSchema: updateAttributeGroupInput,
302
302
  annotations: WRITE_ANNOTATIONS,
303
303
  }, buildUpdateAttributeGroupHandler(gateDeps));
304
304
  registerTool("system_object_attribute_assign_to_group", {
305
305
  description: "Assign an attribute definition to an attribute group (sandbox-only, destructive). " +
306
306
  "PUT .../attribute_groups/{group}/attribute_definitions/{def} with an empty body. " +
307
- "HTTP 403 echoes the OCAPI write-grant JSON.",
307
+ "403 paste-ready grant JSON.",
308
308
  inputSchema: assignAttributeToGroupInput,
309
309
  annotations: WRITE_ANNOTATIONS,
310
310
  }, buildAssignAttributeToGroupHandler(gateDeps));
311
311
  registerTool("custom_preference_definition_create", {
312
312
  description: "Create a custom preference definition (sandbox-only, destructive). " +
313
313
  "PUT .../{SitePreferences|OrganizationPreferences}/attribute_definitions/{id}. " +
314
- "Booleans resolve to null unless default_value is set. HTTP 403 echoes the write-grant JSON.",
314
+ "Booleans resolve to null unless default_value is set. After defining the schema, " +
315
+ "set its value with site_preference_values_set. 403 → paste-ready grant JSON.",
315
316
  inputSchema: createCustomPreferenceDefinitionInput,
316
317
  annotations: WRITE_ANNOTATIONS,
317
318
  }, buildCreateCustomPreferenceDefinitionHandler(gateDeps));
@@ -77,8 +77,17 @@ export function buildEpicIdentityEnv(epic) {
77
77
  // ---------------------------------------------------------------------------
78
78
  /** Default gate name when no `BAPI_CONDUCTOR_GATE_NAME` override is present. */
79
79
  export const DEFAULT_CONDUCTOR_GATE_NAME = "implement-ticket";
80
- /** Resolve a packaged build artifact path relative to this compiled module. */
81
- function defaultResolveBinPath(filename) {
80
+ /**
81
+ * Resolve a packaged build artifact path relative to this compiled module.
82
+ *
83
+ * Exported since BAPI-1020 so the executor resolves the worker-guard bin through
84
+ * the SAME resolver rather than growing a second one. Correct in both build
85
+ * shapes because this module compiles to the build ROOT: the esbuild bundle puts
86
+ * `import.meta.url` at `build/index.js` / `build/conductor-bin.js`, and `tsc` puts
87
+ * it at `build/start-tickets-conductor.js`. A resolver living in a subdirectory
88
+ * module would resolve to `build/<subdir>/<filename>` under `tsc` and be wrong.
89
+ */
90
+ export function defaultResolveBinPath(filename) {
82
91
  return fileURLToPath(new URL(`./${filename}`, import.meta.url));
83
92
  }
84
93
  function nonEmpty(value) {
@@ -54,7 +54,7 @@
54
54
  * unit-testable on Linux CI without spawning real commands or terminals.
55
55
  */
56
56
  import { execFile } from "child_process";
57
- import { readFile, writeFile, mkdir, mkdtemp, stat, readdir, rm } from "fs/promises";
57
+ import { readFile, writeFile, mkdir, mkdtemp, stat, lstat, symlink, unlink, copyFile, readdir, rm, } from "fs/promises";
58
58
  import os from "node:os";
59
59
  import path from "path";
60
60
  import { VERSION } from "./version.generated.js";
@@ -965,6 +965,10 @@ export async function runWithConcurrency(items, limit, worker) {
965
965
  // imports of these names from `./start-tickets.js` keep resolving to the exact
966
966
  // same function references.
967
967
  import { resolveBranchForTicket, branchExists, buildWtSwitchArgs, pathApiForPlatform, extractWorktreePath, isExistingBranchSafeToReuse, createWorktreeForTicket, } from "./worktree-core.js";
968
+ // BAPI-1019: interactive worktrees get the operator's `.env`/`.env.test` back as
969
+ // SYMLINKS after `.config/wt.toml` stopped copying them. Imported HERE, in the
970
+ // interactive orchestration module, and deliberately nowhere under `executor/`.
971
+ import { linkOperatorEnvFiles } from "./env-file-link.js";
968
972
  export { resolveBranchForTicket, branchExists, buildWtSwitchArgs, pathApiForPlatform, extractWorktreePath, isExistingBranchSafeToReuse, createWorktreeForTicket, };
969
973
  /**
970
974
  * BAPI-941: emit the per-spawn `claude-review` workflow-drift advisory.
@@ -1033,7 +1037,70 @@ export async function createWorktrees(deps, options, worktrunkBinary, baseStartP
1033
1037
  const behavior = exactBase
1034
1038
  ? { alignExistingBranchTo: baseStartPoint, verifyHeadMatches: baseStartPoint }
1035
1039
  : {};
1036
- return runWithConcurrency(options.keys, options.maxParallel, (key) => createWorktreeForTicket(deps, key, options.branchOverrides, worktrunkBinary, baseStartPoint, options.guardStaleWorktree === true, behavior));
1040
+ const link = deps.linkOperatorEnvFiles ?? linkOperatorEnvFiles;
1041
+ const linkDeps = buildEnvFileLinkDeps(deps);
1042
+ return runWithConcurrency(options.keys, options.maxParallel, async (key) => {
1043
+ const row = await createWorktreeForTicket(deps, key, options.branchOverrides, worktrunkBinary, baseStartPoint, options.guardStaleWorktree === true, behavior);
1044
+ // BAPI-1019 — re-provide the operator's env files by LINK, and only here.
1045
+ //
1046
+ // `.config/wt.toml` no longer copies `.env*` into any worktree, which is what
1047
+ // keeps a conductor worker from ever holding a real database target. An
1048
+ // interactive worktree still needs that configuration, so it gets a symlink to
1049
+ // the main checkout's file instead of a second copy at rest.
1050
+ //
1051
+ // `deps.cwd` is the canonical main checkout — the same value every other
1052
+ // Worktrunk/git call in this module resolves against — rather than a path
1053
+ // inferred from the new worktree, which would be a guess.
1054
+ //
1055
+ // Deliberately NOT in `worktree-core.ts` or command provisioning: the executor
1056
+ // shares those seams, and an executor worktree must receive neither a copy nor
1057
+ // a link. Keeping the call in this interactive orchestration function is what
1058
+ // makes that separation structural instead of a comment.
1059
+ if (row.status !== "created" || typeof row.path !== "string" || row.path.length === 0) {
1060
+ return row;
1061
+ }
1062
+ try {
1063
+ const linked = await link(deps.cwd, row.path, linkDeps);
1064
+ return linked.warnings.reduce((acc, warning) => appendSummaryRowWarning(acc, warning.message), row);
1065
+ }
1066
+ catch {
1067
+ // The linker already bounds its own failures; this catch covers a seam that
1068
+ // throws anyway (an injected fake, a future refactor). The worktree itself
1069
+ // was created successfully, so the row stays `created` and carries a fixed
1070
+ // warning — never the exception text, which can embed an absolute path.
1071
+ return appendSummaryRowWarning(row, "worktree environment files: could not be provided from the main checkout, so this " +
1072
+ "worktree has none. Commands that need them will fail until they are linked by hand.");
1073
+ }
1074
+ });
1075
+ }
1076
+ /**
1077
+ * Build the interactive env-file linker's filesystem bag from this module's real
1078
+ * `fs/promises` boundary (BAPI-1019), mirroring
1079
+ * {@link buildCommandProvisioningDeps}.
1080
+ *
1081
+ * `stat`/`lstat` are adapted to resolve `null` for a missing path rather than
1082
+ * rejecting, because "absent" is the ordinary case the linker branches on — most
1083
+ * checkouts have `.env` and no `.env.test`. There is no `readFile` here and there
1084
+ * must never be one: the linker's contract is that an environment file's contents
1085
+ * are never read.
1086
+ */
1087
+ export function buildEnvFileLinkDeps(deps) {
1088
+ const metadata = (probe) => async (target) => {
1089
+ try {
1090
+ return await probe(target);
1091
+ }
1092
+ catch {
1093
+ return null;
1094
+ }
1095
+ };
1096
+ return {
1097
+ stat: metadata((target) => stat(target)),
1098
+ lstat: metadata((target) => lstat(target)),
1099
+ symlink: (target, linkPath) => symlink(target, linkPath),
1100
+ unlink: (target) => unlink(target),
1101
+ copyFile: (source, destination) => copyFile(source, destination),
1102
+ platform: deps.platform,
1103
+ };
1037
1104
  }
1038
1105
  /**
1039
1106
  * Resume-mode worktree resolution (BAPI-441). Instead of creating worktrees,
@@ -1,4 +1,4 @@
1
1
  // AUTO-GENERATED — do not edit manually. Regenerate with: npm run build
2
- export const VERSION = "0.2.50";
3
- export const BUILD_COMMIT = "4b5d3e65b2a3";
4
- export const LAUNCHER_ARGS = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.50", "serve"];
2
+ export const VERSION = "0.2.52";
3
+ export const BUILD_COMMIT = "82534a279ae4";
4
+ export const LAUNCHER_ARGS = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.52", "serve"];
@@ -0,0 +1,97 @@
1
+ /**
2
+ * The `doctor` "Worker containment" advisory section (BAPI-1019, A.7).
3
+ *
4
+ * Reports the DECLARED containment posture a conductor worker runs under: that
5
+ * the executor strips operator environment files from a worker's worktree before
6
+ * it spawns, that a worker's database environment names a target that does not
7
+ * exist, and which permission posture the run policy defaults to.
8
+ *
9
+ * ZERO PROBES, and that is a hard constraint rather than an efficiency choice.
10
+ * `doctor` pins that an unsupported platform issues zero command probes (see the
11
+ * note on `collectLocalCliOverrideDiagnostic` in `doctor.ts`), and a section that
12
+ * probed unconditionally would break that guarantee for every platform. So this
13
+ * module runs no command, opens no file, makes no network call, and reads no
14
+ * environment variable. It has no dependency bag at all — there is nothing to
15
+ * inject because there is nothing to do.
16
+ *
17
+ * WHAT THAT MEANS FOR THE READER, stated plainly in the rendered copy: these are
18
+ * DECLARED constants, not measurements. The section says what this build does,
19
+ * not what it just observed on this host. Rendering a static policy as though a
20
+ * live verification had run would be worse than printing nothing — an operator
21
+ * would read "Yes" as evidence about their machine. The real per-spawn proof is
22
+ * the executor's own fail-closed refusal (`ContractError.WorkerEnvFilePresent`),
23
+ * which happens at spawn time on the host that actually runs workers.
24
+ *
25
+ * It also prints no secret and no identity: no sentinel URL (it carries
26
+ * `postgresql://` credential syntax and would teach a reader to paste it
27
+ * somewhere), no path, no username, no discovered filename beyond the generic
28
+ * `.env*`.
29
+ */
30
+ import { DEFAULT_WORKER_PERMISSION_POSTURE } from "./executor/worker-command.js";
31
+ /**
32
+ * Return the declared containment posture.
33
+ *
34
+ * Takes no dependencies and performs no I/O. The two booleans are `true` because
35
+ * this build unconditionally does both things — they are not feature flags, and
36
+ * there is deliberately no configuration that can turn either off. The posture
37
+ * comes from the executor's own exported default so this report and the runtime
38
+ * cannot drift.
39
+ */
40
+ export function collectWorkerContainmentDiagnostic() {
41
+ return {
42
+ envFilesStripped: true,
43
+ sentinelDatabaseEnv: true,
44
+ permissionPosture: DEFAULT_WORKER_PERMISSION_POSTURE,
45
+ };
46
+ }
47
+ /** Rendered when a value is not a usable declaration. */
48
+ const UNAVAILABLE = "Not confirmed";
49
+ /**
50
+ * Column width for the aligned label/value rows. Wide enough that the longest
51
+ * label (`Sentinel DB environment:`) still leaves a gap before its value.
52
+ */
53
+ const LABEL_WIDTH = 26;
54
+ function row(label, value) {
55
+ return `${`${label}:`.padEnd(LABEL_WIDTH)}${value}`;
56
+ }
57
+ function yesNo(value) {
58
+ if (value === true)
59
+ return "Yes";
60
+ if (value === false)
61
+ return "No";
62
+ return UNAVAILABLE;
63
+ }
64
+ /**
65
+ * Render the section (pure formatting), mirroring
66
+ * `formatLocalCliOverrideDiagnosticReport`.
67
+ *
68
+ * Accepts `unknown` on purpose. The caller wraps this in the advisory `try` that
69
+ * never changes doctor's exit code, and a formatter that threw on a malformed
70
+ * input would make the section's own failure mode louder than the thing it
71
+ * reports. A value it cannot read renders as "Not confirmed" — never as "Yes",
72
+ * and never as a claim that a probe ran and came back empty.
73
+ */
74
+ export function formatWorkerContainmentDiagnosticReport(diagnostic) {
75
+ const d = diagnostic && typeof diagnostic === "object"
76
+ ? diagnostic
77
+ : {};
78
+ const posture = typeof d.permissionPosture === "string" ? d.permissionPosture : UNAVAILABLE;
79
+ return [
80
+ "",
81
+ "Worker containment (conductor executor)",
82
+ "",
83
+ row("Env files stripped", yesNo(d.envFilesStripped)),
84
+ row("Sentinel DB environment", yesNo(d.sentinelDatabaseEnv)),
85
+ row("Permission posture", posture),
86
+ "",
87
+ " A conductor worker never inherits this checkout's .env* files: the executor",
88
+ " removes them from the worker's worktree as the last step before it starts,",
89
+ " and refuses to start at all if it cannot confirm they are gone. The worker's",
90
+ " database settings name a target that does not exist, so code that resolves a",
91
+ " database from the environment reaches nothing rather than something real.",
92
+ " Permission posture is the run policy's default and is informational here.",
93
+ " Declared posture, not a live check: this section runs no command, reads no",
94
+ " file, and makes no network call. The per-spawn proof is the executor's own",
95
+ " refusal on the host that runs workers, not this line.",
96
+ ].join("\n");
97
+ }
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import{readFileSync}from"node:fs";import path from"node:path";var WORKER_GUARD_REASON_PREFIX="bridge worker guard",WORKER_GUARD_MALFORMED_LINE=`${WORKER_GUARD_REASON_PREFIX}: payload could not be read; continuing fail-open`;function renderWorkerGuardReason(category,family){return`${WORKER_GUARD_REASON_PREFIX}: denied ${family} (${category}). This command family is restricted to the worker's own branch, worktree, and read-only network access. Use a different approach and continue.`}var UNSAFE_UNQUOTED=new Set(["$","`",">","<"]),SEPARATORS=new Set([";","|","&"]);function tokenizeShellCommand(command){let commands=[],argv=[],current="",hasCurrent=!1,unsafe=!1,endToken=()=>{hasCurrent&&(argv.push(current),current="",hasCurrent=!1)},endCommand=()=>{endToken(),(argv.length>0||unsafe)&&commands.push({argv,unsafe}),argv=[],unsafe=!1};for(let i=0;i<command.length;i+=1){let ch=command[i];if(ch==="\\"){let next=command[i+1];next!==void 0&&(current+=next,hasCurrent=!0,i+=1);continue}if(ch==="'"){let close=command.indexOf("'",i+1);if(close===-1){unsafe=!0,current+=command.slice(i+1),hasCurrent=!0;break}current+=command.slice(i+1,close),hasCurrent=!0,i=close;continue}if(ch==='"'){let j=i+1,body="",closed=!1;for(;j<command.length;){let c=command[j];if(c==="\\"&&j+1<command.length){body+=command[j+1],j+=2;continue}if(c==='"'){closed=!0;break}(c==="$"||c==="`")&&(unsafe=!0),body+=c,j+=1}closed||(unsafe=!0),current+=body,hasCurrent=!0,i=j;continue}if(UNSAFE_UNQUOTED.has(ch)){unsafe=!0,current+=ch,hasCurrent=!0;continue}if(SEPARATORS.has(ch)){endCommand();continue}if(ch===`
3
+ `){endCommand();continue}if(ch===" "||ch===" "||ch==="\r"){endToken();continue}current+=ch,hasCurrent=!0}return endCommand(),commands}var WORKER_GUARD_MAX_PAYLOAD_BYTES=1e6;function asRecord(value){return value!==null&&typeof value=="object"&&!Array.isArray(value)?value:void 0}function isInside(root,candidate){let rel=path.relative(root,candidate);return rel===""?!0:rel.startsWith("..")?!1:!path.isAbsolute(rel)}function resolveEffectiveCwd(ctx,payloadCwd){let root=ctx.worktreePath;if(typeof root!="string"||!path.isAbsolute(root))return;let normalizedRoot=path.resolve(root);if(payloadCwd===void 0)return normalizedRoot;if(typeof payloadCwd!="string"||payloadCwd.length===0)return;let resolved=path.isAbsolute(payloadCwd)?path.resolve(payloadCwd):path.resolve(normalizedRoot,payloadCwd);return isInside(normalizedRoot,resolved)?resolved:void 0}function allTargetsContained(ctx,cwd,targets){let root=ctx.worktreePath;if(typeof root!="string"||!path.isAbsolute(root)||cwd===void 0)return!1;let normalizedRoot=path.resolve(root);return targets.every(target=>{if(target.length===0||target==="~"||target.startsWith("~/"))return!1;let resolved=path.isAbsolute(target)?path.resolve(target):path.resolve(cwd,target);return isInside(normalizedRoot,resolved)})}function parseGitInvocation(argv,baseCwd){let cwd=baseCwd,unsafe=!1,i=1;for(;i<argv.length;){let token=argv[i];if(token==="-C"){let dir=argv[i+1];dir===void 0||cwd===void 0?(unsafe=!0,cwd=void 0):cwd=path.isAbsolute(dir)?path.resolve(dir):path.resolve(cwd,dir),i+=2;continue}if(token==="-c"||token==="--namespace"||token==="--git-dir"||token==="--work-tree"){unsafe=!0,i+=2;continue}if(token.startsWith("-")){i+=1;continue}break}return{subcommand:argv[i]??"",args:argv.slice(i+1),cwd,unsafe}}function isOwnBranchDestination(destination,workerBranch){let colon=destination.indexOf(":"),dst=colon===-1?destination:destination.slice(colon+1);if(dst.length===0)return!1;let bare=dst.startsWith("+")?dst.slice(1):dst;return bare===workerBranch||bare===`refs/heads/${workerBranch}`}function evaluateGitPush(args,ctx){let deny={kind:"deny",category:"git-push",family:"git"},positional=[];for(let i=0;i<args.length;i+=1){let token=args[i];if(token==="--"){positional.push(...args.slice(i+1));break}if(token.startsWith("-")){if(token==="--tags"||token==="--delete"||token==="-d"||token==="--mirror"||token==="--all"||token==="--follow-tags"||token.startsWith("--repo"))return deny;(token==="-o"||token==="--push-option"||token==="--receive-pack"||token==="--exec")&&(i+=1);continue}positional.push(token)}if(positional.length<=1)return{kind:"allow"};let workerBranch=ctx.workerBranch;return typeof workerBranch!="string"||workerBranch.length===0?deny:positional.slice(1).every(d=>isOwnBranchDestination(d,workerBranch))?{kind:"allow"}:deny}function evaluateGitReset(args,ctx){let deny={kind:"deny",category:"git-reset-hard",family:"git"};if(!args.includes("--hard"))return{kind:"allow"};let positional=[];for(let i=0;i<args.length;i+=1){let token=args[i];if(token==="--"){positional.push(...args.slice(i+1));break}token.startsWith("-")||positional.push(token)}if(positional.length===0)return{kind:"allow"};let workerBranch=ctx.workerBranch;if(typeof workerBranch!="string"||workerBranch.length===0)return deny;let allowed=new Set([workerBranch,`origin/${workerBranch}`,`refs/heads/${workerBranch}`,`refs/remotes/origin/${workerBranch}`]);return positional.every(ref=>allowed.has(ref))?{kind:"allow"}:deny}function evaluateGitClean(args,ctx,cwd){let deny={kind:"deny",category:"filesystem-clean",family:"git"};if(cwd===void 0||!allTargetsContained(ctx,cwd,[cwd]))return deny;let pathspecs=[];for(let i=0;i<args.length;i+=1){let token=args[i];if(token==="--"){pathspecs.push(...args.slice(i+1));break}if(token==="-e"||token==="--exclude"){i+=1;continue}token.startsWith("-")||pathspecs.push(token)}return pathspecs.length===0?{kind:"allow"}:allTargetsContained(ctx,cwd,pathspecs)?{kind:"allow"}:deny}function isRecursiveForcedRemove(args){let recursive=!1,force=!1;for(let token of args){if(token==="--")break;if(token==="--recursive")recursive=!0;else if(token==="--force")force=!0;else{if(token.startsWith("--"))continue;if(token.startsWith("-")&&token.length>1)for(let flag of token.slice(1))(flag==="r"||flag==="R")&&(recursive=!0),flag==="f"&&(force=!0)}}return recursive&&force}function evaluateRemove(argv,ctx,cwd){let deny={kind:"deny",category:"filesystem-remove",family:"rm"},args=argv.slice(1);if(!isRecursiveForcedRemove(args))return{kind:"allow"};let targets=[],sawDoubleDash=!1;for(let token of args){if(!sawDoubleDash&&token==="--"){sawDoubleDash=!0;continue}!sawDoubleDash&&token.startsWith("-")&&token.length>1||targets.push(token)}return targets.length===0?deny:allTargetsContained(ctx,cwd,targets)?{kind:"allow"}:deny}function isDatabaseCommand(executable){let base=path.basename(executable);return base==="psql"||base.startsWith("pg_")}var DESTRUCTIVE_SQL=/\b(?:DROP|TRUNCATE)\b/i;function evaluateDatabase(argv){let deny={kind:"deny",category:"database-destructive",family:"psql"};return argv.slice(1).some(arg=>DESTRUCTIVE_SQL.test(arg))?deny:{kind:"allow"}}var MUTATING_METHODS=new Set(["POST","PUT","PATCH","DELETE"]);function evaluateNetwork(argv){let deny={kind:"deny",category:"network-mutating",family:"curl"},base=path.basename(argv[0]??""),args=argv.slice(1);for(let i=0;i<args.length;i+=1){let token=args[i];if(token==="-X"||token==="--request"||token==="--method"){let value=args[i+1];if(value===void 0||MUTATING_METHODS.has(value.toUpperCase()))return deny;i+=1;continue}if(token.startsWith("-X")&&token.length>2){if(MUTATING_METHODS.has(token.slice(2).toUpperCase()))return deny;continue}if(token.startsWith("--request=")||token.startsWith("--method=")){let value=token.slice(token.indexOf("=")+1);if(MUTATING_METHODS.has(value.toUpperCase()))return deny;continue}if(token==="--data"||token.startsWith("--data=")||token.startsWith("--data-")||token==="--form"||token==="-F"||token==="--upload-file"||token==="-T"||token==="--post-data"||token.startsWith("--post-data=")||token==="--post-file"||token.startsWith("--post-file=")||token==="--body-data"||token.startsWith("--body-data=")||token==="--body-file"||token.startsWith("--body-file=")||base==="curl"&&token==="-d")return deny}return{kind:"allow"}}function classify(executable){let base=path.basename(executable);return base==="git"?"git":base==="rm"?"rm":isDatabaseCommand(base)?"database":base==="curl"||base==="wget"?"network":null}function unsafeSegmentDecision(family){switch(family){case"git":return{kind:"deny",category:"git-push",family:"git"};case"rm":return{kind:"deny",category:"filesystem-remove",family:"rm"};case"database":return{kind:"deny",category:"database-destructive",family:"psql"};case"network":return{kind:"deny",category:"network-mutating",family:"curl"}}}var TRANSPARENT_PREFIXES=new Set(["sudo","env","command","nohup","time","nice","exec"]),STDIN_ARGUMENT_PREFIXES=new Set(["xargs"]),SHELL_EXECUTABLES=new Set(["sh","bash","zsh","dash","ksh"]),EVAL_BUILTINS=new Set(["eval"]);function unwrapTransparentPrefixes(argv){let rest=argv;for(let depth=0;depth<8;depth+=1){let head=rest[0];if(head===void 0||!TRANSPARENT_PREFIXES.has(path.basename(head)))return rest;let i=1;for(;i<rest.length;){let token=rest[i];if(path.basename(head)==="env"&&/^[A-Za-z_][A-Za-z0-9_]*=/.test(token)){i+=1;continue}if(token.startsWith("-")){i+=1;continue}break}rest=rest.slice(i)}return rest}function stripStdinPrefixOptions(argv){let VALUE_OPTS=new Set(["-I","-i","-L","-n","-P","-s","-d","-E","-e","-a","--replace","--max-lines","--max-args","--max-procs","--max-chars","--delimiter","--eof","--arg-file"]),i=1;for(;i<argv.length;){let token=argv[i];if(token==="--"){i+=1;break}if(!token.startsWith("-"))break;if(token.includes("=")){i+=1;continue}i+=VALUE_OPTS.has(token)?2:1}return argv.slice(i)}function evaluateSimpleCommand(segment,ctx,payloadCwd,depth=0,stdinFed=!1){let argv=unwrapTransparentPrefixes(segment.argv),executable=argv[0];if(executable===void 0)return{kind:"allow"};if(SHELL_EXECUTABLES.has(path.basename(executable))&&depth<4){let cIndex=argv.findIndex(t=>t==="-c"),inner=cIndex===-1?void 0:argv[cIndex+1];if(inner!==void 0){for(let nested of tokenizeShellCommand(inner)){let decision=evaluateSimpleCommand(nested,ctx,payloadCwd,depth+1,stdinFed);if(decision.kind==="deny")return decision}return{kind:"allow"}}}if(EVAL_BUILTINS.has(path.basename(executable))&&depth<4){for(let nested of tokenizeShellCommand(argv.slice(1).join(" "))){let decision=evaluateSimpleCommand(nested,ctx,payloadCwd,depth+1,stdinFed);if(decision.kind==="deny")return decision}return{kind:"allow"}}if(STDIN_ARGUMENT_PREFIXES.has(path.basename(executable))&&depth<4){let rest=stripStdinPrefixOptions(argv);return rest.length===0?{kind:"allow"}:evaluateSimpleCommand({argv:rest,unsafe:segment.unsafe},ctx,payloadCwd,depth+1,!0)}segment={argv,unsafe:segment.unsafe};let family=classify(executable);if(family===null)return{kind:"allow"};if(stdinFed||segment.unsafe)return unsafeSegmentDecision(family);let cwd=resolveEffectiveCwd(ctx,payloadCwd);if(family==="rm")return evaluateRemove(segment.argv,ctx,cwd);if(family==="database")return evaluateDatabase(segment.argv);if(family==="network")return evaluateNetwork(segment.argv);let git=parseGitInvocation(segment.argv,cwd);return git.subcommand==="push"?git.unsafe?{kind:"deny",category:"git-push",family:"git"}:evaluateGitPush(git.args,ctx):git.subcommand==="reset"?git.unsafe?{kind:"deny",category:"git-reset-hard",family:"git"}:evaluateGitReset(git.args,ctx):git.subcommand==="clean"?git.unsafe?{kind:"deny",category:"filesystem-clean",family:"git"}:evaluateGitClean(git.args,ctx,git.cwd):{kind:"allow"}}function evaluateWorkerGuardPayload(payload,ctx){let root=asRecord(payload);if(root===void 0)return{kind:"malformed"};let toolName=root.tool_name??root.toolName;if(typeof toolName!="string")return{kind:"malformed"};if(toolName!=="Bash")return{kind:"allow"};let input=asRecord(root.tool_input??root.toolInput);if(input===void 0)return{kind:"malformed"};let command=input.command;if(typeof command!="string")return{kind:"malformed"};let rawCwd=root.cwd??input.cwd,payloadCwd=typeof rawCwd=="string"?rawCwd:void 0;for(let segment of tokenizeShellCommand(command)){let decision=evaluateSimpleCommand(segment,ctx,payloadCwd);if(decision.kind==="deny")return decision}return{kind:"allow"}}function evaluateWorkerGuardInput(raw,ctx){if(typeof raw!="string")return{kind:"malformed"};if(raw.length>WORKER_GUARD_MAX_PAYLOAD_BYTES)return{kind:"malformed"};if(raw.trim().length===0)return{kind:"malformed"};let parsed;try{parsed=JSON.parse(raw)}catch{return{kind:"malformed"}}try{return evaluateWorkerGuardPayload(parsed,ctx)}catch{return{kind:"malformed"}}}var WORKER_GUARD_ROOT_FLAG="--worktree-root";function resolveWorkerGuardContext(env,cwd,argv=[]){let ctx={},root;for(let i=0;i<argv.length;i+=1){if(argv[i]===WORKER_GUARD_ROOT_FLAG){root=argv[i+1];break}if(argv[i].startsWith(`${WORKER_GUARD_ROOT_FLAG}=`)){root=argv[i].slice(WORKER_GUARD_ROOT_FLAG.length+1);break}}root===void 0&&typeof cwd=="string"&&(root=cwd),typeof root=="string"&&path.isAbsolute(root)&&(ctx.worktreePath=path.resolve(root));let branch=env.BAPI_WORKER_BRANCH;return typeof branch=="string"&&branch.trim().length>0&&(ctx.workerBranch=branch.trim()),ctx}function readStdinSync(){return readFileSync(0,"utf-8")}var exitCode=0;try{let raw=readStdinSync(),ctx=resolveWorkerGuardContext(process.env,process.cwd(),process.argv.slice(2)),decision=evaluateWorkerGuardInput(raw,ctx);decision.kind==="deny"?(process.stderr.write(`${renderWorkerGuardReason(decision.category,decision.family)}
4
+ `),exitCode=2):decision.kind==="malformed"&&(process.stderr.write(`${WORKER_GUARD_MALFORMED_LINE}
5
+ `),exitCode=0)}catch{try{process.stderr.write(`${WORKER_GUARD_MALFORMED_LINE}
6
+ `)}catch{}exitCode=0}process.exit(exitCode);
package/docs/CONDUCTOR.md CHANGED
@@ -64,6 +64,33 @@ pipeline step. `setup-epic` validates it locally (unique ticket keys, resolvable
64
64
  `depends_on`/edge references, acyclicity) before sending anything, so a malformed
65
65
  plan fails legibly instead of as a bare HTTP 400.
66
66
 
67
+ ### Branch strategy: the epic branch is the default (BAPI-1009)
68
+
69
+ The command above — no branch flag — now runs a **multi-ticket** epic on a
70
+ dedicated `epic/<KEY>` branch. Every child-ticket PR targets that branch, and one
71
+ **draft** integration PR targets the repository base branch. This applies on every
72
+ path, CI and `--json` included. `drive-epic` forwards both flags below to
73
+ `setup-epic`, so the universal entry point can select either strategy.
74
+
75
+ | You want | Pass |
76
+ |---|---|
77
+ | The epic branch on a plan with **2 or more** nodes | *nothing* — it is the default |
78
+ | Your own branch name, or a branch for a **single-node** plan | `--feature-branch <name>` |
79
+ | The old direct-to-base behavior | `--into-base` |
80
+
81
+ > **Behavior change.** Before BAPI-1009 a run with no branch flag went straight to
82
+ > the repository base branch. Automation that relied on that must now pass
83
+ > `--into-base`. A single-node plan still keeps the base branch by default, matching
84
+ > plain `start-tickets`. `--feature-branch` and `--into-base` together are a parse
85
+ > error, refused before any file read, credential resolution, or network call.
86
+
87
+ On an epic-branch run, a shadow-index verification that cannot be proven
88
+ immediately resolves as bounded **hold-then-park** rather than blocking the run:
89
+ independent tickets proceed, a dependent holds, and past the deadline it parks
90
+ through the ordinary `needs_human` path. The epic run's gate snapshot carries a
91
+ run-level `shadow_hold` object naming the reason and the recovery while that is
92
+ live. This addresses BAPI-993 run issue A1.
93
+
67
94
  Since BAPI-754 it then calls the server's **real** validator
68
95
  (`POST /jira/epic-runs/plan/validate`) before creating anything, so a plan the
69
96
  server would reject — a node missing `status`, say, which the local checks do not
@@ -159,6 +159,7 @@ by tier.
159
159
  | Tool | Dependencies (class) |
160
160
  |---|---|
161
161
  | `second_opinion` | — none (pure LLM; repo access only) |
162
+ | `clarify_task` | No Jira ticket, no repository connection required · VCS **[DEGRADE]** (a connected repository adds code-aware questions) |
162
163
  | `generate_image` | — none beyond an image provider |
163
164
  | `visual_diff` | — none (`LOCAL` render + pixel diff; needs a reachable `target_url`) |
164
165
  | `request_deep_research` / `get_deep_research` | Deep-research flag **[BLOCK]** (403 if `deep_research_enabled` off) |
@@ -301,8 +302,8 @@ wanting both selects `sfcc,sfcc-write` (or `full`).
301
302
  |---|---|---|
302
303
  | **Ticket backend** (jira mode) | `create_ticket`, `get_ticket(s)`, `update_ticket_description`, `update_jira_status`, and every AI generator (they read the ticket) | — |
303
304
  | **Jira (only)** | `get_comments`, `add_comment`, `attachment`, `estimate_epic` | — |
304
- | **Version control (VCS)** | `create_pull_request`, `merge_pull_request`, `resolve_ci_checks`, `poll_ci_checks`, `fresh_base`, `parse_repository`, `regenerate_directory_map`; **Tier-3** `request_plan_generation`/`get_plan`, `request_reimplement_context`/`get_reimplement_context`, `create_doc`(tdd/architecture) | **Tier-4** `request_ticket_review`, `create_doc`(prd/fsd) |
305
- | **Code index** (succeeded parse) | Tier-3 plan/reimplement/`create_doc`(tdd/architecture); `request_council` in `technical`/`discovery` modes | Tier-4 review/prd/fsd |
305
+ | **Version control (VCS)** | `create_pull_request`, `merge_pull_request`, `resolve_ci_checks`, `poll_ci_checks`, `fresh_base`, `parse_repository`, `regenerate_directory_map`; **Tier-3** `request_plan_generation`/`get_plan`, `request_reimplement_context`/`get_reimplement_context`, `create_doc`(tdd/architecture) | **Tier-4** `request_ticket_review`, `create_doc`(prd/fsd), `clarify_task` |
306
+ | **Code index** (succeeded parse) | Tier-3 plan/reimplement/`create_doc`(tdd/architecture); `request_council` in `technical`/`discovery` modes | Tier-4 review/prd/fsd, `clarify_task` |
306
307
  | **SFCC OCAPI** | `check_permissions` + every gated SFCC read and write tool | — |
307
308
  | **SFCC WebDAV logs** | `sfcc_log_query` | — |
308
309
  | **Deep-research flag** | `request_deep_research`, `get_deep_research` | — |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bridge_gpt/mcp-server",
3
- "version": "0.2.50",
3
+ "version": "0.2.52",
4
4
  "description": "Bridge API MCP server — exposes Jira endpoints as MCP tools for Claude Code agents",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -8,7 +8,8 @@
8
8
  "mcp-server": "./build/index.js",
9
9
  "bridge-api-mcp-server": "./build/index.js",
10
10
  "conductor": "./build/conductor-bin.js",
11
- "conductor-claude-hook": "./build/conductor-claude-hook-bin.js"
11
+ "conductor-claude-hook": "./build/conductor-claude-hook-bin.js",
12
+ "bridge-worker-guard": "./build/worker-guard-hook-bin.js"
12
13
  },
13
14
  "files": [
14
15
  "build/",
@@ -26,6 +27,7 @@
26
27
  "build": "node scripts/bundle-version.js && node scripts/sync-readme-host-examples.js --check && node scripts/validate-packaged-command-references.js && node scripts/bundle-readme.js && node scripts/bundle-pipelines.js && node scripts/bundle-commands.js && node scripts/bundle-agents.js && node scripts/bundle-docs.js && tsc && node scripts/sync-agent-mirrors.js && node scripts/bundle-esbuild.js",
27
28
  "version": "node scripts/sync-readme-host-examples.js --write && git add -- README.md package.json package-lock.json && git commit -m \"chore(mcp-server): release $npm_package_version\" && git tag \"mcp-server/v$npm_package_version\"",
28
29
  "check:version-generated": "node scripts/bundle-version.js && node scripts/check-version-generated.js",
30
+ "assert:release-identity": "node scripts/assert-release-identity.js",
29
31
  "postbuild": "node scripts/prepend-shebang.cjs",
30
32
  "start": "node build/index.js",
31
33
  "test": "npm run test:normal && npm run test:module-mocks",
@@ -34,7 +36,7 @@
34
36
  "test:integration": "node scripts/run-integration-tests.js",
35
37
  "test:smoke": "node --test build/integration/packaged-cli-smoke.test.js",
36
38
  "canary:agent-capabilities": "npm run build && node scripts/agent-capabilities-canary.mjs",
37
- "prepublishOnly": "node scripts/bundle-assets.js && npm run build && node scripts/verify-shebang.cjs"
39
+ "prepublishOnly": "node scripts/bundle-assets.js && npm run build && node scripts/verify-shebang.cjs && npm run assert:release-identity"
38
40
  },
39
41
  "dependencies": {
40
42
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -45,6 +45,11 @@
45
45
  "instruction_file": "explore-epic-subtasks.md",
46
46
  "description": "Perform focused exploration for each sub-task"
47
47
  },
48
+ {
49
+ "type": "agent_task",
50
+ "instruction_file": "assess-conductor-eligibility.md",
51
+ "description": "Assessing conductor eligibility"
52
+ },
48
53
  {
49
54
  "type": "agent_task",
50
55
  "instruction_file": "write-epic-summary.md",