@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
@@ -1,256 +0,0 @@
1
- /**
2
- * BAPI-634: thin CLI handler exposing the deterministic conductor-bundle helpers
3
- * to `emit-conductor-bundle.md`.
4
- *
5
- * Why this exists: the instruction ships **compiled** to consuming repositories
6
- * inside `build/pipelines.generated.js`. `mcp_server/src/` does not exist there,
7
- * so an instruction that told the agent to "call `validateConductorBundleInputs`"
8
- * would be referencing a path only this repository has. This subcommand is the
9
- * reachable surface, mirroring how `store-and-approve-epic-plan.md` invokes
10
- * `setup-epic` rather than hand-rolling HTTP calls.
11
- *
12
- * It holds no policy of its own: it parses argv, delegates to
13
- * `conductor-bundle-artifacts.ts`, and prints a JSON envelope. All I/O is behind
14
- * an injectable seam so tests never touch a real disk or construct a server.
15
- */
16
- import path from "node:path";
17
- import { finalizeEpicPlanSidecar, serializeSiblingTicketManifest, validateConductorBundleInputs, writeJsonAtomically, } from "./conductor-bundle-artifacts.js";
18
- const USAGE = [
19
- "Usage: emit-conductor-bundle <validate|finalize> --input <file> [--docs-dir <dir>] [--json]",
20
- "",
21
- " validate Validate identities, the node->ticket mapping, manifest agreement,",
22
- " and path containment. Writes nothing.",
23
- " finalize Validate, then finalize epic-plan.dag.json with real keys and",
24
- " per-node touched_files, and write the sibling-ticket manifest.",
25
- "",
26
- "Options:",
27
- " --input <file> JSON document with epic_key, epic_slug, mappings,",
28
- " decomposition_fingerprint, and (for finalize)",
29
- " touched_files_by_key.",
30
- " --docs-dir <dir> Docs directory (default: $BAPI_DOCS_DIR, else docs/tmp).",
31
- " --json Emit a machine-readable result on stdout.",
32
- " -h, --help Show this help.",
33
- ].join("\n");
34
- /** Read a flag's value, rejecting a missing or flag-shaped value. */
35
- function takeValue(argv, index, flag) {
36
- const value = argv[index + 1];
37
- if (value === undefined || value.startsWith("-")) {
38
- throw new Error(`Flag "${flag}" requires a value.`);
39
- }
40
- return value;
41
- }
42
- /** Parse argv into options, or throw with an actionable message. */
43
- export function parseConductorBundleArgs(argv) {
44
- const mode = argv[0];
45
- if (mode === "-h" || mode === "--help" || mode === undefined) {
46
- return { mode: "validate", inputFile: "", json: false, help: true };
47
- }
48
- if (mode !== "validate" && mode !== "finalize") {
49
- throw new Error(`Unknown subcommand "${mode}". Expected "validate" or "finalize".`);
50
- }
51
- let inputFile;
52
- let docsDir;
53
- let json = false;
54
- let help = false;
55
- for (let i = 1; i < argv.length; i++) {
56
- const arg = argv[i];
57
- switch (arg) {
58
- case "--input":
59
- inputFile = takeValue(argv, i, "--input");
60
- i++;
61
- break;
62
- case "--docs-dir":
63
- docsDir = takeValue(argv, i, "--docs-dir");
64
- i++;
65
- break;
66
- case "--json":
67
- json = true;
68
- break;
69
- case "-h":
70
- case "--help":
71
- help = true;
72
- break;
73
- default:
74
- throw new Error(`Unknown argument "${arg}".`);
75
- }
76
- }
77
- if (help)
78
- return { mode, inputFile: inputFile ?? "", docsDir, json, help: true };
79
- if (!inputFile)
80
- throw new Error('Flag "--input" is required.');
81
- return { mode, inputFile, docsDir, json, help: false };
82
- }
83
- /** Parse JSON, converting a syntax error into an actionable message. */
84
- async function readJson(filePath, fs, label) {
85
- let raw;
86
- try {
87
- raw = await fs.readFile(filePath);
88
- }
89
- catch (err) {
90
- throw new Error(`Could not read ${label} at ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
91
- }
92
- try {
93
- return JSON.parse(raw);
94
- }
95
- catch (err) {
96
- throw new Error(`${label} at ${filePath} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
97
- }
98
- }
99
- /** Read an optional JSON file, returning null when it does not exist. */
100
- async function readOptionalJson(filePath, fs, label) {
101
- try {
102
- await fs.readFile(filePath);
103
- }
104
- catch {
105
- return null;
106
- }
107
- return readJson(filePath, fs, label);
108
- }
109
- /**
110
- * Run the `emit-conductor-bundle` subcommand. Returns a process exit code; never
111
- * throws for an expected validation failure.
112
- */
113
- export async function runConductorBundleCli(argv, overrides = {}) {
114
- const deps = {
115
- env: process.env,
116
- cwd: process.cwd(),
117
- fs: createDefaultBundleFs(),
118
- log: (m) => console.log(m),
119
- // Diagnostics go to stderr so `--json` keeps stdout a single result object.
120
- errorLog: (m) => console.error(m),
121
- ...overrides,
122
- };
123
- let opts;
124
- try {
125
- opts = parseConductorBundleArgs(argv);
126
- }
127
- catch (err) {
128
- deps.errorLog(err instanceof Error ? err.message : String(err));
129
- deps.errorLog(USAGE);
130
- return 1;
131
- }
132
- if (opts.help) {
133
- deps.log(USAGE);
134
- return 0;
135
- }
136
- const emitFailure = (error) => {
137
- if (opts.json)
138
- deps.log(JSON.stringify({ ok: false, error }));
139
- else
140
- deps.errorLog(`Error: ${error}`);
141
- return 1;
142
- };
143
- const docsDir = path.resolve(deps.cwd, opts.docsDir ?? deps.env.BAPI_DOCS_DIR ?? path.join("docs", "tmp"));
144
- let input;
145
- try {
146
- input = (await readJson(path.resolve(deps.cwd, opts.inputFile), deps.fs, "--input document"));
147
- }
148
- catch (err) {
149
- return emitFailure(err instanceof Error ? err.message : String(err));
150
- }
151
- if (!input || typeof input !== "object") {
152
- return emitFailure("--input document must be a JSON object.");
153
- }
154
- const epicDir = path.resolve(docsDir, "epic-plans", String(input.epic_slug));
155
- const sidecarPath = path.join(epicDir, "epic-plan.dag.json");
156
- const manifestPath = path.join(epicDir, "sibling-ticket-manifest.json");
157
- let sidecar;
158
- let existingManifest;
159
- try {
160
- sidecar = await readJson(sidecarPath, deps.fs, "epic-plan.dag.json");
161
- existingManifest = await readOptionalJson(manifestPath, deps.fs, "sibling-ticket-manifest.json");
162
- }
163
- catch (err) {
164
- return emitFailure(err instanceof Error ? err.message : String(err));
165
- }
166
- const validation = await validateConductorBundleInputs({
167
- epic_key: input.epic_key,
168
- epic_slug: input.epic_slug,
169
- docs_dir: docsDir,
170
- mappings: input.mappings,
171
- sidecar,
172
- existing_manifest: existingManifest ?? undefined,
173
- decomposition_fingerprint: input.decomposition_fingerprint,
174
- }, deps.fs);
175
- if (!validation.ok)
176
- return emitFailure(validation.error);
177
- if (opts.mode === "validate") {
178
- const result = {
179
- ok: true,
180
- mode: "validate",
181
- epic_key: validation.value.epic_key,
182
- plan_version: validation.value.plan_version,
183
- mapped_nodes: validation.value.mappings.length,
184
- sidecar_path: validation.value.sidecar_path,
185
- };
186
- if (opts.json)
187
- deps.log(JSON.stringify(result));
188
- else {
189
- deps.log(`Validated ${result.mapped_nodes} node mapping(s) for ${result.epic_key} ` +
190
- `(plan v${result.plan_version}). Nothing was written.`);
191
- }
192
- return 0;
193
- }
194
- // ---- finalize ----------------------------------------------------------
195
- const nodeKeyMap = {};
196
- for (const m of validation.value.mappings) {
197
- nodeKeyMap[m.plan_node_id] = m.ticket_key;
198
- }
199
- const finalized = finalizeEpicPlanSidecar({
200
- sidecar,
201
- node_key_map: nodeKeyMap,
202
- touched_files_by_key: input.touched_files_by_key ?? {},
203
- plan_version_already_stored: input.plan_version_already_stored === true,
204
- });
205
- if (!finalized.ok)
206
- return emitFailure(finalized.error);
207
- const sidecarWrite = await writeJsonAtomically(validation.value.sidecar_path, finalized.value, deps.fs);
208
- if (!sidecarWrite.ok)
209
- return emitFailure(sidecarWrite.error);
210
- const manifest = serializeSiblingTicketManifest({
211
- schema_version: 1,
212
- epic_key: validation.value.epic_key,
213
- epic_slug: validation.value.epic_slug,
214
- plan_version: validation.value.plan_version,
215
- decomposition_fingerprint: input.decomposition_fingerprint,
216
- finalized_fingerprint: null,
217
- run_phase: input.run_phase ?? "staged",
218
- mappings: validation.value.mappings.map((m) => ({
219
- plan_node_id: m.plan_node_id,
220
- ticket_key: m.ticket_key,
221
- exploration_path: m.exploration_path,
222
- draft_path: m.draft_path,
223
- })),
224
- decisions: [],
225
- completed_mutations: [],
226
- });
227
- const manifestWrite = await writeJsonAtomically(validation.value.manifest_path, manifest, deps.fs);
228
- if (!manifestWrite.ok)
229
- return emitFailure(manifestWrite.error);
230
- const result = {
231
- ok: true,
232
- mode: "finalize",
233
- epic_key: validation.value.epic_key,
234
- plan_version: validation.value.plan_version,
235
- sidecar_path: validation.value.sidecar_path,
236
- manifest_path: validation.value.manifest_path,
237
- ticket_keys: validation.value.mappings.map((m) => m.ticket_key),
238
- };
239
- if (opts.json)
240
- deps.log(JSON.stringify(result));
241
- else {
242
- deps.log(`Finalized ${result.sidecar_path} with ${result.ticket_keys.length} real key(s) ` +
243
- `and per-node touched_files. Manifest: ${result.manifest_path}`);
244
- }
245
- return 0;
246
- }
247
- /** Real filesystem seam, loaded lazily so importing this module stays cheap. */
248
- function createDefaultBundleFs() {
249
- return {
250
- readFile: async (p) => (await import("node:fs/promises")).readFile(p, "utf-8"),
251
- writeFile: async (p, data) => (await import("node:fs/promises")).writeFile(p, data, "utf-8"),
252
- rename: async (from, to) => (await import("node:fs/promises")).rename(from, to),
253
- unlink: async (p) => (await import("node:fs/promises")).unlink(p),
254
- realpath: async (p) => (await import("node:fs/promises")).realpath(p),
255
- };
256
- }
@@ -1,70 +0,0 @@
1
- /**
2
- * direct-ticket-tools — agent-facing entry points for the ticket updater and the
3
- * single-ticket estimator (BAPI-711).
4
- *
5
- * Both flows were previously reachable only through a Jira-shaped webhook. In
6
- * Jira mode Jira is the driver; in local mode the driver is the agent, which
7
- * already talks to Bridge directly — so the fix is not a local event emitter, it
8
- * is a direct entry point. These tools are that entry point.
9
- *
10
- * Two properties are load-bearing:
11
- *
12
- * - The repository is INJECTED from `BAPI_REPO_NAME`, never accepted from the
13
- * agent. An agent-supplied repo would let a tool call address a repository
14
- * the session was not configured for; the backend re-authorizes it anyway,
15
- * but the schema should not invite the attempt.
16
- * - Every outcome — success, duplicate-active, and backend error — resolves to
17
- * a normal text content envelope. Throwing through the MCP transport turns a
18
- * recoverable 429 into a dead session.
19
- */
20
- function text(value) {
21
- return { content: [{ type: "text", text: value }] };
22
- }
23
- /**
24
- * POST a direct-invocation request and render its outcome as normal content.
25
- *
26
- * A connection failure is reported as text rather than rethrown: the submission
27
- * outcome is genuinely unknown at that point, and the recoverable action (poll
28
- * `get_ticket_state`) is the same either way.
29
- */
30
- async function postDirectInvocation(deps, path, body, ticketNumber) {
31
- let resp;
32
- try {
33
- resp = await (deps.fetchImpl ?? fetch)(deps.buildUrl(path), {
34
- method: "POST",
35
- headers: await deps.getPostHeaders(),
36
- body: JSON.stringify({ ...body, repo_name: deps.repoName }),
37
- });
38
- }
39
- catch {
40
- return text(`The request could not be delivered, so it is unknown whether it was accepted. ` +
41
- `Call get_ticket_state for ${ticketNumber} to check before retrying.`);
42
- }
43
- if (!resp.ok) {
44
- return text(await deps.handleResponse(resp));
45
- }
46
- return text(await resp.text());
47
- }
48
- export async function runRequestTicketUpdate(args, deps) {
49
- return postDirectInvocation(deps, `/ticket/${encodeURIComponent(args.ticket_number)}/generate-ticket-update`, {}, args.ticket_number);
50
- }
51
- export async function runRequestEstimate(args, deps) {
52
- return postDirectInvocation(deps, `/ticket/${encodeURIComponent(args.ticket_number)}/generate-estimate`,
53
- // Sent explicitly rather than omitted so the backend default and the tool
54
- // default can never drift apart silently.
55
- { recreate: args.recreate === true }, args.ticket_number);
56
- }
57
- export async function runGetTicketUpdateReview(args, deps) {
58
- const url = deps.buildGetUrl(`/ticket/${encodeURIComponent(args.ticket_number)}/ticket-update-review`, { repo_name: deps.repoName });
59
- let resp;
60
- try {
61
- resp = await (deps.fetchImpl ?? fetch)(url, { headers: await deps.getHeaders() });
62
- }
63
- catch {
64
- return text(`The held-for-review proposal for ${args.ticket_number} could not be fetched. Retry shortly.`);
65
- }
66
- if (!resp.ok) {
67
- return text(await deps.handleResponse(resp));
68
- }
69
- return text(await resp.text());
70
- }
@@ -1,129 +0,0 @@
1
- /**
2
- * Governance-region comparison for `reconcile_overlap` (BAPI-1152).
3
- *
4
- * Before the wrapper pushes, the candidate tree is compared against the
5
- * governance-guarded regions the SERVER found in the INPUT epic head
6
- * (`required_epic_sha`). A candidate that changes any protected region — its
7
- * content OR either marker line — is never pushed: the job reports the
8
- * `governance_guard_change_required` fact and the reconciler parks naming the tag.
9
- *
10
- * This module deliberately does NOT parse the marker grammar. The server owns it
11
- * (`api/library/epic_conductor/governance_markers.py`) and ships each region as
12
- * its exact BEGIN and END marker lines plus the SHA-256 of the region's content,
13
- * markers included. Here a region is located by those exact lines and compared by
14
- * hash — never by line number, which an edit above the region would shift without
15
- * touching anything it protects. A second grammar implementation would be a
16
- * second place for the two to disagree about what is protected.
17
- *
18
- * Fail-closed everywhere the comparison cannot be proven:
19
- * - the region's file is gone from the candidate (deleted or renamed), binary,
20
- * or unreadable;
21
- * - a marker line is missing, duplicated, or out of order;
22
- * - the server reported the file's marker structure unprovable (unbalanced,
23
- * nested, mismatched, or over the scan bound) and the candidate changes it;
24
- * - the candidate changes a file the server did NOT scan and either side of that
25
- * change contains a marker token — the server made no statement about it.
26
- * The last two carry the synthetic tag {@link UNPROVABLE_GOVERNANCE_TAG}, which is
27
- * itself valid marker grammar, so the park still names something greppable.
28
- */
29
- import { createHash } from "node:crypto";
30
- /** The synthetic tag for protection the comparison could not prove. */
31
- export const UNPROVABLE_GOVERNANCE_TAG = "GOVERNANCE_MARKERS_UNPROVABLE";
32
- /** The token every governance marker line contains. */
33
- export const GOVERNANCE_MARKER_TOKEN = "BRIDGE-GOVERNANCE-GUARD-";
34
- /** Bounds on the reported fact (mirrors the server's result-model bounds). */
35
- export const GOVERNANCE_FACT_MAX_FINDINGS = 8;
36
- export const GOVERNANCE_FACT_MAX_PATHS = 8;
37
- /**
38
- * Split text into lines exactly the way Python's `str.splitlines()` does, so the
39
- * region hash computed here matches the one the server computed.
40
- */
41
- export function splitLinesLikePython(text) {
42
- const lines = text.split(/\r\n|[\n\r\v\f\x1c\x1d\x1e\x85\u2028\u2029]/);
43
- // `splitlines()` yields no trailing empty entry for a terminal line break.
44
- if (lines.length > 0 && lines[lines.length - 1] === "")
45
- lines.pop();
46
- return lines;
47
- }
48
- function sha256(text) {
49
- return createHash("sha256").update(text, "utf8").digest("hex");
50
- }
51
- function indicesOf(lines, needle) {
52
- const out = [];
53
- lines.forEach((line, index) => {
54
- if (line === needle)
55
- out.push(index);
56
- });
57
- return out;
58
- }
59
- /** True when a region is intact in `text`: both markers once, in order, same hash. */
60
- function regionIntact(text, region) {
61
- const lines = splitLinesLikePython(text);
62
- const begins = indicesOf(lines, region.beginMarker);
63
- const ends = indicesOf(lines, region.endMarker);
64
- if (begins.length !== 1 || ends.length !== 1)
65
- return false;
66
- const [begin] = begins;
67
- const [end] = ends;
68
- if (end < begin)
69
- return false;
70
- return sha256(lines.slice(begin, end + 1).join("\n")) === region.contentSha256;
71
- }
72
- function containsMarkerToken(read) {
73
- // An unreadable or binary side cannot be proven marker-free.
74
- if (read.kind === "unreadable" || read.kind === "binary")
75
- return true;
76
- return read.kind === "text" && read.text.includes(GOVERNANCE_MARKER_TOKEN);
77
- }
78
- /**
79
- * Compare the candidate against the input epic head's governance regions.
80
- * Returns the structured fact, or `null` when nothing protected is touched.
81
- */
82
- export async function scanGovernanceRegions(input) {
83
- const hits = new Map();
84
- const hit = (tag, path) => {
85
- let paths = hits.get(tag);
86
- if (!paths) {
87
- paths = new Set();
88
- hits.set(tag, paths);
89
- }
90
- paths.add(path);
91
- };
92
- const changed = new Set(input.changedPaths);
93
- for (const region of input.snapshot.regions) {
94
- const candidate = await input.readCandidate(region.path);
95
- if (candidate.kind !== "text" || !regionIntact(candidate.text, region)) {
96
- hit(region.tag, region.path);
97
- }
98
- }
99
- for (const finding of input.snapshot.findings) {
100
- if (changed.has(finding.path))
101
- hit(UNPROVABLE_GOVERNANCE_TAG, finding.path);
102
- }
103
- const scanned = new Set(input.snapshot.scannedPaths);
104
- for (const path of input.changedPaths) {
105
- if (scanned.has(path))
106
- continue;
107
- const [candidate, original] = await Promise.all([
108
- input.readCandidate(path),
109
- input.readInput(path),
110
- ]);
111
- if (containsMarkerToken(candidate) || containsMarkerToken(original)) {
112
- hit(UNPROVABLE_GOVERNANCE_TAG, path);
113
- }
114
- }
115
- if (hits.size === 0)
116
- return null;
117
- const findings = Array.from(hits.entries())
118
- .map(([tag, paths]) => ({ tag, paths: Array.from(paths).sort().slice(0, GOVERNANCE_FACT_MAX_PATHS) }))
119
- .sort((a, b) => a.tag !== b.tag ? (a.tag < b.tag ? -1 : 1) : (a.paths[0] ?? "") < (b.paths[0] ?? "") ? -1 : 1);
120
- const bounded = findings.slice(0, GOVERNANCE_FACT_MAX_FINDINGS);
121
- const primary = bounded[0];
122
- return {
123
- tag: primary.tag,
124
- paths: primary.paths,
125
- findings: bounded,
126
- truncated: findings.length > GOVERNANCE_FACT_MAX_FINDINGS ||
127
- Array.from(hits.values()).some((paths) => paths.size > GOVERNANCE_FACT_MAX_PATHS),
128
- };
129
- }