@bridge_gpt/mcp-server 0.2.34 → 0.2.36

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 (49) hide show
  1. package/README.md +456 -370
  2. package/build/agent-capabilities/probe-context.js +8 -1
  3. package/build/agent-capabilities/probes.js +7 -1
  4. package/build/agents.generated.js +1 -1
  5. package/build/claude-review-workflow.js +264 -0
  6. package/build/cli-release.js +53 -0
  7. package/build/commands.generated.js +4 -4
  8. package/build/conductor/bridge-api-client.js +215 -0
  9. package/build/conductor/deny-enforcement-preflight.js +1 -0
  10. package/build/conductor/done-gate.js +44 -5
  11. package/build/conductor/epic-reconcile.js +6 -0
  12. package/build/conductor/install-doctor.js +462 -0
  13. package/build/conductor-bin.js +3 -3
  14. package/build/conductor-bundle-artifacts.js +30 -9
  15. package/build/doctor.js +234 -1
  16. package/build/executor/cli.js +32 -5
  17. package/build/executor/credentials.js +45 -11
  18. package/build/executor/deps.js +14 -0
  19. package/build/executor/env.js +23 -6
  20. package/build/executor/index.js +4 -0
  21. package/build/executor/job-runner.js +119 -9
  22. package/build/executor/permissions.js +12 -2
  23. package/build/executor/preflight.js +95 -8
  24. package/build/executor/prompt-spec.js +51 -0
  25. package/build/executor/runner.js +15 -2
  26. package/build/executor/service-unit.js +876 -0
  27. package/build/executor/test-clock.js +8 -0
  28. package/build/executor/types.js +0 -17
  29. package/build/executor/worker-command.js +62 -9
  30. package/build/index.js +575 -143
  31. package/build/init.js +153 -51
  32. package/build/install-bridge-conductor.js +491 -0
  33. package/build/install-bridge.js +628 -175
  34. package/build/install-reexec.js +233 -0
  35. package/build/mcp-host-config.js +11 -1
  36. package/build/mcp-install-state.js +32 -0
  37. package/build/mcp-provisioning.js +22 -6
  38. package/build/pipelines.generated.js +14 -8
  39. package/build/readme.generated.js +1 -1
  40. package/build/run-unit-tests-launcher.js +257 -0
  41. package/build/setup-epic.js +117 -8
  42. package/build/upgrade-cli.js +1 -15
  43. package/build/version.generated.js +1 -1
  44. package/docs/CONDUCTOR.md +115 -4
  45. package/docs/install/mcp-tool-integrations.md +29 -21
  46. package/package.json +8 -5
  47. package/pipelines/implement-ticket.json +6 -1
  48. package/build/conductor/supervisor-judgment-python.js +0 -141
  49. package/build/conductor/supervisor-judgment.js +0 -215
@@ -985,6 +985,15 @@ export async function reconcileShadowMerge(access, request, fetchImpl = globalTh
985
985
  * authoritative commit-watermark verdict for one ready ticket. The caller treats
986
986
  * `covered`/`not_applicable` as dispatchable, `stale` as hold, and `failed` as a
987
987
  * terminal shadow failure (the backend has already persisted the blocked state).
988
+ *
989
+ * BAPI-678: `stale` now covers two backend situations, and deliberately does not
990
+ * distinguish them on the wire — ordinary unproven freshness, and a `failed`
991
+ * shadow lifecycle DOWNGRADED by the backend's `policy_json.shadow.warn_only`
992
+ * posture. Both mean exactly the same thing to this caller (hold, do not claim a
993
+ * dispatch key), which is why no new verdict was introduced. The backend also
994
+ * bounds the warn-only hold with its own stale deadline and parks the ticket if
995
+ * it expires, so a hold here is not unbounded even though nothing on this side
996
+ * tracks its age. See {@link ShadowFreshnessVerdict}.
988
997
  */
989
998
  export async function fetchShadowDispatchFreshness(access, request, fetchImpl = globalThis.fetch) {
990
999
  requireNonEmptyString(request.epicKey);
@@ -1019,6 +1028,38 @@ export async function storeEpicPlan(access, request, fetchImpl = globalThis.fetc
1019
1028
  });
1020
1029
  return fetchConductorJsonPostWithTimeout(url, conductorPostHeaders(access), body, CONDUCTOR_FETCH_TIMEOUT_MS, fetchImpl);
1021
1030
  }
1031
+ /**
1032
+ * PATCH one approved plan node's `ticket_spec` without re-storing or re-approving
1033
+ * the plan (BAPI-722).
1034
+ *
1035
+ * This is how a ticket specification too large for Jira's 32,767-character
1036
+ * description cap reaches the reviewer intact: it is stored on the approved plan
1037
+ * node, which the reconciler already consults FIRST when resolving a spec-review
1038
+ * prompt. Nothing else about the run changes — not the plan version, not the
1039
+ * approval pointers, and not a single `epic_ticket_status` row — which is what
1040
+ * makes it safe against a live epic.
1041
+ *
1042
+ * `expectedPlanHash` is the optimistic-concurrency token; a stale token on a real
1043
+ * content change returns HTTP 409. The API key travels ONLY in the `X-API-Key`
1044
+ * header, and neither the spec text nor any credential is ever incorporated into a
1045
+ * thrown error message. Errors surface as a sanitized
1046
+ * {@link ConductorBridgeApiError}.
1047
+ */
1048
+ export async function updateApprovedPlanNodeTicketSpec(access, request, fetchImpl = globalThis.fetch) {
1049
+ requireNonEmptyString(request.epicKey);
1050
+ requireNonEmptyString(request.ticketKey);
1051
+ requireNonEmptyString(request.expectedPlanHash);
1052
+ requireNonEmptyString(request.ticketSpec);
1053
+ const url = buildConductorJiraUrl(access.baseUrl, `${epicRunApiPath(request.epicKey)}/plan/nodes/` +
1054
+ `${encodeURIComponent(request.ticketKey)}/ticket-spec`);
1055
+ const body = JSON.stringify({
1056
+ repo_name: access.repoName,
1057
+ expected_plan_hash: request.expectedPlanHash,
1058
+ ticket_spec: request.ticketSpec,
1059
+ });
1060
+ const parsed = await fetchConductorJsonPatchWithTimeout(url, conductorPostHeaders(access), body, CONDUCTOR_FETCH_TIMEOUT_MS, fetchImpl);
1061
+ return parsed;
1062
+ }
1022
1063
  /**
1023
1064
  * Atomically approve a plan version. Bumps `epic_runs.approved_plan_hash` to
1024
1065
  * the named version's `plan_hash` in a single CAS write — never a
@@ -1203,3 +1244,177 @@ export async function transitionJiraStatus(access, ticketNumber, targetStatus =
1203
1244
  throw err;
1204
1245
  }
1205
1246
  }
1247
+ const READINESS_SOURCES = new Set(["epic", "project_default", "none"]);
1248
+ const ACTIONS_LEVELS = new Set(["write", "read", "none", "unknown"]);
1249
+ function readinessShapeError() {
1250
+ // Deliberately carries NO field name, value, or payload excerpt: a malformed
1251
+ // body is untrusted input and its contents must not reach output.
1252
+ return new ConductorBridgeApiError("invalid-input", undefined, {
1253
+ errorCode: "READINESS_SHAPE_INVALID",
1254
+ });
1255
+ }
1256
+ function requireObject(value) {
1257
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
1258
+ throw readinessShapeError();
1259
+ }
1260
+ return value;
1261
+ }
1262
+ function requireBool(o, key) {
1263
+ const v = o[key];
1264
+ if (typeof v !== "boolean")
1265
+ throw readinessShapeError();
1266
+ return v;
1267
+ }
1268
+ function requireInt(o, key) {
1269
+ const v = o[key];
1270
+ if (typeof v !== "number" || !Number.isInteger(v))
1271
+ throw readinessShapeError();
1272
+ return v;
1273
+ }
1274
+ function requireNullableInt(o, key) {
1275
+ const v = o[key];
1276
+ if (v === null || v === undefined)
1277
+ return null;
1278
+ if (typeof v !== "number" || !Number.isInteger(v))
1279
+ throw readinessShapeError();
1280
+ return v;
1281
+ }
1282
+ function requireNullableString(o, key) {
1283
+ const v = o[key];
1284
+ if (v === null || v === undefined)
1285
+ return null;
1286
+ if (typeof v !== "string")
1287
+ throw readinessShapeError();
1288
+ return v;
1289
+ }
1290
+ function requireNullableBool(o, key) {
1291
+ const v = o[key];
1292
+ if (v === null || v === undefined)
1293
+ return null;
1294
+ if (typeof v !== "boolean")
1295
+ throw readinessShapeError();
1296
+ return v;
1297
+ }
1298
+ function requireEnum(o, key, allowed) {
1299
+ const v = o[key];
1300
+ if (typeof v !== "string" || !allowed.has(v))
1301
+ throw readinessShapeError();
1302
+ return v;
1303
+ }
1304
+ /**
1305
+ * Validate a raw readiness body into the typed response, FAIL-CLOSED.
1306
+ *
1307
+ * Every field the composed doctor makes a safety decision on is checked for
1308
+ * presence and exact type. This is not defensive nicety: a 200 whose body is
1309
+ * missing `required_checks_empty`, or carries it as the string `"false"`, would
1310
+ * otherwise be cast straight into an apparently-healthy report and the operator
1311
+ * would be told a trivially-passing gate was fine.
1312
+ */
1313
+ export function parseConductorReadinessResponse(body) {
1314
+ const root = requireObject(body);
1315
+ const repoName = root.repo_name;
1316
+ if (typeof repoName !== "string" || repoName.length === 0)
1317
+ throw readinessShapeError();
1318
+ const sup = requireObject(root.supervisor);
1319
+ const gh = requireObject(root.github);
1320
+ const rec = requireObject(root.reconciler);
1321
+ const exec = requireObject(root.executor);
1322
+ const thr = requireObject(root.thresholds);
1323
+ return {
1324
+ repo_name: repoName,
1325
+ supervisor: {
1326
+ setup_present: requireBool(sup, "setup_present"),
1327
+ setup_source: requireEnum(sup, "setup_source", READINESS_SOURCES),
1328
+ setup_created_at: requireNullableString(sup, "setup_created_at"),
1329
+ setup_updated_at: requireNullableString(sup, "setup_updated_at"),
1330
+ config_present: requireBool(sup, "config_present"),
1331
+ config_source: requireEnum(sup, "config_source", READINESS_SOURCES),
1332
+ config_created_at: requireNullableString(sup, "config_created_at"),
1333
+ config_updated_at: requireNullableString(sup, "config_updated_at"),
1334
+ required_checks_count: requireInt(sup, "required_checks_count"),
1335
+ required_checks_empty: requireBool(sup, "required_checks_empty"),
1336
+ auto_merge_enabled: requireBool(sup, "auto_merge_enabled"),
1337
+ merge_approval_required_set: requireBool(sup, "merge_approval_required_set"),
1338
+ },
1339
+ github: {
1340
+ credentials_readable: requireBool(gh, "credentials_readable"),
1341
+ owner_resolved: requireBool(gh, "owner_resolved"),
1342
+ repo_id_resolved: requireBool(gh, "repo_id_resolved"),
1343
+ installation_id_resolved: requireBool(gh, "installation_id_resolved"),
1344
+ credentials_complete: requireBool(gh, "credentials_complete"),
1345
+ actions_probe_succeeded: requireBool(gh, "actions_probe_succeeded"),
1346
+ actions_permission_present: requireBool(gh, "actions_permission_present"),
1347
+ actions_permission_level: requireEnum(gh, "actions_permission_level", ACTIONS_LEVELS),
1348
+ actions_write: requireBool(gh, "actions_write"),
1349
+ },
1350
+ reconciler: {
1351
+ liveness_readable: requireBool(rec, "liveness_readable"),
1352
+ last_tick_at: requireNullableString(rec, "last_tick_at"),
1353
+ last_tick_age_seconds: requireNullableInt(rec, "last_tick_age_seconds"),
1354
+ stale: requireBool(rec, "stale"),
1355
+ active_run_count: requireInt(rec, "active_run_count"),
1356
+ expired_lease_count: requireInt(rec, "expired_lease_count"),
1357
+ },
1358
+ executor: {
1359
+ liveness_readable: requireBool(exec, "liveness_readable"),
1360
+ last_seen_at: requireNullableString(exec, "last_seen_at"),
1361
+ last_seen_age_seconds: requireNullableInt(exec, "last_seen_age_seconds"),
1362
+ ready: requireNullableBool(exec, "ready"),
1363
+ },
1364
+ thresholds: {
1365
+ reconciler_stale_after_seconds: requireInt(thr, "reconciler_stale_after_seconds"),
1366
+ executor_stale_after_seconds: requireInt(thr, "executor_stale_after_seconds"),
1367
+ },
1368
+ };
1369
+ }
1370
+ /**
1371
+ * GET `/jira/epic-runs/conductor-readiness?repo_name=<repo>`.
1372
+ *
1373
+ * The API key travels ONLY in `X-API-Key`; `repo_name` is URL-encoded into the
1374
+ * query string because the endpoint scopes on it. Fails closed on transport,
1375
+ * auth, and shape errors alike.
1376
+ */
1377
+ export async function fetchConductorReadiness(access, fetchImpl = globalThis.fetch) {
1378
+ const url = buildConductorJiraUrl(access.baseUrl, `${EPIC_RUNS_API_PREFIX}/conductor-readiness`, {
1379
+ repo_name: access.repoName,
1380
+ });
1381
+ const body = await fetchConductorJsonWithTimeout(url, conductorGetHeaders(access), CONDUCTOR_FETCH_TIMEOUT_MS, fetchImpl);
1382
+ return parseConductorReadinessResponse(body);
1383
+ }
1384
+ /** Validate a raw bootstrap body, fail-closed (see {@link parseConductorReadinessResponse}). */
1385
+ export function parseConductorSupervisorBootstrapResponse(body) {
1386
+ const o = requireObject(body);
1387
+ const repoName = o.repo_name;
1388
+ if (typeof repoName !== "string" || repoName.length === 0)
1389
+ throw readinessShapeError();
1390
+ const names = o.audited_field_names;
1391
+ if (!Array.isArray(names) || names.some((n) => typeof n !== "string")) {
1392
+ throw readinessShapeError();
1393
+ }
1394
+ return {
1395
+ repo_name: repoName,
1396
+ setup_written: requireBool(o, "setup_written"),
1397
+ config_written: requireBool(o, "config_written"),
1398
+ setup_source: requireEnum(o, "setup_source", READINESS_SOURCES),
1399
+ config_source: requireEnum(o, "config_source", READINESS_SOURCES),
1400
+ setup_created_at: requireNullableString(o, "setup_created_at"),
1401
+ setup_updated_at: requireNullableString(o, "setup_updated_at"),
1402
+ config_created_at: requireNullableString(o, "config_created_at"),
1403
+ config_updated_at: requireNullableString(o, "config_updated_at"),
1404
+ required_checks_count: requireInt(o, "required_checks_count"),
1405
+ audited_field_names: names,
1406
+ };
1407
+ }
1408
+ /**
1409
+ * POST `/jira/epic-runs/supervisor-bootstrap?repo_name=<repo>`.
1410
+ *
1411
+ * The ONLY write this client performs. Deliberately carries no `review_policy`
1412
+ * field: repository-default review policy is owned by BAPI-694.
1413
+ */
1414
+ export async function bootstrapConductorSupervisorDefaults(access, request, fetchImpl) {
1415
+ const url = buildConductorJiraUrl(access.baseUrl, `${EPIC_RUNS_API_PREFIX}/supervisor-bootstrap`, {
1416
+ repo_name: access.repoName,
1417
+ });
1418
+ const body = await fetchConductorJsonPostWithTimeout(url, conductorPostHeaders(access), JSON.stringify(request), CONDUCTOR_FETCH_TIMEOUT_MS, fetchImpl);
1419
+ return parseConductorSupervisorBootstrapResponse(body);
1420
+ }
@@ -47,6 +47,7 @@ export async function runDenyEnforcementPreflight(opts = {}) {
47
47
  const { result, layer } = await runDenyEnforcementCheck(ctx, {
48
48
  model: opts.model,
49
49
  timeoutMs: opts.timeoutMs,
50
+ permissionPosture: opts.permissionPosture,
50
51
  });
51
52
  if (result.status === "pass" && layer === "settings-deny") {
52
53
  return {
@@ -83,15 +83,49 @@ function parseCiChecksCondition(entry) {
83
83
  }
84
84
  return { type: REQUIRED_CI_CHECKS_GREEN, required_checks: normalized };
85
85
  }
86
- /** Valid review source values. */
87
- const VALID_REVIEW_SOURCES = new Set(["sticky_verdict", "native_review_decision", "min_approvals", "combination"]);
86
+ /** Valid — i.e. CANONICAL — review source values (BAPI-689). */
87
+ const VALID_REVIEW_SOURCES = new Set(["verdict_protocol", "native_review_decision", "min_approvals", "combination"]);
88
+ /**
89
+ * Historical review-source spellings accepted on INPUT only.
90
+ *
91
+ * BAPI-689 unified three vocabularies onto one, but `done_gate_config` is
92
+ * free-form operator-authored JSONB that this module only ever reads — there is
93
+ * no migration that rewrites it. So already-stored configs keep naming the
94
+ * sticky-verdict source `sticky_verdict` (this module's own former spelling) or
95
+ * `claude_review_sticky` / `github_review_decision` (the former supervisor
96
+ * spellings), and they must stay readable.
97
+ *
98
+ * Mapping is applied at the parser seam and the CANONICAL value is what the
99
+ * parsed condition carries, so no alias survives into the config hash, the gate
100
+ * evaluator, or any serialized output.
101
+ */
102
+ const REVIEW_SOURCE_ALIASES = Object.freeze({
103
+ sticky_verdict: "verdict_protocol",
104
+ claude_review_sticky: "verdict_protocol",
105
+ github_review_decision: "native_review_decision",
106
+ });
107
+ /**
108
+ * Canonicalize a raw stored review-source value.
109
+ *
110
+ * Returns the canonical spelling for a known alias, the value unchanged when it
111
+ * is already canonical, and the value unchanged when it is neither — so an
112
+ * unsupported spelling still reaches (and fails) `VALID_REVIEW_SOURCES` rather
113
+ * than being coerced into a default. Exact-match only: no case folding and no
114
+ * trimming, matching the strictness of every other field in this parser.
115
+ */
116
+ function normalizeReviewSource(source) {
117
+ return REVIEW_SOURCE_ALIASES[source] ?? source;
118
+ }
88
119
  /**
89
120
  * Parse and validate a single `review_state` condition entry.
90
121
  * Returns the normalized condition or `null` on any validation failure.
91
122
  */
92
123
  function parseReviewStateCondition(entry) {
93
- const source = entry.source;
94
- if (typeof source !== "string" || !VALID_REVIEW_SOURCES.has(source))
124
+ const rawSource = entry.source;
125
+ if (typeof rawSource !== "string")
126
+ return null;
127
+ const source = normalizeReviewSource(rawSource);
128
+ if (!VALID_REVIEW_SOURCES.has(source))
95
129
  return null;
96
130
  const condition = { type: REVIEW_STATE, source: source };
97
131
  if (entry.require_sticky_verdict !== undefined) {
@@ -425,7 +459,12 @@ export function evaluateReviewCondition(condition, snapshot) {
425
459
  return { passed: false, changesRequested: false, reason: "review snapshot unavailable" };
426
460
  }
427
461
  const source = condition.source;
428
- if (source === "sticky_verdict") {
462
+ // BAPI-689: the condition's `source` is always CANONICAL here — every path to a
463
+ // `ReviewStateCondition` goes through `parseReviewStateCondition`, which maps
464
+ // the historical spellings before validating. `snapshot.sticky_verdict` is a
465
+ // different thing entirely (the parsed value OF the sticky comment) and keeps
466
+ // its name.
467
+ if (source === "verdict_protocol") {
429
468
  if (snapshot.sticky_verdict === "approved")
430
469
  return { passed: true, changesRequested: false, reason: "sticky verdict approved" };
431
470
  if (snapshot.sticky_verdict === "changes_requested")
@@ -171,6 +171,12 @@ export async function reconcileEpic(access, observed, plan, deps, supervisorConf
171
171
  deps.log(`[epic-reconcile] shadow-freshness error for ${ticketKey}: ${safeDiagnosticMessage(err, "freshness error")}; holding`);
172
172
  }
173
173
  if (verdict === "stale") {
174
+ // BAPI-678: this covers ordinary unproven freshness AND the backend's
175
+ // warn-only downgrade of a `failed` shadow lifecycle. Identical handling
176
+ // is intentional — either way the watermark is unproven, so hold before
177
+ // claiming a dispatch key. The backend clocks the hold and parks the
178
+ // ticket if its own stale deadline expires, so "hold" is bounded there
179
+ // rather than repeating forever here.
174
180
  deps.log(`[epic-reconcile] holding ${ticketKey}: shadow watermark stale`);
175
181
  continue;
176
182
  }