@bridge_gpt/mcp-server 0.2.49 → 0.2.51

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 (62) hide show
  1. package/README.md +25 -8
  2. package/build/base-ref.js +28 -3
  3. package/build/claude-review-workflow-drift-probe.js +130 -0
  4. package/build/claude-review-workflow-drift.js +173 -0
  5. package/build/claude-review-workflow.js +81 -16
  6. package/build/commands.generated.js +5 -5
  7. package/build/conduct-epic/bridge-client.js +115 -1
  8. package/build/conduct-epic/cli.js +351 -33
  9. package/build/conduct-epic/cut-protocol.js +51 -0
  10. package/build/conductor/done-gate.js +25 -3
  11. package/build/conductor/install-doctor.js +65 -5
  12. package/build/conductor/latest-check-selector.js +170 -0
  13. package/build/conductor/local-merge.js +8 -6
  14. package/build/conductor-bin.js +1 -1
  15. package/build/{brainstorm-files.js → council-files.js} +15 -15
  16. package/build/decision-page-schema.js +1 -1
  17. package/build/docs.generated.js +1 -1
  18. package/build/doctor.js +352 -4
  19. package/build/epic-integration-pr.js +280 -0
  20. package/build/executor/job-runner.js +7 -1
  21. package/build/executor/merge-job.js +46 -1
  22. package/build/executor/worktree.js +46 -1
  23. package/build/index.js +153 -65
  24. package/build/init.js +9 -2
  25. package/build/install-bridge.js +60 -2
  26. package/build/install-reexec.js +47 -9
  27. package/build/pipelines.generated.js +8 -2
  28. package/build/plan-epic-conductor-eligibility.js +183 -0
  29. package/build/plane/cli.js +12 -2
  30. package/build/plane/manifest.js +25 -1
  31. package/build/plane/member-roster.js +61 -7
  32. package/build/plane/preflight.js +24 -9
  33. package/build/plane/supervisor.js +77 -5
  34. package/build/plane/types.js +23 -3
  35. package/build/readme.generated.js +1 -1
  36. package/build/run-unit-tests-launcher.js +2 -1
  37. package/build/setup-epic.js +32 -0
  38. package/build/sfcc/reads-custom-object-def.js +10 -13
  39. package/build/sfcc/reads-site-preference.js +5 -5
  40. package/build/sfcc/reads-system-object.js +4 -4
  41. package/build/sfcc/writes-custom-object-def.js +7 -7
  42. package/build/sfcc/writes-site-preference.js +4 -3
  43. package/build/sfcc/writes-system-object.js +7 -6
  44. package/build/stale-worktree-doctor.js +120 -0
  45. package/build/start-tickets-prereqs.js +70 -0
  46. package/build/start-tickets.js +91 -3
  47. package/build/version.generated.js +3 -2
  48. package/package.json +6 -3
  49. package/pipelines/plan-epic.json +5 -0
  50. package/build/chain-orchestrator.js +0 -1457
  51. package/build/chain-utils.js +0 -68
  52. package/build/command-catalog.js +0 -376
  53. package/build/schedule-run.js +0 -1300
  54. package/build/schedule-store.js +0 -172
  55. package/build/scheduled-prompt.js +0 -115
  56. package/build/scheduler-backends/at-fallback.js +0 -139
  57. package/build/scheduler-backends/escaping.js +0 -143
  58. package/build/scheduler-backends/index.js +0 -72
  59. package/build/scheduler-backends/launchd.js +0 -225
  60. package/build/scheduler-backends/systemd-user.js +0 -250
  61. package/build/scheduler-backends/task-scheduler.js +0 -214
  62. package/build/scheduler-backends/types.js +0 -23
@@ -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));
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Read-only stale-branch diagnostic (BAPI-948).
3
+ *
4
+ * `start-tickets --guard-stale-branch` (and `--conductor`, which implies it) refuses
5
+ * to reuse a pre-existing `feature/<KEY>` branch whose tip is not an ancestor of the
6
+ * resolved base. That refusal is correct but it only tells you AFTER you tried to cut
7
+ * the worktree. This module answers the same question BEFORE the cut, and without
8
+ * touching a single ref.
9
+ *
10
+ * It owns no ancestry logic of its own. `branchExists` and
11
+ * `isExistingBranchSafeToReuse` from `./worktree-core.js` are the exact predicates the
12
+ * guard itself runs, so a `would-refuse` finding here and a `create-failed` row there
13
+ * can never disagree — and the operator sentence is the same sentence in both places.
14
+ * Deliberately NOT a second ahead-count / diff / merge-base implementation: a
15
+ * diagnostic that re-derives the condition is a diagnostic that can be wrong about it.
16
+ *
17
+ * Strictly read-only. The only commands it can reach are the two the predicates issue
18
+ * — `git show-ref --verify --quiet`, `git rev-parse --verify --quiet`, and
19
+ * `git merge-base --is-ancestor` — none of which mutate anything. There is no fetch,
20
+ * reset, switch, checkout, branch deletion, or worktree creation on any path here, and
21
+ * nothing in this module writes a file.
22
+ */
23
+ import { branchExists, isExistingBranchSafeToReuse } from "./worktree-core.js";
24
+ /**
25
+ * The one sentence an `indeterminate` finding renders. Fixed text: the underlying
26
+ * throw is swallowed rather than formatted, so no probe failure can smuggle command
27
+ * output into a report.
28
+ */
29
+ export const STALE_WORKTREE_INDETERMINATE_REASON = "the read-only branch check could not complete (git was unavailable or failed to run); " +
30
+ "re-run the diagnostic, or inspect the branch by hand with " +
31
+ "`git merge-base --is-ancestor <branch> <base>`.";
32
+ /**
33
+ * Diagnose each target independently, read-only.
34
+ *
35
+ * Independence is the point: one unreadable branch must not blank out its siblings'
36
+ * findings, which is why every target is wrapped on its own rather than the loop as a
37
+ * whole. Targets are evaluated in the order given so the rendered report is stable.
38
+ */
39
+ export async function diagnoseStaleWorktreeBranches(deps, targets) {
40
+ const findings = [];
41
+ for (const target of targets) {
42
+ const { key, branch, baseStartPoint } = target;
43
+ try {
44
+ // No local branch → nothing pre-existing for the guard to act on. Return before
45
+ // the ancestry probe: running it would be a wasted command AND would produce a
46
+ // "not an ancestor" reason about a branch that is not there.
47
+ if (!(await branchExists(deps, branch))) {
48
+ findings.push({ key, branch, baseStartPoint, status: "branch-absent" });
49
+ continue;
50
+ }
51
+ // The SAME predicate `createWorktreeForTicket` runs, with the same branch and
52
+ // the same base start point, so the verdict and its remedy are identical to the
53
+ // refusal the guarded cut would emit.
54
+ const safety = await isExistingBranchSafeToReuse(deps, branch, baseStartPoint);
55
+ if (safety.safe) {
56
+ findings.push({ key, branch, baseStartPoint, status: "safe" });
57
+ continue;
58
+ }
59
+ findings.push({
60
+ key,
61
+ branch,
62
+ baseStartPoint,
63
+ status: "would-refuse",
64
+ reason: safety.reason,
65
+ });
66
+ }
67
+ catch {
68
+ findings.push({
69
+ key,
70
+ branch,
71
+ baseStartPoint,
72
+ status: "indeterminate",
73
+ reason: STALE_WORKTREE_INDETERMINATE_REASON,
74
+ });
75
+ }
76
+ }
77
+ return findings;
78
+ }
79
+ /**
80
+ * Render the advisory section. Advisory in the strict doctor sense: the caller never
81
+ * derives an exit code from it.
82
+ *
83
+ * One row per target, each naming the ticket, the resolved branch, and the base it was
84
+ * measured against — an operator diagnosing an epic cut needs all three to act. A
85
+ * `WOULD REFUSE` row is followed by the predicate's remedy on its own indented line
86
+ * rather than being folded into the row, because that sentence is the thing the
87
+ * operator has to read.
88
+ */
89
+ export function formatStaleWorktreeDiagnosticReport(findings) {
90
+ const lines = ["", "Stale ticket branches (advisory)"];
91
+ if (findings.length === 0) {
92
+ lines.push(" SKIP No --stale-branch targets supplied; nothing was checked.");
93
+ return lines.join("\n");
94
+ }
95
+ for (const finding of findings) {
96
+ const where = `${finding.key}: ${finding.branch} vs ${finding.baseStartPoint}`;
97
+ switch (finding.status) {
98
+ case "branch-absent":
99
+ lines.push(` OK ${where} — no local branch; a fresh one is cut from the base.`);
100
+ break;
101
+ case "safe":
102
+ lines.push(` OK ${where} — branch is an ancestor of the base; safe to reuse.`);
103
+ break;
104
+ case "would-refuse":
105
+ lines.push(` WARN ${where} — WOULD REFUSE.`);
106
+ if (finding.reason)
107
+ lines.push(` ${finding.reason}`);
108
+ break;
109
+ case "indeterminate":
110
+ lines.push(` WARN ${where} — indeterminate.`);
111
+ if (finding.reason)
112
+ lines.push(` ${finding.reason}`);
113
+ break;
114
+ }
115
+ }
116
+ lines.push(" Read-only: this section reads current local refs only. It never fetches,");
117
+ lines.push(" resets, switches, deletes, or repairs any git state, and never changes the");
118
+ lines.push(" exit code.");
119
+ return lines.join("\n");
120
+ }
@@ -19,6 +19,11 @@ 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
21
  import { MCP_SERVER_NAME, MCP_PACKAGE_NAME } from "./mcp-identity.js";
22
+ // BAPI-941: both are LEAF modules (the pure classifier and its git-backed
23
+ // acquisition), so importing them here cannot close a cycle with `base-ref.ts`,
24
+ // which imports `commandSucceeded` from this module.
25
+ import { CLAUDE_REVIEW_WORKFLOW_DRIFT_REMEDIATION, CLAUDE_REVIEW_WORKFLOW_RELPATH, summarizeClaudeReviewWorkflowDrift, } from "./claude-review-workflow-drift.js";
26
+ import { probeClaudeReviewWorkflowDrift, resolveCurrentBranch, resolveRepositoryDefaultBranch, } from "./claude-review-workflow-drift-probe.js";
22
27
  import { CLAUDE_MCP_SHADOWING_REMEDIATION_COMMAND, claudeMcpShadowingRemediationCommand, formatClaudeMcpShadowFinding, formatClaudeUserConfigDiagnostic, inspectClaudeUserConfigForMcpShadowing, resolveClaudeUserConfigPath, } from "./claude-user-config-doctor.js";
23
28
  // ---------------------------------------------------------------------------
24
29
  // Constants (moved here from start-tickets.ts so both consumers share them)
@@ -717,6 +722,67 @@ export function liveSourceGuardDescriptor() {
717
722
  },
718
723
  };
719
724
  }
725
+ /** Secret-free remediation hint for the workflow-drift diagnostic. */
726
+ const CLAUDE_REVIEW_WORKFLOW_DRIFT_INSTALL_HINTS = {
727
+ darwin: CLAUDE_REVIEW_WORKFLOW_DRIFT_REMEDIATION,
728
+ linux: CLAUDE_REVIEW_WORKFLOW_DRIFT_REMEDIATION,
729
+ win32: CLAUDE_REVIEW_WORKFLOW_DRIFT_REMEDIATION,
730
+ };
731
+ /**
732
+ * BAPI-941 doctor-only, strictly read-only probe: does the current branch's copy
733
+ * of the reviewed workflow match the repository default branch's copy?
734
+ *
735
+ * Answers, without spawning a worker or creating a worktree, the question that
736
+ * otherwise only surfaces an hour into a conductor run: has this branch gone
737
+ * blind to automated code review? `anthropics/claude-code-action` refuses to run
738
+ * when the invoking workflow differs from the default branch's copy, and a
739
+ * long-lived `epic/**` branch inherits that staleness into every pull request
740
+ * based on it — while the pull requests themselves change no workflow file at
741
+ * all, so no path-based guard fires.
742
+ *
743
+ * **Read-only, and network-free.** It runs `git symbolic-ref`, `git rev-parse`,
744
+ * and two `git show` invocations against the local object database. It never
745
+ * fetches, checks out, installs, migrates, creates a worktree, or writes a
746
+ * credential.
747
+ *
748
+ * **Never blocking.** `doctor`'s exit code is
749
+ * `results.some((r) => !r.found) ? 1 : 0`, so this probe reports `found: true`
750
+ * for ALL THREE outcomes and carries the state in `detail`. Confirmed drift is an
751
+ * advisory an operator should act on, not a missing prerequisite that fails the
752
+ * command — and an unavailable comparison is uncertainty, which is even less of
753
+ * a fault. Registered in `getDoctorOnlyPrereqDescriptors`, never in
754
+ * `getPreflightPrereqDescriptors`.
755
+ *
756
+ * **Bounded output.** Every rendered string comes from the shared classifier's
757
+ * closed vocabulary plus refs and the workflow path. Subprocess stderr, GitHub
758
+ * exception text, and file content never reach the report.
759
+ */
760
+ export function claudeReviewWorkflowDriftDescriptor() {
761
+ return {
762
+ id: "claude-review-workflow-drift",
763
+ label: "claude-review workflow lineage",
764
+ installHint: CLAUDE_REVIEW_WORKFLOW_DRIFT_INSTALL_HINTS,
765
+ probe: async (deps) => {
766
+ const probeDeps = { runCommand: deps.runCommand, cwd: deps.cwd };
767
+ const baseRef = await resolveCurrentBranch(probeDeps);
768
+ if (!baseRef) {
769
+ // A detached HEAD has no branch whose lineage to report. Inconclusive,
770
+ // not a fault.
771
+ return {
772
+ found: true,
773
+ detail: `comparison unavailable (base_ref_unresolved) — advisory check failed open, ` +
774
+ `${CLAUDE_REVIEW_WORKFLOW_RELPATH} lineage unknown`,
775
+ };
776
+ }
777
+ const defaultRef = await resolveRepositoryDefaultBranch(probeDeps);
778
+ const classification = await probeClaudeReviewWorkflowDrift(probeDeps, {
779
+ baseRef,
780
+ defaultRef,
781
+ });
782
+ return { found: true, detail: summarizeClaudeReviewWorkflowDrift(classification) };
783
+ },
784
+ };
785
+ }
720
786
  /**
721
787
  * The exact existing live-preflight requirement set per platform, plus the git
722
788
  * work-tree custom check appended after the command checks. This is the ONLY
@@ -765,6 +831,10 @@ export function getDoctorOnlyPrereqDescriptors(_platform, _env, agent) {
765
831
  reviewTicketsGitDescriptor(),
766
832
  liveSourceGuardDescriptor(),
767
833
  claudeMcpShadowingDescriptor(),
834
+ // BAPI-941: advisory workflow-lineage diagnostic. Doctor-only by design —
835
+ // adding it to `getPreflightPrereqDescriptors` would make a stale base
836
+ // branch block worktree creation, which is the opposite of fail-open.
837
+ claudeReviewWorkflowDriftDescriptor(),
768
838
  ];
769
839
  }
770
840
  /**
@@ -60,6 +60,9 @@ import path from "path";
60
60
  import { VERSION } from "./version.generated.js";
61
61
  import { resolveBapiCredentials, getPrimaryCredentialStorePath, } from "./credential-store.js";
62
62
  import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
63
+ // BAPI-941: the shared drift diagnostic formatter. The leaf drift module imports
64
+ // nothing from here, so this direction stays acyclic.
65
+ import { formatClaudeReviewWorkflowDriftDiagnostic } from "./claude-review-workflow-drift.js";
63
66
  import { resolveStartTicketsRepoName as resolveSharedStartTicketsRepoName, resolveRequiredStartTicketsRepoName, } from "./start-tickets-repo.js";
64
67
  import { provisionMcpRegistrationsForCreatedWorktrees, } from "./mcp-provisioning.js";
65
68
  import { provisionCommandsForCreatedWorktrees, } from "./command-provisioning.js";
@@ -132,7 +135,8 @@ export function getStartTicketsUsage() {
132
135
  " --base-branch BRANCH Cut new worktrees from BRANCH and refresh origin/BRANCH (default: main)",
133
136
  " --no-refresh-main Skip refresh of the configured base branch (default main); historical name retained for backward compatibility",
134
137
  " --max-parallel N Max worktrees to create concurrently (default: 3)",
135
- " --conductor Enable the Conductor system: per-worker hook injection + BAPI_CONDUCTOR_* env, lifecycle ledger events, and a supervisor peer tab (default: off — a plain run just spawns cd <worktree> && <agent> '/implement-ticket <KEY>')",
138
+ " --conductor Enable the Conductor system: per-worker hook injection + BAPI_CONDUCTOR_* env, lifecycle ledger events, and a supervisor peer tab (default: off — a plain run just spawns cd <worktree> && <agent> '/implement-ticket <KEY>'). Implies --guard-stale-branch: a pre-existing ticket branch carrying commits beyond the resolved base is refused, not silently reused.",
139
+ " --guard-stale-branch Refuse to reuse a pre-existing feature/KEY branch whose tip is NOT an ancestor of the resolved base (it carries commits the base does not). The ticket is reported create-failed with the stale-worktree remedy instead of spawning a worker on leftover code. Implied by --conductor; pass it explicitly on the LLM-conductor pilot cut path. Off by default, so a plain interactive run keeps reusing a same-named branch.",
136
140
  " -h, --help Show this help",
137
141
  "",
138
142
  "Environment:",
@@ -176,6 +180,11 @@ export function parseStartTicketsArgs(argv) {
176
180
  let agentName = DEFAULT_AGENT_NAME;
177
181
  let baseBranch = "main";
178
182
  let conductorEnabled = false;
183
+ // BAPI-948: `--guard-stale-branch` (and `--conductor`, which implies it) opt the
184
+ // packaged CLI into the F7 stale-branch guard. Tracked separately from
185
+ // `conductorEnabled` so the pilot can take the safety check WITHOUT taking the
186
+ // v2 conductor context that `--conductor` also switches on.
187
+ let guardStaleBranch = false;
179
188
  let workflow = "implement";
180
189
  let reviewRoundsRaw;
181
190
  // BAPI-642 `--tier` override. `undefined` = flag absent (property omitted from
@@ -358,6 +367,10 @@ export function parseStartTicketsArgs(argv) {
358
367
  conductorEnabled = true;
359
368
  continue;
360
369
  }
370
+ if (arg === "--guard-stale-branch") {
371
+ guardStaleBranch = true;
372
+ continue;
373
+ }
361
374
  if (arg === "--no-refresh-main") {
362
375
  refreshMain = false;
363
376
  continue;
@@ -454,6 +467,11 @@ export function parseStartTicketsArgs(argv) {
454
467
  conductorEnabled,
455
468
  workflow,
456
469
  reviewRounds,
470
+ // BAPI-948: include `guardStaleWorktree` ONLY when the run asked for the F7
471
+ // guard — `--guard-stale-branch`, or `--conductor` which implies it. A plain
472
+ // interactive invocation leaves the property entirely absent (not `false`),
473
+ // so the reuse-friendly human option shape is byte-identical to before.
474
+ ...(guardStaleBranch || conductorEnabled ? { guardStaleWorktree: true } : {}),
457
475
  // BAPI-642: include the injected-tier property ONLY when `--tier` was
458
476
  // supplied, so an omitted flag leaves the legacy option shape untouched
459
477
  // (no own `injectedTier` key, not even `undefined`).
@@ -834,9 +852,23 @@ export async function refreshBaseBranch(deps, options) {
834
852
  return { ok: true };
835
853
  const baseBranch = options.baseBranch;
836
854
  const originRef = `origin/${baseBranch}`;
837
- const fetch = await deps.runCommand("git", ["fetch", "origin", baseBranch], {
855
+ // BAPI-941: refresh the DEFAULT branch alongside the base in ONE invocation so
856
+ // the caller's workflow-drift advisory reads an up-to-date `origin/<default>`
857
+ // without a second network round trip. Deduplicated when they are the same
858
+ // branch, and retried without the extra refspec on failure so this enrichment
859
+ // can never convert a working refresh into an error.
860
+ const defaultBranch = await resolveRepositoryDefaultBranch(deps);
861
+ const extraRefs = defaultBranch && defaultBranch !== baseBranch && validateBranchName(defaultBranch) === null
862
+ ? [defaultBranch]
863
+ : [];
864
+ let fetch = await deps.runCommand("git", ["fetch", "origin", baseBranch, ...extraRefs], {
838
865
  cwd: deps.cwd,
839
866
  });
867
+ if (!commandSucceeded(fetch) && extraRefs.length > 0) {
868
+ fetch = await deps.runCommand("git", ["fetch", "origin", baseBranch], {
869
+ cwd: deps.cwd,
870
+ });
871
+ }
840
872
  if (!commandSucceeded(fetch)) {
841
873
  return {
842
874
  ok: false,
@@ -934,12 +966,49 @@ export async function runWithConcurrency(items, limit, worker) {
934
966
  // same function references.
935
967
  import { resolveBranchForTicket, branchExists, buildWtSwitchArgs, pathApiForPlatform, extractWorktreePath, isExistingBranchSafeToReuse, createWorktreeForTicket, } from "./worktree-core.js";
936
968
  export { resolveBranchForTicket, branchExists, buildWtSwitchArgs, pathApiForPlatform, extractWorktreePath, isExistingBranchSafeToReuse, createWorktreeForTicket, };
969
+ /**
970
+ * BAPI-941: emit the per-spawn `claude-review` workflow-drift advisory.
971
+ *
972
+ * Called once at the shared post-base-resolution, pre-worktree choke point. It
973
+ * returns `void` and swallows everything: this diagnostic exists to tell an
974
+ * operator that their epic branch has gone blind to code review, and a
975
+ * diagnostic that can abort the run it is describing would be strictly worse
976
+ * than no diagnostic at all.
977
+ *
978
+ * All three outcomes continue to worktree creation:
979
+ *
980
+ * - `drifted` — a WARNING naming the stale base and the remedy, and saying
981
+ * plainly that work continues.
982
+ * - `unverified` — an INFO line naming which comparison was unavailable
983
+ * (missing ref, unreadable workflow, unresolved default branch).
984
+ * - `aligned` — silence. A clean spawn prints nothing and performs no
985
+ * additional network work beyond the fetch that already happened.
986
+ */
987
+ async function emitClaudeReviewWorkflowDriftAdvisory(deps, baseBranch, overrides) {
988
+ const warn = overrides.workflowDriftWarningLog ?? ((m) => console.error(m));
989
+ try {
990
+ const probe = overrides.probeWorkflowDrift ?? probeClaudeReviewWorkflowDrift;
991
+ const defaultRef = await resolveRepositoryDefaultBranch(deps);
992
+ const classification = await probe(deps, { baseRef: baseBranch, defaultRef });
993
+ if (classification.state === "aligned")
994
+ return;
995
+ warn(formatClaudeReviewWorkflowDriftDiagnostic(classification));
996
+ }
997
+ catch {
998
+ // A thrown probe is itself an unavailable comparison. Stay silent about the
999
+ // exception (it can echo absolute paths and provider text) and never let it
1000
+ // reach the caller.
1001
+ }
1002
+ }
937
1003
  // BAPI-586: base-ref resolution now lives in the focused `base-ref.ts` module so
938
1004
  // the executor can depend on it without pulling in this large CLI module. We
939
1005
  // import the VALUES here (so internal uses keep working) and re-export the same
940
1006
  // references so existing importers of `./start-tickets.js` (index.ts,
941
1007
  // review-tickets.ts) and the pinned start-tickets tests are unaffected.
942
- import { validateBranchName, fetchAndResolveBaseSha } from "./base-ref.js";
1008
+ import { validateBranchName, fetchAndResolveBaseSha,
1009
+ // BAPI-941: both live in the same leaf module so the interactive refresh path
1010
+ // and the non-mutating conductor path share one drift implementation.
1011
+ resolveRepositoryDefaultBranch, probeClaudeReviewWorkflowDrift, } from "./base-ref.js";
943
1012
  export { validateBranchName, fetchAndResolveBaseSha };
944
1013
  /**
945
1014
  * Create / switch worktrees for every ticket, throttled to `maxParallel`, using
@@ -2898,6 +2967,22 @@ export async function orchestrateStartTickets(deps, options, overrides = {}) {
2898
2967
  if (!refresh.ok)
2899
2968
  return { ok: false, error: refresh.error };
2900
2969
  }
2970
+ // BAPI-941: the `claude-review` workflow-drift advisory, emitted ONCE per
2971
+ // worker spawn at the one choke point both dispatch paths pass through, after
2972
+ // the base is resolved and before any worktree exists.
2973
+ //
2974
+ // Per-spawn timing is the whole point. `epic/BAPI-902` was cut clean and was
2975
+ // blinded 24 hours later by an unrelated commit on `main`, so a check that ran
2976
+ // only at `init`/`setup-epic` time would have passed and still let every
2977
+ // subsequent PR go unreviewed. Both refs were just refreshed by the fetch
2978
+ // above, so the comparison is local and costs no extra round trip.
2979
+ //
2980
+ // ADVISORY ONLY, and fail-open in every degraded case (unreachable remote,
2981
+ // missing ref, unresolvable default branch): it prints to stderr and the run
2982
+ // continues to worktree creation. It is not a `RunPolicy` key and not a
2983
+ // worker-emitted control signal — R14 rules 2 and 3 are about the conductor's
2984
+ // decision surface, and a spawner's warning is neither.
2985
+ await emitClaudeReviewWorkflowDriftAdvisory(deps, options.baseBranch, overrides);
2901
2986
  const createWorktreesFn = overrides.createWorktrees ?? createWorktrees;
2902
2987
  const provisionCommandsFn = overrides.provisionCommands ??
2903
2988
  ((rows, d) => provisionCommandsForCreatedWorktrees(rows, buildCommandProvisioningDeps(d)));
@@ -3113,6 +3198,9 @@ export async function runStartTicketsCli(argv, overrides = {}) {
3113
3198
  modelRoutingLog: log,
3114
3199
  modelRoutingWarningLog: errorLog,
3115
3200
  repoNameWarningLog: errorLog,
3201
+ // BAPI-941: the workflow-drift advisory goes to stderr like every other
3202
+ // spawner diagnostic, so it never contaminates the summary on stdout.
3203
+ workflowDriftWarningLog: errorLog,
3116
3204
  // Conductor is OPT-IN (BAPI-394): the three seams are supplied ONLY when the
3117
3205
  // user passes `--conductor`. Supplying `createConductorContext` activates the
3118
3206
  // conductor stage inside orchestrate (env injection + supervisor tab); the
@@ -1,3 +1,4 @@
1
1
  // AUTO-GENERATED — do not edit manually. Regenerate with: npm run build
2
- export const VERSION = "0.2.49";
3
- export const BUILD_COMMIT = "ccff42dd5212";
2
+ export const VERSION = "0.2.51";
3
+ export const BUILD_COMMIT = "3cdbee6cfd9b";
4
+ export const LAUNCHER_ARGS = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.51", "serve"];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bridge_gpt/mcp-server",
3
- "version": "0.2.49",
3
+ "version": "0.2.51",
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",
@@ -21,9 +21,12 @@
21
21
  "LICENSE"
22
22
  ],
23
23
  "//test": "The `test` script delegates to the bounded launcher (scripts/run-unit-tests.js → src/run-unit-tests-launcher.ts, BAPI-683), which discovers build/*.test.js at runtime and spawns them in size-bounded batches so the argv never exceeds Windows' ~8191-char command-line limit. Two determinism rules from the pre-launcher script are preserved INSIDE the launcher: (1) multi-file batches run WITHOUT --test-force-exit, because that flag makes node:test exit before aggregating per-subprocess summaries and the tally becomes nondeterministic (failures still surface and still set exit 1, but the count cannot be trusted as a completeness signal); (2) build/secret-safety.test.js leaks a handle that holds the event loop ~60s after its tests finish (they run in ~147ms), so the launcher quarantines it into its own single-file --test-force-exit invocation (FORCE_EXIT_QUARANTINE in src/run-unit-tests-launcher.ts) where the tally stays exact. Do NOT add --test-force-exit to batched invocations, and do NOT remove the quarantine, until the underlying handle leak is fixed.",
24
+ "//version": "BAPI-930: fired by `npm version <level>` AFTER npm rewrites package.json's version field, so the bumped version and the repinned README land in the SAME commit. A bump performed by hand-editing package.json instead of running `npm version` bypasses this hook entirely and must repin manually via `node scripts/sync-readme-host-examples.js` — the `--check` wired into `build` below is the backstop that catches that case.",
24
25
  "scripts": {
25
- "build": "node scripts/bundle-version.js && 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",
26
+ "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
+ "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\"",
26
28
  "check:version-generated": "node scripts/bundle-version.js && node scripts/check-version-generated.js",
29
+ "assert:release-identity": "node scripts/assert-release-identity.js",
27
30
  "postbuild": "node scripts/prepend-shebang.cjs",
28
31
  "start": "node build/index.js",
29
32
  "test": "npm run test:normal && npm run test:module-mocks",
@@ -32,7 +35,7 @@
32
35
  "test:integration": "node scripts/run-integration-tests.js",
33
36
  "test:smoke": "node --test build/integration/packaged-cli-smoke.test.js",
34
37
  "canary:agent-capabilities": "npm run build && node scripts/agent-capabilities-canary.mjs",
35
- "prepublishOnly": "node scripts/bundle-assets.js && npm run build && node scripts/verify-shebang.cjs"
38
+ "prepublishOnly": "node scripts/bundle-assets.js && npm run build && node scripts/verify-shebang.cjs && npm run assert:release-identity"
36
39
  },
37
40
  "dependencies": {
38
41
  "@modelcontextprotocol/sdk": "^1.29.0",