@bridge_gpt/mcp-server 0.2.39 → 0.2.41
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.
|
@@ -8,9 +8,13 @@
|
|
|
8
8
|
* `conductor/epic-runtime.ts#parsePrBindingFromGhJson` fails closed on any
|
|
9
9
|
* non-`OPEN` state, which is correct for binding a merge action and exactly
|
|
10
10
|
* wrong here, and importing it would pull the whole v1 runtime along. So this
|
|
11
|
-
* module parses
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* module parses `gh` PR records with a different, terminal-state-preserving
|
|
12
|
+
* contract, while REUSING the shared mergeability normalizer so a conflict is
|
|
13
|
+
* classified identically everywhere.
|
|
14
|
+
*
|
|
15
|
+
* The probe reads `gh pr list --head <branch>`, whose output is an ARRAY, and
|
|
16
|
+
* selects one record from it. `gh pr view` was replaced because it cannot
|
|
17
|
+
* express "no pull request" as anything but a non-zero exit (BAPI-825/B1).
|
|
14
18
|
*
|
|
15
19
|
* **Worktree discovery.** `/review-and-start` names branches
|
|
16
20
|
* `feature/<KEY>-<slug>` and the loop cannot know the slug. When
|
|
@@ -24,8 +28,24 @@
|
|
|
24
28
|
*/
|
|
25
29
|
import { isPrMergeConflict, parseGhPrMergeabilityFields } from "../conductor/github-mergeability.js";
|
|
26
30
|
import { runGhCommand } from "../conductor/pr-discovery.js";
|
|
27
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* The `--json` field list the status contract fixes.
|
|
33
|
+
*
|
|
34
|
+
* Passed to `gh pr list` (BAPI-825/B1), which returns an ARRAY of records with
|
|
35
|
+
* exactly these keys. The name is retained because it is the published contract
|
|
36
|
+
* identifier and is asserted by name in the tests; only the subcommand changed.
|
|
37
|
+
*/
|
|
28
38
|
export const CONDUCT_EPIC_GH_PR_VIEW_FIELDS = "number,state,headRefOid,mergeable,mergeStateStatus,baseRefName,updatedAt";
|
|
39
|
+
/**
|
|
40
|
+
* How many candidate pull requests the probe asks `gh` for.
|
|
41
|
+
*
|
|
42
|
+
* More than one, deliberately: a branch routinely carries a closed pull request
|
|
43
|
+
* plus a reopened one, and `--limit 1` would hand back whichever `gh` ordered
|
|
44
|
+
* first rather than letting {@link selectConductEpicPrState} prefer the OPEN
|
|
45
|
+
* one. Bounded so a branch with a long pull-request history cannot turn one
|
|
46
|
+
* probe into an unbounded read.
|
|
47
|
+
*/
|
|
48
|
+
export const CONDUCT_EPIC_GH_PR_LIST_LIMIT = 20;
|
|
29
49
|
/** The accepted GitHub PR states. Terminal states are RETAINED, not filtered. */
|
|
30
50
|
export const CONDUCT_EPIC_PR_STATES = ["OPEN", "MERGED", "CLOSED"];
|
|
31
51
|
function isRecord(value) {
|
|
@@ -75,14 +95,57 @@ export function conductEpicPrHasConflict(pr) {
|
|
|
75
95
|
return isPrMergeConflict({ mergeable: pr.mergeable, mergeStateStatus: pr.merge_state });
|
|
76
96
|
}
|
|
77
97
|
/**
|
|
78
|
-
*
|
|
98
|
+
* Choose the one pull request the loop acts on from `gh`'s candidate list.
|
|
99
|
+
*
|
|
100
|
+
* An OPEN pull request always wins: it is the only one any action row can
|
|
101
|
+
* advance, and a stale closed sibling must never displace it. Among terminal
|
|
102
|
+
* candidates the most recently updated wins, because that is the one whose
|
|
103
|
+
* MERGED or CLOSED state describes the branch's current reality.
|
|
104
|
+
*
|
|
105
|
+
* Deterministic when timestamps tie or are unavailable: a valid `updated_at`
|
|
106
|
+
* outranks an absent one, and source order breaks any remaining tie. A probe
|
|
107
|
+
* that returned a different pull request run-to-run for identical input would
|
|
108
|
+
* make every downstream row unreproducible.
|
|
109
|
+
*/
|
|
110
|
+
export function selectConductEpicPrState(candidates) {
|
|
111
|
+
if (candidates.length === 0)
|
|
112
|
+
return null;
|
|
113
|
+
const open = candidates.find((pr) => pr.state === "OPEN");
|
|
114
|
+
if (open)
|
|
115
|
+
return open;
|
|
116
|
+
let best = candidates[0];
|
|
117
|
+
for (const candidate of candidates.slice(1)) {
|
|
118
|
+
const bestAt = Date.parse(best.updated_at ?? "");
|
|
119
|
+
const candidateAt = Date.parse(candidate.updated_at ?? "");
|
|
120
|
+
const bestUsable = Number.isFinite(bestAt);
|
|
121
|
+
const candidateUsable = Number.isFinite(candidateAt);
|
|
122
|
+
// Strictly greater, so an equal timestamp keeps the earlier record and
|
|
123
|
+
// source order remains the final tie-breaker.
|
|
124
|
+
if (candidateUsable && (!bestUsable || candidateAt > bestAt))
|
|
125
|
+
best = candidate;
|
|
126
|
+
}
|
|
127
|
+
return best;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Run `gh pr list --head <branch> --state all --json …` and normalize the result.
|
|
79
131
|
*
|
|
80
132
|
* Nothing throws. The three outcomes are kept strictly apart:
|
|
81
133
|
*
|
|
82
|
-
* - **`ok`** — `gh` succeeded and returned
|
|
83
|
-
* - **`none`** — `gh` SUCCEEDED and returned
|
|
134
|
+
* - **`ok`** — `gh` succeeded and returned at least one parseable PR record.
|
|
135
|
+
* - **`none`** — `gh` SUCCEEDED and returned an empty array. Confirmed absence.
|
|
84
136
|
* - **`error`** — `gh` could not be run, exited non-zero, or returned output
|
|
85
|
-
* that is not a valid PR
|
|
137
|
+
* that is not a valid PR array. Unavailable evidence, not absence.
|
|
138
|
+
*
|
|
139
|
+
* **`gh pr list`, not `gh pr view` (BAPI-825/B1).** `gh pr view <branch>` prints
|
|
140
|
+
* `no pull requests found for branch "…"` to stderr and **exits 1** when the
|
|
141
|
+
* branch has no pull request, and `runGhCommand` discards stderr, so every
|
|
142
|
+
* pre-PR tick produced `{ok: false, stdout: ""}`. Under BAPI-814/B2's correct
|
|
143
|
+
* rule that a non-zero exit is a failed probe, `none` became unreachable in
|
|
144
|
+
* production: the loop saw `pr: null` PLUS a `probe_errors` entry on every tick
|
|
145
|
+
* before a worker opened its pull request, so the "no PR yet" rows could never
|
|
146
|
+
* fire and a worker that died early parked at the three-hour hard deadline
|
|
147
|
+
* instead of getting its one-hour respawn. `gh pr list` exits 0 with `[]`,
|
|
148
|
+
* which makes absence expressible without weakening the failure rule.
|
|
86
149
|
*
|
|
87
150
|
* Every `error` reason is a fixed bounded string chosen here. The raw output is
|
|
88
151
|
* never included — a `gh` failure message can echo a URL, a token hint, or the
|
|
@@ -92,33 +155,43 @@ export async function discoverConductEpicPrState(branch, options = {}) {
|
|
|
92
155
|
const runGh = options.runGh ?? runGhCommand;
|
|
93
156
|
let result;
|
|
94
157
|
try {
|
|
95
|
-
result = await runGh([
|
|
96
|
-
|
|
97
|
-
|
|
158
|
+
result = await runGh([
|
|
159
|
+
"pr",
|
|
160
|
+
"list",
|
|
161
|
+
"--head",
|
|
162
|
+
branch,
|
|
163
|
+
"--state",
|
|
164
|
+
"all",
|
|
165
|
+
"--json",
|
|
166
|
+
CONDUCT_EPIC_GH_PR_VIEW_FIELDS,
|
|
167
|
+
"--limit",
|
|
168
|
+
String(CONDUCT_EPIC_GH_PR_LIST_LIMIT),
|
|
169
|
+
], { cwd: options.cwd });
|
|
98
170
|
}
|
|
99
171
|
catch {
|
|
100
172
|
return { kind: "error", reason: "the gh command could not be run" };
|
|
101
173
|
}
|
|
102
174
|
// A non-zero `gh` exit is a FAILED PROBE, never "no PR" (BAPI-814/B2).
|
|
103
175
|
//
|
|
104
|
-
// `gh` exits non-zero
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
// contract turns that into `pr: null` PLUS a `probe_errors` entry, which the
|
|
111
|
-
// command treats as unavailable evidence rather than a negative result.
|
|
176
|
+
// `gh pr list` exits non-zero only when it could not answer — unauthenticated,
|
|
177
|
+
// rate-limited, offline, not a repository. Absence is now expressed as a
|
|
178
|
+
// successful `[]`, so nothing legitimate is lost by keeping this strict: the
|
|
179
|
+
// status contract turns an error into `pr: null` PLUS a `probe_errors` entry,
|
|
180
|
+
// which the command treats as unavailable evidence rather than a negative
|
|
181
|
+
// result.
|
|
112
182
|
//
|
|
113
183
|
// The reason is a FIXED string. `gh`'s own output can echo a URL, a token
|
|
114
184
|
// hint, or the repository layout, so none of it is interpolated here.
|
|
115
185
|
if (!result.ok)
|
|
116
186
|
return { kind: "error", reason: "the gh command failed" };
|
|
117
|
-
//
|
|
118
|
-
//
|
|
187
|
+
// Empty stdout from a SUCCESSFUL `--json` run is malformed, not absence: the
|
|
188
|
+
// documented answer for "no pull requests" is the two bytes `[]`. Silence is
|
|
189
|
+
// a shape this command should never produce, and reading it as a confirmed
|
|
190
|
+
// negative is precisely the inference this module refuses to make.
|
|
119
191
|
const raw = typeof result.stdout === "string" ? result.stdout.trim() : "";
|
|
120
|
-
if (raw.length === 0)
|
|
121
|
-
return { kind: "
|
|
192
|
+
if (raw.length === 0) {
|
|
193
|
+
return { kind: "error", reason: "gh returned no output for a --json query" };
|
|
194
|
+
}
|
|
122
195
|
let parsed;
|
|
123
196
|
try {
|
|
124
197
|
parsed = JSON.parse(raw);
|
|
@@ -126,7 +199,23 @@ export async function discoverConductEpicPrState(branch, options = {}) {
|
|
|
126
199
|
catch {
|
|
127
200
|
return { kind: "error", reason: "gh returned output that is not valid JSON" };
|
|
128
201
|
}
|
|
129
|
-
|
|
202
|
+
if (!Array.isArray(parsed)) {
|
|
203
|
+
return { kind: "error", reason: "gh returned a PR list in an unexpected shape" };
|
|
204
|
+
}
|
|
205
|
+
// A successful, well-formed empty list is the ONLY confirmed absence.
|
|
206
|
+
if (parsed.length === 0)
|
|
207
|
+
return { kind: "none" };
|
|
208
|
+
const records = [];
|
|
209
|
+
for (const entry of parsed) {
|
|
210
|
+
const pr = parseConductEpicPrState(entry);
|
|
211
|
+
// An unusable record is a shape error, never silently dropped: discarding
|
|
212
|
+
// it could empty the list and turn a real pull request into "no PR".
|
|
213
|
+
if (pr === null) {
|
|
214
|
+
return { kind: "error", reason: "gh returned a PR record in an unexpected shape" };
|
|
215
|
+
}
|
|
216
|
+
records.push(pr);
|
|
217
|
+
}
|
|
218
|
+
const pr = selectConductEpicPrState(records);
|
|
130
219
|
if (pr === null)
|
|
131
220
|
return { kind: "error", reason: "gh returned a PR record in an unexpected shape" };
|
|
132
221
|
return { kind: "ok", pr };
|