@bridge_gpt/mcp-server 0.2.0 → 0.2.2
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 +56 -54
- package/build/agent-launchers/claude.js +25 -17
- package/build/agent-launchers/cursor.js +65 -0
- package/build/agent-launchers/index.js +23 -8
- package/build/agent-registry.js +68 -0
- package/build/command-catalog.js +376 -0
- package/build/commands.generated.js +8 -5
- package/build/index.js +406 -120
- package/build/mcp-provisioning.js +94 -1
- package/build/pipeline-utils.js +0 -33
- package/build/pipelines.generated.js +2 -31
- package/build/readme.generated.js +3 -0
- package/build/schedule-run.js +436 -88
- package/build/schedule-store.js +41 -1
- package/build/scheduled-prompt.js +109 -0
- package/build/scheduler-backends/at-fallback.js +5 -10
- package/build/scheduler-backends/escaping.js +40 -10
- package/build/scheduler-backends/launchd.js +23 -14
- package/build/scheduler-backends/systemd-user.js +32 -19
- package/build/scheduler-backends/task-scheduler.js +8 -13
- package/build/start-tickets.js +459 -30
- package/build/version.generated.js +1 -1
- package/package.json +4 -3
- package/pipelines/implement-ticket.json +2 -28
- package/smoke-test/SMOKE-TEST.md +61 -18
package/build/index.js
CHANGED
|
@@ -22,6 +22,7 @@ import { PIPELINES as BUNDLED_PIPELINES, INSTRUCTIONS as BUNDLED_INSTRUCTIONS, C
|
|
|
22
22
|
import { COMMANDS } from "./commands.generated.js";
|
|
23
23
|
import { AGENTS } from "./agents.generated.js";
|
|
24
24
|
import { VERSION } from "./version.generated.js";
|
|
25
|
+
import { README } from "./readme.generated.js";
|
|
25
26
|
import { checkForUpdate } from "./update-check.js";
|
|
26
27
|
import { reconstructAgentMarkdown, translateAgentToCopilot } from "./agent-utils.js";
|
|
27
28
|
import { resolveRecipe, loadCustomPipelines } from "./pipeline-utils.js";
|
|
@@ -87,11 +88,18 @@ async function getResolvedApiKey() {
|
|
|
87
88
|
}
|
|
88
89
|
/** GET auth headers, with the API key resolved once via {@link getResolvedApiKey}. */
|
|
89
90
|
async function getGetHeaders() {
|
|
90
|
-
return {
|
|
91
|
+
return {
|
|
92
|
+
"X-API-Key": await getResolvedApiKey(),
|
|
93
|
+
"X-Bridge-MCP-Version": VERSION,
|
|
94
|
+
};
|
|
91
95
|
}
|
|
92
96
|
/** POST auth headers (API key + JSON content type). */
|
|
93
97
|
async function getPostHeaders() {
|
|
94
|
-
return {
|
|
98
|
+
return {
|
|
99
|
+
"X-API-Key": await getResolvedApiKey(),
|
|
100
|
+
"Content-Type": "application/json",
|
|
101
|
+
"X-Bridge-MCP-Version": VERSION,
|
|
102
|
+
};
|
|
95
103
|
}
|
|
96
104
|
/** Set true immediately after the server connection (transport) completes. */
|
|
97
105
|
let serverConnected = false;
|
|
@@ -313,8 +321,8 @@ async function saveLocally(dir, filename, content) {
|
|
|
313
321
|
// Heavy-read truncate-and-save helpers (BAPI-342)
|
|
314
322
|
// ---------------------------------------------------------------------------
|
|
315
323
|
//
|
|
316
|
-
//
|
|
317
|
-
// list_attachments) can return very large
|
|
324
|
+
// Five heavy read tools (get_project_standards, get_tickets, get_ticket,
|
|
325
|
+
// get_comments, list_attachments) can return very large payloads. To avoid flooding the
|
|
318
326
|
// agent's context, when a successful payload exceeds MAX_INLINE_TEXT_LENGTH the
|
|
319
327
|
// FULL payload is saved to disk first and only then is a truncated,
|
|
320
328
|
// markdown-fenced inline preview returned. Nothing is lost: if the save fails
|
|
@@ -502,6 +510,18 @@ const TICKET_ARTIFACTS = {
|
|
|
502
510
|
`Use get_architecture with ticket_number "${n}" to retrieve the architecture plan once processing completes.`,
|
|
503
511
|
pollLabel: (n) => `Architecture generation for ${n}`,
|
|
504
512
|
},
|
|
513
|
+
fsd: {
|
|
514
|
+
kind: "single",
|
|
515
|
+
generateEndpoint: (n) => `/ticket/${encodeURIComponent(n)}/generate-fsd`,
|
|
516
|
+
getEndpoint: (n) => `/ticket/${encodeURIComponent(n)}/fsd`,
|
|
517
|
+
saveSubdir: "fsd",
|
|
518
|
+
filename: (n) => `${n}-fsd-plan.md`,
|
|
519
|
+
requestErrorPrefix: "Failed to request FSD generation: ",
|
|
520
|
+
confirmationText: (n) => `FSD generation requested for ${n}. ` +
|
|
521
|
+
`Processing typically takes 2-4 minutes. ` +
|
|
522
|
+
`Use get_design_doc with ticket_number "${n}" and doc_type "fsd" to retrieve the functional specification document once processing completes.`,
|
|
523
|
+
pollLabel: (n) => `FSD generation for ${n}`,
|
|
524
|
+
},
|
|
505
525
|
clarifying_questions: {
|
|
506
526
|
kind: "single",
|
|
507
527
|
generateEndpoint: (n) => `/ticket/${encodeURIComponent(n)}/generate-clarifying-questions`,
|
|
@@ -576,6 +596,8 @@ async function getTicketArtifactDocsPath(subdir) {
|
|
|
576
596
|
return getDocsPath("plans");
|
|
577
597
|
case "architecture":
|
|
578
598
|
return getDocsPath("architecture");
|
|
599
|
+
case "fsd":
|
|
600
|
+
return getDocsPath("fsd");
|
|
579
601
|
case "clarifying-questions":
|
|
580
602
|
return getDocsPath("clarifying-questions");
|
|
581
603
|
case "ticket-critiques":
|
|
@@ -584,6 +606,14 @@ async function getTicketArtifactDocsPath(subdir) {
|
|
|
584
606
|
return getDocsPath("reimplementations");
|
|
585
607
|
}
|
|
586
608
|
}
|
|
609
|
+
// Map the public unified design-doc `doc_type` to the internal artifact key.
|
|
610
|
+
// `tdd` reuses the existing "architecture" artifact (the TDD/architecture
|
|
611
|
+
// document) WITHOUT renaming it, preserving the back-compatible
|
|
612
|
+
// request_architecture / get_architecture tools. `fsd` routes to the new "fsd"
|
|
613
|
+
// artifact.
|
|
614
|
+
function resolveDesignDocArtifactType(docType) {
|
|
615
|
+
return docType === "fsd" ? "fsd" : "architecture";
|
|
616
|
+
}
|
|
587
617
|
// Shared request flow for the five single-artifact request_* tools: POST the
|
|
588
618
|
// generate endpoint, return the per-tool error prefix on a non-OK POST, and on
|
|
589
619
|
// wait_for_result poll the get endpoint (900_000 ms) and optionally save.
|
|
@@ -1306,6 +1336,28 @@ const server = new McpServer({
|
|
|
1306
1336
|
version: "1.0.0",
|
|
1307
1337
|
});
|
|
1308
1338
|
// ---------------------------------------------------------------------------
|
|
1339
|
+
// README resource
|
|
1340
|
+
// ---------------------------------------------------------------------------
|
|
1341
|
+
//
|
|
1342
|
+
// Expose the package README as a readable MCP resource so a connected agent can
|
|
1343
|
+
// discover the server's feature set without leaving the session. Without this,
|
|
1344
|
+
// `listMcpResources` returns nothing and the README is only visible on npm. The
|
|
1345
|
+
// markdown is bundled at build time (scripts/bundle-readme.js → readme.generated)
|
|
1346
|
+
// so there is no runtime file read.
|
|
1347
|
+
server.registerResource("readme", "bridge-api://readme", {
|
|
1348
|
+
title: "Bridge API MCP — README",
|
|
1349
|
+
description: "Overview and feature reference for the Bridge API MCP server (the same README published with the npm package).",
|
|
1350
|
+
mimeType: "text/markdown",
|
|
1351
|
+
}, async (uri) => ({
|
|
1352
|
+
contents: [
|
|
1353
|
+
{
|
|
1354
|
+
uri: uri.href,
|
|
1355
|
+
mimeType: "text/markdown",
|
|
1356
|
+
text: README,
|
|
1357
|
+
},
|
|
1358
|
+
],
|
|
1359
|
+
}));
|
|
1360
|
+
// ---------------------------------------------------------------------------
|
|
1309
1361
|
// Tool registration wrapper (BAPI-275)
|
|
1310
1362
|
// ---------------------------------------------------------------------------
|
|
1311
1363
|
//
|
|
@@ -1366,15 +1418,6 @@ const registerTool = ((name, config, handler) => {
|
|
|
1366
1418
|
}
|
|
1367
1419
|
return toolHandle;
|
|
1368
1420
|
});
|
|
1369
|
-
// ---------------------------------------------------------------------------
|
|
1370
|
-
// Tools
|
|
1371
|
-
// ---------------------------------------------------------------------------
|
|
1372
|
-
// SECURITY: The `annotations` objects below (readOnlyHint, destructiveHint,
|
|
1373
|
-
// idempotentHint, openWorldHint) are UNTRUSTED UX / model-routing hints only.
|
|
1374
|
-
// They are advisory metadata forwarded verbatim to the MCP client and MUST
|
|
1375
|
-
// NEVER be used for authorization, permission checks, access control, tool
|
|
1376
|
-
// enable/disable decisions, or any other security decision. Authorization is
|
|
1377
|
-
// enforced server-side via the API key + repo access checks, never here.
|
|
1378
1421
|
registerTool("ping", {
|
|
1379
1422
|
annotations: {
|
|
1380
1423
|
readOnlyHint: true,
|
|
@@ -1384,6 +1427,9 @@ registerTool("ping", {
|
|
|
1384
1427
|
},
|
|
1385
1428
|
description: "Test connectivity to Bridge API. Validates that the API key is accepted and the configured repository is accessible. " +
|
|
1386
1429
|
"Returns JSON with {status: 'ok', repo_name: '<configured repo>'}. " +
|
|
1430
|
+
"The response also reports MCP version metadata: mcp_version (your installed MCP server version), " +
|
|
1431
|
+
"latest_mcp_version (latest published), upgrade_available, upgrade_advice (a short, optional note when a newer version is available), " +
|
|
1432
|
+
"and release_state_stale. These are informational — an available upgrade is not an error. " +
|
|
1387
1433
|
"Use this as a quick health check before other operations, or to verify your Bridge API configuration is working. " +
|
|
1388
1434
|
"A 403 response means the API key is invalid or the repo is not authorized. " +
|
|
1389
1435
|
"If the server is unreachable, check that BAPI_BASE_URL points to a running Bridge API instance.",
|
|
@@ -1391,8 +1437,28 @@ registerTool("ping", {
|
|
|
1391
1437
|
}, async () => {
|
|
1392
1438
|
const url = buildGetUrl("/ping", { repo_name: REPO_NAME });
|
|
1393
1439
|
const resp = await fetch(url, { headers: await getGetHeaders() });
|
|
1394
|
-
|
|
1395
|
-
|
|
1440
|
+
// Preserve existing behavior for non-OK responses (structured error relay).
|
|
1441
|
+
if (!resp.ok) {
|
|
1442
|
+
const text = await handleResponse(resp);
|
|
1443
|
+
return { content: [{ type: "text", text }] };
|
|
1444
|
+
}
|
|
1445
|
+
// OK: keep the first content item independently JSON.parse()-able, and emit
|
|
1446
|
+
// any upgrade advice as a SEPARATE text item so prose never pollutes the JSON.
|
|
1447
|
+
const raw = await resp.text();
|
|
1448
|
+
try {
|
|
1449
|
+
const body = JSON.parse(raw);
|
|
1450
|
+
const content = [
|
|
1451
|
+
{ type: "text", text: JSON.stringify(body, null, 2) },
|
|
1452
|
+
];
|
|
1453
|
+
if (typeof body.upgrade_advice === "string" && body.upgrade_advice.length > 0) {
|
|
1454
|
+
content.push({ type: "text", text: body.upgrade_advice });
|
|
1455
|
+
}
|
|
1456
|
+
return { content };
|
|
1457
|
+
}
|
|
1458
|
+
catch {
|
|
1459
|
+
// Unexpected non-JSON 200 — fall back to the raw text in one content item.
|
|
1460
|
+
return { content: [{ type: "text", text: raw }] };
|
|
1461
|
+
}
|
|
1396
1462
|
});
|
|
1397
1463
|
registerTool("second_opinion", {
|
|
1398
1464
|
annotations: {
|
|
@@ -1448,6 +1514,7 @@ registerTool("generate_image", {
|
|
|
1448
1514
|
"This tool spends provider credits on every call — cost scales with quality (low/medium/high). " +
|
|
1449
1515
|
"Defaults to low quality to minimize provider spend; increase quality only when fidelity matters. " +
|
|
1450
1516
|
"Returns native MCP image content (type: 'image') so the caller receives the image directly. " +
|
|
1517
|
+
"The image is always also saved to the local BAPI_DOCS_DIR/images/ directory. " +
|
|
1451
1518
|
"Google Imagen outputs (provider='gemini') include an invisible SynthID watermark applied server-side by Google.",
|
|
1452
1519
|
inputSchema: {
|
|
1453
1520
|
prompt: z
|
|
@@ -1470,14 +1537,12 @@ registerTool("generate_image", {
|
|
|
1470
1537
|
.optional()
|
|
1471
1538
|
.default("1024x1024")
|
|
1472
1539
|
.describe("Image dimensions. Defaults to '1024x1024'."),
|
|
1473
|
-
save_locally: z
|
|
1474
|
-
.boolean()
|
|
1475
|
-
.optional()
|
|
1476
|
-
.default(false)
|
|
1477
|
-
.describe("When true, save the generated image to the local docs/images directory."),
|
|
1478
1540
|
},
|
|
1479
|
-
}, async ({ prompt, provider, quality, size
|
|
1480
|
-
|
|
1541
|
+
}, async ({ prompt, provider, quality, size }) => {
|
|
1542
|
+
// The backend is async (submit -> poll -> result) so a slow generation no
|
|
1543
|
+
// longer exceeds the server's 30s request timeout. This tool absorbs the
|
|
1544
|
+
// polling so the agent still gets the image back in a single call.
|
|
1545
|
+
const submitResp = await fetch(buildApiUrl("/llm/generate-image"), {
|
|
1481
1546
|
method: "POST",
|
|
1482
1547
|
headers: await getPostHeaders(),
|
|
1483
1548
|
body: JSON.stringify({
|
|
@@ -1488,11 +1553,71 @@ registerTool("generate_image", {
|
|
|
1488
1553
|
size,
|
|
1489
1554
|
}),
|
|
1490
1555
|
});
|
|
1491
|
-
if (!
|
|
1492
|
-
const text = await handleResponse(
|
|
1556
|
+
if (!submitResp.ok) {
|
|
1557
|
+
const text = await handleResponse(submitResp);
|
|
1558
|
+
return { content: [{ type: "text", text }] };
|
|
1559
|
+
}
|
|
1560
|
+
const submitBody = (await submitResp.json());
|
|
1561
|
+
const requestId = submitBody.request_id;
|
|
1562
|
+
if (typeof requestId !== "number") {
|
|
1563
|
+
return {
|
|
1564
|
+
content: [
|
|
1565
|
+
{
|
|
1566
|
+
type: "text",
|
|
1567
|
+
text: JSON.stringify({ error: "Image generation submit response is missing request_id", status: 500 }),
|
|
1568
|
+
},
|
|
1569
|
+
],
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
const repoQuery = `repo_name=${encodeURIComponent(REPO_NAME)}`;
|
|
1573
|
+
const statusUrl = buildApiUrl(`/llm/generate-image/${requestId}/status?${repoQuery}`);
|
|
1574
|
+
const resultUrl = buildApiUrl(`/llm/generate-image/${requestId}/result?${repoQuery}`);
|
|
1575
|
+
// Poll /status until terminal. Low quality is ~13s, but high quality can run
|
|
1576
|
+
// 2-4+ min; start snappy at 2s, back off to 5s after 20s, cap at 300s. The
|
|
1577
|
+
// server-side janitor fails truly-stuck rows only after 15 min, so this cap
|
|
1578
|
+
// always returns the recoverable message well before a live row is touched.
|
|
1579
|
+
const startTime = Date.now();
|
|
1580
|
+
const timeoutMs = 300_000;
|
|
1581
|
+
let pollIntervalMs = 2_000;
|
|
1582
|
+
let finalStatus = "";
|
|
1583
|
+
while (true) {
|
|
1584
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
1585
|
+
if (Date.now() - startTime >= timeoutMs) {
|
|
1586
|
+
return {
|
|
1587
|
+
content: [
|
|
1588
|
+
{
|
|
1589
|
+
type: "text",
|
|
1590
|
+
text: `Image generation is still processing after ${Math.round(timeoutMs / 1000)}s (request_id=${requestId}). The result is recoverable from the server via GET /llm/generate-image/${requestId}/result once it finishes.`,
|
|
1591
|
+
},
|
|
1592
|
+
],
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
if (Date.now() - startTime > 20_000) {
|
|
1596
|
+
pollIntervalMs = 5_000;
|
|
1597
|
+
}
|
|
1598
|
+
const statusResp = await fetch(statusUrl, { headers: await getGetHeaders() });
|
|
1599
|
+
if (!statusResp.ok) {
|
|
1600
|
+
const text = await handleResponse(statusResp);
|
|
1601
|
+
return { content: [{ type: "text", text }] };
|
|
1602
|
+
}
|
|
1603
|
+
const statusBody = (await statusResp.json());
|
|
1604
|
+
finalStatus = typeof statusBody.status === "string" ? statusBody.status : "";
|
|
1605
|
+
if (finalStatus === "completed" || finalStatus === "failed") {
|
|
1606
|
+
break;
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
if (finalStatus === "failed") {
|
|
1610
|
+
// /result returns the sanitized 409 failure detail.
|
|
1611
|
+
const failResp = await fetch(resultUrl, { headers: await getGetHeaders() });
|
|
1612
|
+
const text = await handleResponse(failResp);
|
|
1493
1613
|
return { content: [{ type: "text", text }] };
|
|
1494
1614
|
}
|
|
1495
|
-
const
|
|
1615
|
+
const resultResp = await fetch(resultUrl, { headers: await getGetHeaders() });
|
|
1616
|
+
if (!resultResp.ok) {
|
|
1617
|
+
const text = await handleResponse(resultResp);
|
|
1618
|
+
return { content: [{ type: "text", text }] };
|
|
1619
|
+
}
|
|
1620
|
+
const body = (await resultResp.json());
|
|
1496
1621
|
const imageBase64 = body.image_base64;
|
|
1497
1622
|
if (typeof imageBase64 !== "string" || imageBase64.length === 0) {
|
|
1498
1623
|
return {
|
|
@@ -1510,21 +1635,21 @@ registerTool("generate_image", {
|
|
|
1510
1635
|
const content = [
|
|
1511
1636
|
{ type: "image", data: imageBase64, mimeType },
|
|
1512
1637
|
];
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
}
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
}
|
|
1527
|
-
}
|
|
1638
|
+
// Always persist to BAPI_DOCS_DIR/images/. A write failure must not discard
|
|
1639
|
+
// the already-paid-for generation, so warn and still return the inline image.
|
|
1640
|
+
try {
|
|
1641
|
+
const imagesDir = await getDocsPath("images");
|
|
1642
|
+
const filename = `generated-image-${safeTimestampForFilename()}.png`;
|
|
1643
|
+
const filePath = `${imagesDir}/${filename}`;
|
|
1644
|
+
await mkdir(imagesDir, { recursive: true });
|
|
1645
|
+
await writeFile(filePath, Buffer.from(imageBase64, "base64"));
|
|
1646
|
+
content.push({ type: "text", text: `Saved to ${filePath}` });
|
|
1647
|
+
}
|
|
1648
|
+
catch (saveErr) {
|
|
1649
|
+
content.push({
|
|
1650
|
+
type: "text",
|
|
1651
|
+
text: `Warning: image generated successfully but local save failed: ${saveErr instanceof Error ? saveErr.message : String(saveErr)}`,
|
|
1652
|
+
});
|
|
1528
1653
|
}
|
|
1529
1654
|
return { content };
|
|
1530
1655
|
});
|
|
@@ -1538,12 +1663,19 @@ registerTool("get_project_standards", {
|
|
|
1538
1663
|
description: "Retrieve project-specific coding standards, architecture guidelines, testing standards, code review standards, and project context (platform, version, project description) for the configured repository. " +
|
|
1539
1664
|
"Returns structured markdown with sections for project context, architecture instructions, code review correctness standards, testing stack information, and build analysis. " +
|
|
1540
1665
|
"Only sections with configured values are included. Returns 404 if no standards are configured. " +
|
|
1541
|
-
"Consult these standards before writing or reviewing code to ensure compliance with project conventions."
|
|
1666
|
+
"Consult these standards before writing or reviewing code to ensure compliance with project conventions. " +
|
|
1667
|
+
"Successful oversized output is automatically saved under the local docs directory and returned inline as a truncated preview.",
|
|
1542
1668
|
inputSchema: {},
|
|
1543
1669
|
}, async () => {
|
|
1544
1670
|
const url = buildGetUrl("/project-standards", { repo_name: REPO_NAME });
|
|
1545
1671
|
const resp = await fetch(url, { headers: await getGetHeaders() });
|
|
1546
|
-
const
|
|
1672
|
+
const ok = resp.ok;
|
|
1673
|
+
let text = await handleResponse(resp);
|
|
1674
|
+
if (ok) {
|
|
1675
|
+
const safeRepo = safeTicketFileSegment(REPO_NAME || "repo");
|
|
1676
|
+
const filename = `${safeRepo}-project-standards.md`;
|
|
1677
|
+
text = await truncateAndSaveIfNeeded(text, await getDocsPath("project-standards"), filename);
|
|
1678
|
+
}
|
|
1547
1679
|
return { content: [{ type: "text", text }] };
|
|
1548
1680
|
});
|
|
1549
1681
|
registerTool("get_tickets", {
|
|
@@ -1647,6 +1779,29 @@ registerTool("get_ticket", {
|
|
|
1647
1779
|
}
|
|
1648
1780
|
return { content: [{ type: "text", text }] };
|
|
1649
1781
|
});
|
|
1782
|
+
registerTool("get_ticket_model_tier", {
|
|
1783
|
+
annotations: {
|
|
1784
|
+
readOnlyHint: true,
|
|
1785
|
+
destructiveHint: false,
|
|
1786
|
+
idempotentHint: true,
|
|
1787
|
+
openWorldHint: true,
|
|
1788
|
+
},
|
|
1789
|
+
description: "Resolve the coarse implementation-model TIER for a Jira ticket from its difficulty. " +
|
|
1790
|
+
"Returns { difficulty: int|null, tier: \"cheap\"|\"basic\"|\"premium\"|null, source: \"cached\"|\"computed\"|\"fallback\" }. " +
|
|
1791
|
+
"The backend computes difficulty on demand (and caches it) when missing, and never returns a model id — " +
|
|
1792
|
+
"the tier->model mapping is owned by the start-tickets CLI. A null tier (source=\"fallback\") means the " +
|
|
1793
|
+
"model could not be resolved and callers should use the agent's default model.",
|
|
1794
|
+
inputSchema: {
|
|
1795
|
+
ticket_number: z
|
|
1796
|
+
.string()
|
|
1797
|
+
.describe("Jira ticket key in PROJECT-NUMBER format (e.g. PROJ-123)"),
|
|
1798
|
+
},
|
|
1799
|
+
}, async ({ ticket_number }) => {
|
|
1800
|
+
const url = buildGetUrl(`/tickets/${encodeURIComponent(ticket_number)}/model-tier`, { repo_name: REPO_NAME });
|
|
1801
|
+
const resp = await fetch(url, { headers: await getGetHeaders() });
|
|
1802
|
+
const text = await handleResponse(resp);
|
|
1803
|
+
return { content: [{ type: "text", text }] };
|
|
1804
|
+
});
|
|
1650
1805
|
registerTool("get_comments", {
|
|
1651
1806
|
annotations: {
|
|
1652
1807
|
readOnlyHint: true,
|
|
@@ -2000,7 +2155,7 @@ registerTool("upload_attachment", {
|
|
|
2000
2155
|
"Optionally syncs the content to Bridge API's tickets_links table so retrieval endpoints " +
|
|
2001
2156
|
"(get_clarifying_questions, get_ticket_critique, get_plan) return the updated content without re-generation. " +
|
|
2002
2157
|
"Use link_type to specify which retrieval endpoint should serve this content. " +
|
|
2003
|
-
"Known link_type values: clarifying-questions.md, debugging-guidance.md, ticket-quality-critique.md, architecture-plan.md.",
|
|
2158
|
+
"Known link_type values: clarifying-questions.md, debugging-guidance.md, ticket-quality-critique.md, architecture-plan.md, fsd-plan.md.",
|
|
2004
2159
|
inputSchema: {
|
|
2005
2160
|
ticket_number: z
|
|
2006
2161
|
.string()
|
|
@@ -2283,6 +2438,87 @@ registerTool("request_architecture", {
|
|
|
2283
2438
|
}, async (args) => {
|
|
2284
2439
|
return requestTicketArtifact("architecture", args);
|
|
2285
2440
|
});
|
|
2441
|
+
registerTool("request_design_doc", {
|
|
2442
|
+
annotations: {
|
|
2443
|
+
readOnlyHint: false,
|
|
2444
|
+
destructiveHint: false,
|
|
2445
|
+
idempotentHint: false,
|
|
2446
|
+
openWorldHint: true,
|
|
2447
|
+
},
|
|
2448
|
+
description: "START (or refresh) async generation of a design document for a Jira ticket, routed by doc_type. " +
|
|
2449
|
+
"Use doc_type 'tdd' for a Technical Design Document (architecture-focused, for engineers) or 'fsd' for a " +
|
|
2450
|
+
"Functional Specification Document (product/functional-focused, for PMs, designers, and QA). " +
|
|
2451
|
+
"This triggers an asynchronous background job — results are NOT immediate. " +
|
|
2452
|
+
"Processing typically takes 2-4 minutes depending on ticket complexity. " +
|
|
2453
|
+
"The matching get_design_doc tool retrieves the generated document later (call get_design_doc with the same ticket_number and doc_type) — unless you set wait_for_result, in which case this call blocks and returns it directly. " +
|
|
2454
|
+
"Returns 202 if the request was accepted, 404 if the ticket does not exist in Jira, or 403 if the API key is unauthorized.",
|
|
2455
|
+
inputSchema: {
|
|
2456
|
+
ticket_number: z
|
|
2457
|
+
.string()
|
|
2458
|
+
.describe("Jira ticket key in PROJECT-NUMBER format (e.g. PROJ-123) to generate a design document for"),
|
|
2459
|
+
doc_type: z
|
|
2460
|
+
.enum(["tdd", "fsd"])
|
|
2461
|
+
.describe("Which design document to generate: 'tdd' (Technical Design Document, engineer audience) or " +
|
|
2462
|
+
"'fsd' (Functional Specification Document, product/functional audience)."),
|
|
2463
|
+
wait_for_result: z
|
|
2464
|
+
.boolean()
|
|
2465
|
+
.optional()
|
|
2466
|
+
.default(false)
|
|
2467
|
+
.describe("When true, the tool blocks and polls until the document is ready (typically 2-4 minutes), " +
|
|
2468
|
+
"then returns the full content directly. When false (default), returns immediately " +
|
|
2469
|
+
"with a confirmation message — use get_design_doc later to retrieve results."),
|
|
2470
|
+
save_locally: z
|
|
2471
|
+
.boolean()
|
|
2472
|
+
.optional()
|
|
2473
|
+
.default(true)
|
|
2474
|
+
.describe("When wait_for_result is true, whether to save the generated document to a local file under " +
|
|
2475
|
+
"BAPI_DOCS_DIR. Defaults to true. Only takes effect when wait_for_result is true."),
|
|
2476
|
+
second_opinion: z
|
|
2477
|
+
.string()
|
|
2478
|
+
.optional()
|
|
2479
|
+
.describe("Provider routing override for THIS artifact-generation request " +
|
|
2480
|
+
"(e.g. 'anthropic', 'openai', 'gemini'). When set, the artifact is " +
|
|
2481
|
+
"generated by the named provider and, where supported, a cross-provider " +
|
|
2482
|
+
"second-opinion pass is applied to this request only. " +
|
|
2483
|
+
"Takes precedence over `provider` when both are set."),
|
|
2484
|
+
provider: z.string().optional().describe("Pure provider switch — use a specific LLM provider (openai, anthropic, gemini) without " +
|
|
2485
|
+
"triggering second-opinion semantics. If both provider and second_opinion are set, " +
|
|
2486
|
+
"second_opinion takes precedence."),
|
|
2487
|
+
},
|
|
2488
|
+
}, async (args) => {
|
|
2489
|
+
return requestTicketArtifact(resolveDesignDocArtifactType(args.doc_type), args);
|
|
2490
|
+
});
|
|
2491
|
+
registerTool("get_design_doc", {
|
|
2492
|
+
annotations: {
|
|
2493
|
+
readOnlyHint: true,
|
|
2494
|
+
destructiveHint: false,
|
|
2495
|
+
idempotentHint: true,
|
|
2496
|
+
openWorldHint: true,
|
|
2497
|
+
},
|
|
2498
|
+
description: "RETRIEVE an already-generated design document for a Jira ticket, routed by doc_type. " +
|
|
2499
|
+
"Use doc_type 'tdd' for the Technical Design Document or 'fsd' for the Functional Specification Document. " +
|
|
2500
|
+
"This tool only fetches an existing document — it does NOT start or trigger generation. " +
|
|
2501
|
+
"If no document exists yet (or you need a fresh one), call `request_design_doc` first with the same doc_type. " +
|
|
2502
|
+
"Returns the full document as markdown text — present it verbatim without summarizing. " +
|
|
2503
|
+
"Returns a 404 / not-found response when no document is ready yet — that means generation has not run, not that this tool failed.",
|
|
2504
|
+
inputSchema: {
|
|
2505
|
+
ticket_number: z
|
|
2506
|
+
.string()
|
|
2507
|
+
.describe("Jira ticket key in PROJECT-NUMBER format (e.g. PROJ-123, BAPI-42)"),
|
|
2508
|
+
doc_type: z
|
|
2509
|
+
.enum(["tdd", "fsd"])
|
|
2510
|
+
.describe("Which design document to retrieve: 'tdd' (Technical Design Document) or " +
|
|
2511
|
+
"'fsd' (Functional Specification Document)."),
|
|
2512
|
+
save_locally: z
|
|
2513
|
+
.boolean()
|
|
2514
|
+
.optional()
|
|
2515
|
+
.default(true)
|
|
2516
|
+
.describe("Whether to save the retrieved document to a local file under BAPI_DOCS_DIR. " +
|
|
2517
|
+
"Defaults to true. Set to false to skip saving."),
|
|
2518
|
+
},
|
|
2519
|
+
}, async (args) => {
|
|
2520
|
+
return getTicketArtifact(resolveDesignDocArtifactType(args.doc_type), args);
|
|
2521
|
+
});
|
|
2286
2522
|
registerTool("request_clarifying_questions", {
|
|
2287
2523
|
annotations: {
|
|
2288
2524
|
readOnlyHint: false,
|
|
@@ -2705,6 +2941,7 @@ registerTool("resolve_target_status", {
|
|
|
2705
2941
|
// ---------------------------------------------------------------------------
|
|
2706
2942
|
const VALID_CONFIG_FIELDS = [
|
|
2707
2943
|
"review_instructions", "documentation_instructions", "architecture_instructions",
|
|
2944
|
+
"tdd_document_instructions", "fsd_document_instructions",
|
|
2708
2945
|
"unit_testing_instructions", "e2e_testing_instructions",
|
|
2709
2946
|
"unit_testing_stack", "e2e_testing_stack",
|
|
2710
2947
|
"frontend_correctness_standards", "backend_correctness_standards",
|
|
@@ -2714,7 +2951,16 @@ const VALID_CONFIG_FIELDS = [
|
|
|
2714
2951
|
"allow_mutating_smoke_ops",
|
|
2715
2952
|
"selected_mcp_slugs",
|
|
2716
2953
|
"base_branch",
|
|
2717
|
-
"
|
|
2954
|
+
"difficulty_model_routing_enabled",
|
|
2955
|
+
"difficulty_model_tier_overrides",
|
|
2956
|
+
// BAPI-356 easy-install bootstrap fields (also exposed via the registry).
|
|
2957
|
+
"working_in",
|
|
2958
|
+
"version_control_system",
|
|
2959
|
+
"version",
|
|
2960
|
+
"project_description",
|
|
2961
|
+
"custom_directories",
|
|
2962
|
+
"exclude_directories",
|
|
2963
|
+
"exclude_file_extensions",
|
|
2718
2964
|
].join(", ");
|
|
2719
2965
|
registerTool("list_config_fields", {
|
|
2720
2966
|
annotations: {
|
|
@@ -2761,7 +3007,8 @@ registerTool("get_config_field", {
|
|
|
2761
3007
|
},
|
|
2762
3008
|
description: "Read the current value and metadata for a specific configuration field. " +
|
|
2763
3009
|
"Returns the field's current database value (or null if not set), along with a description of its purpose and examples of helpful content. " +
|
|
2764
|
-
"Use this before update_config_field to understand the current state and build upon it rather than overwriting blindly."
|
|
3010
|
+
"Use this before update_config_field to understand the current state and build upon it rather than overwriting blindly. " +
|
|
3011
|
+
"For install bootstrap, prefer get_install_manifest (one read of all bootstrap fields) over many individual get_config_field reads.",
|
|
2765
3012
|
inputSchema: {
|
|
2766
3013
|
field_name: z.string().describe(`The configuration field to read. Valid options: ${VALID_CONFIG_FIELDS}`),
|
|
2767
3014
|
},
|
|
@@ -2781,19 +3028,30 @@ registerTool("update_config_field", {
|
|
|
2781
3028
|
description: "Update a specific configuration field in the Bridge database. " +
|
|
2782
3029
|
"These fields control LLM behavior during code review, planning, and documentation. " +
|
|
2783
3030
|
"Always call get_config_field first to read the current value and build upon it. " +
|
|
3031
|
+
"For install bootstrap, prefer apply_install_manifest (one atomic write of all bootstrap fields, " +
|
|
3032
|
+
"with server-owned skip/conflict/confirmation semantics) over many individual update_config_field writes. " +
|
|
2784
3033
|
"Returns 400 if the field name is invalid, 404 if the repository has no configuration row yet.",
|
|
2785
3034
|
inputSchema: {
|
|
2786
3035
|
field_name: z.string().describe(`The configuration field to update. Valid options: ${VALID_CONFIG_FIELDS}`),
|
|
2787
|
-
value: z.union([
|
|
2788
|
-
|
|
3036
|
+
value: z.union([
|
|
3037
|
+
z.string(),
|
|
3038
|
+
z.boolean(),
|
|
3039
|
+
z.array(z.string()),
|
|
3040
|
+
z.record(z.string(), z.union([z.string(), z.null()])),
|
|
3041
|
+
]).optional().describe("The new value for the configuration field. Provide either value or file_path, not both. " +
|
|
3042
|
+
"Most fields take a string; scalar boolean fields (e.g. allow_mutating_smoke_ops, " +
|
|
3043
|
+
"difficulty_model_routing_enabled) take true/false. " +
|
|
2789
3044
|
"The selected_mcp_slugs field takes a JSON array of supported MCP validation manual slug strings " +
|
|
2790
3045
|
"(e.g. [\"b2c-commerce-developer\", \"playwright-mcp\", \"pwa-kit-mcp\"]) — pass an array of strings, " +
|
|
2791
3046
|
"not a comma-delimited string; an empty array clears the selection. " +
|
|
3047
|
+
"The difficulty_model_tier_overrides field takes a JSON object mapping tier names " +
|
|
3048
|
+
"(\"cheap\"/\"basic\"/\"premium\") to per-repo model aliases (e.g. {\"premium\": \"opus\"}) — pass " +
|
|
3049
|
+
"an object, not a string; an empty object clears all overrides. " +
|
|
3050
|
+
"The difficulty_model_routing_enabled field enables difficulty-based /start-tickets model routing " +
|
|
3051
|
+
"(default ON); pass true/false. " +
|
|
2792
3052
|
"The base_branch field is a string/null field controlling the development base branch used by PR " +
|
|
2793
3053
|
"creation (/create-pr) and start-tickets worktree creation; an empty/null value clears it and " +
|
|
2794
3054
|
"automations fall back to 'main'. " +
|
|
2795
|
-
"The tiered_execution field is a string enum with valid values 'off', 'claude_code_only', and " +
|
|
2796
|
-
"'all_capable'; omitting/clearing the value resolves to the backend default 'claude_code_only'. " +
|
|
2797
3055
|
"For string fields, omit both value and file_path to set the field to NULL (clearing it). " +
|
|
2798
3056
|
"Scalar boolean fields are NOT NULL and have no clear/null state: omitting the value writes false " +
|
|
2799
3057
|
"(matching the API-layer coercion), so pass true/false explicitly."),
|
|
@@ -2801,8 +3059,17 @@ registerTool("update_config_field", {
|
|
|
2801
3059
|
"Useful for large configuration values like detailed review instructions. " +
|
|
2802
3060
|
"The file must be UTF-8 encoded and under 1MB. " +
|
|
2803
3061
|
"Not supported for scalar boolean fields like allow_mutating_smoke_ops."),
|
|
3062
|
+
only_if_null: z.boolean().optional().describe("Secondary conditional-write guard: when true, the field is updated only if its column is " +
|
|
3063
|
+
"currently NULL (returns status 'skipped'/reason 'already_set' otherwise). Legal only for " +
|
|
3064
|
+
"nullable columns (HTTP 422 otherwise). This is NOT the install bootstrap write path — for " +
|
|
3065
|
+
"easy install, prefer apply_install_manifest, which owns skip/conflict/confirmation semantics."),
|
|
2804
3066
|
},
|
|
2805
|
-
}, async ({ field_name, value, file_path }) => {
|
|
3067
|
+
}, async ({ field_name, value, file_path, only_if_null }) => {
|
|
3068
|
+
// Build the PUT body, including only_if_null only when explicitly true so
|
|
3069
|
+
// the default (false) request shape is unchanged for existing callers.
|
|
3070
|
+
const withGuard = (v) => only_if_null === true
|
|
3071
|
+
? { repo_name: REPO_NAME, value: v, only_if_null: true }
|
|
3072
|
+
: { repo_name: REPO_NAME, value: v };
|
|
2806
3073
|
// JSONB array config fields (e.g. selected_mcp_slugs): forward the array value
|
|
2807
3074
|
// as JSON. Never join into a comma-delimited string — the backend expects a
|
|
2808
3075
|
// real JSON array and validates each slug against the mcp_docs allowlist.
|
|
@@ -2825,14 +3092,39 @@ registerTool("update_config_field", {
|
|
|
2825
3092
|
const resp = await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`), {
|
|
2826
3093
|
method: "PUT",
|
|
2827
3094
|
headers: await getPostHeaders(),
|
|
2828
|
-
body: JSON.stringify(
|
|
3095
|
+
body: JSON.stringify(withGuard(arrayValue)),
|
|
3096
|
+
});
|
|
3097
|
+
const text = await handleResponse(resp);
|
|
3098
|
+
return { content: [{ type: "text", text }] };
|
|
3099
|
+
}
|
|
3100
|
+
// JSONB object config fields (e.g. difficulty_model_tier_overrides): forward the
|
|
3101
|
+
// object value as JSON. Reject file_path; the backend validates/normalizes keys
|
|
3102
|
+
// and alias values. An omitted value clears all overrides (empty object).
|
|
3103
|
+
const JSON_OBJECT_CONFIG_FIELDS = ["difficulty_model_tier_overrides"];
|
|
3104
|
+
if (JSON_OBJECT_CONFIG_FIELDS.includes(field_name)) {
|
|
3105
|
+
if (file_path) {
|
|
3106
|
+
return {
|
|
3107
|
+
isError: true,
|
|
3108
|
+
content: [{
|
|
3109
|
+
type: "text",
|
|
3110
|
+
text: JSON.stringify({
|
|
3111
|
+
error: `'${field_name}' is a JSON object field; file_path updates are not supported. Pass value as an object mapping tier names to model aliases.`,
|
|
3112
|
+
}),
|
|
3113
|
+
}],
|
|
3114
|
+
};
|
|
3115
|
+
}
|
|
3116
|
+
const objectValue = value === undefined ? {} : value;
|
|
3117
|
+
const resp = await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`), {
|
|
3118
|
+
method: "PUT",
|
|
3119
|
+
headers: await getPostHeaders(),
|
|
3120
|
+
body: JSON.stringify(withGuard(objectValue)),
|
|
2829
3121
|
});
|
|
2830
3122
|
const text = await handleResponse(resp);
|
|
2831
3123
|
return { content: [{ type: "text", text }] };
|
|
2832
3124
|
}
|
|
2833
3125
|
// Scalar boolean config fields: reject file-path updates and normalize boolean
|
|
2834
3126
|
// true/false and string "true"/"false" to a real boolean before persisting.
|
|
2835
|
-
const BOOLEAN_CONFIG_FIELDS = ["allow_mutating_smoke_ops"];
|
|
3127
|
+
const BOOLEAN_CONFIG_FIELDS = ["allow_mutating_smoke_ops", "difficulty_model_routing_enabled"];
|
|
2836
3128
|
if (BOOLEAN_CONFIG_FIELDS.includes(field_name)) {
|
|
2837
3129
|
if (file_path) {
|
|
2838
3130
|
return {
|
|
@@ -2874,7 +3166,7 @@ registerTool("update_config_field", {
|
|
|
2874
3166
|
const resp = await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`), {
|
|
2875
3167
|
method: "PUT",
|
|
2876
3168
|
headers: await getPostHeaders(),
|
|
2877
|
-
body: JSON.stringify(
|
|
3169
|
+
body: JSON.stringify(withGuard(boolValue)),
|
|
2878
3170
|
});
|
|
2879
3171
|
const text = await handleResponse(resp);
|
|
2880
3172
|
return { content: [{ type: "text", text }] };
|
|
@@ -2892,11 +3184,71 @@ registerTool("update_config_field", {
|
|
|
2892
3184
|
const resp = await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`), {
|
|
2893
3185
|
method: "PUT",
|
|
2894
3186
|
headers: await getPostHeaders(),
|
|
2895
|
-
body: JSON.stringify(
|
|
3187
|
+
body: JSON.stringify(withGuard(finalValue)),
|
|
2896
3188
|
});
|
|
2897
3189
|
const text = await handleResponse(resp);
|
|
2898
3190
|
return { content: [{ type: "text", text: text + note }] };
|
|
2899
3191
|
});
|
|
3192
|
+
// ---------------------------------------------------------------------------
|
|
3193
|
+
// Easy Install (BAPI-356): read-the-manifest / apply-the-manifest
|
|
3194
|
+
// ---------------------------------------------------------------------------
|
|
3195
|
+
registerTool("get_install_manifest", {
|
|
3196
|
+
annotations: {
|
|
3197
|
+
readOnlyHint: true,
|
|
3198
|
+
destructiveHint: false,
|
|
3199
|
+
idempotentHint: true,
|
|
3200
|
+
openWorldHint: true,
|
|
3201
|
+
},
|
|
3202
|
+
description: "Read the easy-install configuration manifest for the configured repository in one call. " +
|
|
3203
|
+
"Returns ordered field groups (each bootstrap field with its current value, is_set flag, agent " +
|
|
3204
|
+
"guidance, examples, and validation summary), a list of deferred fields (owned by " +
|
|
3205
|
+
"/learn-repository or set deliberately), a next_step pointer, done_criteria, and a signed " +
|
|
3206
|
+
"snapshot_token. Pass that exact snapshot_token to apply_install_manifest. " +
|
|
3207
|
+
"Prefer this over many individual get_config_field reads during install bootstrap.",
|
|
3208
|
+
inputSchema: {
|
|
3209
|
+
save_locally: z.boolean().optional().default(true).describe("When true (default), also save the full JSON manifest under BAPI_DOCS_DIR/install/."),
|
|
3210
|
+
},
|
|
3211
|
+
}, async ({ save_locally }) => {
|
|
3212
|
+
const url = buildGetUrl("/config/install-manifest", { repo_name: REPO_NAME });
|
|
3213
|
+
const resp = await fetch(url, { headers: await getGetHeaders() });
|
|
3214
|
+
const ok = resp.ok;
|
|
3215
|
+
let text = await handleResponse(resp);
|
|
3216
|
+
if (ok && save_locally !== false) {
|
|
3217
|
+
const safeRepo = safeTicketFileSegment(REPO_NAME || "repo");
|
|
3218
|
+
const filename = `${safeRepo}-install-manifest-${safeTimestampForFilename()}.json`;
|
|
3219
|
+
const note = await saveLocally(await getDocsPath("install"), filename, text);
|
|
3220
|
+
text = text + note;
|
|
3221
|
+
}
|
|
3222
|
+
return { content: [{ type: "text", text }] };
|
|
3223
|
+
});
|
|
3224
|
+
registerTool("apply_install_manifest", {
|
|
3225
|
+
annotations: {
|
|
3226
|
+
readOnlyHint: false,
|
|
3227
|
+
destructiveHint: true,
|
|
3228
|
+
idempotentHint: false,
|
|
3229
|
+
openWorldHint: true,
|
|
3230
|
+
},
|
|
3231
|
+
description: "Apply easy-install configuration in one atomic call. Pass the snapshot_token returned by " +
|
|
3232
|
+
"get_install_manifest plus a fields object. Each field value is either a scalar " +
|
|
3233
|
+
"(e.g. \"base_branch\": \"main\") or an object (e.g. \"project_description\": {\"value\": \"...\", " +
|
|
3234
|
+
"\"confirmed\": true}). project_description MUST be passed as {value, confirmed: true} and only " +
|
|
3235
|
+
"after explicit human approval. The server owns skip-if-set, conflict detection, and " +
|
|
3236
|
+
"confirmation semantics and returns six buckets: applied, skipped, conflict, rejected, " +
|
|
3237
|
+
"deferred, needs_confirmation. Only bootstrap-eligible fields are accepted (others are rejected).",
|
|
3238
|
+
inputSchema: {
|
|
3239
|
+
snapshot_token: z.string().describe("The exact snapshot_token returned by get_install_manifest for this repository."),
|
|
3240
|
+
fields: z.record(z.string(), z.any()).describe("Map of field_name to value. A value is either a scalar or an object {value, confirmed}. " +
|
|
3241
|
+
"Pass project_description only as {value: \"...\", confirmed: true} after human approval."),
|
|
3242
|
+
},
|
|
3243
|
+
}, async ({ snapshot_token, fields }) => {
|
|
3244
|
+
const resp = await fetch(buildUrl("/config/apply-install-manifest"), {
|
|
3245
|
+
method: "POST",
|
|
3246
|
+
headers: await getPostHeaders(),
|
|
3247
|
+
body: JSON.stringify({ repo_name: REPO_NAME, snapshot_token, fields }),
|
|
3248
|
+
});
|
|
3249
|
+
const text = await handleResponse(resp);
|
|
3250
|
+
return { content: [{ type: "text", text }] };
|
|
3251
|
+
});
|
|
2900
3252
|
function formatDeepResearchProviderReason(meta) {
|
|
2901
3253
|
if (!meta)
|
|
2902
3254
|
return "";
|
|
@@ -4125,72 +4477,6 @@ registerTool("generate_decision_page", {
|
|
|
4125
4477
|
};
|
|
4126
4478
|
});
|
|
4127
4479
|
// ---------------------------------------------------------------------------
|
|
4128
|
-
// Tiered section-executor telemetry (BAPI-346, Ticket 2)
|
|
4129
|
-
// ---------------------------------------------------------------------------
|
|
4130
|
-
/**
|
|
4131
|
-
* POST one per-section execution-telemetry row through the Bridge API HTTP
|
|
4132
|
-
* boundary (`/jira/tiered-section-metrics`). Keeps the MCP layer free of any
|
|
4133
|
-
* Python DAL import — the server route owns the dormant BAPI-345 DAL seam.
|
|
4134
|
-
*/
|
|
4135
|
-
async function recordTieredSectionMetric(args) {
|
|
4136
|
-
const payload = {
|
|
4137
|
-
repo_name: REPO_NAME,
|
|
4138
|
-
ticket_number: args.ticket_number,
|
|
4139
|
-
section_id: args.section_id,
|
|
4140
|
-
tier_assigned: args.tier_assigned,
|
|
4141
|
-
mode_run: args.mode_run,
|
|
4142
|
-
metrics: args.metrics ?? {},
|
|
4143
|
-
};
|
|
4144
|
-
const resp = await fetch(buildUrl("/tiered-section-metrics"), {
|
|
4145
|
-
method: "POST",
|
|
4146
|
-
headers: await getPostHeaders(),
|
|
4147
|
-
body: JSON.stringify(payload),
|
|
4148
|
-
});
|
|
4149
|
-
const text = await handleResponse(resp);
|
|
4150
|
-
return { content: [{ type: "text", text }] };
|
|
4151
|
-
}
|
|
4152
|
-
registerTool("record_tiered_section_metric", {
|
|
4153
|
-
annotations: {
|
|
4154
|
-
readOnlyHint: false,
|
|
4155
|
-
destructiveHint: false,
|
|
4156
|
-
idempotentHint: false,
|
|
4157
|
-
openWorldHint: true,
|
|
4158
|
-
},
|
|
4159
|
-
description: "Record one per-section execution-telemetry row for the Claude Code tiered " +
|
|
4160
|
-
"section executor (BAPI-346). This writes server-side telemetry state only — " +
|
|
4161
|
-
"it does NOT mutate Jira, code, or any ticket. It is intended to be called by " +
|
|
4162
|
-
"the tiered section executor after each section attempt; the executor Warns and " +
|
|
4163
|
-
"continues if recording fails, so telemetry never blocks an implementation. " +
|
|
4164
|
-
"Typed columns are ticket_number, section_id, tier_assigned, and mode_run; put " +
|
|
4165
|
-
"any additional per-section detail in the flexible `metrics` object.",
|
|
4166
|
-
inputSchema: {
|
|
4167
|
-
ticket_number: z
|
|
4168
|
-
.string()
|
|
4169
|
-
.describe("Jira ticket key in PROJECT-NUMBER format (e.g. PROJ-123, BAPI-42)"),
|
|
4170
|
-
section_id: z
|
|
4171
|
-
.string()
|
|
4172
|
-
.describe("The BAPI-345 section graph `id` of the section this metric is for."),
|
|
4173
|
-
tier_assigned: z
|
|
4174
|
-
.enum(["cheap", "basic", "premium"])
|
|
4175
|
-
.describe("The tier resolved for the section (cheap→haiku, basic→sonnet, premium→opus)."),
|
|
4176
|
-
mode_run: z
|
|
4177
|
-
.enum(["sub_agent", "inline_tiered", "inline_default"])
|
|
4178
|
-
.describe("The execution mode the section actually ran in."),
|
|
4179
|
-
metrics: z
|
|
4180
|
-
.record(z.string(), z.unknown())
|
|
4181
|
-
.optional()
|
|
4182
|
-
.describe("Optional flexible JSON detail (e.g. contract_version, mode_intended, " +
|
|
4183
|
-
"model_resolved, tokens, cache, verification, escalation_count, " +
|
|
4184
|
-
"budget_snapshot, files_changed, handoff, degraded_reason)."),
|
|
4185
|
-
},
|
|
4186
|
-
}, async ({ ticket_number, section_id, tier_assigned, mode_run, metrics }) => recordTieredSectionMetric({
|
|
4187
|
-
ticket_number,
|
|
4188
|
-
section_id,
|
|
4189
|
-
tier_assigned,
|
|
4190
|
-
mode_run,
|
|
4191
|
-
metrics,
|
|
4192
|
-
}));
|
|
4193
|
-
// ---------------------------------------------------------------------------
|
|
4194
4480
|
// Entry point
|
|
4195
4481
|
// ---------------------------------------------------------------------------
|
|
4196
4482
|
const transport = new StdioServerTransport();
|