@bridge_gpt/mcp-server 0.2.59 → 0.2.60

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 (71) hide show
  1. package/README.md +173 -51
  2. package/build/agent-guidance.generated.js +1 -1
  3. package/build/agent-notes.js +7 -1
  4. package/build/commands.generated.js +4 -4
  5. package/build/conductor/bridge-api-client.js +53 -2
  6. package/build/conductor/doctor.js +255 -53
  7. package/build/conductor/epic-runtime.js +11 -2
  8. package/build/conductor/install-doctor.js +24 -2
  9. package/build/conductor/local-merge.js +319 -26
  10. package/build/conductor/merge-execution-contract.js +61 -0
  11. package/build/conductor/merge-execution-eligibility.js +129 -0
  12. package/build/conductor/readiness-sections.js +159 -2
  13. package/build/conductor/readiness.js +95 -14
  14. package/build/conductor/recovery-operations.js +8 -2
  15. package/build/connect-bitbucket-api.js +6 -1
  16. package/build/connect-bitbucket.js +398 -88
  17. package/build/credential-store.js +16 -0
  18. package/build/docs.generated.js +2 -2
  19. package/build/doctor.js +183 -9
  20. package/build/drive-epic.js +23 -0
  21. package/build/epic-implementer-bin.js +10 -10
  22. package/build/epic-implementer-claude-hook-bin.js +1 -1
  23. package/build/epic-implementer-env.js +43 -14
  24. package/build/executor/ci-rerun-job.js +269 -0
  25. package/build/executor/cli.js +31 -0
  26. package/build/executor/credentials.js +11 -0
  27. package/build/executor/env.js +60 -0
  28. package/build/executor/index.js +21 -4
  29. package/build/executor/job-runner.js +126 -0
  30. package/build/executor/job-types.js +12 -2
  31. package/build/executor/merge-job.js +11 -2
  32. package/build/executor/service-lifecycle.js +136 -0
  33. package/build/executor/service-unit.js +427 -9
  34. package/build/executor/types.js +5 -0
  35. package/build/index.js +194 -179
  36. package/build/install-bridge-conductor.js +201 -8
  37. package/build/install-bridge.js +356 -30
  38. package/build/mcp-install-state.js +42 -4
  39. package/build/merge-pull-request.js +103 -10
  40. package/build/merge-workflow-codeowners.js +224 -0
  41. package/build/merge-workflow-config.js +133 -0
  42. package/build/merge-workflow-drift.js +66 -0
  43. package/build/merge-workflow-install.js +638 -0
  44. package/build/merge-workflow.js +572 -0
  45. package/build/pipelines.generated.js +2 -2
  46. package/build/plane/cli.js +24 -2
  47. package/build/plane/preflight.js +68 -0
  48. package/build/readme.generated.js +1 -1
  49. package/build/run-unit-tests-launcher.js +5 -0
  50. package/build/setup-epic.js +163 -9
  51. package/build/version.generated.js +3 -3
  52. package/package.json +3 -3
  53. package/build/conduct-epic/bridge-client.js +0 -773
  54. package/build/conduct-epic/checkpoint-store.js +0 -542
  55. package/build/conduct-epic/cli.js +0 -3145
  56. package/build/conduct-epic/cut-protocol.js +0 -392
  57. package/build/conduct-epic/lock.js +0 -302
  58. package/build/conduct-epic/pr-state.js +0 -286
  59. package/build/conduct-epic/spawn.js +0 -113
  60. package/build/conductor/cli.js +0 -1278
  61. package/build/conductor-bundle-cli.js +0 -256
  62. package/build/direct-ticket-tools.js +0 -70
  63. package/build/executor/reconcile-overlap-governance.js +0 -129
  64. package/build/implement-epic/bridge-client.js +0 -773
  65. package/build/implement-epic/checkpoint-store.js +0 -542
  66. package/build/implement-epic/cli.js +0 -3158
  67. package/build/implement-epic/cut-protocol.js +0 -392
  68. package/build/implement-epic/lock.js +0 -302
  69. package/build/implement-epic/pr-state.js +0 -286
  70. package/build/implement-epic/spawn.js +0 -113
  71. package/build/plan-epic-conductor-eligibility.js +0 -213
@@ -78,7 +78,7 @@ import { DEFAULT_GATE_NAME, REQUIRED_CI_CHECKS_GREEN, REVIEW_STATE, normalizeChe
78
78
  import { MERGE_REVIEW_WAIVED_BY_DEGRADATION_REASON, VERDICTLESS_DISPOSITION_FAIL_OPEN, VERDICTLESS_DISPOSITION_PARK, } from "./conductor/git-ci-types.js";
79
79
  import { evaluateReviewCondition, normalizeReviewSnapshot, parseDoneGateConfig, } from "./conductor/done-gate.js";
80
80
  import { buildGateIdentity, makeMergeActionKey } from "./conductor/merge-identity.js";
81
- import { resolveLocalMergeMethod, runApprovedLocalMerge, } from "./conductor/local-merge.js";
81
+ import { resolveLocalMergeMethod, runApprovedLocalMerge, resolveLocalMergeCredential, LOCAL_MERGE_CREDENTIAL_ENV_KEYS, LOCAL_MERGE_CREDENTIAL_MISSING_REASONS, } from "./conductor/local-merge.js";
82
82
  /**
83
83
  * The exact remediation for a `dry_run`. Fixed text, because a caller that has to
84
84
  * infer WHICH setting to flip from a reason code guesses wrong.
@@ -629,22 +629,86 @@ function interpretMergeResponse(body, expectedHeadSha, prNumber) {
629
629
  // ---------------------------------------------------------------------------
630
630
  /** The non-terminal status the server returns when it approves a local merge. */
631
631
  const LOCAL_APPROVAL_STATUS = "approved_for_local_execution";
632
- const DEFAULT_MERGE_EXECUTION = "local";
633
- /** Operator-fixable hints for the two preflight refusals. */
632
+ export const DEFAULT_MERGE_EXECUTION = "local";
633
+ /**
634
+ * The ONE place the legacy repository vocabulary is reconciled onto the typed
635
+ * policy vocabulary (BAPI-1133, A2). Exhaustive and pure.
636
+ *
637
+ * `local -> local` is the identity case. `server -> machine_token`, NOT
638
+ * `github_actions`: legacy `server` means the existing direct credentialed
639
+ * provider-API write through the App-mediated merge path
640
+ * (`executeApprovedLocalMerge`'s sibling branch on the server), so its typed
641
+ * equivalent is the direct-credential declaration. `github_actions` represents
642
+ * workflow-DISPATCH intent, which the legacy `server` behavior never performed
643
+ * and so can never be inferred from it. `server` is therefore structurally
644
+ * impossible as a {@link TypedMergeExecutionMode} — this function is the only
645
+ * place that mapping exists, so no other consumer can reimplement it
646
+ * differently.
647
+ */
648
+ export function mapLegacyMergeExecutionModeToPolicyMode(mode) {
649
+ switch (mode) {
650
+ case "local":
651
+ return "local";
652
+ case "server":
653
+ return "machine_token";
654
+ }
655
+ }
656
+ /**
657
+ * Which configured credential a resolved legacy mode reads (BAPI-1136).
658
+ *
659
+ * Routed through {@link mapLegacyMergeExecutionModeToPolicyMode} rather than
660
+ * switching on the legacy value again, so there remains exactly ONE place where
661
+ * legacy `server` becomes typed `machine_token`. Typed `github_actions` never
662
+ * reaches this local path — that mode dispatches the customer's workflow and
663
+ * performs no local subprocess — so it falls back to the local key rather than
664
+ * inventing a third credential.
665
+ */
666
+ export function resolveLocalCredentialMode(mode) {
667
+ const typed = mapLegacyMergeExecutionModeToPolicyMode(mode);
668
+ return typed === "machine_token" ? "machine_token" : "local";
669
+ }
670
+ /**
671
+ * Operator-fixable hints for every preflight and credential refusal.
672
+ *
673
+ * BAPI-1136 removed the `gh auth login` remediation outright. A merge no longer
674
+ * authenticates as whoever is logged into `gh`, so telling an operator to log in
675
+ * would send them to fix a thing that is not consulted — the most expensive kind
676
+ * of wrong hint, because following it appears reasonable and changes nothing.
677
+ *
678
+ * Each hint names the CONFIGURATION KEY to set, never where or how a secret
679
+ * manager should materialize the value: that is the operator's own infrastructure
680
+ * decision, and prescribing one would be guidance this tool cannot verify.
681
+ */
634
682
  const LOCAL_GH_HINTS = {
635
683
  local_gh_unavailable: "Install the GitHub CLI (`gh`) on the machine running this MCP server — local merges execute there.",
636
- local_gh_unauthenticated: "Run `gh auth login` in the shell that hosts this MCP server — local merges use its GitHub session.",
684
+ local_merge_credential_missing: `Set ${LOCAL_MERGE_CREDENTIAL_ENV_KEYS.local} in the environment this MCP server is launched with. ` +
685
+ "It is a configured local scoped credential; Bridge never stores it and there is no ambient-login fallback.",
686
+ machine_merge_credential_missing: `Set ${LOCAL_MERGE_CREDENTIAL_ENV_KEYS.machine_token} in the environment this MCP server is launched with. ` +
687
+ "It is your machine user's credential; Bridge never stores it and there is no ambient-login fallback.",
688
+ local_merge_credential_invalid: `The configured ${LOCAL_MERGE_CREDENTIAL_ENV_KEYS.local} was rejected by GitHub. Rotate it and relaunch this MCP server.`,
689
+ machine_merge_credential_invalid: `The configured ${LOCAL_MERGE_CREDENTIAL_ENV_KEYS.machine_token} was rejected by GitHub. Rotate it and relaunch this MCP server.`,
690
+ local_merge_credential_unpermitted: `The configured ${LOCAL_MERGE_CREDENTIAL_ENV_KEYS.local} authenticates but cannot write to this repository. Grant it merge permission.`,
691
+ machine_merge_credential_unpermitted: `The configured ${LOCAL_MERGE_CREDENTIAL_ENV_KEYS.machine_token} authenticates but cannot write to this repository. Grant the machine user merge permission.`,
637
692
  };
638
693
  /**
639
- * Read the repository's merge execution mode.
694
+ * Read the repository's LEGACY merge execution mode.
640
695
  *
641
696
  * Mode is server-side configuration, never a tool argument: an agent must not be
642
697
  * able to choose who performs a privileged write. An unreadable or unrecognized
643
698
  * value falls back to `local`, which is the mode that works on an installation
644
699
  * without `contents: write` — defaulting to the broken mode on a transient
645
- * config read failure would park a perfectly mergeable pull request.
700
+ * config read failure would park a perfectly mergeable pull request. Total: every
701
+ * failure mode (network error, non-2xx, malformed body, missing/unknown
702
+ * `merge_execution`) resolves to {@link DEFAULT_MERGE_EXECUTION} rather than
703
+ * throwing.
704
+ *
705
+ * Returns only the legacy vocabulary — `local | server` — never a typed mode.
706
+ * `setup-epic.ts` maps this result through
707
+ * {@link mapLegacyMergeExecutionModeToPolicyMode} before composing it into a
708
+ * `RunPolicy` declaration; `mergePullRequestHandler` sends it straight to the
709
+ * existing merge route unchanged.
646
710
  */
647
- async function resolveMergeExecutionMode(deps) {
711
+ export async function resolveMergeExecutionMode(deps) {
648
712
  try {
649
713
  const resp = await (deps.fetchImpl ?? fetch)(deps.buildGetUrl("/epic-runs/supervisor-config/defaults/", { repo_name: deps.repoName }), { headers: await deps.getHeaders() });
650
714
  if (!resp.ok)
@@ -718,7 +782,7 @@ async function reportLocalCompletion(deps, prNumber, body) {
718
782
  * left to time out; and a refusal here is always `needs_human`, because every
719
783
  * local failure this can produce is an operator fix, not a wait.
720
784
  */
721
- async function executeApprovedLocalMerge(deps, approval, prNumber, expectedHeadSha, actionKey) {
785
+ async function executeApprovedLocalMerge(deps, approval, prNumber, expectedHeadSha, actionKey, credentialMode) {
722
786
  if (localApprovalMismatch(approval, prNumber, expectedHeadSha, actionKey)) {
723
787
  return envelope(false, "refused", "local_approval_mismatch", "needs_human", expectedHeadSha, prNumber);
724
788
  }
@@ -730,10 +794,34 @@ async function executeApprovedLocalMerge(deps, approval, prNumber, expectedHeadS
730
794
  gate: { name: DEFAULT_GATE_NAME },
731
795
  action_key: actionKey,
732
796
  };
797
+ // BAPI-1136. The credential is resolved HERE, before the executor is called,
798
+ // so an unconfigured mode refuses without spawning a privileged subprocess —
799
+ // and so the refusal names which key to set rather than surfacing whatever
800
+ // `gh` says when it has no identity.
801
+ const credential = resolveLocalMergeCredential(deps.runtimeEnv ?? process.env, credentialMode);
802
+ if (credential === null) {
803
+ const reason = LOCAL_MERGE_CREDENTIAL_MISSING_REASONS[credentialMode];
804
+ // The lease is still closed: an attempt that cannot run is an attempt that
805
+ // is over, and leaving the ledger row open would make the reconciler wait
806
+ // out a lease for a merge nobody is going to perform.
807
+ await reportLocalCompletion(deps, prNumber, {
808
+ repo_name: deps.repoName,
809
+ action_key: actionKey,
810
+ expected_head_sha: expectedHeadSha,
811
+ result: "failed",
812
+ reason,
813
+ });
814
+ const missingHint = LOCAL_GH_HINTS[reason];
815
+ return envelope(false, "refused", reason, "needs_human", expectedHeadSha, prNumber, {
816
+ ...(missingHint ? { hint: missingHint } : {}),
817
+ });
818
+ }
733
819
  let local;
734
820
  try {
735
821
  const runLocal = deps.runLocalMerge ?? runApprovedLocalMerge;
736
- local = await runLocal(request, { method }, { env: process.env });
822
+ // Exactly the resolved credential — never `{ env: process.env }`, which is
823
+ // how the ambient identity used to reach the subprocess.
824
+ local = await runLocal(request, { method }, { credential });
737
825
  }
738
826
  catch {
739
827
  // The executor itself threw. Treat it exactly like a reported failure: close
@@ -891,7 +979,12 @@ export async function mergePullRequestHandler(deps, args) {
891
979
  if (executionMode === "local" &&
892
980
  isPlainObject(mergeJson) &&
893
981
  mergeJson.status === LOCAL_APPROVAL_STATUS) {
894
- return text(withReviewWaiver(await executeApprovedLocalMerge(deps, mergeJson, prNumber, expectedHeadSha, actionKey), reviewWaiver));
982
+ return text(withReviewWaiver(await executeApprovedLocalMerge(deps, mergeJson, prNumber, expectedHeadSha, actionKey,
983
+ // The AUTHORITATIVE typed axis decides which credential is read.
984
+ // Legacy `server` maps to typed `machine_token` through the one
985
+ // existing mapping function; nothing here re-derives that, and no
986
+ // tool argument can influence it.
987
+ resolveLocalCredentialMode(executionMode)), reviewWaiver));
895
988
  }
896
989
  return text(withReviewWaiver(interpretMergeResponse(mergeJson, expectedHeadSha, prNumber), reviewWaiver));
897
990
  }
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Customer-owned review artifacts for the merge workflow (BAPI-1136).
3
+ *
4
+ * The merge workflow is only as safe as the review gate on its path. This module
5
+ * renders the two artifacts that make that gate possible, and it renders NOTHING
6
+ * that enforces it — enforcement is a repository setting only the customer can
7
+ * apply.
8
+ *
9
+ * ## Why a fragment and not an edit to `.github/CODEOWNERS`
10
+ *
11
+ * A repository's CODEOWNERS file is live policy. An installer that rewrote it
12
+ * could reassign ownership of unrelated paths, drop a rule whose syntax it
13
+ * misparsed, or resolve a conflict nobody asked it to resolve — and it would do
14
+ * so inside a pull request whose reviewer is reading a workflow file, not a
15
+ * policy diff. So this emits a self-contained FRAGMENT at a dedicated path and
16
+ * tells the customer to merge it themselves. The fragment is real, reviewable
17
+ * CODEOWNERS syntax; it simply is not yet in force.
18
+ *
19
+ * ## Why the guidance is plain text
20
+ *
21
+ * It is read in a pull-request diff, in a terminal, and by a screen reader. So:
22
+ * descriptive headings, explicit "Required:" labels, short paragraphs, stable
23
+ * ordering, and no code block that needs horizontal scrolling.
24
+ *
25
+ * ## Secret discipline
26
+ *
27
+ * Neither renderer accepts, embeds, or invites a secret VALUE. The Bridge API
28
+ * key is named only — `BAPI_API_KEY` — and every command shown is one that
29
+ * cannot take a credential on argv.
30
+ */
31
+ import { MERGE_WORKFLOW_EXPECTED_VERSION, MERGE_WORKFLOW_PATH, } from "./conductor/merge-execution-contract.js";
32
+ import { MERGE_WORKFLOW_API_KEY_SECRET_NAME, MERGE_WORKFLOW_BASE_URL_VARIABLE_NAME, } from "./merge-workflow-config.js";
33
+ /** Repo-relative path of the generated CODEOWNERS fragment. */
34
+ export const MERGE_WORKFLOW_CODEOWNERS_RELPATH = ".bridge/merge-workflow/CODEOWNERS.fragment";
35
+ /** Repo-relative path of the generated repository-rule guidance. */
36
+ export const MERGE_WORKFLOW_GUIDANCE_RELPATH = ".bridge/merge-workflow/REPOSITORY-RULES.md";
37
+ /**
38
+ * Every repo-relative path this installer may write, in the order a reader
39
+ * should see them. Exported as the single manifest so the preflight, the
40
+ * consent summary, the commit, and the install-state record cannot disagree
41
+ * about what "the managed artifacts" are.
42
+ */
43
+ export const MERGE_WORKFLOW_MANAGED_RELPATHS = [
44
+ MERGE_WORKFLOW_PATH,
45
+ MERGE_WORKFLOW_CODEOWNERS_RELPATH,
46
+ MERGE_WORKFLOW_GUIDANCE_RELPATH,
47
+ ];
48
+ /** Thrown when a code-owner value fails validation. Carries no rendered output. */
49
+ export class MergeWorkflowCodeOwnerError extends Error {
50
+ constructor(message) {
51
+ super(message);
52
+ this.name = "MergeWorkflowCodeOwnerError";
53
+ }
54
+ }
55
+ /**
56
+ * A GitHub code owner: `@user` or `@org/team`.
57
+ *
58
+ * Deliberately rejects whitespace, quotes, `#` (which would comment out the rest
59
+ * of the CODEOWNERS line), `$`, `{`, `}`, and any second `/`. The pattern is an
60
+ * allowlist rather than a denylist so a character nobody thought about is
61
+ * rejected rather than rendered.
62
+ */
63
+ const CODE_OWNER_PATTERN = /^@[A-Za-z0-9][A-Za-z0-9-]*(\/[A-Za-z0-9][A-Za-z0-9._-]*)?$/;
64
+ const MAX_CODE_OWNER_LENGTH = 100;
65
+ /**
66
+ * Values that parse as a code owner but are obviously a placeholder someone
67
+ * pasted from documentation. Accepting one would open a pull request assigning
68
+ * review of a write-enabled workflow to an account that does not exist, which
69
+ * reads as protected while protecting nothing.
70
+ */
71
+ const PLACEHOLDER_CODE_OWNERS = new Set([
72
+ "@org/team",
73
+ "@owner/team",
74
+ "@your-org/your-team",
75
+ "@user",
76
+ "@username",
77
+ "@your-username",
78
+ "@example",
79
+ "@example/team",
80
+ ]);
81
+ /**
82
+ * Validate a single code owner.
83
+ *
84
+ * Runs BEFORE any filesystem or git mutation — an invalid owner must cost
85
+ * nothing, and must never leave a half-written branch behind.
86
+ *
87
+ * @throws MergeWorkflowCodeOwnerError naming the problem, never echoing a value
88
+ * longer than the bound above.
89
+ */
90
+ export function validateMergeWorkflowCodeOwner(value) {
91
+ if (typeof value !== "string") {
92
+ throw new MergeWorkflowCodeOwnerError("--codeowner requires a value");
93
+ }
94
+ const trimmed = value.trim();
95
+ if (trimmed.length === 0) {
96
+ throw new MergeWorkflowCodeOwnerError("--codeowner requires a non-empty value");
97
+ }
98
+ if (trimmed.length > MAX_CODE_OWNER_LENGTH) {
99
+ throw new MergeWorkflowCodeOwnerError("--codeowner value is too long");
100
+ }
101
+ if (/\s/.test(trimmed)) {
102
+ throw new MergeWorkflowCodeOwnerError("--codeowner takes exactly one owner; supply a single @user or @org/team");
103
+ }
104
+ if (!trimmed.startsWith("@")) {
105
+ throw new MergeWorkflowCodeOwnerError("--codeowner must start with @ (e.g. @org/team or @user)");
106
+ }
107
+ if (!CODE_OWNER_PATTERN.test(trimmed)) {
108
+ throw new MergeWorkflowCodeOwnerError("--codeowner must be a GitHub @user or @org/team handle");
109
+ }
110
+ if (PLACEHOLDER_CODE_OWNERS.has(trimmed.toLowerCase())) {
111
+ throw new MergeWorkflowCodeOwnerError("--codeowner looks like a documentation placeholder; supply the real reviewing team or user");
112
+ }
113
+ return trimmed;
114
+ }
115
+ /**
116
+ * Render the CODEOWNERS fragment for the merge-workflow path.
117
+ *
118
+ * Deterministic for a given owner, newline-terminated, and covering exactly one
119
+ * path. It claims no other file.
120
+ */
121
+ export function renderMergeWorkflowCodeownersFragment(codeOwner) {
122
+ const owner = validateMergeWorkflowCodeOwner(codeOwner);
123
+ return [
124
+ "# Bridge-managed CODEOWNERS fragment for the Epic Implementer merge workflow.",
125
+ "#",
126
+ "# This file is NOT a CODEOWNERS file. GitHub reads CODEOWNERS only from",
127
+ "# .github/CODEOWNERS, CODEOWNERS, or docs/CODEOWNERS, so nothing here is in",
128
+ "# force until you copy the rule below into the file your repository actually",
129
+ "# uses. It is generated separately so an installer never rewrites live review",
130
+ "# policy on your behalf.",
131
+ "#",
132
+ "# The rule exists because " + MERGE_WORKFLOW_PATH + " is the one file in this",
133
+ "# repository that can perform a privileged merge. Whoever can change it can",
134
+ "# change what merges.",
135
+ "",
136
+ MERGE_WORKFLOW_PATH + " " + owner,
137
+ "",
138
+ ].join("\n");
139
+ }
140
+ /**
141
+ * Render the repository-rule guidance document.
142
+ *
143
+ * Deterministic for a given owner. States, in order: what was generated, what
144
+ * the customer must do, and what is NOT protected until they do it.
145
+ */
146
+ export function renderMergeWorkflowGuidance(codeOwner) {
147
+ const owner = validateMergeWorkflowCodeOwner(codeOwner);
148
+ return [
149
+ "# Epic Implementer merge workflow — required repository setup",
150
+ "",
151
+ "Bridge generated three files in this pull request. Merging the pull request",
152
+ "installs them. It does not protect them: two of the three steps below are",
153
+ "repository settings that only you can apply.",
154
+ "",
155
+ "## What was generated",
156
+ "",
157
+ "- `" + MERGE_WORKFLOW_PATH + "` — the merge workflow itself, contract version " +
158
+ String(MERGE_WORKFLOW_EXPECTED_VERSION) + ".",
159
+ "- `" + MERGE_WORKFLOW_CODEOWNERS_RELPATH + "` — a CODEOWNERS rule for that path.",
160
+ "- `" + MERGE_WORKFLOW_GUIDANCE_RELPATH + "` — this document.",
161
+ "",
162
+ "## Required: create the repository secret",
163
+ "",
164
+ "Required. The workflow authenticates to Bridge with a repository-level Actions",
165
+ "secret named `" + MERGE_WORKFLOW_API_KEY_SECRET_NAME + "`, holding a Bridge API",
166
+ "key for this repository.",
167
+ "",
168
+ "Create it through GitHub's own secret interface — repository Settings, then",
169
+ "Secrets and variables, then Actions. Bridge never sees, transmits, stores, or",
170
+ "writes the value: the installer that opened this pull request has no flag, no",
171
+ "prompt, and no file field for a credential, and the value must never be",
172
+ "committed, printed, or pasted into a project file.",
173
+ "",
174
+ "Optional: set a repository Actions VARIABLE named",
175
+ "`" + MERGE_WORKFLOW_BASE_URL_VARIABLE_NAME + "` if this repository talks to a",
176
+ "Bridge deployment other than the default. A variable, not a secret, and not a",
177
+ "workflow input — the base URL decides which server authorizes a merge, so it",
178
+ "is repository configuration rather than something a caller supplies per run.",
179
+ "",
180
+ "## Required: merge the CODEOWNERS rule into your active CODEOWNERS file",
181
+ "",
182
+ "Required. GitHub reads code owners only from `.github/CODEOWNERS`,",
183
+ "`CODEOWNERS`, or `docs/CODEOWNERS`. The generated fragment sits at",
184
+ "`" + MERGE_WORKFLOW_CODEOWNERS_RELPATH + "` and enforces nothing where it is.",
185
+ "",
186
+ "Copy its single rule into the CODEOWNERS file your repository actually uses,",
187
+ "keeping your existing rules intact. The rule is:",
188
+ "",
189
+ "```",
190
+ MERGE_WORKFLOW_PATH + " " + owner,
191
+ "```",
192
+ "",
193
+ "CODEOWNERS is last-match-wins, so place it after any broader rule that also",
194
+ "matches `.github/workflows/`.",
195
+ "",
196
+ "## Required: add the default-branch repository rule",
197
+ "",
198
+ "Required. A CODEOWNERS entry requests a reviewer. It does not require one.",
199
+ "Until you add a rule, anyone who can push to the default branch can rewrite",
200
+ "the merge workflow.",
201
+ "",
202
+ "In repository Settings, under Rules, add a ruleset targeting the default",
203
+ "branch that requires a pull request before merging and requires review from",
204
+ "code owners. Restricting who can bypass the ruleset matters as much as the",
205
+ "rule itself.",
206
+ "",
207
+ "## What is not protected until you finish",
208
+ "",
209
+ "Enforcement is absent until the repository rule above is in place. Between",
210
+ "merging this pull request and applying that rule, the workflow is installed",
211
+ "and reviewable but its path is not review-gated.",
212
+ "",
213
+ "## If something fails later",
214
+ "",
215
+ "Bridge refuses to dispatch a merge when it cannot detect the workflow, or when",
216
+ "the workflow's contract version is not the one Bridge understands. Escalate",
217
+ "both to a repository administrator — they are the only person who can check",
218
+ "that the workflow is present on the default branch, that Actions is enabled",
219
+ "for this repository, and that the file has not been hand-edited. Re-running",
220
+ "the installer reports which of the four states the file is in and never",
221
+ "overwrites a file it did not write.",
222
+ "",
223
+ ].join("\n");
224
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Configuration names and the command-fetch contract the RENDERED merge workflow
3
+ * consumes (BAPI-1136).
4
+ *
5
+ * ## Why these live here and not in `conductor/merge-execution-contract.ts`
6
+ *
7
+ * That module documents itself as the byte-for-byte TypeScript MIRROR of
8
+ * `api/library/epic_implementer/merge_execution_contract.py`: every constant in
9
+ * it has a Python twin whose spelling must match exactly. None of the values
10
+ * below has one. They are the workflow's OWN runtime configuration — a GitHub
11
+ * Actions secret name, a GitHub Actions variable name, a fallback base URL, and
12
+ * the shape of an endpoint the rendered YAML calls — so keeping them in the
13
+ * mirror would have made that module two things at once.
14
+ *
15
+ * There is also a mechanical reason, and it is a good one: a static invariant
16
+ * forbids any `conductor/*.ts` except `bridge-api-client.ts` from containing the
17
+ * literal `BAPI_BASE_URL`, because conductor code must reach the Bridge API only
18
+ * through its dedicated sanitized client. This module reaches the Bridge API not
19
+ * at all — it names a string that the CUSTOMER's workflow reads from its own
20
+ * repository variables — but the guard is textual, and the right response to it
21
+ * was to put these values where they belong rather than to work around it.
22
+ */
23
+ import { MERGE_WORKFLOW_EXPECTED_VERSION } from "./conductor/merge-execution-contract.js";
24
+ /** Re-exported so a consumer needs one import for the workflow's whole contract. */
25
+ export { MERGE_WORKFLOW_EXPECTED_VERSION };
26
+ /**
27
+ * The ONE repository-level GitHub Actions secret the rendered workflow reads.
28
+ *
29
+ * Only the NAME lives here, and only the name ever appears in a rendered
30
+ * template, an installer message, or the README. The customer provisions the
31
+ * value themselves through GitHub's own secret UI or API; the installer never
32
+ * accepts, transmits, prints, or writes it (ratified review decision E-18).
33
+ *
34
+ * Repository-scoped rather than organization-scoped on purpose: a repository
35
+ * reviewer can create a repository secret, while an organization secret needs a
36
+ * second actor the install command cannot name in advance.
37
+ */
38
+ export const MERGE_WORKFLOW_API_KEY_SECRET_NAME = "BAPI_API_KEY";
39
+ /**
40
+ * The repository-level Actions VARIABLE (not a secret) that overrides the Bridge
41
+ * base URL the workflow calls back into.
42
+ *
43
+ * A variable, not a dispatch input: the two dispatch inputs are fixed at
44
+ * `action_key` and `lease_owner` (BAPI-1134), and a base URL supplied by whoever
45
+ * triggered the dispatch would let the caller choose which server authorizes the
46
+ * merge. Absent, the workflow uses {@link MERGE_WORKFLOW_DEFAULT_BASE_URL}.
47
+ */
48
+ export const MERGE_WORKFLOW_BASE_URL_VARIABLE_NAME = "BAPI_BASE_URL";
49
+ /** The hosted Bridge API the rendered workflow falls back to. */
50
+ export const MERGE_WORKFLOW_DEFAULT_BASE_URL = "https://bridgegpt-api.com";
51
+ /**
52
+ * The durable merge-command fetch contract the rendered workflow consumes.
53
+ *
54
+ * ## Why a named contract rather than a live route
55
+ *
56
+ * The SERVER half of this contract is not in the tree. BAPI-1134 shipped the
57
+ * dispatch emitter with its durable-command persistence resolved lazily
58
+ * (`api/library/epic_implementer/reconciler.py`'s
59
+ * `_resolve_durable_merge_command_store`, which imports
60
+ * `api.library.vcs.epic_implementer_merge_command_store.persist_merge_command`
61
+ * and returns `None` today), and recorded that store as "a slice of its own"
62
+ * blocked on an unnamed sibling. The BAPI-1136 review ratified carving that
63
+ * store and its fetch endpoint out as a separate A2 child that lands FIRST
64
+ * (decision E-14), with this child writing the workflow's fetch step against the
65
+ * named contract below.
66
+ *
67
+ * ## What that means operationally
68
+ *
69
+ * Until the sibling child lands, the endpoint answers 404 and the rendered
70
+ * workflow refuses with `merge_command_unavailable` before touching the pull
71
+ * request — no retry, no merge, no completion report. The merge lane is
72
+ * therefore INERT by construction rather than by a flag someone could flip: no
73
+ * dispatch is emitted at all while the command store is absent, and a dispatch
74
+ * that somehow arrived would be refused at the first step. That is the block on
75
+ * operational enablement; {@link MERGE_WORKFLOW_EXPECTED_VERSION} exact equality
76
+ * remains the SEPARATE deploy-ordering refusal it already was, and neither
77
+ * substitutes for the other.
78
+ *
79
+ * ## Shape
80
+ *
81
+ * Keyed by `action_key`, NOT by pull request number: the workflow learns the
82
+ * pull request FROM the command, so a PR-scoped route could not be called. That
83
+ * is also why a fetch failure cannot be reported through
84
+ * `POST /vcs/pull-requests/{pr}/merge/complete` — the path parameter is not yet
85
+ * resolved — and the lease's own expiry is the reconciler's backstop for that
86
+ * one window.
87
+ */
88
+ export const MERGE_COMMAND_FETCH_METHOD = "POST";
89
+ /** Root-mounted, repo-authorized path of the durable command fetch. */
90
+ export const MERGE_COMMAND_FETCH_PATH = "/vcs/merge-commands/fetch";
91
+ /**
92
+ * The request fields the fetch accepts, in the contract's own spelling.
93
+ *
94
+ * `lease_owner` is the per-attempt secret BAPI-1134 mints; it is presented here
95
+ * as PROOF that this dispatch is the live attempt, never echoed, and never a
96
+ * response field.
97
+ */
98
+ export const MERGE_COMMAND_FETCH_REQUEST_FIELDS = [
99
+ "repo_name",
100
+ "action_key",
101
+ "lease_owner",
102
+ ];
103
+ /**
104
+ * The separator and field position the Bridge repository name occupies inside a
105
+ * merge action key (`merge:{repo}:{pr}:{head_sha}:{gate_identity}`, built by
106
+ * `makeMergeActionKey`).
107
+ *
108
+ * The workflow derives `repo_name` from the key it was dispatched with rather
109
+ * than accepting a third dispatch input, so the Bridge-side identity and the
110
+ * attempt identity cannot disagree.
111
+ */
112
+ export const MERGE_ACTION_KEY_REPO_FIELD_INDEX = 1;
113
+ /**
114
+ * The response projection the workflow consumes, in the contract's own spelling.
115
+ *
116
+ * `lease_owner` is deliberately ABSENT: a response that echoed the live
117
+ * per-attempt secret would hand it to every log that captured the body.
118
+ *
119
+ * `github_repository` is the provider-side `owner/name` the command was issued
120
+ * against. The workflow compares it with `github.repository` before reading the
121
+ * pull request, so a command minted for a different repository cannot actuate
122
+ * here even if its key and lease were presented correctly.
123
+ */
124
+ export const MERGE_COMMAND_FETCH_RESPONSE_FIELDS = [
125
+ "action_key",
126
+ "repo_name",
127
+ "github_repository",
128
+ "pr_number",
129
+ "expected_head_sha",
130
+ "merge_method",
131
+ "execution",
132
+ "lease_expires_at",
133
+ ];
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Canonical byte identity for the Epic Implementer merge workflow (BAPI-1136).
3
+ *
4
+ * The merge workflow is the one file in a customer repository that can perform a
5
+ * privileged write on Bridge's behalf, so "is the installed copy the copy Bridge
6
+ * ships?" has to be answerable cheaply, in more than one place, and with one
7
+ * answer. This module is that one place.
8
+ *
9
+ * It follows `claude-review-workflow-drift.ts`'s shape deliberately: a leaf
10
+ * module with NO filesystem, network, or subprocess access, importing only the
11
+ * shared Git blob hash so two workflow-identity implementations cannot drift
12
+ * apart about what "the same file" means. Callers acquire bytes however their
13
+ * own layer already does and hand them in.
14
+ *
15
+ * Nothing here logs file content: a classification carries a path, a pair of
16
+ * SHAs, and a closed-vocabulary state — never bytes, never provider text.
17
+ */
18
+ import { computeGitBlobSha } from "./claude-review-workflow-drift.js";
19
+ import { MERGE_WORKFLOW_PATH } from "./conductor/merge-execution-contract.js";
20
+ import { renderEpicImplementerMergeWorkflow } from "./merge-workflow.js";
21
+ export { MERGE_WORKFLOW_PATH };
22
+ /**
23
+ * The Git blob SHA-1 of the workflow this build renders.
24
+ *
25
+ * Computed, never hardcoded. A checked-in literal would be a second byte source
26
+ * the renderer could silently diverge from — exactly the drift this module
27
+ * exists to make impossible.
28
+ */
29
+ export function canonicalMergeWorkflowSha() {
30
+ return computeGitBlobSha(Buffer.from(renderEpicImplementerMergeWorkflow(), "utf8"));
31
+ }
32
+ /**
33
+ * Every Bridge-published historical rendering of the merge workflow, by Git blob
34
+ * SHA, with the contract version it declared.
35
+ *
36
+ * Empty at contract version 1: there is no earlier published version to upgrade
37
+ * FROM. A future contract version adds its predecessor's SHA here, and only
38
+ * then can {@link classifyMergeWorkflowContent} return `managed_version_older`.
39
+ */
40
+ export const MANAGED_MERGE_WORKFLOW_HISTORY = new Map();
41
+ /**
42
+ * Classify an installed copy against the canonical bytes.
43
+ *
44
+ * `existing` is `null` for an absent file. The comparison is on RAW BYTES, not
45
+ * normalized text: a copy that differs only in line endings is still a copy this
46
+ * installer did not write, and treating it as identical would let a rewrite hide
47
+ * behind a whitespace-insensitive compare.
48
+ */
49
+ export function classifyMergeWorkflowContent(existing) {
50
+ if (existing === null)
51
+ return "not_installed";
52
+ const canonical = renderEpicImplementerMergeWorkflow();
53
+ if (existing === canonical)
54
+ return "identical";
55
+ const sha = computeGitBlobSha(Buffer.from(existing, "utf8"));
56
+ if (MANAGED_MERGE_WORKFLOW_HISTORY.has(sha))
57
+ return "managed_version_older";
58
+ return "modified_or_unrecognized";
59
+ }
60
+ /** Operator-facing gloss for each classification. Secret-free and path-only. */
61
+ export const MERGE_WORKFLOW_CLASSIFICATION_TEXT = {
62
+ not_installed: `${MERGE_WORKFLOW_PATH} is not installed`,
63
+ identical: `${MERGE_WORKFLOW_PATH} already matches the version this build ships`,
64
+ managed_version_older: `${MERGE_WORKFLOW_PATH} is an older Bridge-managed version and can be upgraded`,
65
+ modified_or_unrecognized: `${MERGE_WORKFLOW_PATH} has been modified or is not a version Bridge published`,
66
+ };