@bridge_gpt/mcp-server 0.2.66 → 0.2.68

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 (171) hide show
  1. package/README.md +421 -83
  2. package/build/agent-capabilities/probes.js +15 -5
  3. package/build/agent-launchers/claude-executor-adapter.js +70 -16
  4. package/build/agent-launchers/executor-adapter.js +7 -4
  5. package/build/agents.generated.js +22 -2
  6. package/build/bitbucket-workspace-remote.js +136 -0
  7. package/build/bootstrap-project-identity.js +24 -0
  8. package/build/bounded-wait.js +67 -10
  9. package/build/bridge-permissions.js +829 -0
  10. package/build/claude-settings.js +18 -0
  11. package/build/cli-help.js +21 -0
  12. package/build/command-assets-doctor.js +24 -70
  13. package/build/command-provisioning.js +29 -5
  14. package/build/commands.generated.js +12 -12
  15. package/build/conductor/bridge-api-client.js +857 -1
  16. package/build/conductor/bring-up-facts.js +1 -1
  17. package/build/conductor/command-drift-production.js +92 -0
  18. package/build/conductor/command-drift.js +89 -0
  19. package/build/conductor/doctor.js +72 -29
  20. package/build/conductor/epic-branch-ci-inspection.js +1375 -0
  21. package/build/conductor/epic-implementer-cli.js +98 -28
  22. package/build/conductor/errors.js +4 -3
  23. package/build/conductor/gate-discovery.js +890 -0
  24. package/build/conductor/guided-repair.js +6 -2
  25. package/build/conductor/hook-runtime-qualification.js +1 -1
  26. package/build/conductor/install-doctor.js +15 -3
  27. package/build/conductor/json-output.js +63 -0
  28. package/build/conductor/merge-execution-contract.js +27 -8
  29. package/build/conductor/merge-execution-eligibility.js +3 -2
  30. package/build/conductor/pr-ci-producer.js +1 -1
  31. package/build/conductor/readiness-cli.js +113 -12
  32. package/build/conductor/readiness-sections.js +410 -9
  33. package/build/conductor/readiness.js +388 -33
  34. package/build/conductor/recovery-cli.js +310 -12
  35. package/build/conductor/recovery-operations.js +167 -3
  36. package/build/conductor/repair-contract.js +54 -3
  37. package/build/conductor/repository-workflow-remedy.js +125 -0
  38. package/build/conductor/tools.js +2 -2
  39. package/build/conductor-bundle-artifacts.js +8 -0
  40. package/build/connect-bitbucket-api.js +41 -6
  41. package/build/connect-bitbucket.js +106 -33
  42. package/build/credential-store.js +207 -47
  43. package/build/docs.generated.js +62 -3
  44. package/build/doctor.js +92 -1
  45. package/build/drive-epic-key.js +6 -5
  46. package/build/drive-epic.js +129 -61
  47. package/build/epic-coherence-input.js +2 -1
  48. package/build/epic-coherence-input.test-fixtures.js +4 -3
  49. package/build/epic-implementer-bin.js +1379 -28
  50. package/build/epic-implementer-bundle-cli.js +2 -2
  51. package/build/epic-workarounds-path.js +39 -0
  52. package/build/executor/cli.js +33 -2
  53. package/build/executor/completion-contract.js +66 -10
  54. package/build/executor/completion-supervision.js +4 -0
  55. package/build/executor/deps.js +4 -1
  56. package/build/executor/env.js +19 -0
  57. package/build/executor/gate-qualification-job.js +499 -0
  58. package/build/executor/guarded-fast-forward-push.js +129 -0
  59. package/build/executor/heartbeat.js +1 -1
  60. package/build/executor/http-client.js +20 -0
  61. package/build/executor/integration-job.js +122 -8
  62. package/build/executor/job-errors.js +33 -1
  63. package/build/executor/job-runner.js +813 -87
  64. package/build/executor/likely-tests.js +290 -0
  65. package/build/executor/live-worker-registry.js +1 -1
  66. package/build/executor/observation.js +37 -3
  67. package/build/executor/packaged-recipe.js +106 -0
  68. package/build/executor/permissions.js +115 -28
  69. package/build/executor/platform.js +1 -1
  70. package/build/executor/preflight.js +112 -26
  71. package/build/executor/process-containment.js +36 -12
  72. package/build/executor/process.js +15 -7
  73. package/build/executor/prompt-spec.js +33 -1
  74. package/build/executor/recently-failed-tests-file.js +71 -0
  75. package/build/executor/reconcile-overlap-job.js +41 -23
  76. package/build/executor/required-verification.js +1 -1
  77. package/build/executor/required-work-ledger.js +111 -18
  78. package/build/executor/resume-pre-spawn.js +2 -0
  79. package/build/executor/retirement.js +69 -0
  80. package/build/executor/runner.js +122 -13
  81. package/build/executor/service-unit.js +7 -1
  82. package/build/executor/test-clock.js +15 -0
  83. package/build/executor/types.js +32 -0
  84. package/build/executor/viewer-tabs.js +58 -8
  85. package/build/executor/worker-command.js +23 -7
  86. package/build/executor/worker-diagnostics.js +37 -0
  87. package/build/executor/worker-finalization.js +113 -34
  88. package/build/executor/worker-guard-hook.js +316 -58
  89. package/build/executor/worker-log.js +4 -3
  90. package/build/executor/worker-verification-guidance.js +37 -0
  91. package/build/executor/worktree.js +166 -8
  92. package/build/first-build-advisory.js +97 -0
  93. package/build/generated-assets-advisory.js +14 -0
  94. package/build/generated-assets-catalog.js +66 -0
  95. package/build/generated-assets-context.js +46 -0
  96. package/build/generated-assets-dependencies.js +26 -0
  97. package/build/generated-assets-doctor.js +50 -0
  98. package/build/generated-assets-health.js +65 -0
  99. package/build/generated-assets-host.js +17 -0
  100. package/build/generated-assets-inventory.js +17 -0
  101. package/build/generated-assets-paths.js +60 -0
  102. package/build/generated-assets-publisher.js +133 -0
  103. package/build/generated-assets-remedy.js +25 -0
  104. package/build/generated-assets-repair.js +72 -0
  105. package/build/generated-assets-session.js +71 -0
  106. package/build/generated-assets-test-support.js +35 -0
  107. package/build/index.js +1224 -1054
  108. package/build/init.js +54 -177
  109. package/build/install-bridge-conductor-gate.js +1372 -0
  110. package/build/install-bridge-conductor.js +296 -41
  111. package/build/install-bridge.js +547 -40
  112. package/build/mcp-install-state.js +50 -4
  113. package/build/merge-workflow-install.js +3 -4
  114. package/build/phase-context-git-probe.js +68 -0
  115. package/build/phase-result-artifacts.js +173 -5
  116. package/build/pipeline-utils.js +6 -2
  117. package/build/pipelines.generated.js +34 -160
  118. package/build/plan-execution-ledger.js +2 -32
  119. package/build/plane/alembic-head.js +12 -3
  120. package/build/plane/base-worktree-guard.js +147 -0
  121. package/build/plane/cli.js +217 -17
  122. package/build/plane/code-snapshot.js +468 -0
  123. package/build/plane/defaults.js +48 -3
  124. package/build/plane/endpoint.js +41 -0
  125. package/build/plane/manifest.js +136 -2
  126. package/build/plane/member-roster.js +94 -19
  127. package/build/plane/preflight.js +83 -2
  128. package/build/plane/process-cwd.js +67 -0
  129. package/build/plane/restart.js +542 -0
  130. package/build/plane/shutdown.js +31 -0
  131. package/build/plane/status.js +143 -8
  132. package/build/plane/supervisor.js +586 -179
  133. package/build/plane/types.js +54 -2
  134. package/build/printed-remedy-commands.js +52 -0
  135. package/build/readiness-check.js +1 -1
  136. package/build/readme.generated.js +1 -1
  137. package/build/repair-assets.js +93 -0
  138. package/build/review-generation.js +22 -0
  139. package/build/run-unit-tests-launcher.js +130 -49
  140. package/build/secret-redaction.js +33 -0
  141. package/build/setup-epic.js +152 -22
  142. package/build/sfcc/config.js +9 -0
  143. package/build/sfcc/credentials.js +100 -38
  144. package/build/sfcc/hostname.js +67 -0
  145. package/build/sfcc/permissions.js +17 -13
  146. package/build/sfcc/register.js +1 -1
  147. package/build/sfcc/tool-wrapper.js +39 -43
  148. package/build/sfcc/write-grants.js +17 -11
  149. package/build/sfcc/write-result.js +4 -3
  150. package/build/sfcc/writes-site-preference.js +20 -8
  151. package/build/start-tickets-prereqs.js +70 -16
  152. package/build/start-tickets.js +208 -36
  153. package/build/tool-error-envelope.js +61 -0
  154. package/build/tool-surface-gating.js +36 -0
  155. package/build/upgrade-cli.js +55 -1
  156. package/build/version.generated.js +3 -3
  157. package/build/visual-diff.js +22 -1
  158. package/build/worker-guard-hook-bin.js +4 -6
  159. package/build/worker-guard-hook-main.js +47 -0
  160. package/build/worktree-core.js +145 -6
  161. package/docs/EPIC-IMPLEMENTER-INSTALL.md +867 -0
  162. package/docs/EPIC-IMPLEMENTER.md +728 -0
  163. package/docs/install/mcp-tool-integrations.md +24 -15
  164. package/docs/install/sfcc-integration.md +12 -4
  165. package/package.json +3 -3
  166. package/pipelines/greenfield-setup.json +4 -128
  167. package/pipelines/implement-ticket.json +3 -2
  168. package/pipelines/plan-epic.json +0 -5
  169. package/pipelines/review-ticket.json +1 -1
  170. package/docs/CONDUCTOR.md +0 -605
  171. package/pipelines/idea-to-pr.json +0 -49
@@ -1,8 +1,8 @@
1
1
  /**
2
- * The shared contract for the seven operator repair verbs: BAPI-1154's
2
+ * The shared contract for the eight operator repair verbs: BAPI-1154's
3
3
  * `scope-reparse`, `dispatch-remediation`, and `merge-child`, BAPI-1176's
4
- * `scope-catch-up`, `patch-policy`, and `resume-run`, and BAPI-1326's
5
- * ticket-addressed `resume-ci`.
4
+ * `scope-catch-up`, `patch-policy`, and `resume-run`, BAPI-1326's
5
+ * ticket-addressed `resume-ci`, and BAPI-1388's ticket-addressed `rerun-ci`.
6
6
  *
7
7
  * CLI-only, exactly like the BAPI-872 recovery verbs: none of these names is ever
8
8
  * registered as an MCP tool (`index-static.test.ts` pins that). This module is the
@@ -22,6 +22,7 @@ export const VERB_SCOPE_CATCH_UP = "scope-catch-up";
22
22
  export const VERB_PATCH_POLICY = "patch-policy";
23
23
  export const VERB_RESUME_RUN = "resume-run";
24
24
  export const VERB_RESUME_CI = "resume-ci";
25
+ export const VERB_RERUN_CI = "rerun-ci";
25
26
  /** Every repair verb, in the order an operator meets them. */
26
27
  export const OPERATOR_REPAIR_VERBS = [
27
28
  VERB_SCOPE_REPARSE,
@@ -31,6 +32,7 @@ export const OPERATOR_REPAIR_VERBS = [
31
32
  VERB_PATCH_POLICY,
32
33
  VERB_RESUME_RUN,
33
34
  VERB_RESUME_CI,
35
+ VERB_RERUN_CI,
34
36
  ];
35
37
  /**
36
38
  * The ONE cap on driver context, in UTF-8 BYTES — the same number the server
@@ -156,3 +158,52 @@ export const CI_RESUME_RECEIPT_RETENTION = "latest_only";
156
158
  export function isCiResumeRefusalCode(code) {
157
159
  return (typeof code === "string" && CI_RESUME_REFUSAL_CODES.includes(code));
158
160
  }
161
+ // ---------------------------------------------------------------------------
162
+ // BAPI-1388 — `rerun-ci`
163
+ // ---------------------------------------------------------------------------
164
+ /**
165
+ * The closed `rerun-ci` refusal subset — mirrors `RERUN_CI_REFUSAL_CODES` in
166
+ * `operator_repair_contract.py`. It mints no new code: every member already
167
+ * exists in {@link OPERATOR_REPAIR_REFUSAL_CODES}.
168
+ */
169
+ export const RERUN_CI_REFUSAL_CODES = [
170
+ "TICKET_NOT_IN_RUN",
171
+ "GUIDED_REPAIR_DISABLED",
172
+ "REPAIR_SUBJECT_STALE",
173
+ "RUN_NOT_DISPATCHABLE",
174
+ "TICKET_NOT_REMEDIABLE",
175
+ "REMEDIATION_JOB_IN_FLIGHT",
176
+ "REMEDIATION_LANE_BUSY",
177
+ "REMEDIATION_HELD",
178
+ "REMEDIATION_NO_PULL_REQUEST",
179
+ "ROW_VERSION_CONFLICT",
180
+ "DISPATCH_ENQUEUE_FAILED",
181
+ ];
182
+ /** True when `code` is one of the closed `rerun-ci` refusal codes. */
183
+ export function isRerunCiRefusalCode(code) {
184
+ return (typeof code === "string" && RERUN_CI_REFUSAL_CODES.includes(code));
185
+ }
186
+ /**
187
+ * The bounded reason a `rerun-ci` `ROW_VERSION_CONFLICT` carries when releasing a
188
+ * reservation lost its guarded write. Whether the unenqueued workflow runs are
189
+ * still recorded is UNCONFIRMED; the CLI exits 3.
190
+ */
191
+ export const RERUN_CI_UNCONFIRMED_REASON = "rerun_ci_unconfirmed";
192
+ /**
193
+ * The closed per-check outcome vocabulary. None of them says a check is green: a
194
+ * provider re-run is an action, not a verdict.
195
+ */
196
+ export const RERUN_CI_CHECK_STATUSES = [
197
+ "enqueued",
198
+ "deduplicated",
199
+ "deferred",
200
+ "not_re_runnable",
201
+ ];
202
+ /** Why one failed check is not re-run: a closed token. */
203
+ export const RERUN_CI_NOT_RUNNABLE_REASONS = [
204
+ "not_github_actions",
205
+ "no_workflow_run_id",
206
+ "claude_review_excluded",
207
+ ];
208
+ /** The closed overall outcomes of an applied `rerun-ci`. */
209
+ export const RERUN_CI_OUTCOMES = ["enqueued", "partial", "nothing_to_enqueue"];
@@ -0,0 +1,125 @@
1
+ /** The two fixed workflow files unattended auto-merge reads. Named once here. */
2
+ export const REVIEW_WORKFLOW_PATH = ".github/workflows/claude-review.yml";
3
+ export const CI_WORKFLOW_PATH = ".github/workflows/epic-implementer-ci.yml";
4
+ /** The migration guard's stable job id and its exact checker invocation. */
5
+ export const MIGRATION_GUARD_JOB_ID = "migration-guard";
6
+ export const MIGRATION_GUARD_CHECKER_PATH = "scripts/check_alembic_migration_graph.py";
7
+ export const MIGRATION_GUARD_INVOCATION = `python ${MIGRATION_GUARD_CHECKER_PATH}`;
8
+ const RETRY = "Run the readiness check again, or pass --attended. " +
9
+ "If it keeps failing, contact Bridge operations.";
10
+ const TEMPLATES = {
11
+ review_workflow_file: `${REVIEW_WORKFLOW_PATH} is missing from the default branch. Add it, or pass --attended.`,
12
+ review_run_id_marker: `${REVIEW_WORKFLOW_PATH} does not emit the run-id: marker in its verdict comment. ` +
13
+ "Add it, or pass --attended.",
14
+ review_reviewed_sha_marker: `${REVIEW_WORKFLOW_PATH} does not emit the Reviewed-SHA: marker in its verdict comment. ` +
15
+ "Add it, or pass --attended.",
16
+ review_workflow_unverifiable: `Bridge could not read ${REVIEW_WORKFLOW_PATH} on the default branch. ${RETRY}`,
17
+ ci_workflow_file: `${CI_WORKFLOW_PATH} is missing from the default branch. Add it, or pass --attended.`,
18
+ migration_guard_job: `${CI_WORKFLOW_PATH} has no job with id ${MIGRATION_GUARD_JOB_ID} running ${MIGRATION_GUARD_INVOCATION}. ` +
19
+ "It is required because this repository has alembic.ini on its default branch, " +
20
+ `and an equivalent checker at ${MIGRATION_GUARD_CHECKER_PATH} is required too. ` +
21
+ "Add that job, or pass --attended.",
22
+ migration_chain_unverifiable: "Bridge could not confirm whether this repository has alembic.ini on its default branch, " +
23
+ `so it could not decide if the ${MIGRATION_GUARD_JOB_ID} job is required. ${RETRY}`,
24
+ ci_workflow_unverifiable: `Bridge could not read ${CI_WORKFLOW_PATH} on the default branch. ${RETRY}`,
25
+ // An older server never reported whether the guard is conditional, so its
26
+ // guard has always been required. This states that requirement and NOTHING
27
+ // about alembic.ini: an older server observed no such file.
28
+ migration_guard_job_legacy: `${CI_WORKFLOW_PATH} has no job with id ${MIGRATION_GUARD_JOB_ID} running ${MIGRATION_GUARD_INVOCATION}. ` +
29
+ "Add that job, or pass --attended.",
30
+ };
31
+ /** Shown when the report is missing, incomplete, or not in a shape we can name. */
32
+ const UNVERIFIABLE_FALLBACK = "Bridge could not verify the review and CI workflows on the default branch. " + RETRY;
33
+ function classifyReview(w) {
34
+ if (w === null || w === undefined || typeof w !== "object")
35
+ return "malformed";
36
+ if (w.probe_succeeded !== true)
37
+ return ["review_workflow_unverifiable"];
38
+ if (w.workflow_present === false)
39
+ return ["review_workflow_file"];
40
+ if (w.workflow_present !== true ||
41
+ typeof w.emits_run_id !== "boolean" ||
42
+ typeof w.emits_reviewed_sha !== "boolean") {
43
+ return "malformed";
44
+ }
45
+ const missing = [];
46
+ if (!w.emits_run_id)
47
+ missing.push("review_run_id_marker");
48
+ if (!w.emits_reviewed_sha)
49
+ missing.push("review_reviewed_sha_marker");
50
+ return missing;
51
+ }
52
+ function classifyCi(c) {
53
+ if (c === null || c === undefined || typeof c !== "object")
54
+ return "malformed";
55
+ if (c.probe_succeeded !== true)
56
+ return ["ci_workflow_unverifiable"];
57
+ if (c.workflow_present === false)
58
+ return ["ci_workflow_file"];
59
+ if (c.workflow_present !== true || typeof c.migration_guard_present !== "boolean") {
60
+ return "malformed";
61
+ }
62
+ if (c.migration_guard_present)
63
+ return [];
64
+ const required = c.migration_guard_required;
65
+ const chainProbed = c.migration_chain_probe_succeeded;
66
+ // Older server: neither field reported. The guard was always required there,
67
+ // and the absent fields must never read as "not required".
68
+ if (required === undefined && chainProbed === undefined) {
69
+ return ["migration_guard_job_legacy"];
70
+ }
71
+ if (typeof required !== "boolean" || typeof chainProbed !== "boolean")
72
+ return "malformed";
73
+ if (chainProbed && !required)
74
+ return []; // confirmed guard-free
75
+ if (chainProbed && required)
76
+ return ["migration_guard_job"];
77
+ return ["migration_chain_unverifiable"];
78
+ }
79
+ /**
80
+ * Name the first failing workflow section of a parsed readiness report, its
81
+ * missing facts in stable order, and one fixed-template message.
82
+ *
83
+ * `section: null` with the fallback text means the report is missing or malformed
84
+ * — "could not verify", never "fine". An empty `missing` with `section: null`
85
+ * cannot occur: a report with nothing failing yields the fallback only when it is
86
+ * unreadable, and otherwise the empty result below.
87
+ */
88
+ export function describeRepositoryWorkflowRemedy(readiness) {
89
+ const review = classifyReview(readiness?.review_workflow);
90
+ if (review === "malformed") {
91
+ return { section: null, missing: [], text: UNVERIFIABLE_FALLBACK };
92
+ }
93
+ if (review.length > 0)
94
+ return build("review_workflow", review);
95
+ const ci = classifyCi(readiness?.conductor_ci_workflow);
96
+ if (ci === "malformed") {
97
+ return { section: null, missing: [], text: UNVERIFIABLE_FALLBACK };
98
+ }
99
+ if (ci.length > 0)
100
+ return build("conductor_ci_workflow", ci);
101
+ return { section: null, missing: [], text: "" };
102
+ }
103
+ function build(section, missing) {
104
+ return { section, missing, text: missing.map((token) => TEMPLATES[token]).join(" ") };
105
+ }
106
+ /**
107
+ * The remedy sentence for a repository whose workflows are not confirmed. Never
108
+ * empty: a report with nothing to name (the server said unconfirmed but this
109
+ * client can name no item) gets the unverifiable fallback rather than silence.
110
+ */
111
+ export function repositoryWorkflowRemedy(readiness) {
112
+ const described = describeRepositoryWorkflowRemedy(readiness);
113
+ return described.text === "" ? UNVERIFIABLE_FALLBACK : described.text;
114
+ }
115
+ /**
116
+ * The remedy for the CI workflow section alone — what the readiness CI row shows
117
+ * when the migration guard is absent. Same templates, same words as
118
+ * {@link repositoryWorkflowRemedy}, so the row and the refusals cannot drift.
119
+ */
120
+ export function ciWorkflowRemedy(c) {
121
+ const tokens = classifyCi(c);
122
+ if (tokens === "malformed" || tokens.length === 0)
123
+ return UNVERIFIABLE_FALLBACK;
124
+ return tokens.map((token) => TEMPLATES[token]).join(" ");
125
+ }
@@ -82,9 +82,9 @@ function registerExplainRunTool(registerTool) {
82
82
  // is the field list: a caller cannot discover the answer shape from a
83
83
  // tool that only says "explain a run", and the whole point of the tool is
84
84
  // that the answer is in one place.
85
- description: "Explain why a Conductor epic run, or one ticket in it, is stuck. One snapshot: " +
85
+ description: "Explain why an Epic Implementer run, or one ticket in it, is stuck. One snapshot: " +
86
86
  "current gate, latest observation and age, outstanding jobs with queue position, " +
87
- "lease owner/expiry, blocker reasons, expected-vs-observed head SHA, committed and " +
87
+ "lease owner/expiry, blocker reasons, expected/observed head SHA, committed and " +
88
88
  "pending disposition, usage ceiling. Pass ticket_key to narrow. Read-only; 404 " +
89
89
  "returns status 'unknown'.",
90
90
  inputSchema: {
@@ -27,6 +27,14 @@ import { validateTicketKey } from "./ticket-key-utils.js";
27
27
  const EPIC_SLUG_RE = /^[A-Za-z][A-Za-z0-9_-]*$/;
28
28
  /** Placeholder node identity written by `decompose-epic.md` before tickets exist. */
29
29
  const PLACEHOLDER_RE = /TBD-\d+/;
30
+ /**
31
+ * A whole plan-node key that is still an authoring placeholder (`TBD-<digits>`),
32
+ * case-insensitive. Anchored on purpose and stateless (no `g` or `y` flag, so
33
+ * `.test()` carries no `lastIndex`). This is NOT {@link PLACEHOLDER_RE}: that one
34
+ * is an unanchored scan of a whole serialized plan, and
35
+ * {@link findResidualPlaceholder} keeps using it unchanged.
36
+ */
37
+ export const PLACEHOLDER_NODE_KEY_RE = /^TBD-\d+$/i;
30
38
  /**
31
39
  * The conductor's hard spec bound, mirroring `_SPEC_REVIEW_TICKET_SPEC_MAX_CHARS`
32
40
  * (64,000) in `api/library/epic_implementer/reconciler.py`. Since PR #848 the
@@ -87,6 +87,25 @@ function asString(value) {
87
87
  function asNullableString(value) {
88
88
  return typeof value === "string" && value.length > 0 ? value : null;
89
89
  }
90
+ /** Same unambiguous alphabet the server draws its codes and check values from. */
91
+ const APPROVAL_CHECK_RE = /^[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{4,8}$/;
92
+ /**
93
+ * Decide the mint's mode defensively (BAPI-1401). Approval mode needs BOTH an explicit
94
+ * `approval_mode: "approve"` and a well-formed check value; any other combination is
95
+ * code mode, so a partial or unexpected response can never strand the user without the
96
+ * code they would need.
97
+ */
98
+ function parseApprovalMode(body) {
99
+ const check = typeof body.approval_check === "string" ? body.approval_check.trim() : "";
100
+ if (body.approval_mode === "approve" && APPROVAL_CHECK_RE.test(check)) {
101
+ return { approvalMode: "approve", approvalCheck: check };
102
+ }
103
+ return { approvalMode: "code", approvalCheck: null };
104
+ }
105
+ /** Parse the optional, additive `reason` on a status body. Unknown → null. */
106
+ function parseStatusReason(value) {
107
+ return value === "declined" ? "declined" : null;
108
+ }
90
109
  // ---------------------------------------------------------------------------
91
110
  // Retry-After
92
111
  // ---------------------------------------------------------------------------
@@ -121,13 +140,23 @@ export function parseRetryAfterMs(header, remainingMs, nowMs) {
121
140
  // ---------------------------------------------------------------------------
122
141
  // Endpoints
123
142
  // ---------------------------------------------------------------------------
124
- /** Mint a CLI-origin connection code and its Forge install-page URL. */
125
- export async function mintBitbucketConnection(deps, repoName) {
126
- const res = await postJson(deps, "/setup/bitbucket/cli/connection-code", {
127
- repo_name: repoName,
128
- });
143
+ /**
144
+ * Mint a CLI-origin connection code and its Forge install-page URL.
145
+ *
146
+ * BAPI-1401: when `workspaceSlug` is given, it is sent as the request's workspace claim,
147
+ * asking the server for an approval-mode connection request. Without it the request body
148
+ * is exactly the legacy shape. Either way the response decides the mode.
149
+ */
150
+ export async function mintBitbucketConnection(deps, repoName, workspaceSlug) {
151
+ const payload = { repo_name: repoName };
152
+ if (workspaceSlug)
153
+ payload.workspace_slug = workspaceSlug;
154
+ const res = await postJson(deps, "/setup/bitbucket/cli/connection-code", payload);
129
155
  if (!res.ok)
130
156
  return res;
157
+ if (res.value.status === 429) {
158
+ return { ok: false, kind: "too-many-pending" };
159
+ }
131
160
  if (res.value.status !== 200) {
132
161
  return { ok: false, kind: classifyStatus(res.value.status) };
133
162
  }
@@ -143,7 +172,11 @@ export async function mintBitbucketConnection(deps, repoName) {
143
172
  // uses. Required-field strictness above is untouched — an optional field must not
144
173
  // become a new way for a server change to fail a mint.
145
174
  const redeemUrlTemplate = asNullableString(body.redeem_url_template);
146
- return { ok: true, value: { state, installUrl, ttlSeconds, redeemUrlTemplate } };
175
+ const { approvalMode, approvalCheck } = parseApprovalMode(body);
176
+ return {
177
+ ok: true,
178
+ value: { state, installUrl, ttlSeconds, redeemUrlTemplate, approvalMode, approvalCheck },
179
+ };
147
180
  }
148
181
  /** Strictly parse candidates. A candidate missing a usable identity fails the whole list. */
149
182
  function parseCandidates(value) {
@@ -198,6 +231,7 @@ export async function fetchBitbucketConnectionStatus(deps, repoName, state, nowM
198
231
  status: status,
199
232
  candidates,
200
233
  repoDisplayName: asNullableString(body.repo_display_name),
234
+ reason: parseStatusReason(body.reason),
201
235
  // A single read has no deadline of its own to clamp against; the poller applies
202
236
  // its own bound. POLL_DEADLINE_MS is the widest a caller could honor.
203
237
  retryAfterMs: parseRetryAfterMs(res.value.retryAfter, POLL_DEADLINE_MS, nowMs),
@@ -348,6 +382,7 @@ export async function pollBitbucketConnection(deps, poll, repoName, state) {
348
382
  status: typed,
349
383
  candidates,
350
384
  repoDisplayName: asNullableString(body.repo_display_name),
385
+ reason: parseStatusReason(body.reason),
351
386
  retryAfterMs: null,
352
387
  },
353
388
  };
@@ -5,20 +5,28 @@
5
5
  * The whole point of this command is that it needs no Bridge web session: a user who has
6
6
  * a Bridge API key can connect Bitbucket without ever opening the setup UI. The flow is
7
7
  *
8
- * mint → browser (install the Forge app on Bitbucket, paste the printed code into its
9
- * admin panel) → poll → pick a repository → confirm
8
+ * mint → browser (install the Forge app on Bitbucket, then approve the request on the
9
+ * app's workspace settings page) → poll → pick a repository → confirm
10
10
  *
11
11
  * and the browser half authenticates to *Bitbucket*, not to Bridge. Bridge never asks
12
12
  * for, accepts, echoes, or transports a Bitbucket credential. The only thing crossing
13
13
  * between the browser and this process is a Bridge-issued state nonce, held in memory.
14
14
  *
15
+ * BAPI-1401 — two modes. **Approval mode** (the default when a Bitbucket workspace is
16
+ * known from `--workspace` or this project's git remote, and the server supports it):
17
+ * the mint is a connection request listed on that workspace's Forge settings page, and a
18
+ * workspace admin clicks Approve on the card showing the check value printed here. No
19
+ * code is printed. **Code mode** (`--use-code`, no workspace, or an older server): the
20
+ * legacy flow — the printed code is pasted into the same settings page.
21
+ *
15
22
  * Two properties are load-bearing and easy to erode:
16
23
  *
17
24
  * 1. **The nonce and install URL are never printed to output the CLI narrates around.**
18
25
  * The URL carries the state as a query parameter, so putting it in a log line or an
19
26
  * error message would paste a live credential-equivalent into the user's scrollback.
20
- * The printed code shown to the user for pasting into Bitbucket's own admin panel is
21
- * the one deliberate exception — see `runBitbucketConnectionFlow` below.
27
+ * In code mode the printed code shown to the user for pasting into Bitbucket's own
28
+ * settings page is the one deliberate exception — see `runBitbucketConnectionFlow`
29
+ * below. In approval mode it is never printed.
22
30
  * 2. **Nothing binds without an explicit human choice.** Even a single-repository
23
31
  * installation is confirmed by hand — the server stages candidates and this command
24
32
  * asks. There is deliberately no `--yes`.
@@ -35,25 +43,30 @@ import { DEFAULT_BAPI_BASE_URL } from "./install-bridge.js";
35
43
  import { validateRepoName } from "./bridge-config.js";
36
44
  import { resolveStartTicketsRepoName } from "./start-tickets-repo.js";
37
45
  import { resolveBapiCredentials, } from "./credential-store.js";
46
+ import { defaultGitRemoteRunner, resolveBitbucketWorkspace, } from "./bitbucket-workspace-remote.js";
38
47
  import { POLL_DEADLINE_MS, confirmBitbucketConnection, mintBitbucketConnection, pollBitbucketConnection, } from "./connect-bitbucket-api.js";
39
- const USAGE = `Usage: connect-bitbucket [--repo <repo_name>] [--workspace <slug>]
48
+ const USAGE = `Usage: connect-bitbucket [--repo <repo_name>] [--workspace <slug>] [--use-code]
40
49
 
41
50
  Connect a Bitbucket repository to a Bridge project from your terminal.
42
51
 
43
- Opens the Bitbucket Forge app's install page in your browser and prints a
44
- connection code. Install the app, then redeem the code in Bitbucket under
45
- Workspace settings → Apps → "Connect Bitbucket to Bridge API" — that is how you
46
- authenticate to Bitbucket; Bridge never asks for a Bitbucket token or app
47
- password. Once the app reports the code back, this command asks which single
48
- repository to connect.
52
+ Opens the Bitbucket Forge app's install page in your browser. Install the app,
53
+ then in Bitbucket open Workspace settings → Forge Apps → "Connect Bitbucket to
54
+ Bridge API" and click Approve on the request this command started (it prints a
55
+ short check value so you can tell it apart). That is how you authenticate to
56
+ Bitbucket; Bridge never asks for a Bitbucket token or app password. Once the
57
+ app reports back, this command asks which single repository to connect.
58
+
59
+ Without a known workspace, or with --use-code, it prints a connection code to
60
+ paste on that same page instead.
49
61
 
50
62
  Options:
51
63
  --repo <repo_name> Bridge project to connect (inferred from this directory
52
64
  when omitted; you will be asked to confirm).
53
- --workspace <slug> Bitbucket workspace slug. Prints a direct link to that
54
- workspace's settings page for the app. The connection
55
- code still has to be pasted there by hand — the link only
56
- saves you finding the page.
65
+ --workspace <slug> Bitbucket workspace slug. Inferred from this project's
66
+ origin git remote when omitted. Lists the request on that
67
+ workspace's settings page and prints a direct link to it.
68
+ --use-code Print a connection code to paste instead of approving a
69
+ request.
57
70
  --help Show this message.`;
58
71
  /**
59
72
  * Bitbucket workspace slugs are NOT Bridge project names, so they do not go through
@@ -94,7 +107,7 @@ export function validateBitbucketWorkspace(raw) {
94
107
  * resolve only through the shared credential-store seam).
95
108
  */
96
109
  export function parseConnectBitbucketArgs(argv) {
97
- const out = { help: false };
110
+ const out = { useCode: false, help: false };
98
111
  for (let i = 0; i < argv.length; i += 1) {
99
112
  const arg = argv[i];
100
113
  if (arg === "--help" || arg === "-h") {
@@ -137,6 +150,10 @@ export function parseConnectBitbucketArgs(argv) {
137
150
  out.workspace = validated.value;
138
151
  continue;
139
152
  }
153
+ if (arg === "--use-code") {
154
+ out.useCode = true;
155
+ continue;
156
+ }
140
157
  if (arg === "--yes" || arg === "-y") {
141
158
  return {
142
159
  ok: false,
@@ -255,6 +272,7 @@ export function createDefaultConnectBitbucketDeps() {
255
272
  openBrowser: (url) => defaultOpenBrowser(process.platform, url),
256
273
  stdout: (message) => process.stdout.write(`${message}\n`),
257
274
  stderr: (message) => process.stderr.write(`${message}\n`),
275
+ runGit: defaultGitRemoteRunner,
258
276
  };
259
277
  }
260
278
  /**
@@ -313,7 +331,7 @@ export async function openBitbucketInstallPage(deps, installUrl) {
313
331
  // ---------------------------------------------------------------------------
314
332
  const STEPS = [
315
333
  "Connect Bitbucket",
316
- "Complete Bitbucket Forge install in browser",
334
+ "Install the Bitbucket Forge app and connect it in Bitbucket",
317
335
  "Verify connection",
318
336
  "Choose repository",
319
337
  "Confirm connection",
@@ -333,7 +351,7 @@ function renderStep(deps, index) {
333
351
  * must never leave the user with nothing. The words are the app's own Bitbucket
334
352
  * navigation, so they stay correct without any Bridge-side configuration at all.
335
353
  */
336
- export const BITBUCKET_REDEEM_NAVIGATION_PATH = 'Workspace settings → Apps → "Connect Bitbucket to Bridge API"';
354
+ export const BITBUCKET_REDEEM_NAVIGATION_PATH = 'Workspace settings → Forge Apps → "Connect Bitbucket to Bridge API"';
337
355
  /** The one `{workspace}` token a redemption template may carry. */
338
356
  const WORKSPACE_TOKEN = "{workspace}";
339
357
  /**
@@ -431,6 +449,24 @@ function renderRedemptionGuidance(deps, template, workspace) {
431
449
  deps.stderr(" Replace {workspace} with your Bitbucket workspace slug.");
432
450
  }
433
451
  }
452
+ /**
453
+ * Print what to do in approval mode (BAPI-1401): install, then Approve the request that
454
+ * shows this run's check value. The code is never printed. `firstRequest` is false on a
455
+ * renewal, where the browser is already open and only the new request's check changes.
456
+ */
457
+ function renderApprovalGuidance(deps, repoName, approvalCheck, template, workspace, firstRequest) {
458
+ if (firstRequest) {
459
+ deps.stderr("Step 1: install the app in the browser window that just opened.");
460
+ }
461
+ else {
462
+ deps.stderr("A new request was created. The app install page is already open in your browser.");
463
+ }
464
+ deps.stderr(`Step 2: in Bitbucket, go to ${BITBUCKET_REDEEM_NAVIGATION_PATH} and click Approve on ` +
465
+ `the request for ${repoName} showing check ${approvalCheck}.`);
466
+ const link = renderBitbucketRedeemLink(template, workspace);
467
+ if (link)
468
+ deps.stderr(` Direct link: ${link}`);
469
+ }
434
470
  /** Display identity for a candidate: prefer the unambiguous workspace/repo slug form. */
435
471
  function candidateLabel(c) {
436
472
  const slugForm = `${c.workspace_slug}/${c.repository_slug}`;
@@ -443,6 +479,8 @@ function candidateLabel(c) {
443
479
  const OUTCOME_MESSAGES = {
444
480
  expired: "The connection request expired before the Forge app reported back. Run connect-bitbucket again.",
445
481
  invalid: "This connection request is no longer valid. Run connect-bitbucket again.",
482
+ // BAPI-1401: an `invalid` status that carries reason "declined".
483
+ declined: "The connection request was declined in Bitbucket. Run connect-bitbucket again to start a new request.",
446
484
  "no-repositories": "The Bitbucket Forge app installation did not include any repositories Bridge can " +
447
485
  "access. Re-run connect-bitbucket and grant access to at least one repository.",
448
486
  };
@@ -453,9 +491,13 @@ const FAILURE_MESSAGES = {
453
491
  "not-found": "Bridge does not recognize this project. Check --repo matches your Bridge project name.",
454
492
  server: "Bridge API returned an error. Run connect-bitbucket again shortly.",
455
493
  malformed: "Bridge API returned an unexpected response. Run connect-bitbucket again shortly.",
456
- deadline: "Timed out waiting for the Bitbucket Forge app. If you completed the install and pasted " +
457
- "the code, run connect-bitbucket again to pick up the connection.",
494
+ deadline: "Timed out waiting for the Bitbucket Forge app. If you installed the app and approved " +
495
+ "the request (or pasted the code), run connect-bitbucket again to pick up the connection.",
496
+ "too-many-pending": "Too many Bitbucket connection requests are waiting. Approve or decline them in " +
497
+ "Bitbucket, or wait up to 15 minutes for them to expire, then run connect-bitbucket again.",
458
498
  };
499
+ /** BAPI-1401: offered after the first approval-mode expiry or deadline. */
500
+ const USE_CODE_FALLBACK_HINT = "Can't see the request? Re-run with --use-code";
459
501
  /** Only an explicit, case-insensitive `y`/`yes` approves. One definition, used everywhere. */
460
502
  function isAffirmative(answer) {
461
503
  const normalized = answer.trim().toLowerCase();
@@ -548,7 +590,7 @@ async function chooseCandidate(deps, candidates) {
548
590
  * a renewal can never confirm against a previous code.
549
591
  */
550
592
  export async function runBitbucketConnectionFlow(deps, api, repoName, options = {}) {
551
- const { workspace, initialMintApproved = false } = options;
593
+ const { workspace, useCode = false, initialMintApproved = false } = options;
552
594
  renderStep(deps, 0);
553
595
  if (!initialMintApproved) {
554
596
  // A standalone run asks before it mints: minting is a side effect (a live code, a
@@ -559,8 +601,11 @@ export async function runBitbucketConnectionFlow(deps, api, repoName, options =
559
601
  }
560
602
  let browserOpened = false;
561
603
  let expiryCount = 0;
604
+ let useCodeHintShown = false;
562
605
  for (;;) {
563
- const minted = await mintBitbucketConnection(api, repoName);
606
+ // BAPI-1401: the workspace is sent as an approval-mode claim unless the user asked
607
+ // for the code flow. The server's response, not this request, decides the mode.
608
+ const minted = await mintBitbucketConnection(api, repoName, useCode ? undefined : workspace);
564
609
  if (!minted.ok) {
565
610
  // Includes a renewal mint: a failed fresh mint is terminal and resumable, never
566
611
  // silently retried. An automatic retry here would spend codes the user cannot see.
@@ -568,6 +613,13 @@ export async function runBitbucketConnectionFlow(deps, api, repoName, options =
568
613
  }
569
614
  // Iteration-local. Nothing outside this block may read a previous code.
570
615
  const connection = minted.value;
616
+ // Approval mode needs a workspace to have been claimed; a server that answers
617
+ // "approve" to a claimless mint is treated as code mode rather than trusted.
618
+ const approval = connection.approvalMode === "approve" &&
619
+ connection.approvalCheck !== null &&
620
+ !useCode &&
621
+ workspace !== undefined;
622
+ const firstRequest = !browserOpened;
571
623
  if (!browserOpened) {
572
624
  renderStep(deps, 1);
573
625
  deps.stderr("Opening Bitbucket…");
@@ -581,13 +633,20 @@ export async function runBitbucketConnectionFlow(deps, api, repoName, options =
581
633
  // both noise and a stale destination.
582
634
  browserOpened = true;
583
635
  }
584
- renderRedemptionGuidance(deps, connection.redeemUrlTemplate, workspace);
585
- // The code itself is not a bearer credential the way the URL's state parameter is
586
- // treated elsewhere in this flow — it must be visible so the user can paste it into
587
- // the installed Forge app's admin panel, which is how redemption actually happens for
588
- // a Forge app (there is no GitHub-style browser redirect back to Bridge). This is
589
- // the single statement in the command that prints a code, and it runs once per mint.
590
- deps.stderr(`Paste this code there: ${connection.state}`);
636
+ if (approval) {
637
+ // BAPI-1401: nothing to paste. `connection.state` stays in memory, used only for
638
+ // polling and confirmation below.
639
+ renderApprovalGuidance(deps, repoName, connection.approvalCheck, connection.redeemUrlTemplate, workspace, firstRequest);
640
+ }
641
+ else {
642
+ renderRedemptionGuidance(deps, connection.redeemUrlTemplate, workspace);
643
+ // The code itself is not a bearer credential the way the URL's state parameter is
644
+ // treated elsewhere in this flow — it must be visible so the user can paste it into
645
+ // the installed Forge app's settings page, which is how redemption happens in code
646
+ // mode (there is no GitHub-style browser redirect back to Bridge). This is the
647
+ // single statement in the command that prints a code, and it runs once per mint.
648
+ deps.stderr(`Paste this code there: ${connection.state}`);
649
+ }
591
650
  renderStep(deps, 2);
592
651
  const minutes = Math.floor(POLL_DEADLINE_MS / 60_000);
593
652
  deps.stderr(`Waiting for the Bitbucket Forge app… (up to ~${minutes} minutes)`);
@@ -599,14 +658,23 @@ export async function runBitbucketConnectionFlow(deps, api, repoName, options =
599
658
  if (renewable) {
600
659
  expiryCount += 1;
601
660
  deps.stderr("");
602
- deps.stderr("That code expired and can no longer be used.");
661
+ deps.stderr(approval
662
+ ? "That request expired and can no longer be approved."
663
+ : "That code expired and can no longer be used.");
664
+ if (approval && !useCodeHintShown) {
665
+ deps.stderr(USE_CODE_FALLBACK_HINT);
666
+ useCodeHintShown = true;
667
+ }
603
668
  if (expiryCount > 1) {
604
669
  // Said only after it has happened more than once — by then the likely cause is
605
670
  // not slowness but a wrong page or an app that was never installed.
606
671
  deps.stderr("If this keeps happening, check that the Forge app is installed and that you " +
607
672
  "have the right workspace's settings page open.");
608
673
  }
609
- if (await confirm(deps, "Issue a fresh code and keep waiting? [y/N]: ")) {
674
+ const renewQuestion = approval
675
+ ? "Create a fresh request and keep waiting? [y/N]: "
676
+ : "Issue a fresh code and keep waiting? [y/N]: ";
677
+ if (await confirm(deps, renewQuestion)) {
610
678
  continue;
611
679
  }
612
680
  return reportNoConnection(deps, polled.ok ? OUTCOME_MESSAGES.expired : FAILURE_MESSAGES.deadline, repoName, "incomplete");
@@ -624,7 +692,8 @@ export async function runBitbucketConnectionFlow(deps, api, repoName, options =
624
692
  return { kind: "connected", displayName };
625
693
  }
626
694
  if (result.status !== "staged") {
627
- return reportNoConnection(deps, OUTCOME_MESSAGES[result.status] ?? OUTCOME_MESSAGES.invalid, repoName);
695
+ const outcomeKey = result.status === "invalid" && result.reason === "declined" ? "declined" : result.status;
696
+ return reportNoConnection(deps, OUTCOME_MESSAGES[outcomeKey] ?? OUTCOME_MESSAGES.invalid, repoName);
628
697
  }
629
698
  const candidates = result.candidates;
630
699
  if (candidates.length === 0) {
@@ -729,8 +798,12 @@ export async function runConnectBitbucketCli(argv, injected) {
729
798
  baseUrl: deps.env.BAPI_BASE_URL?.trim() || DEFAULT_BAPI_BASE_URL,
730
799
  apiKey: cred.credentials.apiKey,
731
800
  };
801
+ // BAPI-1401: explicit --workspace, else the origin remote. Inferred even with
802
+ // --use-code, where it still resolves the printed settings-page link.
803
+ const workspace = await resolveBitbucketWorkspace(parsed.value.workspace, deps.cwd, deps.runGit, deps.stderr);
732
804
  const result = await runBitbucketConnectionFlow(deps, api, repo.value, {
733
- workspace: parsed.value.workspace,
805
+ workspace,
806
+ useCode: parsed.value.useCode,
734
807
  // Standalone: nobody has asked yet, so the flow owns the initial confirmation.
735
808
  initialMintApproved: false,
736
809
  });