@bridge_gpt/mcp-server 0.2.52 → 0.2.54
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.
- package/README.md +121 -15
- package/build/agent-launchers/claude.js +3 -3
- package/build/agent-launchers/prompt.js +8 -11
- package/build/base-ref.js +33 -9
- package/build/bounded-wait.js +174 -0
- package/build/commands.generated.js +7 -5
- package/build/conductor/bridge-api-client.js +97 -8
- package/build/conductor/cli.js +23 -0
- package/build/conductor/doctor.js +428 -5
- package/build/conductor/epic-runtime.js +133 -97
- package/build/conductor/install-doctor.js +65 -656
- package/build/conductor/readiness-cli.js +152 -0
- package/build/conductor/readiness-sections.js +666 -0
- package/build/conductor/readiness.js +795 -0
- package/build/conductor/run-branch.js +137 -0
- package/build/conductor/test-run-branch-vectors.js +165 -0
- package/build/conductor/tools.js +56 -3
- package/build/conductor-bin.js +21 -17
- package/build/doctor.js +68 -1
- package/build/drive-epic.js +287 -51
- package/build/executor/claim-scope.js +104 -0
- package/build/executor/cli.js +14 -25
- package/build/executor/env-file-guard.js +82 -3
- package/build/executor/job-runner.js +60 -0
- package/build/index.js +4496 -4697
- package/build/install-doctor.js +154 -2
- package/build/local-artifact-storage.js +130 -0
- package/build/pipelines.generated.js +17 -10
- package/build/plane/alembic-head.js +40 -11
- package/build/plane/build-freshness.js +22 -11
- package/build/plane/cli.js +285 -36
- package/build/plane/manifest.js +209 -1
- package/build/plane/member-roster.js +70 -0
- package/build/plane/preflight.js +363 -48
- package/build/plane/shutdown.js +14 -1
- package/build/plane/status.js +35 -1
- package/build/plane/supervisor.js +546 -164
- package/build/plane/types.js +61 -2
- package/build/polling-policy.js +72 -0
- package/build/readiness-check.js +412 -0
- package/build/readme.generated.js +1 -1
- package/build/review-generation.js +219 -0
- package/build/run-unit-tests-launcher.js +5 -0
- package/build/setup-epic.js +514 -23
- package/build/ticket-key-utils.js +4 -3
- package/build/ticket-review-artifact-gate.js +461 -0
- package/build/upgrade-cli.js +5 -26
- package/build/version.generated.js +3 -3
- package/docs/install/mcp-tool-integrations.md +23 -1
- package/package.json +2 -2
- package/pipelines/{full-automation.json → idea-to-pr.json} +1 -1
- package/pipelines/review-ticket.json +17 -4
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical TypeScript run-branch DECLARATION resolution (BAPI-1127).
|
|
3
|
+
*
|
|
4
|
+
* The TypeScript counterpart of `api/models/run_branch.py`, and the only place
|
|
5
|
+
* in this package that answers "which base branch, if any, does this persisted
|
|
6
|
+
* run's `policy_json` DECLARE?" Before BAPI-1127 the question was answered in
|
|
7
|
+
* three places that disagreed — the defaulting resolver in `epic-runtime.ts`
|
|
8
|
+
* (a `??` scan that stopped on a present-but-blank `base_branch` and therefore
|
|
9
|
+
* never consulted the camelCase alias), `bridge-api-client.ts#readEpicRunCompletionState`
|
|
10
|
+
* (snake_case only, and it did not trim), and the Python resolver across the
|
|
11
|
+
* wire. The two runtimes feed the SAME epic run from opposite sides, so a
|
|
12
|
+
* disagreement means the server provisions one branch while the CLI cuts worker
|
|
13
|
+
* branches from another, silently.
|
|
14
|
+
*
|
|
15
|
+
* **Declaration only.** This module answers exactly one question and stops:
|
|
16
|
+
* what branch did the policy declare? It deliberately does NOT:
|
|
17
|
+
*
|
|
18
|
+
* - apply the TypeScript-only `main` default. That is a DISPATCH-layer policy
|
|
19
|
+
* owned by `epic-runtime.ts#resolveEffectiveRunBaseBranch`. A classification
|
|
20
|
+
* or observation consumer that adopted it would report `main` for a run that
|
|
21
|
+
* declared nothing, conflating "declared no branch" with "dispatches from
|
|
22
|
+
* main" — the exact confusion that kept `featureBranch` honest before.
|
|
23
|
+
* - apply branch-name validity rules. `validateBranchName` is an OPERATIONAL
|
|
24
|
+
* judgment made by whichever caller is about to hand the value to git or a
|
|
25
|
+
* provider, and it is applied there.
|
|
26
|
+
*
|
|
27
|
+
* **The frozen cross-runtime dispositions** (BAPI-1127, and the same two rules
|
|
28
|
+
* `api/models/run_branch.py` states):
|
|
29
|
+
*
|
|
30
|
+
* - A NON-STRING candidate is ABSENT, not fatal. The ordered scan skips it and
|
|
31
|
+
* continues to the next key, so the legacy row
|
|
32
|
+
* `{"base_branch": 42, "baseBranch": "epic/X"}` resolves to `epic/X` rather
|
|
33
|
+
* than failing closed on the unusable snake_case value. The strict server-side
|
|
34
|
+
* `RunPolicy` boundary already rejects a non-string `base_branch` before it can
|
|
35
|
+
* be stored, so this rule governs legacy and out-of-band rows only.
|
|
36
|
+
* - A MALFORMED or over-length declaration is still a DECLARATION. It is
|
|
37
|
+
* returned complete and trimmed, never truncated, rejected, or replaced with
|
|
38
|
+
* a default; format and length are validated at the point of operational use.
|
|
39
|
+
*
|
|
40
|
+
* **Why a leaf.** `epic-runtime.ts` already imports from `bridge-api-client.ts`,
|
|
41
|
+
* so putting the shared resolver in either one and importing it from the other
|
|
42
|
+
* would close a cycle. This module imports NOTHING — not the API client, not a
|
|
43
|
+
* command runner, not `base-ref.js`'s validator — so every consumer can depend on
|
|
44
|
+
* it freely. That is the same reasoning `api/models/run_branch.py:13-24` gives for
|
|
45
|
+
* its own placement.
|
|
46
|
+
*
|
|
47
|
+
* The cross-language contract is frozen as data in
|
|
48
|
+
* `tests/pytest/fixtures/run_branch_vectors.json` and enforced from both sides by
|
|
49
|
+
* `mcp_server/src/conductor/run-branch.test.ts` and
|
|
50
|
+
* `tests/pytest/models/test_run_branch_vectors.py`.
|
|
51
|
+
*/
|
|
52
|
+
/**
|
|
53
|
+
* The policy keys consulted, IN ORDER. Both spellings are accepted because both
|
|
54
|
+
* have been written by real clients; `base_branch` is canonical and `baseBranch`
|
|
55
|
+
* is the legacy alias. Named once so the precedence is stated in exactly one
|
|
56
|
+
* place, mirroring `POLICY_BASE_BRANCH_KEYS` in `api/models/run_branch.py`.
|
|
57
|
+
*/
|
|
58
|
+
export const POLICY_BASE_BRANCH_KEYS = ["base_branch", "baseBranch"];
|
|
59
|
+
/**
|
|
60
|
+
* Return the trimmed string, or `undefined` for anything that is not a usable
|
|
61
|
+
* one.
|
|
62
|
+
*
|
|
63
|
+
* A blank or whitespace-only value is NOT a branch: accepting it would let a run
|
|
64
|
+
* created with `base_branch: " "` classify as a feature-branch run, which at
|
|
65
|
+
* the merge-admission boundary would hand it the ungated-auto-merge exception on
|
|
66
|
+
* the strength of whitespace. The Python twin is `run_branch._nonblank`.
|
|
67
|
+
*
|
|
68
|
+
* KNOWN GAP — the trim is proven equivalent only for ASCII whitespace
|
|
69
|
+
* (BAPI-1127 review). `String.prototype.trim()` and Python's `str.strip()` do
|
|
70
|
+
* not strip the same character class, so four inputs resolve differently and
|
|
71
|
+
* none is in the vector table:
|
|
72
|
+
*
|
|
73
|
+
* | declared value | Python `_nonblank` | this function |
|
|
74
|
+
* |---------------------|----------------------|----------------------|
|
|
75
|
+
* | `"\u001cepic/X"` | `"epic/X"` | `"\u001cepic/X"` |
|
|
76
|
+
* | `"\u0085"` | absent (blank) | `"\u0085"` |
|
|
77
|
+
* | `"\u0085epic/X"` | `"epic/X"` | `"\u0085epic/X"` |
|
|
78
|
+
* | `"\ufeffmain"` | `"\ufeffmain"` | `"main"` |
|
|
79
|
+
*
|
|
80
|
+
* `str.strip()` also strips U+001C-U+001F and U+0085; `trim()` also strips
|
|
81
|
+
* U+FEFF. Note the second row is the sharp one: U+0085 is above 0x7F, so
|
|
82
|
+
* `validateBranchName`'s control-character rule does NOT reject it either.
|
|
83
|
+
*
|
|
84
|
+
* Left as FOLLOW-UP rather than fixed here, deliberately. Picking the canonical
|
|
85
|
+
* class is a contract decision of the same kind BAPI-1127 froze for the
|
|
86
|
+
* non-string and malformed-name rows — and it was frozen in the TICKET, not by
|
|
87
|
+
* the implementer. Normalizing Python's class would additionally change
|
|
88
|
+
* feature-branch classification for a policy the `RunPolicy` boundary accepts
|
|
89
|
+
* today (`{"base_branch": "\u0085"}` classifies as no-feature-branch now), which
|
|
90
|
+
* BAPI-1127's acceptance criteria forbid. `RunPolicy` accepts any strict string,
|
|
91
|
+
* so these values are storable, but they are vanishingly unlikely in practice.
|
|
92
|
+
* Recorded alongside the `git check-ref-format` follow-up in `base-ref.ts`.
|
|
93
|
+
*/
|
|
94
|
+
function nonblank(candidate) {
|
|
95
|
+
if (typeof candidate !== "string")
|
|
96
|
+
return undefined;
|
|
97
|
+
const trimmed = candidate.trim();
|
|
98
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Resolve the branch a persisted run's `policy_json` declares, if any.
|
|
102
|
+
*
|
|
103
|
+
* Returns the first non-blank string found across {@link POLICY_BASE_BRANCH_KEYS},
|
|
104
|
+
* in that order, trimmed; or `undefined` when the policy declares none.
|
|
105
|
+
*
|
|
106
|
+
* The scan is SEQUENTIAL, not nullish-coalescing: a candidate that is not a
|
|
107
|
+
* non-blank string is skipped and the scan CONTINUES to the next key. `??` falls
|
|
108
|
+
* through only for `null`/`undefined`, which is why the pre-BAPI-1127 expression
|
|
109
|
+
* stopped at a present-but-blank `base_branch` and reported the dispatch default
|
|
110
|
+
* for a policy that plainly declared `baseBranch: "epic/X"`.
|
|
111
|
+
*
|
|
112
|
+
* A non-mapping policy (`null`, `undefined`, a primitive, an array) declares
|
|
113
|
+
* nothing. Shape is the typed boundary's job, not this resolver's — the Python
|
|
114
|
+
* twin makes the same call for the same reason.
|
|
115
|
+
*
|
|
116
|
+
* The returned value is never truncated and carries no operational length or
|
|
117
|
+
* format bound. See the module docstring for both frozen dispositions.
|
|
118
|
+
*/
|
|
119
|
+
export function resolveDeclaredRunBaseBranch(policyJson) {
|
|
120
|
+
if (policyJson === null || typeof policyJson !== "object" || Array.isArray(policyJson)) {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
const policy = policyJson;
|
|
124
|
+
for (const key of POLICY_BASE_BRANCH_KEYS) {
|
|
125
|
+
// OWN properties only. A Python dict has no prototype chain, so a plain
|
|
126
|
+
// `policy[key]` would not be the same read: it also sees `Object.prototype`,
|
|
127
|
+
// and a polluted prototype would make EVERY policy in the process appear to
|
|
128
|
+
// declare a branch. Real inputs come from `JSON.parse`, which never produces
|
|
129
|
+
// inherited keys, so this costs nothing and closes the gap.
|
|
130
|
+
if (!Object.prototype.hasOwnProperty.call(policy, key))
|
|
131
|
+
continue;
|
|
132
|
+
const resolved = nonblank(policy[key]);
|
|
133
|
+
if (resolved !== undefined)
|
|
134
|
+
return resolved;
|
|
135
|
+
}
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BAPI-1127 — TypeScript-side loader for the SHARED cross-runtime vector table.
|
|
3
|
+
*
|
|
4
|
+
* `tests/pytest/fixtures/run_branch_vectors.json` lives at the repository root,
|
|
5
|
+
* outside this package, because it is not this package's fixture: it is the
|
|
6
|
+
* contract between this package and the Python server, and both sides read the
|
|
7
|
+
* same bytes. Python reaches it from `tests/pytest/models/`; this module is how
|
|
8
|
+
* the compiled `node:test` suites reach it.
|
|
9
|
+
*
|
|
10
|
+
* **Why `readFileSync` and not a JSON import.** Importing the JSON would require
|
|
11
|
+
* `resolveJsonModule` plus a widened `include` in `tsconfig.json`, which would
|
|
12
|
+
* inline a test fixture into the PUBLISHED bundle. `build/**\/*.test.js` is
|
|
13
|
+
* excluded from the package `files` list, so a compiled test reading a
|
|
14
|
+
* repository-root path ships nothing; a JSON module baked into the emitted
|
|
15
|
+
* source would.
|
|
16
|
+
*
|
|
17
|
+
* **Why the path is searched rather than computed.** The compiled test executes
|
|
18
|
+
* from `mcp_server/build/conductor/`, not `mcp_server/src/conductor/`, so a
|
|
19
|
+
* hard-coded `../../../` is correct for exactly one of the two and silently
|
|
20
|
+
* wrong for the other. Walking up until the fixture is found is correct from
|
|
21
|
+
* either, and from any CI checkout depth.
|
|
22
|
+
*
|
|
23
|
+
* Nothing here is reachable from the MCP server runtime — it is imported only by
|
|
24
|
+
* `*.test.ts` — so it adds no startup cost and touches no stdio.
|
|
25
|
+
*/
|
|
26
|
+
import { readFileSync } from "node:fs";
|
|
27
|
+
import path from "node:path";
|
|
28
|
+
import { fileURLToPath } from "node:url";
|
|
29
|
+
/** Repository-root-relative location of the shared table. */
|
|
30
|
+
const FIXTURE_RELATIVE_PATH = path.join("tests", "pytest", "fixtures", "run_branch_vectors.json");
|
|
31
|
+
function repositoryRoot() {
|
|
32
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
// 8 levels is far more than the 3 either real layout needs; the loop exists to
|
|
34
|
+
// be layout-independent, not to search the whole filesystem.
|
|
35
|
+
for (let i = 0; i < 8; i++) {
|
|
36
|
+
try {
|
|
37
|
+
readFileSync(path.join(dir, FIXTURE_RELATIVE_PATH));
|
|
38
|
+
return dir;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
const parent = path.dirname(dir);
|
|
42
|
+
if (parent === dir)
|
|
43
|
+
break;
|
|
44
|
+
dir = parent;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
throw new Error(`run_branch_vectors.json not found above ${fileURLToPath(import.meta.url)}. ` +
|
|
48
|
+
"The shared cross-runtime vector table is required; this suite must not " +
|
|
49
|
+
"pass without it.");
|
|
50
|
+
}
|
|
51
|
+
function readDocument() {
|
|
52
|
+
const fixturePath = path.join(repositoryRoot(), FIXTURE_RELATIVE_PATH);
|
|
53
|
+
let raw;
|
|
54
|
+
try {
|
|
55
|
+
raw = readFileSync(fixturePath, "utf8");
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
throw new Error(`shared vector fixture is unreadable at ${fixturePath}: ${String(err)}`);
|
|
59
|
+
}
|
|
60
|
+
let parsed;
|
|
61
|
+
try {
|
|
62
|
+
parsed = JSON.parse(raw);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
throw new Error(`shared vector fixture is not valid JSON (${fixturePath}): ${String(err)}`);
|
|
66
|
+
}
|
|
67
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
68
|
+
throw new Error(`shared vector fixture is not a JSON object (${fixturePath}).`);
|
|
69
|
+
}
|
|
70
|
+
return parsed;
|
|
71
|
+
}
|
|
72
|
+
/** JSON `null` means "no declaration"; TypeScript spells that `undefined`. */
|
|
73
|
+
function nullToUndefined(value) {
|
|
74
|
+
return value === null ? undefined : value;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Load the shared resolution vectors, THROWING on every vacuous state.
|
|
78
|
+
*
|
|
79
|
+
* A missing, unreadable, malformed, or empty table is a failure and never a
|
|
80
|
+
* skip: a parity suite that quietly parametrizes zero cases reports green while
|
|
81
|
+
* proving nothing, which is the exact outcome these vectors exist to prevent.
|
|
82
|
+
*/
|
|
83
|
+
export function loadResolutionVectors() {
|
|
84
|
+
const raw = readDocument()["resolution_vectors"];
|
|
85
|
+
if (!Array.isArray(raw)) {
|
|
86
|
+
throw new Error("shared vector fixture has no 'resolution_vectors' array.");
|
|
87
|
+
}
|
|
88
|
+
if (raw.length === 0) {
|
|
89
|
+
throw new Error("shared vector fixture declares zero resolution vectors; every parity " +
|
|
90
|
+
"assertion below would be vacuous.");
|
|
91
|
+
}
|
|
92
|
+
return raw.map((entry, index) => {
|
|
93
|
+
if (!entry || typeof entry !== "object") {
|
|
94
|
+
throw new Error(`resolution vector #${index} is not an object.`);
|
|
95
|
+
}
|
|
96
|
+
const e = entry;
|
|
97
|
+
if (typeof e["id"] !== "string" || e["id"].length === 0) {
|
|
98
|
+
throw new Error(`resolution vector #${index} has no id.`);
|
|
99
|
+
}
|
|
100
|
+
if (typeof e["policy_present"] !== "boolean") {
|
|
101
|
+
throw new Error(`resolution vector ${e["id"]} has no boolean policy_present.`);
|
|
102
|
+
}
|
|
103
|
+
if (!("expected_declared_branch" in e)) {
|
|
104
|
+
throw new Error(`resolution vector ${e["id"]} has no expected_declared_branch.`);
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
id: e["id"],
|
|
108
|
+
note: typeof e["note"] === "string" ? e["note"] : "",
|
|
109
|
+
policyPresent: e["policy_present"],
|
|
110
|
+
policyJson: e["policy_present"] ? e["policy_json"] : undefined,
|
|
111
|
+
expectedDeclaredBranch: nullToUndefined(e["expected_declared_branch"]),
|
|
112
|
+
typescriptEffectiveBase: nullToUndefined(e["typescript_effective_base"]),
|
|
113
|
+
typescriptEffectiveErrorType: nullToUndefined(e["typescript_effective_error_type"]),
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/** Load the shared branch-name validity vectors, with the same loud failures. */
|
|
118
|
+
export function loadBranchNameVectors() {
|
|
119
|
+
const raw = readDocument()["branch_name_validation"];
|
|
120
|
+
if (!Array.isArray(raw)) {
|
|
121
|
+
throw new Error("shared vector fixture has no 'branch_name_validation' array.");
|
|
122
|
+
}
|
|
123
|
+
if (raw.length === 0) {
|
|
124
|
+
throw new Error("shared vector fixture declares zero branch-name vectors; every validator " +
|
|
125
|
+
"assertion below would be vacuous.");
|
|
126
|
+
}
|
|
127
|
+
return raw.map((entry, index) => {
|
|
128
|
+
if (!entry || typeof entry !== "object") {
|
|
129
|
+
throw new Error(`branch-name vector #${index} is not an object.`);
|
|
130
|
+
}
|
|
131
|
+
const e = entry;
|
|
132
|
+
if (typeof e["id"] !== "string" || e["id"].length === 0) {
|
|
133
|
+
throw new Error(`branch-name vector #${index} has no id.`);
|
|
134
|
+
}
|
|
135
|
+
if (typeof e["input"] !== "string") {
|
|
136
|
+
throw new Error(`branch-name vector ${e["id"]} has a non-string input.`);
|
|
137
|
+
}
|
|
138
|
+
if (typeof e["valid"] !== "boolean") {
|
|
139
|
+
throw new Error(`branch-name vector ${e["id"]} has no boolean 'valid'.`);
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
id: e["id"],
|
|
143
|
+
note: typeof e["note"] === "string" ? e["note"] : "",
|
|
144
|
+
input: e["input"],
|
|
145
|
+
valid: e["valid"],
|
|
146
|
+
errorType: nullToUndefined(e["error_type"]),
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* The substring each `error_type` must appear as in a validator diagnostic.
|
|
152
|
+
*
|
|
153
|
+
* The adapters assert the ACTUAL message, not merely that some error occurred,
|
|
154
|
+
* so a validator that started reporting the wrong rule would fail rather than
|
|
155
|
+
* pass on a coincidentally-truthy result. Kept here, beside the loader, because
|
|
156
|
+
* both the TypeScript validator suite and the effective-base suite need it.
|
|
157
|
+
*/
|
|
158
|
+
export const ERROR_TYPE_MESSAGE_FRAGMENTS = {
|
|
159
|
+
empty: "must not be empty",
|
|
160
|
+
too_long: "255 characters or fewer",
|
|
161
|
+
leading_hyphen: "must not start with '-'",
|
|
162
|
+
double_dot: "must not contain '..'",
|
|
163
|
+
lock_suffix: "must not end with '.lock'",
|
|
164
|
+
control_character: "must not contain control characters",
|
|
165
|
+
};
|
package/build/conductor/tools.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { z } from "zod";
|
|
16
16
|
import { ConductorValidationError, toConductorErrorEnvelope } from "./errors.js";
|
|
17
|
-
import { resolveConductorBridgeApiAccess, fetchEpicRunState, ConductorBridgeApiError } from "./bridge-api-client.js";
|
|
17
|
+
import { resolveConductorBridgeApiAccess, fetchEpicRunState, fetchExplainRun, ConductorBridgeApiError, } from "./bridge-api-client.js";
|
|
18
18
|
function jsonResult(value) {
|
|
19
19
|
return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] };
|
|
20
20
|
}
|
|
@@ -70,6 +70,57 @@ function registerGetEpicSnapshotTool(registerTool) {
|
|
|
70
70
|
}
|
|
71
71
|
}));
|
|
72
72
|
}
|
|
73
|
+
function registerExplainRunTool(registerTool) {
|
|
74
|
+
registerTool("explain_run", {
|
|
75
|
+
annotations: {
|
|
76
|
+
readOnlyHint: true,
|
|
77
|
+
destructiveHint: false,
|
|
78
|
+
idempotentHint: true,
|
|
79
|
+
openWorldHint: true,
|
|
80
|
+
},
|
|
81
|
+
// Compressed to fit MAX_DESC_CHARS (350) without a waiver. What survives
|
|
82
|
+
// is the field list: a caller cannot discover the answer shape from a
|
|
83
|
+
// tool that only says "explain a run", and the whole point of the tool is
|
|
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: " +
|
|
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 " +
|
|
88
|
+
"pending disposition, usage ceiling. Pass ticket_key to narrow. Read-only; 404 " +
|
|
89
|
+
"returns status 'unknown'.",
|
|
90
|
+
inputSchema: {
|
|
91
|
+
epic_run_id: z
|
|
92
|
+
.string()
|
|
93
|
+
.min(1)
|
|
94
|
+
.describe("Epic run UUID or epic key (e.g. EPIC-123)."),
|
|
95
|
+
ticket_key: z
|
|
96
|
+
.string()
|
|
97
|
+
.min(1)
|
|
98
|
+
.optional()
|
|
99
|
+
.describe("Narrow the answer to one ticket in the run."),
|
|
100
|
+
},
|
|
101
|
+
}, withConductorToolErrorHandling(async (args) => {
|
|
102
|
+
const epicRunId = args.epic_run_id;
|
|
103
|
+
const ticketKey = args.ticket_key;
|
|
104
|
+
const scope = ticketKey === undefined ? "run" : "ticket";
|
|
105
|
+
const accessResult = await resolveConductorBridgeApiAccess();
|
|
106
|
+
if (!accessResult.ok) {
|
|
107
|
+
throw new ConductorValidationError(accessResult.error);
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
return jsonResult(await fetchExplainRun(accessResult.access, epicRunId, ticketKey));
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
if (error instanceof ConductorBridgeApiError && error.status === 404) {
|
|
114
|
+
// Scope-specific, so a caller can tell "no such run" from "that run
|
|
115
|
+
// exists but has no such ticket" — the server distinguishes them and
|
|
116
|
+
// collapsing both to one envelope would throw that away. No server
|
|
117
|
+
// detail is echoed either way.
|
|
118
|
+
return jsonResult({ status: "unknown", scope });
|
|
119
|
+
}
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
73
124
|
/**
|
|
74
125
|
* Register the conductor MCP tool surface through the host's `registerTool`
|
|
75
126
|
* wrapper. Keeping registration in this single module keeps `index.ts` thin.
|
|
@@ -78,10 +129,12 @@ function registerGetEpicSnapshotTool(registerTool) {
|
|
|
78
129
|
* handler returns the same `{ content: [{ type: "text", text }] }` shape every
|
|
79
130
|
* Bridge tool uses, just expressed with a simpler local type.
|
|
80
131
|
*
|
|
81
|
-
* BAPI-909 reduced this registrar to `get_epic_snapshot
|
|
82
|
-
*
|
|
132
|
+
* BAPI-909 reduced this registrar to `get_epic_snapshot`; BAPI-1028 added
|
|
133
|
+
* `explain_run` beside it. The registrar shape is what made that a one-line
|
|
134
|
+
* change rather than an edit to `index.ts`.
|
|
83
135
|
*/
|
|
84
136
|
export function registerConductorTools(registerTool) {
|
|
85
137
|
const reg = registerTool;
|
|
86
138
|
registerGetEpicSnapshotTool(reg);
|
|
139
|
+
registerExplainRunTool(reg);
|
|
87
140
|
}
|