@bridge_gpt/mcp-server 0.2.53 → 0.2.55

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 (103) hide show
  1. package/README.md +116 -56
  2. package/build/agent-guidance.generated.js +3 -0
  3. package/build/agent-launchers/claude-executor-adapter.js +3 -0
  4. package/build/agent-launchers/claude.js +3 -3
  5. package/build/agent-launchers/prompt.js +8 -11
  6. package/build/agent-notes.js +178 -0
  7. package/build/agent-registry.js +5 -2
  8. package/build/agent-utils.js +58 -0
  9. package/build/agents.generated.js +1 -1
  10. package/build/base-ref.js +33 -9
  11. package/build/bounded-wait.js +174 -0
  12. package/build/codex-skill-adapter.js +55 -0
  13. package/build/commands.generated.js +6 -5
  14. package/build/conductor/bridge-api-client.js +235 -14
  15. package/build/conductor/bring-up-facts.js +187 -0
  16. package/build/conductor/claude-hook.js +7 -5
  17. package/build/conductor/cli.js +28 -0
  18. package/build/conductor/doctor.js +80 -9
  19. package/build/conductor/epic-implementer-cli.js +1298 -0
  20. package/build/conductor/epic-runtime.js +134 -98
  21. package/build/conductor/errors.js +2 -2
  22. package/build/conductor/git-ci-types.js +1 -1
  23. package/build/conductor/git-hooks.js +28 -14
  24. package/build/conductor/install-doctor.js +11 -5
  25. package/build/conductor/readiness-cli.js +10 -10
  26. package/build/conductor/readiness-sections.js +58 -9
  27. package/build/conductor/readiness.js +120 -4
  28. package/build/conductor/recovery-cli.js +289 -10
  29. package/build/conductor/recovery-operations.js +125 -2
  30. package/build/conductor/repair-contract.js +58 -0
  31. package/build/conductor/run-branch.js +137 -0
  32. package/build/conductor/store.js +2 -2
  33. package/build/conductor/supervisor-runtime.js +1 -1
  34. package/build/conductor/test-run-branch-vectors.js +165 -0
  35. package/build/conductor-bin.js +2 -139
  36. package/build/conductor-claude-hook-bin.js +2 -2
  37. package/build/conductor-claude-hook-removed-stub-bin.js +31 -0
  38. package/build/conductor-removed-stub-bin.js +30 -0
  39. package/build/docs.generated.js +1 -1
  40. package/build/doctor.js +145 -18
  41. package/build/drive-epic.js +752 -90
  42. package/build/epic-implementer-bin.js +145 -0
  43. package/build/epic-implementer-bundle-cli.js +264 -0
  44. package/build/epic-implementer-claude-hook-bin.js +3 -0
  45. package/build/epic-integration-pr.js +5 -3
  46. package/build/executor/claim-scope.js +104 -0
  47. package/build/executor/cli.js +14 -25
  48. package/build/executor/env-file-guard.js +82 -3
  49. package/build/executor/env.js +6 -0
  50. package/build/executor/interrupted-worktree.js +60 -0
  51. package/build/executor/job-errors.js +45 -0
  52. package/build/executor/job-runner.js +334 -9
  53. package/build/executor/job-types.js +25 -9
  54. package/build/executor/merge-tree-classifier.js +171 -0
  55. package/build/executor/reconcile-overlap-governance.js +129 -0
  56. package/build/executor/reconcile-overlap-job.js +989 -0
  57. package/build/executor/reconcile-overlap-types.js +14 -0
  58. package/build/executor/spawn-job-driver.js +1 -0
  59. package/build/executor/types.js +2 -0
  60. package/build/executor/worker-finalization.js +25 -2
  61. package/build/executor/worker-guard-hook.js +15 -7
  62. package/build/implement-epic/bridge-client.js +773 -0
  63. package/build/implement-epic/checkpoint-store.js +542 -0
  64. package/build/implement-epic/cli.js +3158 -0
  65. package/build/implement-epic/cut-protocol.js +392 -0
  66. package/build/implement-epic/lock.js +302 -0
  67. package/build/implement-epic/pr-state.js +286 -0
  68. package/build/implement-epic/spawn.js +113 -0
  69. package/build/index.js +657 -481
  70. package/build/init.js +72 -8
  71. package/build/install-bridge-conductor.js +5 -5
  72. package/build/install-bridge.js +403 -70
  73. package/build/local-artifact-storage.js +130 -0
  74. package/build/mcp-host-config.js +22 -60
  75. package/build/mcp-host-entry-adapter.js +18 -0
  76. package/build/mcp-host-targets.js +1 -21
  77. package/build/merge-pull-request.js +1 -1
  78. package/build/pipelines.generated.js +23 -16
  79. package/build/plan-epic-conductor-eligibility.js +1 -1
  80. package/build/plane/cli.js +321 -41
  81. package/build/plane/manifest.js +209 -1
  82. package/build/plane/member-roster.js +70 -0
  83. package/build/plane/preflight.js +128 -12
  84. package/build/plane/shutdown.js +18 -5
  85. package/build/plane/status.js +35 -1
  86. package/build/plane/supervisor.js +546 -164
  87. package/build/plane/types.js +25 -2
  88. package/build/polling-policy.js +72 -0
  89. package/build/readiness-check.js +3 -3
  90. package/build/readme.generated.js +1 -1
  91. package/build/review-generation.js +219 -0
  92. package/build/run-unit-tests-launcher.js +6 -1
  93. package/build/setup-epic.js +567 -38
  94. package/build/start-tickets-conductor.js +8 -7
  95. package/build/ticket-key-utils.js +4 -3
  96. package/build/ticket-review-artifact-gate.js +461 -0
  97. package/build/upgrade-cli.js +5 -26
  98. package/build/version.generated.js +3 -3
  99. package/build/worker-guard-hook-bin.js +1 -1
  100. package/docs/CONDUCTOR.md +8 -6
  101. package/docs/install/mcp-tool-integrations.md +23 -1
  102. package/package.json +5 -3
  103. package/pipelines/review-ticket.json +17 -4
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Deterministic merge classification for `reconcile_overlap` (BAPI-1152).
3
+ *
4
+ * Classifies a (child head, required epic SHA) pair with
5
+ * `git merge-tree --write-tree` BEFORE anything touches the held worktree.
6
+ * `merge-tree` computes the merge entirely in the object database: it writes a
7
+ * tree object and reports conflicts, but never touches a working tree, an index,
8
+ * `HEAD`, or a ref. That is what lets the job decide "clean or conflicted" first
9
+ * and only then materialize a real merge.
10
+ *
11
+ * Output is read in the machine-readable `-z --name-only --messages` form, never
12
+ * scraped from human text. Verified against git 2.50.1:
13
+ *
14
+ * <tree-oid> NUL <conflicted path> NUL ... NUL NUL
15
+ * ( <N> NUL <path 1> NUL ... <path N> NUL <type> NUL <message> NUL )*
16
+ *
17
+ * with exit 0 for a clean merge and 1 for a conflicted one. Exit 1 is ALSO what
18
+ * git returns for an unknown commit — with no tree OID on stdout — so the exit
19
+ * code alone never decides "conflicted": the parsed tree OID does. Anything that
20
+ * is not a proven clean or conflicted result is `operational_error`, and the job
21
+ * fails rather than guessing.
22
+ *
23
+ * Every git call is an argv list through the injected `RunCommand` (`execFile`,
24
+ * `shell: false`); no shell string is ever built.
25
+ */
26
+ import { normalizeRepoRelativePath } from "../conductor/file-scope-guard.js";
27
+ /** Bound on the conflicted paths recorded. More than this is still `conflicted`. */
28
+ export const MERGE_TREE_MAX_CONFLICTS = 200;
29
+ /** Bound on the informational lines kept per conflicted path. */
30
+ export const MERGE_TREE_MAX_MESSAGES_PER_PATH = 5;
31
+ /** Bound on one message line. */
32
+ export const MERGE_TREE_MESSAGE_MAX_CHARS = 300;
33
+ /** Wall-clock bound on each git call the classifier makes. */
34
+ export const MERGE_TREE_COMMAND_TIMEOUT_MS = 120_000;
35
+ const OID_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
36
+ function descriptorFor(type, message) {
37
+ // The MESSAGE carries git's user-facing spelling ("CONFLICT (content)"); the
38
+ // type token is an internal name ("CONFLICT (contents)"). Prefer the message's
39
+ // own prefix so the descriptor matches what an operator sees in `git merge`.
40
+ const fromMessage = /^(CONFLICT \([^)]*\))/.exec(message.trim());
41
+ if (fromMessage)
42
+ return fromMessage[1];
43
+ return type.trim().length > 0 ? type.trim() : "CONFLICT (unknown)";
44
+ }
45
+ /**
46
+ * Parse `git merge-tree --write-tree -z --name-only --messages` stdout.
47
+ *
48
+ * Exported for the unit tests; `classifyMergeTree` is the runtime entry point.
49
+ * Returns `null` when stdout does not start with a tree OID — the caller treats
50
+ * that as an operational error.
51
+ */
52
+ export function parseMergeTreeOutput(stdout) {
53
+ const tokens = stdout.split("\0");
54
+ const treeOid = (tokens[0] ?? "").trim().toLowerCase();
55
+ if (!OID_RE.test(treeOid))
56
+ return null;
57
+ let i = 1;
58
+ const conflictedPaths = [];
59
+ while (i < tokens.length && tokens[i] !== "") {
60
+ conflictedPaths.push(tokens[i]);
61
+ i += 1;
62
+ }
63
+ i += 1; // the empty token separating conflicted paths from messages
64
+ const byPath = new Map();
65
+ const record = (rawPath, descriptor, message) => {
66
+ const path = normalizeRepoRelativePath(rawPath) ?? rawPath;
67
+ let entry = byPath.get(path);
68
+ if (!entry) {
69
+ entry = { path, descriptor: descriptor ?? "CONFLICT (unknown)", messages: [] };
70
+ byPath.set(path, entry);
71
+ }
72
+ else if (descriptor && entry.descriptor === "CONFLICT (unknown)") {
73
+ entry.descriptor = descriptor;
74
+ }
75
+ if (message && entry.messages.length < MERGE_TREE_MAX_MESSAGES_PER_PATH) {
76
+ entry.messages.push(message.trim().slice(0, MERGE_TREE_MESSAGE_MAX_CHARS));
77
+ }
78
+ };
79
+ while (i < tokens.length) {
80
+ const countToken = tokens[i];
81
+ if (countToken === "" || countToken === undefined)
82
+ break;
83
+ const count = Number.parseInt(countToken, 10);
84
+ // A group is `count` paths plus a type and a message; a truncated group ends
85
+ // the parse rather than reading past stdout.
86
+ if (!Number.isInteger(count) || count < 0 || i + count + 2 >= tokens.length)
87
+ break;
88
+ const paths = tokens.slice(i + 1, i + 1 + count);
89
+ const type = tokens[i + 1 + count] ?? "";
90
+ const message = tokens[i + 2 + count] ?? "";
91
+ i += count + 3;
92
+ if (!type.startsWith("CONFLICT"))
93
+ continue;
94
+ const descriptor = descriptorFor(type, message);
95
+ // The FIRST path of a multi-path message (e.g. rename/delete names the new
96
+ // and the old path) is the one git reports as conflicted; the rest are
97
+ // context. Recording every named path would report a path that no longer
98
+ // exists in the merge result as conflicted.
99
+ const conflicted = paths.find((p) => conflictedPaths.includes(p)) ?? paths[0];
100
+ if (conflicted !== undefined)
101
+ record(conflicted, descriptor, message);
102
+ }
103
+ // A path git listed as conflicted with no CONFLICT message is still conflicted.
104
+ for (const path of conflictedPaths) {
105
+ const normalized = normalizeRepoRelativePath(path) ?? path;
106
+ if (!byPath.has(normalized))
107
+ record(path, null, null);
108
+ }
109
+ const conflicts = Array.from(byPath.values())
110
+ .sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))
111
+ .slice(0, MERGE_TREE_MAX_CONFLICTS);
112
+ return { treeOid, conflicts };
113
+ }
114
+ /** Verify a commit object is present locally. */
115
+ async function commitPresent(runCommand, repoDir, sha) {
116
+ try {
117
+ const result = await runCommand("git", ["cat-file", "-e", `${sha}^{commit}`], {
118
+ cwd: repoDir,
119
+ timeoutMs: MERGE_TREE_COMMAND_TIMEOUT_MS,
120
+ });
121
+ return result.exitCode === 0;
122
+ }
123
+ catch {
124
+ return false;
125
+ }
126
+ }
127
+ /**
128
+ * Classify merging `requiredEpicSha` into `childHead`. Never mutates a worktree,
129
+ * an index, `HEAD`, or a ref. Both SHAs must already be present in `repoDir`'s
130
+ * object database (the caller fetches them).
131
+ */
132
+ export async function classifyMergeTree(runCommand, repoDir, childHead, requiredEpicSha) {
133
+ for (const sha of [childHead, requiredEpicSha]) {
134
+ if (!OID_RE.test(sha)) {
135
+ return { kind: "operational_error", reason: "a merge input is not a full commit SHA" };
136
+ }
137
+ if (!(await commitPresent(runCommand, repoDir, sha))) {
138
+ return {
139
+ kind: "operational_error",
140
+ reason: `commit ${sha.slice(0, 12)} is not present in the local object database`,
141
+ };
142
+ }
143
+ }
144
+ let result;
145
+ try {
146
+ result = await runCommand("git", ["merge-tree", "--write-tree", "-z", "--name-only", "--messages", childHead, requiredEpicSha], { cwd: repoDir, timeoutMs: MERGE_TREE_COMMAND_TIMEOUT_MS });
147
+ }
148
+ catch {
149
+ return { kind: "operational_error", reason: "git merge-tree could not be run" };
150
+ }
151
+ if (result.exitCode !== 0 && result.exitCode !== 1) {
152
+ return { kind: "operational_error", reason: `git merge-tree exited ${result.exitCode}` };
153
+ }
154
+ const parsed = parseMergeTreeOutput(result.stdout);
155
+ if (parsed === null) {
156
+ return { kind: "operational_error", reason: "git merge-tree produced no tree object" };
157
+ }
158
+ if (result.exitCode === 0) {
159
+ return { kind: "clean", treeOid: parsed.treeOid };
160
+ }
161
+ if (parsed.conflicts.length === 0) {
162
+ // Exit 1 WITH a tree but no conflict record: git said "not clean" and did not
163
+ // say why. Unknown is conflicted, never clean.
164
+ return {
165
+ kind: "conflicted",
166
+ treeOid: parsed.treeOid,
167
+ conflicts: [{ path: "(unreported)", descriptor: "CONFLICT (unknown)", messages: [] }],
168
+ };
169
+ }
170
+ return { kind: "conflicted", treeOid: parsed.treeOid, conflicts: parsed.conflicts };
171
+ }
@@ -0,0 +1,129 @@
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
+ }