@bridge_gpt/mcp-server 0.1.12 → 0.1.14
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 +13 -0
- package/build/commands.generated.js +4 -3
- package/build/decision-page-schema.js +101 -0
- package/build/decision-page-template.js +222 -109
- package/build/index.js +651 -98
- package/build/pipeline-orchestrator.js +593 -0
- package/build/pipeline-utils.js +50 -12
- package/build/pipelines.generated.js +50 -55
- package/build/version.generated.js +1 -1
- package/package.json +5 -7
- package/pipelines/check-ci-ticket.json +9 -8
- package/pipelines/implement-ticket.json +10 -9
- package/pipelines/pr-ticket.json +3 -9
- package/pipelines/review-ticket.json +3 -8
package/build/index.js
CHANGED
|
@@ -24,6 +24,8 @@ import { checkForUpdate } from "./update-check.js";
|
|
|
24
24
|
import { reconstructAgentMarkdown, translateAgentToCopilot } from "./agent-utils.js";
|
|
25
25
|
import { resolveRecipe, loadCustomPipelines } from "./pipeline-utils.js";
|
|
26
26
|
import { generateDecisionPageHtml } from "./decision-page-template.js";
|
|
27
|
+
import { DecisionPageInputShape } from "./decision-page-schema.js";
|
|
28
|
+
import { runPipeline, resumePipeline, listPipelineRuns, deletePipelineRun, } from "./pipeline-orchestrator.js";
|
|
27
29
|
// Mutable pipeline/instruction state — starts with bundled, merged with user at startup
|
|
28
30
|
const PIPELINES = { ...BUNDLED_PIPELINES };
|
|
29
31
|
const INSTRUCTIONS = { ...BUNDLED_INSTRUCTIONS };
|
|
@@ -48,6 +50,9 @@ const POST_HEADERS = {
|
|
|
48
50
|
function buildUrl(path) {
|
|
49
51
|
return `${BASE_URL.replace(/\/+$/, "")}/jira${path}`;
|
|
50
52
|
}
|
|
53
|
+
function buildApiUrl(path) {
|
|
54
|
+
return `${BASE_URL.replace(/\/+$/, "")}${path}`;
|
|
55
|
+
}
|
|
51
56
|
function buildGetUrl(path, params) {
|
|
52
57
|
const url = new URL(buildUrl(path));
|
|
53
58
|
for (const [key, value] of Object.entries(params)) {
|
|
@@ -517,6 +522,32 @@ automatically provided by the server.
|
|
|
517
522
|
|
|
518
523
|
- \`"halt"\` (default) — stop the pipeline immediately on failure
|
|
519
524
|
- \`"warn_and_continue"\` — log a warning and proceed to the next step
|
|
525
|
+
|
|
526
|
+
## Executing Pipelines
|
|
527
|
+
|
|
528
|
+
Pipelines defined here can be executed end-to-end via the \`run_pipeline\` MCP
|
|
529
|
+
tool. \`run_pipeline\` returns a unified envelope keyed on \`status\`:
|
|
530
|
+
|
|
531
|
+
- \`completed\` — the pipeline finished and \`results\` holds the per-step output.
|
|
532
|
+
- \`needs_agent_task\` — the orchestrator paused on an \`agent_task\` step (or an
|
|
533
|
+
approval-gated \`mcp_call\` when \`auto_approve\` was false). Perform the task
|
|
534
|
+
described by \`instruction\`, then call \`resume_pipeline\` with the
|
|
535
|
+
\`pipeline_run_id\` and the resulting string as \`agent_result\`.
|
|
536
|
+
- \`failed\` — terminal failure; \`error_code\` is one of \`VALIDATION\`,
|
|
537
|
+
\`NOT_FOUND\`, \`EXPIRED\`, \`REPO_MISMATCH\`, or \`TOOL_ERROR\`.
|
|
538
|
+
|
|
539
|
+
Paused runs auto-expire after an idle TTL (default 24 hours, override via
|
|
540
|
+
\`ttl_seconds\` on \`run_pipeline\`). The TTL is reset on every state transition.
|
|
541
|
+
Use \`list_pipeline_runs\` to recover a \`pipeline_run_id\` if the prior
|
|
542
|
+
\`needs_agent_task\` envelope is no longer in scope.
|
|
543
|
+
|
|
544
|
+
## \`agent_task\` Instruction Files
|
|
545
|
+
|
|
546
|
+
When you reference an instruction file (\`instruction_file: "…"\`), the file
|
|
547
|
+
markdown MUST end with a terminal \`## Return\` H2 section describing what the
|
|
548
|
+
agent should pass back as \`resume_pipeline.agent_result\`. The return value
|
|
549
|
+
is a string — do NOT ask the agent to wrap it in JSON unless the instruction
|
|
550
|
+
body explicitly says to serialize structured output.
|
|
520
551
|
`;
|
|
521
552
|
const exampleContent = JSON.stringify({
|
|
522
553
|
name: "Example Pipeline",
|
|
@@ -645,9 +676,70 @@ const server = new McpServer({
|
|
|
645
676
|
version: "1.0.0",
|
|
646
677
|
});
|
|
647
678
|
// ---------------------------------------------------------------------------
|
|
679
|
+
// Tool registration wrapper (BAPI-275)
|
|
680
|
+
// ---------------------------------------------------------------------------
|
|
681
|
+
//
|
|
682
|
+
// Every MCP tool registered through ``registerTool`` is also recorded in
|
|
683
|
+
// ``TOOL_HANDLERS`` so the new pipeline orchestrator can dispatch existing
|
|
684
|
+
// tools in-process (without round-tripping through stdio). The wrapper
|
|
685
|
+
// preserves the exact return value of ``server.registerTool`` — including
|
|
686
|
+
// its ``.enable()`` / ``.disable()`` methods — and keeps a closure-local
|
|
687
|
+
// ``active`` flag in sync with them, so disabling a tool through the SDK
|
|
688
|
+
// also blocks in-process dispatch through ``TOOL_HANDLERS``.
|
|
689
|
+
const TOOL_HANDLERS = new Map();
|
|
690
|
+
// The wrapper preserves the SDK's generic type signature so existing call
|
|
691
|
+
// sites keep their per-tool input-type inference.
|
|
692
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
693
|
+
const registerTool = ((name, config, handler) => {
|
|
694
|
+
let active = true;
|
|
695
|
+
const wrappedHandler = async (args, extra) => {
|
|
696
|
+
if (!active) {
|
|
697
|
+
return {
|
|
698
|
+
content: [
|
|
699
|
+
{
|
|
700
|
+
type: "text",
|
|
701
|
+
text: JSON.stringify({
|
|
702
|
+
error: "TOOL_DISABLED",
|
|
703
|
+
status: 503,
|
|
704
|
+
message: `Tool "${name}" is currently disabled.`,
|
|
705
|
+
}),
|
|
706
|
+
},
|
|
707
|
+
],
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
return await handler(args, extra);
|
|
711
|
+
};
|
|
712
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
713
|
+
const sdkRegister = server.registerTool.bind(server);
|
|
714
|
+
const toolHandle = sdkRegister(name, config, wrappedHandler);
|
|
715
|
+
const inProcessHandler = async (params) => {
|
|
716
|
+
return await wrappedHandler(params);
|
|
717
|
+
};
|
|
718
|
+
TOOL_HANDLERS.set(name, {
|
|
719
|
+
handler: inProcessHandler,
|
|
720
|
+
isEnabled: () => active,
|
|
721
|
+
});
|
|
722
|
+
// Wrap enable/disable to keep the closure flag in sync with SDK state.
|
|
723
|
+
if (toolHandle && typeof toolHandle.enable === "function") {
|
|
724
|
+
const sdkEnable = toolHandle.enable.bind(toolHandle);
|
|
725
|
+
toolHandle.enable = () => {
|
|
726
|
+
active = true;
|
|
727
|
+
return sdkEnable();
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
if (toolHandle && typeof toolHandle.disable === "function") {
|
|
731
|
+
const sdkDisable = toolHandle.disable.bind(toolHandle);
|
|
732
|
+
toolHandle.disable = () => {
|
|
733
|
+
active = false;
|
|
734
|
+
return sdkDisable();
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
return toolHandle;
|
|
738
|
+
});
|
|
739
|
+
// ---------------------------------------------------------------------------
|
|
648
740
|
// Tools
|
|
649
741
|
// ---------------------------------------------------------------------------
|
|
650
|
-
|
|
742
|
+
registerTool("ping", {
|
|
651
743
|
description: "Test connectivity to Bridge API. Validates that the API key is accepted and the configured repository is accessible. " +
|
|
652
744
|
"Returns JSON with {status: 'ok', repo_name: '<configured repo>'}. " +
|
|
653
745
|
"Use this as a quick health check before other operations, or to verify your Bridge API configuration is working. " +
|
|
@@ -660,7 +752,42 @@ server.registerTool("ping", {
|
|
|
660
752
|
const text = await handleResponse(resp);
|
|
661
753
|
return { content: [{ type: "text", text }] };
|
|
662
754
|
});
|
|
663
|
-
|
|
755
|
+
registerTool("second_opinion", {
|
|
756
|
+
description: "Consult a different LLM model family for a sanity check or technical pushback on a recommendation, plan, or analysis you have already produced. " +
|
|
757
|
+
"Use this when you want a second, independent opinion before acting on a non-trivial decision — for example, when committing to an implementation approach, a risky refactor, an architectural trade-off, or a recommendation you would otherwise present to a user with no further validation. " +
|
|
758
|
+
"Pick a provider from a DIFFERENT model family than the one you are running on so the response is genuinely independent (e.g. if you are an OpenAI agent, ask 'anthropic' or 'gemini'). " +
|
|
759
|
+
"Pick a model tier appropriate for the depth of pushback you want: CHEAP_MODEL for a quick sanity check, BASIC_MODEL for a focused review, PREMIUM_MODEL for serious architectural pushback. " +
|
|
760
|
+
"The 'prompt' you supply should be a complete, self-contained brief: the full plan, recommendation, or question you want challenged, along with whatever context is needed to evaluate it. The server adds a system prompt that frames the responder as an independent senior engineer and injects lightweight project context. " +
|
|
761
|
+
"Returns the responding model's reply text plus the resolved provider, tier, and model id. The reply is returned to you as-is; you decide whether to incorporate the feedback. " +
|
|
762
|
+
"Project authorization uses the standard repo access check; per-project provider gating is intentionally not enforced for this tool.",
|
|
763
|
+
inputSchema: {
|
|
764
|
+
prompt: z
|
|
765
|
+
.string()
|
|
766
|
+
.describe("The complete, self-contained brief to send to the second-opinion model. " +
|
|
767
|
+
"Include the full plan, recommendation, analysis, or question you want challenged, plus enough context for the responder to evaluate it independently. " +
|
|
768
|
+
"This is sent as the user message; the server constructs the system prompt."),
|
|
769
|
+
provider: z
|
|
770
|
+
.enum(["anthropic", "openai", "gemini"])
|
|
771
|
+
.describe("LLM provider family for the second opinion. Choose a family DIFFERENT from the one you are running on so the response is genuinely independent."),
|
|
772
|
+
model: z
|
|
773
|
+
.enum(["CHEAP_MODEL", "BASIC_MODEL", "PREMIUM_MODEL"])
|
|
774
|
+
.describe("Model tier within the chosen provider. CHEAP_MODEL for quick sanity checks, BASIC_MODEL for focused reviews, PREMIUM_MODEL for serious architectural pushback."),
|
|
775
|
+
},
|
|
776
|
+
}, async ({ prompt, provider, model }) => {
|
|
777
|
+
const resp = await fetch(buildApiUrl("/llm/second-opinion"), {
|
|
778
|
+
method: "POST",
|
|
779
|
+
headers: POST_HEADERS,
|
|
780
|
+
body: JSON.stringify({
|
|
781
|
+
repo_name: REPO_NAME,
|
|
782
|
+
prompt,
|
|
783
|
+
provider,
|
|
784
|
+
model,
|
|
785
|
+
}),
|
|
786
|
+
});
|
|
787
|
+
const text = await handleResponse(resp);
|
|
788
|
+
return { content: [{ type: "text", text }] };
|
|
789
|
+
});
|
|
790
|
+
registerTool("get_project_standards", {
|
|
664
791
|
description: "Retrieve project-specific coding standards, architecture guidelines, testing standards, code review standards, and project context (platform, version, project description) for the configured repository. " +
|
|
665
792
|
"Returns structured markdown with sections for project context, architecture instructions, code review correctness standards, testing stack information, and build analysis. " +
|
|
666
793
|
"Only sections with configured values are included. Returns 404 if no standards are configured. " +
|
|
@@ -672,7 +799,7 @@ server.registerTool("get_project_standards", {
|
|
|
672
799
|
const text = await handleResponse(resp);
|
|
673
800
|
return { content: [{ type: "text", text }] };
|
|
674
801
|
});
|
|
675
|
-
|
|
802
|
+
registerTool("get_tickets", {
|
|
676
803
|
description: "Search for and list Jira tickets from the configured project. " +
|
|
677
804
|
"Filters by query text, status name, or date. Returns up to 'limit' tickets ordered by most recently updated. " +
|
|
678
805
|
"All data is fetched live from Jira. Use get_ticket to retrieve full details for a specific ticket.",
|
|
@@ -719,7 +846,7 @@ server.registerTool("get_tickets", {
|
|
|
719
846
|
const text = await handleResponse(resp);
|
|
720
847
|
return { content: [{ type: "text", text }] };
|
|
721
848
|
});
|
|
722
|
-
|
|
849
|
+
registerTool("get_ticket", {
|
|
723
850
|
description: "Retrieve full details for a single Jira ticket by its key. " +
|
|
724
851
|
"Returns summary, status, type, assignee, reporter, description, and timestamps. " +
|
|
725
852
|
"All data is fetched live from Jira. Use get_tickets to search/list multiple tickets.",
|
|
@@ -734,7 +861,7 @@ server.registerTool("get_ticket", {
|
|
|
734
861
|
const text = await handleResponse(resp);
|
|
735
862
|
return { content: [{ type: "text", text }] };
|
|
736
863
|
});
|
|
737
|
-
|
|
864
|
+
registerTool("create_ticket", {
|
|
738
865
|
description: "Create a new Jira ticket in the configured project. Requires either description or file_path (or both — file_path takes precedence). " +
|
|
739
866
|
"Returns JSON with {ticket_key: 'PROJ-123', url: 'https://...'}. " +
|
|
740
867
|
"The ticket is created immediately in Jira — confirm details with the user before calling. " +
|
|
@@ -776,7 +903,7 @@ server.registerTool("create_ticket", {
|
|
|
776
903
|
const text = await createTicketRequest({ summary, description: resolved.text, issue_type, priority, labels, assignee });
|
|
777
904
|
return { content: [{ type: "text", text: text + resolved.note }] };
|
|
778
905
|
});
|
|
779
|
-
|
|
906
|
+
registerTool("get_plan", {
|
|
780
907
|
description: "Retrieve the AI-generated implementation plan for a Jira ticket. " +
|
|
781
908
|
"Returns the full plan as markdown text — present it verbatim without summarizing. " +
|
|
782
909
|
"The plan includes step-by-step implementation guidance with code file references. " +
|
|
@@ -804,7 +931,7 @@ server.registerTool("get_plan", {
|
|
|
804
931
|
}
|
|
805
932
|
return { content: [{ type: "text", text }] };
|
|
806
933
|
});
|
|
807
|
-
|
|
934
|
+
registerTool("get_architecture", {
|
|
808
935
|
description: "Retrieve the AI-generated architecture plan for a Jira ticket. " +
|
|
809
936
|
"Returns the full architecture plan as markdown text — present it verbatim without summarizing. " +
|
|
810
937
|
"The plan includes high-level architectural decisions, component design, and integration guidance. " +
|
|
@@ -832,7 +959,7 @@ server.registerTool("get_architecture", {
|
|
|
832
959
|
}
|
|
833
960
|
return { content: [{ type: "text", text }] };
|
|
834
961
|
});
|
|
835
|
-
|
|
962
|
+
registerTool("get_clarifying_questions", {
|
|
836
963
|
description: "Retrieve AI-generated clarifying questions (for feature/task tickets) or debugging guidance (for bug tickets) for a Jira ticket. " +
|
|
837
964
|
"Returns markdown text with questions that should be resolved before implementation begins. " +
|
|
838
965
|
"Returns 404 if no questions have been generated yet. " +
|
|
@@ -859,7 +986,7 @@ server.registerTool("get_clarifying_questions", {
|
|
|
859
986
|
}
|
|
860
987
|
return { content: [{ type: "text", text }] };
|
|
861
988
|
});
|
|
862
|
-
|
|
989
|
+
registerTool("parse_repository", {
|
|
863
990
|
description: "Queue a background job to parse and index the repository for Bridge API's AI agents. " +
|
|
864
991
|
"This should be run after major codebase changes so that plans and questions reflect the latest code. " +
|
|
865
992
|
"Returns 202 with {message: 'Repository parsing queued'} on success, " +
|
|
@@ -885,7 +1012,7 @@ server.registerTool("parse_repository", {
|
|
|
885
1012
|
const text = await handleResponse(resp);
|
|
886
1013
|
return { content: [{ type: "text", text }] };
|
|
887
1014
|
});
|
|
888
|
-
|
|
1015
|
+
registerTool("regenerate_directory_map", {
|
|
889
1016
|
description: "Regenerate the repository directory map and return the result. " +
|
|
890
1017
|
"Unlike parse_repository (which is async), this tool is synchronous — it blocks until " +
|
|
891
1018
|
"the directory map is generated and returns the full map text directly. " +
|
|
@@ -909,7 +1036,7 @@ server.registerTool("regenerate_directory_map", {
|
|
|
909
1036
|
clearTimeout(timeout);
|
|
910
1037
|
}
|
|
911
1038
|
});
|
|
912
|
-
|
|
1039
|
+
registerTool("get_parse_status", {
|
|
913
1040
|
description: "Check whether a repository parse job is currently running. " +
|
|
914
1041
|
"Returns {status: 'in_progress', started_at: '<ISO timestamp>'} if a parse is active, " +
|
|
915
1042
|
"or {status: 'idle'} if no parse is running. " +
|
|
@@ -922,7 +1049,7 @@ server.registerTool("get_parse_status", {
|
|
|
922
1049
|
const text = await handleResponse(resp);
|
|
923
1050
|
return { content: [{ type: "text", text }] };
|
|
924
1051
|
});
|
|
925
|
-
|
|
1052
|
+
registerTool("add_comment", {
|
|
926
1053
|
description: "Post a comment on a Jira ticket. The comment appears immediately in Jira. " +
|
|
927
1054
|
"Supports markdown formatting. " +
|
|
928
1055
|
"For long comments (over ~2000 characters), set attach_as_file to true — " +
|
|
@@ -976,7 +1103,7 @@ server.registerTool("add_comment", {
|
|
|
976
1103
|
const text = await handleResponse(resp);
|
|
977
1104
|
return { content: [{ type: "text", text: text + resolved.note }] };
|
|
978
1105
|
});
|
|
979
|
-
|
|
1106
|
+
registerTool("update_ticket_description", {
|
|
980
1107
|
description: "Update the description of an existing Jira ticket. This is a direct, synchronous update that overwrites the existing description with the provided text. " +
|
|
981
1108
|
"The description should be in markdown format — it will be automatically converted to Jira wiki markup. " +
|
|
982
1109
|
"This does NOT create a new ticket. Use create_ticket for that. " +
|
|
@@ -1008,7 +1135,7 @@ server.registerTool("update_ticket_description", {
|
|
|
1008
1135
|
const text = await handleResponse(resp);
|
|
1009
1136
|
return { content: [{ type: "text", text: text + resolved.note }] };
|
|
1010
1137
|
});
|
|
1011
|
-
|
|
1138
|
+
registerTool("upload_attachment", {
|
|
1012
1139
|
description: "Upload a local file as an attachment to a Jira ticket. " +
|
|
1013
1140
|
"Supports text/UTF-8 files only (markdown, plain text, etc.). " +
|
|
1014
1141
|
"Optionally syncs the content to Bridge API's tickets_links table so retrieval endpoints " +
|
|
@@ -1071,7 +1198,7 @@ server.registerTool("upload_attachment", {
|
|
|
1071
1198
|
const text = await handleResponse(resp);
|
|
1072
1199
|
return { content: [{ type: "text", text: text + resolved.note }] };
|
|
1073
1200
|
});
|
|
1074
|
-
|
|
1201
|
+
registerTool("list_attachments", {
|
|
1075
1202
|
description: "List attachments on a Jira ticket. " +
|
|
1076
1203
|
"Returns metadata (ID, filename, MIME type, size, created date) for each attachment. " +
|
|
1077
1204
|
"By default, AI-generated attachments are excluded. " +
|
|
@@ -1096,7 +1223,7 @@ server.registerTool("list_attachments", {
|
|
|
1096
1223
|
return { content: [{ type: "text", text }] };
|
|
1097
1224
|
});
|
|
1098
1225
|
const MAX_INLINE_TEXT_LENGTH = 50_000;
|
|
1099
|
-
|
|
1226
|
+
registerTool("download_attachment", {
|
|
1100
1227
|
description: "Download an attachment from a Jira ticket and save it to disk. " +
|
|
1101
1228
|
"Specify either attachment_id or filename (not both). " +
|
|
1102
1229
|
"For text files, the content is returned inline (truncated at ~50KB) and also saved to disk. " +
|
|
@@ -1184,7 +1311,7 @@ server.registerTool("download_attachment", {
|
|
|
1184
1311
|
}
|
|
1185
1312
|
return { content: [{ type: "text", text: resultText }] };
|
|
1186
1313
|
});
|
|
1187
|
-
|
|
1314
|
+
registerTool("request_plan_generation", {
|
|
1188
1315
|
description: "Request AI-generated implementation plan for a Jira ticket. " +
|
|
1189
1316
|
"This triggers an asynchronous background job — results are NOT immediate. " +
|
|
1190
1317
|
"Processing typically takes 1-5 minutes depending on ticket complexity and number of attachments. " +
|
|
@@ -1253,7 +1380,7 @@ server.registerTool("request_plan_generation", {
|
|
|
1253
1380
|
`Use get_plan with ticket_number "${ticket_number}" to retrieve the plan once processing completes.`;
|
|
1254
1381
|
return { content: [{ type: "text", text: confirmationText }] };
|
|
1255
1382
|
});
|
|
1256
|
-
|
|
1383
|
+
registerTool("request_architecture", {
|
|
1257
1384
|
description: "Request AI-generated architecture plan for a Jira ticket. " +
|
|
1258
1385
|
"This triggers an asynchronous background job — results are NOT immediate. " +
|
|
1259
1386
|
"Processing typically takes 2-4 minutes depending on ticket complexity. " +
|
|
@@ -1322,7 +1449,7 @@ server.registerTool("request_architecture", {
|
|
|
1322
1449
|
`Use get_architecture with ticket_number "${ticket_number}" to retrieve the architecture plan once processing completes.`;
|
|
1323
1450
|
return { content: [{ type: "text", text: confirmationText }] };
|
|
1324
1451
|
});
|
|
1325
|
-
|
|
1452
|
+
registerTool("request_clarifying_questions", {
|
|
1326
1453
|
description: "Request AI-generated clarifying questions or debugging guidance for a Jira ticket. " +
|
|
1327
1454
|
"This triggers an asynchronous background job — results are NOT immediate. " +
|
|
1328
1455
|
"Processing typically takes 1-5 minutes. " +
|
|
@@ -1395,7 +1522,7 @@ server.registerTool("request_clarifying_questions", {
|
|
|
1395
1522
|
// ---------------------------------------------------------------------------
|
|
1396
1523
|
// Ticket Quality Critique
|
|
1397
1524
|
// ---------------------------------------------------------------------------
|
|
1398
|
-
|
|
1525
|
+
registerTool("get_ticket_critique", {
|
|
1399
1526
|
description: "Retrieve AI-generated ticket quality critique for a Jira ticket. " +
|
|
1400
1527
|
"Returns markdown text with a structured critique covering Standards Conformance Analysis, " +
|
|
1401
1528
|
"Standards Deviations, and Suggested Improvements. " +
|
|
@@ -1423,7 +1550,7 @@ server.registerTool("get_ticket_critique", {
|
|
|
1423
1550
|
}
|
|
1424
1551
|
return { content: [{ type: "text", text }] };
|
|
1425
1552
|
});
|
|
1426
|
-
|
|
1553
|
+
registerTool("request_ticket_critique", {
|
|
1427
1554
|
description: "Request AI-generated ticket critique for a Jira ticket. " +
|
|
1428
1555
|
"This triggers an asynchronous background job — results are NOT immediate. " +
|
|
1429
1556
|
"Processing typically takes 1-5 minutes. " +
|
|
@@ -1495,7 +1622,7 @@ server.registerTool("request_ticket_critique", {
|
|
|
1495
1622
|
// ---------------------------------------------------------------------------
|
|
1496
1623
|
// Combined Ticket Review (clarify + critique)
|
|
1497
1624
|
// ---------------------------------------------------------------------------
|
|
1498
|
-
|
|
1625
|
+
registerTool("request_ticket_review", {
|
|
1499
1626
|
description: "Request a combined ticket review that generates BOTH clarifying questions (or debugging guidance for bug tickets) " +
|
|
1500
1627
|
"AND a ticket quality critique in parallel on the server, halving wall-clock latency vs. running the two " +
|
|
1501
1628
|
"requests sequentially. This triggers an asynchronous background job — results are NOT immediate. " +
|
|
@@ -1602,7 +1729,7 @@ server.registerTool("request_ticket_review", {
|
|
|
1602
1729
|
// ---------------------------------------------------------------------------
|
|
1603
1730
|
// Reimplement Context
|
|
1604
1731
|
// ---------------------------------------------------------------------------
|
|
1605
|
-
|
|
1732
|
+
registerTool("request_reimplement_context", {
|
|
1606
1733
|
description: "Request processing of new attachments and context assembly for a previously-implemented Jira ticket. " +
|
|
1607
1734
|
"Use this for follow-up requests on tickets that have already been through the plan+implement cycle. " +
|
|
1608
1735
|
"This triggers an asynchronous background job to process new attachments/images. " +
|
|
@@ -1668,7 +1795,7 @@ server.registerTool("request_reimplement_context", {
|
|
|
1668
1795
|
`Use get_reimplement_context with ticket_number "${ticket_number}" to retrieve the results once processing completes.`;
|
|
1669
1796
|
return { content: [{ type: "text", text: confirmationText }] };
|
|
1670
1797
|
});
|
|
1671
|
-
|
|
1798
|
+
registerTool("get_reimplement_context", {
|
|
1672
1799
|
description: "Retrieve the assembled reimplement context for a Jira ticket. " +
|
|
1673
1800
|
"Returns a markdown document with new/changed information diffed against stored state, " +
|
|
1674
1801
|
"the original ticket description, and the existing implementation plan. " +
|
|
@@ -1709,7 +1836,7 @@ server.registerTool("get_reimplement_context", {
|
|
|
1709
1836
|
// ---------------------------------------------------------------------------
|
|
1710
1837
|
// Ticket Lifecycle Tracking
|
|
1711
1838
|
// ---------------------------------------------------------------------------
|
|
1712
|
-
|
|
1839
|
+
registerTool("track_ticket", {
|
|
1713
1840
|
description: "Insert a ticket into Bridge API's database for lifecycle tracking. This registers the ticket so that workflow state timestamps (critique, clarify, plan, implement) can be tracked. " +
|
|
1714
1841
|
"If the ticket is already tracked, this is a safe no-op — it upserts the description and repo_name without error. " +
|
|
1715
1842
|
"Call this after creating a ticket with create_ticket to enable state tracking. " +
|
|
@@ -1730,7 +1857,7 @@ server.registerTool("track_ticket", {
|
|
|
1730
1857
|
const text = await handleResponse(resp);
|
|
1731
1858
|
return { content: [{ type: "text", text }] };
|
|
1732
1859
|
});
|
|
1733
|
-
|
|
1860
|
+
registerTool("update_ticket_state", {
|
|
1734
1861
|
description: "Update workflow state timestamps on a tracked ticket. Each specified field is set to the current UTC timestamp on the server. " +
|
|
1735
1862
|
"Valid field names: 'critique_called', 'critique_answered', 'clarify_called', 'clarify_answered', 'plan_generated', 'implemented', 'reimplement_called'. " +
|
|
1736
1863
|
"The ticket must already be tracked (via track_ticket) or a 404 error is returned. " +
|
|
@@ -1750,7 +1877,7 @@ server.registerTool("update_ticket_state", {
|
|
|
1750
1877
|
const text = await handleResponse(resp);
|
|
1751
1878
|
return { content: [{ type: "text", text }] };
|
|
1752
1879
|
});
|
|
1753
|
-
|
|
1880
|
+
registerTool("get_ticket_state", {
|
|
1754
1881
|
description: "Retrieve workflow state timestamps and artifact existence flags for a tracked ticket. " +
|
|
1755
1882
|
"Returns timestamps for each state field (critique_called, critique_answered, clarify_called, clarify_answered, plan_generated, implemented, reimplement_called) " +
|
|
1756
1883
|
"and boolean flags indicating whether artifacts exist (has_clarifying_questions, has_critique, has_plan). " +
|
|
@@ -1768,7 +1895,7 @@ server.registerTool("get_ticket_state", {
|
|
|
1768
1895
|
// ---------------------------------------------------------------------------
|
|
1769
1896
|
// Jira Transitions
|
|
1770
1897
|
// ---------------------------------------------------------------------------
|
|
1771
|
-
|
|
1898
|
+
registerTool("get_jira_transitions", {
|
|
1772
1899
|
description: "List available Jira workflow transitions for a ticket. Returns each transition's id, name, and target status. " +
|
|
1773
1900
|
"Use this to discover what status changes are possible for a given ticket. " +
|
|
1774
1901
|
"The repo_name is automatically injected from the configured environment.",
|
|
@@ -1781,7 +1908,7 @@ server.registerTool("get_jira_transitions", {
|
|
|
1781
1908
|
const text = await handleResponse(resp);
|
|
1782
1909
|
return { content: [{ type: "text", text }] };
|
|
1783
1910
|
});
|
|
1784
|
-
|
|
1911
|
+
registerTool("update_jira_status", {
|
|
1785
1912
|
description: "Transition a Jira ticket to a specified target status by executing a workflow transition. " +
|
|
1786
1913
|
"Provide either target_status (matched case-insensitively against available transitions) or transition_id (used directly). " +
|
|
1787
1914
|
"If transition_id is provided, it takes precedence over target_status. " +
|
|
@@ -1808,7 +1935,7 @@ server.registerTool("update_jira_status", {
|
|
|
1808
1935
|
const text = await handleResponse(resp);
|
|
1809
1936
|
return { content: [{ type: "text", text }] };
|
|
1810
1937
|
});
|
|
1811
|
-
|
|
1938
|
+
registerTool("resolve_target_status", {
|
|
1812
1939
|
description: "Resolve the post-PR target Jira status for the configured repository using an LLM agent. " +
|
|
1813
1940
|
"The agent selects the workflow status that best represents 'code committed via PR but not yet tested.' " +
|
|
1814
1941
|
"Results are cached per-project — subsequent calls return the cached value unless force_rerun is true. " +
|
|
@@ -1839,12 +1966,13 @@ server.registerTool("resolve_target_status", {
|
|
|
1839
1966
|
const VALID_CONFIG_FIELDS = [
|
|
1840
1967
|
"review_instructions", "documentation_instructions", "architecture_instructions",
|
|
1841
1968
|
"unit_testing_instructions", "e2e_testing_instructions",
|
|
1969
|
+
"unit_testing_stack", "e2e_testing_stack",
|
|
1842
1970
|
"frontend_correctness_standards", "backend_correctness_standards",
|
|
1843
1971
|
"template_correctness_standards", "style_correctness_standards",
|
|
1844
1972
|
"design_principles",
|
|
1845
|
-
"post_pr_target_status", "ci_check_config",
|
|
1973
|
+
"post_pr_target_status", "ci_check_config", "ci_followup_config",
|
|
1846
1974
|
].join(", ");
|
|
1847
|
-
|
|
1975
|
+
registerTool("list_config_fields", {
|
|
1848
1976
|
description: "List all configurable fields available for reading and updating via the Bridge API. " +
|
|
1849
1977
|
"Returns each field's name and a description of its purpose. No database values are returned — " +
|
|
1850
1978
|
"use get_config_field to read a specific field's current value. " +
|
|
@@ -1856,7 +1984,7 @@ server.registerTool("list_config_fields", {
|
|
|
1856
1984
|
const text = await handleResponse(resp);
|
|
1857
1985
|
return { content: [{ type: "text", text }] };
|
|
1858
1986
|
});
|
|
1859
|
-
|
|
1987
|
+
registerTool("get_my_role", {
|
|
1860
1988
|
description: "Check the role and auth source for the current API key. " +
|
|
1861
1989
|
"Returns JSON with {role: \"admin\" | \"member\" | null, source: \"user_access\" | \"legacy\"}. " +
|
|
1862
1990
|
"Use this to check if the current key has admin permissions before attempting configuration updates " +
|
|
@@ -1868,7 +1996,7 @@ server.registerTool("get_my_role", {
|
|
|
1868
1996
|
const text = await handleResponse(resp);
|
|
1869
1997
|
return { content: [{ type: "text", text }] };
|
|
1870
1998
|
});
|
|
1871
|
-
|
|
1999
|
+
registerTool("get_config_field", {
|
|
1872
2000
|
description: "Read the current value and metadata for a specific configuration field. " +
|
|
1873
2001
|
"Returns the field's current database value (or null if not set), along with a description of its purpose and examples of helpful content. " +
|
|
1874
2002
|
"Use this before update_config_field to understand the current state and build upon it rather than overwriting blindly.",
|
|
@@ -1881,7 +2009,7 @@ server.registerTool("get_config_field", {
|
|
|
1881
2009
|
const text = await handleResponse(resp);
|
|
1882
2010
|
return { content: [{ type: "text", text }] };
|
|
1883
2011
|
});
|
|
1884
|
-
|
|
2012
|
+
registerTool("update_config_field", {
|
|
1885
2013
|
description: "Update a specific configuration field in the Bridge database. " +
|
|
1886
2014
|
"These fields control LLM behavior during code review, planning, and documentation. " +
|
|
1887
2015
|
"Always call get_config_field first to read the current value and build upon it. " +
|
|
@@ -1913,10 +2041,63 @@ server.registerTool("update_config_field", {
|
|
|
1913
2041
|
const text = await handleResponse(resp);
|
|
1914
2042
|
return { content: [{ type: "text", text: text + note }] };
|
|
1915
2043
|
});
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
2044
|
+
function formatDeepResearchProviderReason(meta) {
|
|
2045
|
+
if (!meta)
|
|
2046
|
+
return "";
|
|
2047
|
+
const parts = [];
|
|
2048
|
+
const reason = meta.incomplete_details?.reason;
|
|
2049
|
+
if (reason) {
|
|
2050
|
+
parts.push(`provider reason: ${reason}`);
|
|
2051
|
+
}
|
|
2052
|
+
const errMsg = meta.error?.message;
|
|
2053
|
+
const errCode = meta.error?.code;
|
|
2054
|
+
if (errMsg || errCode) {
|
|
2055
|
+
if (errCode && errMsg) {
|
|
2056
|
+
parts.push(`provider error: ${errCode}: ${errMsg}`);
|
|
2057
|
+
}
|
|
2058
|
+
else if (errMsg) {
|
|
2059
|
+
parts.push(`provider error: ${errMsg}`);
|
|
2060
|
+
}
|
|
2061
|
+
else if (errCode) {
|
|
2062
|
+
parts.push(`provider error: ${errCode}`);
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
return parts.length ? ` (${parts.join("; ")})` : "";
|
|
2066
|
+
}
|
|
2067
|
+
function _safeIsoMs(value) {
|
|
2068
|
+
if (!value)
|
|
2069
|
+
return null;
|
|
2070
|
+
const ms = new Date(value).getTime();
|
|
2071
|
+
if (Number.isNaN(ms))
|
|
2072
|
+
return null;
|
|
2073
|
+
return ms;
|
|
2074
|
+
}
|
|
2075
|
+
function formatDeepResearchElapsed(createdAt, lastPollAt) {
|
|
2076
|
+
const createdMs = _safeIsoMs(createdAt);
|
|
2077
|
+
if (createdMs === null)
|
|
2078
|
+
return "";
|
|
2079
|
+
const now = Date.now();
|
|
2080
|
+
const startedMs = Math.max(0, now - createdMs);
|
|
2081
|
+
const startedMin = Math.floor(startedMs / 60_000);
|
|
2082
|
+
const lastPollMs = _safeIsoMs(lastPollAt);
|
|
2083
|
+
let pollSuffix = "";
|
|
2084
|
+
if (lastPollMs !== null) {
|
|
2085
|
+
const ageSec = Math.max(0, Math.floor((now - lastPollMs) / 1000));
|
|
2086
|
+
pollSuffix = `, last poll ${ageSec}s ago`;
|
|
2087
|
+
}
|
|
2088
|
+
return ` (running ${startedMin}m${pollSuffix})`;
|
|
2089
|
+
}
|
|
2090
|
+
function formatDeepResearchFailure(body) {
|
|
2091
|
+
const kind = body.error_kind || body.error_message || "Unknown error";
|
|
2092
|
+
const reason = formatDeepResearchProviderReason(body.provider_status_meta);
|
|
2093
|
+
return `Deep research failed: ${kind}${reason}. Consider using standard web searches to gather the information incrementally.`;
|
|
2094
|
+
}
|
|
2095
|
+
function formatDeepResearchStatus(body, taskId) {
|
|
2096
|
+
const elapsed = formatDeepResearchElapsed(body.created_at, body.last_poll_at);
|
|
2097
|
+
const reason = formatDeepResearchProviderReason(body.provider_status_meta);
|
|
2098
|
+
return `Status: ${body.status}${elapsed}${reason} (task_id: ${taskId}). Try again in a minute.`;
|
|
2099
|
+
}
|
|
2100
|
+
registerTool("request_deep_research", {
|
|
1920
2101
|
description: "Submit a deep research request on a technical topic using AI-powered web search. " +
|
|
1921
2102
|
"Returns a task_id for tracking the research progress. " +
|
|
1922
2103
|
"\n\n" +
|
|
@@ -1980,6 +2161,7 @@ server.registerTool("request_deep_research", {
|
|
|
1980
2161
|
const MAX_TIMEOUT_MS = 15 * 60 * 1000; // 15 minutes
|
|
1981
2162
|
let pollIntervalMs = 15_000; // start at 15 seconds
|
|
1982
2163
|
let lastStatus = "queued";
|
|
2164
|
+
let latestStatusBody = null;
|
|
1983
2165
|
while (Date.now() - startTime < MAX_TIMEOUT_MS) {
|
|
1984
2166
|
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
1985
2167
|
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
@@ -1992,15 +2174,15 @@ server.registerTool("request_deep_research", {
|
|
|
1992
2174
|
}
|
|
1993
2175
|
const statusBody = (await statusResp.json());
|
|
1994
2176
|
lastStatus = statusBody.status;
|
|
2177
|
+
latestStatusBody = statusBody;
|
|
1995
2178
|
if (lastStatus === "completed") {
|
|
1996
2179
|
break;
|
|
1997
2180
|
}
|
|
1998
2181
|
if (lastStatus === "failed") {
|
|
1999
|
-
const errorMsg = statusBody.error_message || "Unknown error";
|
|
2000
2182
|
return {
|
|
2001
2183
|
content: [{
|
|
2002
2184
|
type: "text",
|
|
2003
|
-
text:
|
|
2185
|
+
text: formatDeepResearchFailure(statusBody),
|
|
2004
2186
|
}],
|
|
2005
2187
|
};
|
|
2006
2188
|
}
|
|
@@ -2010,10 +2192,13 @@ server.registerTool("request_deep_research", {
|
|
|
2010
2192
|
}
|
|
2011
2193
|
}
|
|
2012
2194
|
if (lastStatus !== "completed") {
|
|
2195
|
+
const statusSuffix = latestStatusBody
|
|
2196
|
+
? ` ${formatDeepResearchStatus(latestStatusBody, taskId)}`
|
|
2197
|
+
: "";
|
|
2013
2198
|
return {
|
|
2014
2199
|
content: [{
|
|
2015
2200
|
type: "text",
|
|
2016
|
-
text:
|
|
2201
|
+
text: `Deep research timed out after 15 minutes (task_id: ${taskId}).${statusSuffix} The task may still be processing on the server. Use get_deep_research with this task_id to check later, or use standard web searches to gather the information incrementally.`,
|
|
2017
2202
|
}],
|
|
2018
2203
|
};
|
|
2019
2204
|
}
|
|
@@ -2033,7 +2218,7 @@ server.registerTool("request_deep_research", {
|
|
|
2033
2218
|
}
|
|
2034
2219
|
return { content: [{ type: "text", text: resultText }] };
|
|
2035
2220
|
});
|
|
2036
|
-
|
|
2221
|
+
registerTool("get_deep_research", {
|
|
2037
2222
|
description: "Retrieve the result of a previously submitted deep research request. " +
|
|
2038
2223
|
"Returns the full markdown research report if the task is completed, " +
|
|
2039
2224
|
"or a structured status response if still processing or failed. " +
|
|
@@ -2054,11 +2239,10 @@ server.registerTool("get_deep_research", {
|
|
|
2054
2239
|
}
|
|
2055
2240
|
const statusBody = (await statusResp.json());
|
|
2056
2241
|
if (statusBody.status === "failed") {
|
|
2057
|
-
const errorMsg = statusBody.error_message || "Unknown error";
|
|
2058
2242
|
return {
|
|
2059
2243
|
content: [{
|
|
2060
2244
|
type: "text",
|
|
2061
|
-
text:
|
|
2245
|
+
text: formatDeepResearchFailure(statusBody),
|
|
2062
2246
|
}],
|
|
2063
2247
|
};
|
|
2064
2248
|
}
|
|
@@ -2066,7 +2250,7 @@ server.registerTool("get_deep_research", {
|
|
|
2066
2250
|
return {
|
|
2067
2251
|
content: [{
|
|
2068
2252
|
type: "text",
|
|
2069
|
-
text:
|
|
2253
|
+
text: formatDeepResearchStatus(statusBody, task_id),
|
|
2070
2254
|
}],
|
|
2071
2255
|
};
|
|
2072
2256
|
}
|
|
@@ -2086,10 +2270,219 @@ server.registerTool("get_deep_research", {
|
|
|
2086
2270
|
}
|
|
2087
2271
|
return { content: [{ type: "text", text: resultText }] };
|
|
2088
2272
|
});
|
|
2273
|
+
const BRAINSTORM_TERMINAL_STATUSES = new Set([
|
|
2274
|
+
"completed",
|
|
2275
|
+
"failed",
|
|
2276
|
+
"skipped",
|
|
2277
|
+
]);
|
|
2278
|
+
function isBrainstormTerminalStatus(status) {
|
|
2279
|
+
return BRAINSTORM_TERMINAL_STATUSES.has(status);
|
|
2280
|
+
}
|
|
2281
|
+
function sanitizeProviderForFilename(provider) {
|
|
2282
|
+
// Defensive: allow letters/digits/hyphen/underscore only; collapse runs and
|
|
2283
|
+
// trim trailing punctuation. Falls back to "provider" for an empty result.
|
|
2284
|
+
const cleaned = provider
|
|
2285
|
+
.toLowerCase()
|
|
2286
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
2287
|
+
.replace(/-+/g, "-")
|
|
2288
|
+
.replace(/^-+|-+$/g, "");
|
|
2289
|
+
return cleaned || "provider";
|
|
2290
|
+
}
|
|
2291
|
+
async function pollBrainstormUntilTerminal(brainstormId, repoName) {
|
|
2292
|
+
const startTime = Date.now();
|
|
2293
|
+
const MAX_TIMEOUT_MS = 15 * 60 * 1000;
|
|
2294
|
+
let pollIntervalMs = 15_000;
|
|
2295
|
+
let latest = null;
|
|
2296
|
+
while (Date.now() - startTime < MAX_TIMEOUT_MS) {
|
|
2297
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
2298
|
+
const statusUrl = buildGetUrl(`/brainstorms/${brainstormId}/status`, { repo_name: repoName });
|
|
2299
|
+
const statusResp = await fetch(statusUrl, { headers: GET_HEADERS });
|
|
2300
|
+
if (!statusResp.ok) {
|
|
2301
|
+
return latest;
|
|
2302
|
+
}
|
|
2303
|
+
latest = (await statusResp.json());
|
|
2304
|
+
const allTerminal = latest.rows.every((row) => isBrainstormTerminalStatus(row.status));
|
|
2305
|
+
if (allTerminal) {
|
|
2306
|
+
return latest;
|
|
2307
|
+
}
|
|
2308
|
+
if (Date.now() - startTime > 60_000) {
|
|
2309
|
+
pollIntervalMs = 30_000;
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
return latest;
|
|
2313
|
+
}
|
|
2314
|
+
async function saveBrainstormResultsLocally(envelope) {
|
|
2315
|
+
const dir = getDocsPath("brainstorm");
|
|
2316
|
+
const savedPaths = [];
|
|
2317
|
+
for (const row of envelope.results) {
|
|
2318
|
+
const markdown = row.markdown;
|
|
2319
|
+
if (!markdown) {
|
|
2320
|
+
continue;
|
|
2321
|
+
}
|
|
2322
|
+
const providerSegment = sanitizeProviderForFilename(row.provider);
|
|
2323
|
+
const filename = `${envelope.brainstorm_id}-${providerSegment}.md`;
|
|
2324
|
+
const filePath = path.join(dir, filename);
|
|
2325
|
+
try {
|
|
2326
|
+
await mkdir(dir, { recursive: true });
|
|
2327
|
+
await writeFile(filePath, markdown, "utf-8");
|
|
2328
|
+
savedPaths.push(filePath);
|
|
2329
|
+
}
|
|
2330
|
+
catch {
|
|
2331
|
+
// Skip rows that fail to write — never block the response.
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
return savedPaths;
|
|
2335
|
+
}
|
|
2336
|
+
function formatBrainstormToolResponse(envelope, savedPaths) {
|
|
2337
|
+
const lines = [];
|
|
2338
|
+
lines.push(`# Brainstorm ${envelope.brainstorm_id}`);
|
|
2339
|
+
lines.push(`Repo: ${envelope.repo_name}`);
|
|
2340
|
+
lines.push("");
|
|
2341
|
+
for (const row of envelope.results) {
|
|
2342
|
+
lines.push(`## ${row.provider} — status: ${row.status}`);
|
|
2343
|
+
lines.push(`error_kind: ${row.error_kind ?? "null"}`);
|
|
2344
|
+
if (row.error_message) {
|
|
2345
|
+
lines.push(`error_message: ${row.error_message}`);
|
|
2346
|
+
}
|
|
2347
|
+
if (row.markdown) {
|
|
2348
|
+
lines.push("");
|
|
2349
|
+
lines.push(row.markdown);
|
|
2350
|
+
}
|
|
2351
|
+
lines.push("");
|
|
2352
|
+
}
|
|
2353
|
+
if (savedPaths.length > 0) {
|
|
2354
|
+
lines.push("---");
|
|
2355
|
+
lines.push("Saved files:");
|
|
2356
|
+
for (const p of savedPaths) {
|
|
2357
|
+
lines.push(`- ${p}`);
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
return lines.join("\n");
|
|
2361
|
+
}
|
|
2362
|
+
registerTool("request_brainstorm", {
|
|
2363
|
+
description: "Submit a brainstorm request that fans out the task to two opinion-provider LLMs " +
|
|
2364
|
+
"(default: OpenAI + Gemini) and then runs a synthesizer pass over the completed opinions. " +
|
|
2365
|
+
"Returns a brainstorm_id you can use with get_brainstorm. " +
|
|
2366
|
+
"\n\n" +
|
|
2367
|
+
"BEHAVIOR: By default, returns immediately with a brainstorm_id. Set wait_for_result=true " +
|
|
2368
|
+
"to poll for terminal status (up to 15 minutes), then retrieve and optionally save the result. " +
|
|
2369
|
+
"When save_locally=true (default), each provider's markdown is written to " +
|
|
2370
|
+
"BAPI_DOCS_DIR/brainstorm/{brainstorm_id}-{provider}.md, including {brainstorm_id}-synthesizer.md.",
|
|
2371
|
+
inputSchema: {
|
|
2372
|
+
task_description: z.string().describe("Free-form description of the task to brainstorm about. Sent verbatim — " +
|
|
2373
|
+
"this tool does NOT read task_description from a file."),
|
|
2374
|
+
repo_name: z.string().optional().describe("Repository name. Defaults to BAPI_REPO_NAME from the environment."),
|
|
2375
|
+
ticket_number: z.string().optional().describe("Optional Jira ticket key (e.g. PROJ-123) to associate with the brainstorm. " +
|
|
2376
|
+
"Ticket 1 only stores this for cross-reference — no Jira writes happen."),
|
|
2377
|
+
providers: z.array(z.string()).optional().describe("Opinion-provider LLMs. Defaults to ['openai', 'gemini']. " +
|
|
2378
|
+
"A single-provider request still inserts a synthesizer row but pre-skips it."),
|
|
2379
|
+
concerns: z.string().optional().describe("Optional caller-supplied concerns to surface to the brainstorm agents."),
|
|
2380
|
+
wait_for_result: z.boolean().optional().describe("When true, polls until every row reaches a terminal status (max 15 minutes), " +
|
|
2381
|
+
"then returns the full result envelope. When false (default), returns immediately."),
|
|
2382
|
+
save_locally: z.boolean().optional().describe("When true (default), writes each provider's markdown to BAPI_DOCS_DIR/brainstorm/ " +
|
|
2383
|
+
"after the result is fetched."),
|
|
2384
|
+
prior_brainstorm_id: z.string().optional().describe("Optional brainstorm_id from an earlier brainstorm to refine. " +
|
|
2385
|
+
"When provided, the new brainstorm receives the prior brainstorm's " +
|
|
2386
|
+
"synthesizer markdown, or a completed opinion-provider fallback, as prior context."),
|
|
2387
|
+
},
|
|
2388
|
+
}, async ({ task_description, repo_name, ticket_number, providers, concerns, wait_for_result, save_locally, prior_brainstorm_id, }) => {
|
|
2389
|
+
const effectiveRepo = repo_name && repo_name.length > 0 ? repo_name : REPO_NAME;
|
|
2390
|
+
const effectiveProviders = providers !== undefined ? providers : ["openai", "gemini"];
|
|
2391
|
+
const shouldWait = wait_for_result === true;
|
|
2392
|
+
const shouldSave = save_locally !== false;
|
|
2393
|
+
const submitPayload = {
|
|
2394
|
+
repo_name: effectiveRepo,
|
|
2395
|
+
task_description,
|
|
2396
|
+
providers: effectiveProviders,
|
|
2397
|
+
};
|
|
2398
|
+
if (ticket_number)
|
|
2399
|
+
submitPayload.ticket_number = ticket_number;
|
|
2400
|
+
if (concerns)
|
|
2401
|
+
submitPayload.concerns = concerns;
|
|
2402
|
+
if (prior_brainstorm_id) {
|
|
2403
|
+
submitPayload.prior_brainstorm_request_id = prior_brainstorm_id;
|
|
2404
|
+
}
|
|
2405
|
+
const submitResp = await fetch(buildUrl("/brainstorms"), {
|
|
2406
|
+
method: "POST",
|
|
2407
|
+
headers: POST_HEADERS,
|
|
2408
|
+
body: JSON.stringify(submitPayload),
|
|
2409
|
+
});
|
|
2410
|
+
if (!submitResp.ok) {
|
|
2411
|
+
const errorText = await handleResponse(submitResp);
|
|
2412
|
+
return { content: [{ type: "text", text: errorText }] };
|
|
2413
|
+
}
|
|
2414
|
+
const submitBody = (await submitResp.json());
|
|
2415
|
+
if (!shouldWait) {
|
|
2416
|
+
const confirmation = `Brainstorm submitted (brainstorm_id: ${submitBody.brainstorm_id}). ` +
|
|
2417
|
+
`Providers: ${submitBody.providers.join(", ")}. ` +
|
|
2418
|
+
`Synthesizer status: ${submitBody.synthesizer_status}. ` +
|
|
2419
|
+
`Use get_brainstorm with brainstorm_id ${submitBody.brainstorm_id} to retrieve results.`;
|
|
2420
|
+
return { content: [{ type: "text", text: confirmation }] };
|
|
2421
|
+
}
|
|
2422
|
+
const finalStatus = await pollBrainstormUntilTerminal(submitBody.brainstorm_id, effectiveRepo);
|
|
2423
|
+
if (!finalStatus) {
|
|
2424
|
+
return {
|
|
2425
|
+
content: [{
|
|
2426
|
+
type: "text",
|
|
2427
|
+
text: `Brainstorm timed out before reaching terminal status ` +
|
|
2428
|
+
`(brainstorm_id: ${submitBody.brainstorm_id}). Use get_brainstorm later.`,
|
|
2429
|
+
}],
|
|
2430
|
+
};
|
|
2431
|
+
}
|
|
2432
|
+
const resultUrl = buildGetUrl(`/brainstorms/${submitBody.brainstorm_id}/result`, { repo_name: effectiveRepo });
|
|
2433
|
+
const resultResp = await fetch(resultUrl, { headers: GET_HEADERS });
|
|
2434
|
+
if (!resultResp.ok) {
|
|
2435
|
+
const errorText = await handleResponse(resultResp);
|
|
2436
|
+
return { content: [{ type: "text", text: errorText }] };
|
|
2437
|
+
}
|
|
2438
|
+
const envelope = (await resultResp.json());
|
|
2439
|
+
let savedPaths = [];
|
|
2440
|
+
if (shouldSave) {
|
|
2441
|
+
savedPaths = await saveBrainstormResultsLocally(envelope);
|
|
2442
|
+
}
|
|
2443
|
+
return {
|
|
2444
|
+
content: [{
|
|
2445
|
+
type: "text",
|
|
2446
|
+
text: formatBrainstormToolResponse(envelope, savedPaths),
|
|
2447
|
+
}],
|
|
2448
|
+
};
|
|
2449
|
+
});
|
|
2450
|
+
registerTool("get_brainstorm", {
|
|
2451
|
+
description: "Retrieve the result envelope for a previously submitted brainstorm by brainstorm_id. " +
|
|
2452
|
+
"Returns all rows (opinion providers + synthesizer), including error_kind for every row. " +
|
|
2453
|
+
"When save_locally=true (default), writes each provider's markdown to " +
|
|
2454
|
+
"BAPI_DOCS_DIR/brainstorm/{brainstorm_id}-{provider}.md (including the synthesizer file). " +
|
|
2455
|
+
"DB-only retrieval — never falls back to Jira attachments.",
|
|
2456
|
+
inputSchema: {
|
|
2457
|
+
brainstorm_id: z.string().describe("The brainstorm_id (UUID) returned by request_brainstorm."),
|
|
2458
|
+
repo_name: z.string().optional().describe("Repository name. Defaults to BAPI_REPO_NAME from the environment."),
|
|
2459
|
+
save_locally: z.boolean().optional().describe("When true (default), writes each provider's markdown to BAPI_DOCS_DIR/brainstorm/."),
|
|
2460
|
+
},
|
|
2461
|
+
}, async ({ brainstorm_id, repo_name, save_locally }) => {
|
|
2462
|
+
const effectiveRepo = repo_name && repo_name.length > 0 ? repo_name : REPO_NAME;
|
|
2463
|
+
const shouldSave = save_locally !== false;
|
|
2464
|
+
const resultUrl = buildGetUrl(`/brainstorms/${brainstorm_id}/result`, { repo_name: effectiveRepo });
|
|
2465
|
+
const resultResp = await fetch(resultUrl, { headers: GET_HEADERS });
|
|
2466
|
+
if (!resultResp.ok) {
|
|
2467
|
+
const errorText = await handleResponse(resultResp);
|
|
2468
|
+
return { content: [{ type: "text", text: errorText }] };
|
|
2469
|
+
}
|
|
2470
|
+
const envelope = (await resultResp.json());
|
|
2471
|
+
let savedPaths = [];
|
|
2472
|
+
if (shouldSave) {
|
|
2473
|
+
savedPaths = await saveBrainstormResultsLocally(envelope);
|
|
2474
|
+
}
|
|
2475
|
+
return {
|
|
2476
|
+
content: [{
|
|
2477
|
+
type: "text",
|
|
2478
|
+
text: formatBrainstormToolResponse(envelope, savedPaths),
|
|
2479
|
+
}],
|
|
2480
|
+
};
|
|
2481
|
+
});
|
|
2089
2482
|
// ---------------------------------------------------------------------------
|
|
2090
2483
|
// VCS & CI Tools
|
|
2091
2484
|
// ---------------------------------------------------------------------------
|
|
2092
|
-
|
|
2485
|
+
registerTool("create_pull_request", {
|
|
2093
2486
|
description: "Create a pull request on the configured VCS provider (GitHub or Bitbucket). " +
|
|
2094
2487
|
"Returns a structured response with {available, reason, action, detail}. " +
|
|
2095
2488
|
"If a PR already exists for the head branch, returns it with created=false. " +
|
|
@@ -2118,7 +2511,7 @@ server.registerTool("create_pull_request", {
|
|
|
2118
2511
|
const text = await handleResponse(resp);
|
|
2119
2512
|
return { content: [{ type: "text", text }] };
|
|
2120
2513
|
});
|
|
2121
|
-
const resolveCiChecksTool =
|
|
2514
|
+
const resolveCiChecksTool = registerTool("resolve_ci_checks", {
|
|
2122
2515
|
description: "Discover and classify CI checks for the configured repository. " +
|
|
2123
2516
|
"Queries GitHub Check Runs + Commit Statuses APIs (or Bitbucket Build Statuses), " +
|
|
2124
2517
|
"then uses Branch Protection API or LLM to determine which checks are required for merging. " +
|
|
@@ -2154,7 +2547,7 @@ const resolveCiChecksTool = server.registerTool("resolve_ci_checks", {
|
|
|
2154
2547
|
}
|
|
2155
2548
|
return { content: [{ type: "text", text }] };
|
|
2156
2549
|
});
|
|
2157
|
-
const pollCiChecksTool =
|
|
2550
|
+
const pollCiChecksTool = registerTool("poll_ci_checks", {
|
|
2158
2551
|
description: "Poll the current status of CI checks for a specific commit. " +
|
|
2159
2552
|
"Requires that resolve_ci_checks has been called first to populate the check configuration. " +
|
|
2160
2553
|
"Returns per-check status, all_complete, all_passed, and unknown_checks fields. " +
|
|
@@ -2203,9 +2596,30 @@ async function checkCiConfigAndDisablePoll() {
|
|
|
2203
2596
|
// Check config before connecting
|
|
2204
2597
|
await checkCiConfigAndDisablePoll();
|
|
2205
2598
|
// ---------------------------------------------------------------------------
|
|
2599
|
+
// Custom pipeline loading (BAPI-275)
|
|
2600
|
+
// ---------------------------------------------------------------------------
|
|
2601
|
+
//
|
|
2602
|
+
// Custom user pipelines must be merged into PIPELINES + INSTRUCTIONS BEFORE
|
|
2603
|
+
// the run_pipeline tool is registered so its embedded catalog includes both
|
|
2604
|
+
// bundled and user entries. The merge result is also used by list_pipelines
|
|
2605
|
+
// and get_pipeline_recipe below — every caller must see the same final
|
|
2606
|
+
// objects.
|
|
2607
|
+
{
|
|
2608
|
+
const instructionsDir = path.join(path.dirname(BAPI_PIPELINES_DIR), "instructions");
|
|
2609
|
+
const customResult = await loadCustomPipelines(BAPI_PIPELINES_DIR, instructionsDir, BUNDLED_INSTRUCTIONS);
|
|
2610
|
+
for (const [key, pipeline] of Object.entries(customResult.pipelines)) {
|
|
2611
|
+
if (key in BUNDLED_PIPELINES) {
|
|
2612
|
+
console.error(`Warning: user pipeline "${key}" overrides bundled pipeline.`);
|
|
2613
|
+
}
|
|
2614
|
+
PIPELINES[key] = pipeline;
|
|
2615
|
+
}
|
|
2616
|
+
Object.assign(INSTRUCTIONS, customResult.instructions);
|
|
2617
|
+
userPipelineKeys = customResult.userPipelineKeys;
|
|
2618
|
+
}
|
|
2619
|
+
// ---------------------------------------------------------------------------
|
|
2206
2620
|
// Pipeline Recipe Tools
|
|
2207
2621
|
// ---------------------------------------------------------------------------
|
|
2208
|
-
|
|
2622
|
+
registerTool("get_docs_dir", {
|
|
2209
2623
|
description: "Return the locally configured docs directory path (BAPI_DOCS_DIR, default docs/tmp). " +
|
|
2210
2624
|
"No parameters. Use this instead of reading the BAPI_DOCS_DIR environment variable directly, " +
|
|
2211
2625
|
"which requires shell access and may be blocked on some AI coding platforms.",
|
|
@@ -2213,7 +2627,7 @@ server.registerTool("get_docs_dir", {
|
|
|
2213
2627
|
}, async () => {
|
|
2214
2628
|
return { content: [{ type: "text", text: BAPI_DOCS_DIR }] };
|
|
2215
2629
|
});
|
|
2216
|
-
|
|
2630
|
+
registerTool("list_pipelines", {
|
|
2217
2631
|
description: "List all available pipeline recipes with their names, descriptions, and required variables. " +
|
|
2218
2632
|
"No parameters. Use this to discover available pipelines before calling get_pipeline_recipe.",
|
|
2219
2633
|
inputSchema: {},
|
|
@@ -2228,7 +2642,7 @@ server.registerTool("list_pipelines", {
|
|
|
2228
2642
|
content: [{ type: "text", text: JSON.stringify(list, null, 2) }],
|
|
2229
2643
|
};
|
|
2230
2644
|
});
|
|
2231
|
-
|
|
2645
|
+
registerTool("get_pipeline_recipe", {
|
|
2232
2646
|
description: "Retrieve a fully resolved pipeline recipe by name. Substitutes variables, resolves instruction " +
|
|
2233
2647
|
"file references to inline content, and returns an ordered array of executable steps. " +
|
|
2234
2648
|
"Each step is either an mcp_call (with tool name and params) or an agent_task (with instruction text). " +
|
|
@@ -2246,8 +2660,16 @@ server.registerTool("get_pipeline_recipe", {
|
|
|
2246
2660
|
.array(z.string())
|
|
2247
2661
|
.optional()
|
|
2248
2662
|
.describe("Step tool names or descriptions to omit from the recipe"),
|
|
2663
|
+
auto_approve: z
|
|
2664
|
+
.boolean()
|
|
2665
|
+
.optional()
|
|
2666
|
+
.describe("When true, automatically approve all approval-gated steps. " +
|
|
2667
|
+
"For implement-ticket this skips the commit/push approval pause; " +
|
|
2668
|
+
"for review-ticket this skips the HTML decision page and selects " +
|
|
2669
|
+
"each item's recommended option. Pass via this top-level parameter, " +
|
|
2670
|
+
"not via the variables map."),
|
|
2249
2671
|
},
|
|
2250
|
-
}, async ({ pipeline: pipelineName, variables, skip_steps }) => {
|
|
2672
|
+
}, async ({ pipeline: pipelineName, variables, skip_steps, auto_approve }) => {
|
|
2251
2673
|
const pipelineDef = PIPELINES[pipelineName];
|
|
2252
2674
|
if (!pipelineDef) {
|
|
2253
2675
|
const available = Object.keys(PIPELINES).join(", ");
|
|
@@ -2262,9 +2684,27 @@ server.registerTool("get_pipeline_recipe", {
|
|
|
2262
2684
|
}],
|
|
2263
2685
|
};
|
|
2264
2686
|
}
|
|
2687
|
+
if (variables && "auto_approve" in variables) {
|
|
2688
|
+
return {
|
|
2689
|
+
content: [{
|
|
2690
|
+
type: "text",
|
|
2691
|
+
text: JSON.stringify({
|
|
2692
|
+
error: ERROR_CODES[400] ?? "BAD_REQUEST",
|
|
2693
|
+
status: 400,
|
|
2694
|
+
message: "Pass auto_approve via the top-level parameter, not via the variables map.",
|
|
2695
|
+
}),
|
|
2696
|
+
}],
|
|
2697
|
+
};
|
|
2698
|
+
}
|
|
2265
2699
|
try {
|
|
2266
|
-
const mergedVariables = {
|
|
2267
|
-
|
|
2700
|
+
const mergedVariables = {
|
|
2701
|
+
docs_dir: BAPI_DOCS_DIR,
|
|
2702
|
+
provider: "",
|
|
2703
|
+
second_opinion: "",
|
|
2704
|
+
auto_approve: auto_approve ? "true" : "",
|
|
2705
|
+
...(variables ?? {}),
|
|
2706
|
+
};
|
|
2707
|
+
const recipe = resolveRecipe(pipelineDef, INSTRUCTIONS, mergedVariables, skip_steps, !!auto_approve);
|
|
2268
2708
|
return {
|
|
2269
2709
|
content: [{
|
|
2270
2710
|
type: "text",
|
|
@@ -2286,40 +2726,158 @@ server.registerTool("get_pipeline_recipe", {
|
|
|
2286
2726
|
}
|
|
2287
2727
|
});
|
|
2288
2728
|
// ---------------------------------------------------------------------------
|
|
2289
|
-
//
|
|
2729
|
+
// Pipeline Execution Tools (BAPI-275)
|
|
2290
2730
|
// ---------------------------------------------------------------------------
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2731
|
+
//
|
|
2732
|
+
// run_pipeline / resume_pipeline / list_pipeline_runs promote pipelines from
|
|
2733
|
+
// declarative JSON recipes into first-class callable operations. They share
|
|
2734
|
+
// a unified response envelope (status: completed | needs_agent_task | failed)
|
|
2735
|
+
// with a strict error_code enum (VALIDATION | NOT_FOUND | EXPIRED |
|
|
2736
|
+
// REPO_MISMATCH | TOOL_ERROR). Pipeline state is persisted server-side; the
|
|
2737
|
+
// idle TTL defaults to 24 hours and is auto-extended on every state
|
|
2738
|
+
// transition.
|
|
2739
|
+
function buildPipelineCatalogDescription() {
|
|
2740
|
+
const lines = [];
|
|
2741
|
+
for (const [key, pipeline] of Object.entries(PIPELINES)) {
|
|
2742
|
+
const source = userPipelineKeys.has(key) ? " (user)" : "";
|
|
2743
|
+
const desc = pipeline.description ?? "";
|
|
2744
|
+
lines.push(`- ${key}${source} — ${desc}`);
|
|
2745
|
+
}
|
|
2746
|
+
return lines.join("\n");
|
|
2747
|
+
}
|
|
2748
|
+
function buildPipelineOrchestratorDeps() {
|
|
2749
|
+
return {
|
|
2750
|
+
baseUrl: BASE_URL,
|
|
2751
|
+
apiKey: API_KEY,
|
|
2752
|
+
repoName: REPO_NAME,
|
|
2753
|
+
docsDir: BAPI_DOCS_DIR,
|
|
2754
|
+
pipelines: PIPELINES,
|
|
2755
|
+
instructions: INSTRUCTIONS,
|
|
2756
|
+
toolHandlers: TOOL_HANDLERS,
|
|
2757
|
+
};
|
|
2758
|
+
}
|
|
2759
|
+
registerTool("run_pipeline", {
|
|
2760
|
+
description: "Execute a Bridge API pipeline by name. The orchestrator runs steps sequentially, " +
|
|
2761
|
+
"dispatching mcp_call steps in-process and pausing on agent_task steps with a " +
|
|
2762
|
+
"needs_agent_task envelope. Returns a unified envelope keyed on `status`: " +
|
|
2763
|
+
"`completed` (terminal success with `results`), `needs_agent_task` (pause — read " +
|
|
2764
|
+
"`instruction`, perform the task, then call `resume_pipeline` with the resulting " +
|
|
2765
|
+
"string as `agent_result`), or `failed` (terminal error — check `error_code`: " +
|
|
2766
|
+
"VALIDATION | NOT_FOUND | EXPIRED | REPO_MISMATCH | TOOL_ERROR). " +
|
|
2767
|
+
"Paused runs auto-expire after an idle TTL (default 24 hours; override with " +
|
|
2768
|
+
"`ttl_seconds`). The TTL is reset on every state transition.\n\n" +
|
|
2769
|
+
"Available pipelines:\n" +
|
|
2770
|
+
buildPipelineCatalogDescription(),
|
|
2296
2771
|
inputSchema: {
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
.
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
context: z.string().min(1),
|
|
2303
|
-
source: z.string().min(1).describe("Source reference from the evaluation, e.g. 'Clarifying Q3 (initial round)'"),
|
|
2304
|
-
recommendation_index: z.number().int().min(0).describe("0-based index of the recommended option in the options array"),
|
|
2305
|
-
options: z.array(z.string().min(1)).min(1).describe("Option labels from resolution guide decision tree branches. Values are auto-generated."),
|
|
2306
|
-
}))
|
|
2772
|
+
pipeline: z
|
|
2773
|
+
.string()
|
|
2774
|
+
.describe("Pipeline name (e.g. 'review-ticket', 'implement-ticket')"),
|
|
2775
|
+
variables: z
|
|
2776
|
+
.record(z.string())
|
|
2307
2777
|
.optional()
|
|
2308
|
-
.
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
.
|
|
2312
|
-
id: z.string().min(1),
|
|
2313
|
-
title: z.string().min(1),
|
|
2314
|
-
action: z.string().min(1),
|
|
2315
|
-
confidence: z.string().min(1),
|
|
2316
|
-
source: z.string().min(1).describe("Source reference from the evaluation"),
|
|
2317
|
-
}))
|
|
2778
|
+
.describe("Key-value pairs for variable substitution (e.g. { ticket_key: 'BAPI-123' }). " +
|
|
2779
|
+
"Do NOT pass `auto_approve` here — use the top-level parameter."),
|
|
2780
|
+
auto_approve: z
|
|
2781
|
+
.union([z.boolean(), z.literal("true"), z.literal("false")])
|
|
2318
2782
|
.optional()
|
|
2319
|
-
.
|
|
2320
|
-
|
|
2783
|
+
.describe("When true, approval-gated mcp_call steps execute directly. When false or " +
|
|
2784
|
+
"omitted, the orchestrator synthesises a needs_agent_task pause so the agent " +
|
|
2785
|
+
"can confirm with the user before resuming. Accepts boolean or 'true'/'false' " +
|
|
2786
|
+
"strings for MCP clients that serialize booleans as strings."),
|
|
2787
|
+
ttl_seconds: z
|
|
2788
|
+
.number()
|
|
2789
|
+
.int()
|
|
2790
|
+
.positive()
|
|
2791
|
+
.optional()
|
|
2792
|
+
.describe("Override the default 24-hour idle TTL for this run. Must be a positive integer."),
|
|
2793
|
+
},
|
|
2794
|
+
}, async (input) => {
|
|
2795
|
+
const result = await runPipeline(buildPipelineOrchestratorDeps(), input);
|
|
2796
|
+
return {
|
|
2797
|
+
content: [
|
|
2798
|
+
{ type: "text", text: JSON.stringify(result, null, 2) },
|
|
2799
|
+
],
|
|
2800
|
+
};
|
|
2801
|
+
});
|
|
2802
|
+
registerTool("resume_pipeline", {
|
|
2803
|
+
description: "Resume a paused pipeline run with the result of the agent_task. Provide the " +
|
|
2804
|
+
"`pipeline_run_id` returned by the prior needs_agent_task envelope, and the string " +
|
|
2805
|
+
"the instruction's `## Return` section asked you to produce as `agent_result`. " +
|
|
2806
|
+
"`agent_result` is always a string — do not wrap it in JSON unless the instruction " +
|
|
2807
|
+
"explicitly asked you to serialize structured output. Returns the same unified " +
|
|
2808
|
+
"envelope shape as `run_pipeline`.",
|
|
2809
|
+
inputSchema: {
|
|
2810
|
+
pipeline_run_id: z
|
|
2811
|
+
.string()
|
|
2812
|
+
.describe("The pipeline_run_id returned by a prior needs_agent_task envelope"),
|
|
2813
|
+
agent_result: z
|
|
2814
|
+
.string()
|
|
2815
|
+
.describe("The string the paused instruction's ## Return section asked you to produce"),
|
|
2321
2816
|
},
|
|
2322
2817
|
}, async (input) => {
|
|
2818
|
+
const result = await resumePipeline(buildPipelineOrchestratorDeps(), input);
|
|
2819
|
+
return {
|
|
2820
|
+
content: [
|
|
2821
|
+
{ type: "text", text: JSON.stringify(result, null, 2) },
|
|
2822
|
+
],
|
|
2823
|
+
};
|
|
2824
|
+
});
|
|
2825
|
+
registerTool("list_pipeline_runs", {
|
|
2826
|
+
description: "List recent pipeline runs for the configured repository, newest first. Returns " +
|
|
2827
|
+
"metadata only — `resolved_recipe`, resolved params, instruction text, results, " +
|
|
2828
|
+
"and agent outputs are intentionally excluded. Use this to recover a " +
|
|
2829
|
+
"`pipeline_run_id` when an earlier needs_agent_task envelope is no longer in " +
|
|
2830
|
+
"scope (e.g. after compaction or a client restart). " +
|
|
2831
|
+
"Optionally filter by `status`: running | paused | completed | failed | expired.",
|
|
2832
|
+
inputSchema: {
|
|
2833
|
+
status: z
|
|
2834
|
+
.enum(["running", "paused", "completed", "failed", "expired"])
|
|
2835
|
+
.optional()
|
|
2836
|
+
.describe("Optional status filter"),
|
|
2837
|
+
},
|
|
2838
|
+
}, async (input) => {
|
|
2839
|
+
const result = await listPipelineRuns(buildPipelineOrchestratorDeps(), input);
|
|
2840
|
+
return {
|
|
2841
|
+
content: [
|
|
2842
|
+
{ type: "text", text: JSON.stringify(result, null, 2) },
|
|
2843
|
+
],
|
|
2844
|
+
};
|
|
2845
|
+
});
|
|
2846
|
+
registerTool("delete_pipeline_run", {
|
|
2847
|
+
description: "Delete a pipeline run row (any status). Use this to discard orphaned `running` " +
|
|
2848
|
+
"rows from a previous session that can't be resumed (resume_pipeline only accepts " +
|
|
2849
|
+
"`paused`), to clean up after a failed run, or to remove a no-longer-needed paused " +
|
|
2850
|
+
"session. Returns `{ status: 'completed', deleted: true, pipeline_run_id }` on " +
|
|
2851
|
+
"success, or a `failed` envelope with error_code in (VALIDATION | NOT_FOUND | " +
|
|
2852
|
+
"REPO_MISMATCH | TOOL_ERROR). Repo-scoped: the row's stored repo_name must match " +
|
|
2853
|
+
"the caller's repo.",
|
|
2854
|
+
inputSchema: {
|
|
2855
|
+
pipeline_run_id: z
|
|
2856
|
+
.string()
|
|
2857
|
+
.describe("UUID of the pipeline run to delete."),
|
|
2858
|
+
},
|
|
2859
|
+
}, async (input) => {
|
|
2860
|
+
const result = await deletePipelineRun(buildPipelineOrchestratorDeps(), input);
|
|
2861
|
+
return {
|
|
2862
|
+
content: [
|
|
2863
|
+
{ type: "text", text: JSON.stringify(result, null, 2) },
|
|
2864
|
+
],
|
|
2865
|
+
};
|
|
2866
|
+
});
|
|
2867
|
+
// ---------------------------------------------------------------------------
|
|
2868
|
+
// generate_decision_page
|
|
2869
|
+
// ---------------------------------------------------------------------------
|
|
2870
|
+
registerTool("generate_decision_page", {
|
|
2871
|
+
description: "Generate a local HTML decision page for capturing user decisions on ticket review findings. " +
|
|
2872
|
+
"Renders recommendation-driven review decisions sourced from the combined review-and-resolution " +
|
|
2873
|
+
"document, with per-option consequence lines, a closed-by-default codebase-evidence disclosure, " +
|
|
2874
|
+
"and confirmed improvements. The user opens the HTML file in a browser, makes selections, and " +
|
|
2875
|
+
"copies the resulting JSON output back to the agent.",
|
|
2876
|
+
inputSchema: DecisionPageInputShape,
|
|
2877
|
+
}, async (input) => {
|
|
2878
|
+
// Returned messages travel through JSON.stringify in the MCP envelope below,
|
|
2879
|
+
// never into HTML, so echoing untrusted input back to the caller is safe here.
|
|
2880
|
+
// Do not reuse this helper in any path that renders the message into HTML.
|
|
2323
2881
|
const validationError = (message) => ({
|
|
2324
2882
|
content: [{
|
|
2325
2883
|
type: "text",
|
|
@@ -2343,21 +2901,27 @@ server.registerTool("generate_decision_page", {
|
|
|
2343
2901
|
}],
|
|
2344
2902
|
};
|
|
2345
2903
|
}
|
|
2346
|
-
// Validate actionable_items
|
|
2904
|
+
// Validate actionable_items: cross-item invariants Zod cannot express.
|
|
2905
|
+
// Per-item bounds (recommendation_index < options.length, parity, branch
|
|
2906
|
+
// count, etc.) are enforced by the schema's superRefine.
|
|
2347
2907
|
const seenIds = new Set();
|
|
2348
2908
|
for (const item of input.actionable_items) {
|
|
2349
2909
|
if (seenIds.has(item.id)) {
|
|
2350
2910
|
return validationError(`Duplicate actionable_items id: "${item.id}"`);
|
|
2351
2911
|
}
|
|
2352
2912
|
seenIds.add(item.id);
|
|
2353
|
-
if (item.recommendation_index >= item.options.length) {
|
|
2354
|
-
return validationError(`Item "${item.id}": recommendation_index ${item.recommendation_index} is out of bounds (${item.options.length} options).`);
|
|
2355
|
-
}
|
|
2356
2913
|
const noneLabel = item.options.find((label) => label.toLowerCase() === "none of these");
|
|
2357
2914
|
if (noneLabel) {
|
|
2358
2915
|
return validationError(`Item "${item.id}": option label "${noneLabel}" is reserved and auto-appended by the tool.`);
|
|
2359
2916
|
}
|
|
2360
2917
|
}
|
|
2918
|
+
const seenCiIds = new Set();
|
|
2919
|
+
for (const ci of input.clear_improvements) {
|
|
2920
|
+
if (seenCiIds.has(ci.id)) {
|
|
2921
|
+
return validationError(`Duplicate clear_improvements id: "${ci.id}"`);
|
|
2922
|
+
}
|
|
2923
|
+
seenCiIds.add(ci.id);
|
|
2924
|
+
}
|
|
2361
2925
|
// Read design assets and base64-encode for embedding
|
|
2362
2926
|
const assetsDir = path.join(PROJECT_ROOT, "design-assets");
|
|
2363
2927
|
const fontsDir = path.join(PROJECT_ROOT, "public", "fonts");
|
|
@@ -2399,17 +2963,6 @@ server.registerTool("generate_decision_page", {
|
|
|
2399
2963
|
// ---------------------------------------------------------------------------
|
|
2400
2964
|
// Entry point
|
|
2401
2965
|
// ---------------------------------------------------------------------------
|
|
2402
|
-
// Load custom user pipelines before accepting connections
|
|
2403
|
-
const instructionsDir = path.join(path.dirname(BAPI_PIPELINES_DIR), "instructions");
|
|
2404
|
-
const customResult = await loadCustomPipelines(BAPI_PIPELINES_DIR, instructionsDir, BUNDLED_INSTRUCTIONS);
|
|
2405
|
-
for (const [key, pipeline] of Object.entries(customResult.pipelines)) {
|
|
2406
|
-
if (key in BUNDLED_PIPELINES) {
|
|
2407
|
-
console.error(`Warning: user pipeline "${key}" overrides bundled pipeline.`);
|
|
2408
|
-
}
|
|
2409
|
-
PIPELINES[key] = pipeline;
|
|
2410
|
-
}
|
|
2411
|
-
Object.assign(INSTRUCTIONS, customResult.instructions);
|
|
2412
|
-
userPipelineKeys = customResult.userPipelineKeys;
|
|
2413
2966
|
const transport = new StdioServerTransport();
|
|
2414
2967
|
await server.connect(transport);
|
|
2415
2968
|
console.error("Bridge API MCP server running on stdio");
|