@bridge_gpt/mcp-server 0.2.1 → 0.2.3
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 +97 -15
- package/build/agent-config-credential-migration.js +272 -0
- package/build/agents.generated.js +1 -1
- package/build/chain-orchestrator.js +16 -1
- package/build/commands.generated.js +9 -7
- package/build/conductor/bridge-api-client.js +625 -0
- package/build/conductor/claude-hook.js +251 -0
- package/build/conductor/cli.js +1048 -0
- package/build/conductor/data-normalization.js +114 -0
- package/build/conductor/doctor.js +164 -0
- package/build/conductor/done-gate.js +325 -0
- package/build/conductor/epic-reconcile.js +139 -0
- package/build/conductor/epic-runtime.js +611 -0
- package/build/conductor/epic-state.js +125 -0
- package/build/conductor/errors.js +85 -0
- package/build/conductor/git-ci-types.js +129 -0
- package/build/conductor/git-hooks.js +218 -0
- package/build/conductor/git-inspection.js +185 -0
- package/build/conductor/git-producer.js +137 -0
- package/build/conductor/merge-ledger.js +198 -0
- package/build/conductor/paths.js +224 -0
- package/build/conductor/plan.js +77 -0
- package/build/conductor/pr-ci-producer.js +427 -0
- package/build/conductor/pr-discovery.js +135 -0
- package/build/conductor/producer-ledger.js +125 -0
- package/build/conductor/redaction.js +112 -0
- package/build/conductor/store.js +1156 -0
- package/build/conductor/supervisor-config.js +150 -0
- package/build/conductor/supervisor-escalation.js +244 -0
- package/build/conductor/supervisor-judgment-python.js +141 -0
- package/build/conductor/supervisor-judgment.js +215 -0
- package/build/conductor/supervisor-ledger.js +119 -0
- package/build/conductor/supervisor-merge.js +127 -0
- package/build/conductor/supervisor-message-relay.js +61 -0
- package/build/conductor/supervisor-notification.js +39 -0
- package/build/conductor/supervisor-runtime.js +351 -0
- package/build/conductor/supervisor-state.js +572 -0
- package/build/conductor/supervisor-types.js +16 -0
- package/build/conductor/taxonomy.js +58 -0
- package/build/conductor/tools.js +367 -0
- package/build/conductor/types.js +9 -0
- package/build/conductor-bin.js +21 -0
- package/build/conductor-claude-hook-bin.js +21 -0
- package/build/credential-store.js +175 -4
- package/build/credentials-cli.js +223 -0
- package/build/decision-page-schema.js +60 -0
- package/build/decision-page-template.js +262 -10
- package/build/doctor.js +5 -1
- package/build/index.js +558 -63
- package/build/pipeline-orchestrator.js +5 -1
- package/build/pipeline-utils.js +45 -5
- package/build/pipelines.generated.js +37 -9
- package/build/readme.generated.js +3 -0
- package/build/review-tickets.js +596 -0
- package/build/scheduled-prompt.js +16 -10
- package/build/start-tickets-conductor.js +496 -0
- package/build/start-tickets-prereqs.js +32 -23
- package/build/start-tickets-repo.js +49 -0
- package/build/start-tickets.js +683 -82
- package/build/version.generated.js +1 -1
- package/design-assets/favicon/android-chrome-192x192.png +0 -0
- package/design-assets/favicon/android-chrome-512x512.png +0 -0
- package/design-assets/favicon/apple-touch-icon.png +0 -0
- package/design-assets/favicon/favicon-16x16.png +0 -0
- package/design-assets/favicon/favicon-32x32.png +0 -0
- package/design-assets/favicon/favicon.ico +0 -0
- package/design-assets/favicon/site.webmanifest +1 -0
- package/design-assets/just-logo-rough-draft.png +0 -0
- package/package.json +18 -6
- package/pipelines/idea-to-ticket.json +5 -0
- package/pipelines/plan-epic.json +16 -1
- package/pipelines/review-ticket.json +2 -1
- package/public/css/main.min.css +2 -0
- package/public/css/main.min.css.map +1 -0
- package/public/fonts/OFL.txt +93 -0
- package/public/fonts/SourceSansPro-Black.ttf +0 -0
- package/public/fonts/SourceSansPro-BlackItalic.ttf +0 -0
- package/public/fonts/SourceSansPro-Bold.ttf +0 -0
- package/public/fonts/SourceSansPro-BoldItalic.ttf +0 -0
- package/public/fonts/SourceSansPro-ExtraLight.ttf +0 -0
- package/public/fonts/SourceSansPro-ExtraLightItalic.ttf +0 -0
- package/public/fonts/SourceSansPro-Italic.ttf +0 -0
- package/public/fonts/SourceSansPro-Light.ttf +0 -0
- package/public/fonts/SourceSansPro-LightItalic.ttf +0 -0
- package/public/fonts/SourceSansPro-Regular.ttf +0 -0
- package/public/fonts/SourceSansPro-SemiBold.ttf +0 -0
- package/public/fonts/SourceSansPro-SemiBoldItalic.ttf +0 -0
- package/public/img/bridge-logo-160x51.webp +0 -0
- package/public/img/bridge-logo-300x92.webp +0 -0
- package/public/img/favicon/android-chrome-192x192.png +0 -0
- package/public/img/favicon/android-chrome-512x512.png +0 -0
- package/public/img/favicon/apple-touch-icon.png +0 -0
- package/public/img/favicon/favicon-16x16.png +0 -0
- package/public/img/favicon/favicon-32x32.png +0 -0
- package/public/img/favicon/favicon.ico +0 -0
- package/public/img/favicon/site.webmanifest +1 -0
- package/public/img/installation/bitbucket/app-password-1.png +0 -0
- package/public/img/installation/bitbucket/app-password-2.png +0 -0
- package/public/img/installation/bitbucket/create-token-1.png +0 -0
- package/public/img/installation/bitbucket/create-token-2.png +0 -0
- package/public/img/installation/bitbucket/webhook-1.png +0 -0
- package/public/img/installation/github/github-review-webhook.png +0 -0
- package/public/img/installation/jira/credentials/api-key.png +0 -0
- package/public/img/installation/jira/webhook/create-rule.png +0 -0
- package/public/img/installation/jira/webhook/project-settings.png +0 -0
- package/public/img/installation/jira/webhook/rule-create-1.png +0 -0
- package/public/img/installation/jira/webhook/rule-create-2.png +0 -0
- package/public/img/installation/jira/webhook/rule-create-3.png +0 -0
- package/public/img/installation/pinecone/pinecone-api-key.png +0 -0
- package/public/img/installation/pinecone/pinecone-index.png +0 -0
- package/public/js/main.min.js +2 -0
- package/public/js/main.min.js.map +1 -0
- package/smoke-test/SMOKE-TEST.md +16 -8
package/build/index.js
CHANGED
|
@@ -8,11 +8,16 @@
|
|
|
8
8
|
* BAPI_REPO_NAME - Default repository name injected into every request
|
|
9
9
|
* BAPI_API_KEY - API key for X-API-Key authentication
|
|
10
10
|
* BAPI_DOCS_DIR - Base directory for local file output (default: docs/tmp)
|
|
11
|
+
* BAPI_MCP_UPGRADE_ADVICE_ENABLED - Proactively surface the server's upgrade
|
|
12
|
+
* advice in pipeline recipe preambles (BAPI-375). Defaults to ENABLED; set
|
|
13
|
+
* to false/0/no/off/disabled to suppress proactive recipe-preamble
|
|
14
|
+
* surfacing. Does NOT change the /jira/ping response or server-side upgrade
|
|
15
|
+
* computation — it only gates the recipe-preamble convention.
|
|
11
16
|
*/
|
|
12
17
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
13
18
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
14
19
|
import { z } from "zod";
|
|
15
|
-
import { writeFile, mkdir, readFile, stat } from "fs/promises";
|
|
20
|
+
import { writeFile, mkdir, readFile, stat, rename, chmod, unlink } from "fs/promises";
|
|
16
21
|
import path from "path";
|
|
17
22
|
import os from "os";
|
|
18
23
|
import { fileURLToPath } from "url";
|
|
@@ -22,17 +27,23 @@ import { PIPELINES as BUNDLED_PIPELINES, INSTRUCTIONS as BUNDLED_INSTRUCTIONS, C
|
|
|
22
27
|
import { COMMANDS } from "./commands.generated.js";
|
|
23
28
|
import { AGENTS } from "./agents.generated.js";
|
|
24
29
|
import { VERSION } from "./version.generated.js";
|
|
30
|
+
import { README } from "./readme.generated.js";
|
|
25
31
|
import { checkForUpdate } from "./update-check.js";
|
|
26
32
|
import { reconstructAgentMarkdown, translateAgentToCopilot } from "./agent-utils.js";
|
|
27
33
|
import { resolveRecipe, loadCustomPipelines } from "./pipeline-utils.js";
|
|
28
34
|
import { runStartTicketsCli } from "./start-tickets.js";
|
|
35
|
+
import { runReviewTicketsCli } from "./review-tickets.js";
|
|
29
36
|
import { runDoctorCli } from "./doctor.js";
|
|
30
37
|
import { runScheduleRunCli } from "./schedule-run.js";
|
|
31
38
|
import { runMcpInvokeCli } from "./mcp-invoke.js";
|
|
32
39
|
import { runAgentCapabilitiesCli } from "./agent-capabilities/cli.js";
|
|
33
40
|
import { validateRepoName } from "./bridge-config.js";
|
|
34
41
|
import { ensureGitignored as ensureGitignoredShared } from "./git-ignore-utils.js";
|
|
35
|
-
import { resolveBapiCredentials } from "./credential-store.js";
|
|
42
|
+
import { resolveBapiCredentials, upsertBapiCredential, getPrimaryCredentialStorePath, } from "./credential-store.js";
|
|
43
|
+
import { runCredentialsCli } from "./credentials-cli.js";
|
|
44
|
+
import { registerConductorTools } from "./conductor/tools.js";
|
|
45
|
+
import { runConductorCli } from "./conductor/cli.js";
|
|
46
|
+
import { observePrCiFromPollResponse } from "./conductor/pr-ci-producer.js";
|
|
36
47
|
import { generateDecisionPageHtml } from "./decision-page-template.js";
|
|
37
48
|
import { DecisionPageInputShape } from "./decision-page-schema.js";
|
|
38
49
|
import { slugify, saveBrainstormResultsToDir, } from "./brainstorm-files.js";
|
|
@@ -47,6 +58,25 @@ let userPipelineKeys = new Set();
|
|
|
47
58
|
// ---------------------------------------------------------------------------
|
|
48
59
|
const BASE_URL = process.env.BAPI_BASE_URL ?? "https://bridgegpt-api.com";
|
|
49
60
|
const REPO_NAME = process.env.BAPI_REPO_NAME ?? "";
|
|
61
|
+
/**
|
|
62
|
+
* Parse a default-ON boolean env flag. `undefined` / blank → true; the
|
|
63
|
+
* normalized off-tokens (`false`, `0`, `no`, `off`, `disabled`) → false; any
|
|
64
|
+
* other value → true (preserving default-on / fail-open behavior).
|
|
65
|
+
*/
|
|
66
|
+
function parseDefaultOnEnvFlag(value) {
|
|
67
|
+
if (value === undefined)
|
|
68
|
+
return true;
|
|
69
|
+
const normalized = value.trim().toLowerCase();
|
|
70
|
+
if (normalized === "")
|
|
71
|
+
return true;
|
|
72
|
+
return !["false", "0", "no", "off", "disabled"].includes(normalized);
|
|
73
|
+
}
|
|
74
|
+
// BAPI-375: gate proactive upgrade-advice surfacing in recipe preambles with an
|
|
75
|
+
// MCP-LOCAL env-var rather than backend config-field plumbing. This flag only
|
|
76
|
+
// controls whether the surfacing convention is emitted; the backend remains
|
|
77
|
+
// authoritative for whether advice exists and for its exact wording
|
|
78
|
+
// (compute_upgrade()). Defaults to enabled; fail-open on any unexpected value.
|
|
79
|
+
const UPGRADE_ADVICE_SURFACING_ENABLED = parseDefaultOnEnvFlag(process.env.BAPI_MCP_UPGRADE_ADVICE_ENABLED);
|
|
50
80
|
// ---------------------------------------------------------------------------
|
|
51
81
|
// Resolved-once credential + path accessors (BAPI-338)
|
|
52
82
|
//
|
|
@@ -85,6 +115,45 @@ async function getResolvedApiKey() {
|
|
|
85
115
|
}
|
|
86
116
|
return resolvedApiKeyPromise;
|
|
87
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Resolve the Bridge API key env-first, then from the home-dir credential store,
|
|
120
|
+
* for an EXPLICIT `repoName` (not the module-level `REPO_NAME`). Used by the
|
|
121
|
+
* install-time persistence tool, which targets the repo the user is installing
|
|
122
|
+
* into. Uncached. Returns an empty string on any failure and never logs secrets.
|
|
123
|
+
*/
|
|
124
|
+
async function getResolvedApiKeyForRepo(repoName) {
|
|
125
|
+
try {
|
|
126
|
+
const result = await resolveBapiCredentials(repoName, {
|
|
127
|
+
env: process.env,
|
|
128
|
+
homedir: os.homedir,
|
|
129
|
+
platform: process.platform,
|
|
130
|
+
readFile: (p) => readFile(p, "utf-8"),
|
|
131
|
+
stat: (p) => stat(p),
|
|
132
|
+
});
|
|
133
|
+
return result.ok ? result.credentials.apiKey : "";
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return "";
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Build {@link CredentialStoreWriteDeps} from the local MCP-server runtime
|
|
141
|
+
* boundaries (`process.env`, `os.homedir`, `process.platform`, `fs/promises`).
|
|
142
|
+
* The returned deps are what the writer mutates the user-scoped store through.
|
|
143
|
+
*/
|
|
144
|
+
function buildCredentialStoreWriteDeps() {
|
|
145
|
+
return {
|
|
146
|
+
env: process.env,
|
|
147
|
+
homedir: os.homedir,
|
|
148
|
+
platform: process.platform,
|
|
149
|
+
readFile: (p) => readFile(p, "utf-8"),
|
|
150
|
+
mkdir: (p, options) => mkdir(p, options),
|
|
151
|
+
writeFile: (p, data, options) => writeFile(p, data, options),
|
|
152
|
+
rename: (oldPath, newPath) => rename(oldPath, newPath),
|
|
153
|
+
chmod: (p, mode) => chmod(p, mode),
|
|
154
|
+
unlink: (p) => unlink(p),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
88
157
|
/** GET auth headers, with the API key resolved once via {@link getResolvedApiKey}. */
|
|
89
158
|
async function getGetHeaders() {
|
|
90
159
|
return {
|
|
@@ -518,9 +587,21 @@ const TICKET_ARTIFACTS = {
|
|
|
518
587
|
requestErrorPrefix: "Failed to request FSD generation: ",
|
|
519
588
|
confirmationText: (n) => `FSD generation requested for ${n}. ` +
|
|
520
589
|
`Processing typically takes 2-4 minutes. ` +
|
|
521
|
-
`Use
|
|
590
|
+
`Use get_doc with ticket_number "${n}" and doc_type "fsd" to retrieve the functional specification document once processing completes.`,
|
|
522
591
|
pollLabel: (n) => `FSD generation for ${n}`,
|
|
523
592
|
},
|
|
593
|
+
prd: {
|
|
594
|
+
kind: "single",
|
|
595
|
+
generateEndpoint: (n) => `/ticket/${encodeURIComponent(n)}/generate-prd`,
|
|
596
|
+
getEndpoint: (n) => `/ticket/${encodeURIComponent(n)}/prd`,
|
|
597
|
+
saveSubdir: "prd",
|
|
598
|
+
filename: (n) => `${n}-prd-plan.md`,
|
|
599
|
+
requestErrorPrefix: "Failed to request PRD generation: ",
|
|
600
|
+
confirmationText: (n) => `PRD generation requested for ${n}. ` +
|
|
601
|
+
`Processing typically takes 2-4 minutes. ` +
|
|
602
|
+
`Use get_prd with ticket_number "${n}" to retrieve the PRD once processing completes.`,
|
|
603
|
+
pollLabel: (n) => `PRD generation for ${n}`,
|
|
604
|
+
},
|
|
524
605
|
clarifying_questions: {
|
|
525
606
|
kind: "single",
|
|
526
607
|
generateEndpoint: (n) => `/ticket/${encodeURIComponent(n)}/generate-clarifying-questions`,
|
|
@@ -597,6 +678,7 @@ async function getTicketArtifactDocsPath(subdir) {
|
|
|
597
678
|
return getDocsPath("architecture");
|
|
598
679
|
case "fsd":
|
|
599
680
|
return getDocsPath("fsd");
|
|
681
|
+
case "prd": return getDocsPath("prd");
|
|
600
682
|
case "clarifying-questions":
|
|
601
683
|
return getDocsPath("clarifying-questions");
|
|
602
684
|
case "ticket-critiques":
|
|
@@ -609,11 +691,15 @@ async function getTicketArtifactDocsPath(subdir) {
|
|
|
609
691
|
// `tdd` reuses the existing "architecture" artifact (the TDD/architecture
|
|
610
692
|
// document) WITHOUT renaming it, preserving the back-compatible
|
|
611
693
|
// request_architecture / get_architecture tools. `fsd` routes to the new "fsd"
|
|
612
|
-
// artifact.
|
|
694
|
+
// artifact, and `prd` routes to the new "prd" artifact.
|
|
613
695
|
function resolveDesignDocArtifactType(docType) {
|
|
614
|
-
|
|
696
|
+
if (docType === "fsd")
|
|
697
|
+
return "fsd";
|
|
698
|
+
if (docType === "prd")
|
|
699
|
+
return "prd";
|
|
700
|
+
return "architecture";
|
|
615
701
|
}
|
|
616
|
-
// Shared request flow for the
|
|
702
|
+
// Shared request flow for the six single-artifact request_* tools: POST the
|
|
617
703
|
// generate endpoint, return the per-tool error prefix on a non-OK POST, and on
|
|
618
704
|
// wait_for_result poll the get endpoint (900_000 ms) and optionally save.
|
|
619
705
|
async function requestTicketArtifact(type, args) {
|
|
@@ -646,7 +732,7 @@ async function requestTicketArtifact(type, args) {
|
|
|
646
732
|
content: [{ type: "text", text: config.confirmationText(args.ticket_number) }],
|
|
647
733
|
};
|
|
648
734
|
}
|
|
649
|
-
// Shared GET/save flow for the
|
|
735
|
+
// Shared GET/save flow for the six single-artifact get_* tools. Normal gets
|
|
650
736
|
// save only on `resp.ok && save_locally`. reimplement_context is asymmetric: it
|
|
651
737
|
// short-circuits a 404 with a custom NOT_FOUND envelope (without leaking the
|
|
652
738
|
// backend body) and otherwise saves on ANY non-404 response when save_locally.
|
|
@@ -1285,6 +1371,9 @@ async function dispatchCliSubcommand(argv) {
|
|
|
1285
1371
|
if (argv[0] === "start-tickets") {
|
|
1286
1372
|
return runStartTicketsCli(argv.slice(1));
|
|
1287
1373
|
}
|
|
1374
|
+
if (argv[0] === "review-tickets") {
|
|
1375
|
+
return runReviewTicketsCli(argv.slice(1));
|
|
1376
|
+
}
|
|
1288
1377
|
// The internal `mcp-invoke` worktree shim (BAPI-337) is a positional
|
|
1289
1378
|
// subcommand routed before the flag guards and well before MCP server
|
|
1290
1379
|
// construction: it resolves identity/credentials from `--project-root` and
|
|
@@ -1310,6 +1399,21 @@ async function dispatchCliSubcommand(argv) {
|
|
|
1310
1399
|
if (argv[0] === "agent-capabilities") {
|
|
1311
1400
|
return runAgentCapabilitiesCli(argv.slice(1));
|
|
1312
1401
|
}
|
|
1402
|
+
// The `credentials` subcommand (BAPI-377) hosts the consent-gated agent-config
|
|
1403
|
+
// credential migration. It is a positional subcommand routed before the
|
|
1404
|
+
// --init / --upgrade flag guards and well before MCP server construction so
|
|
1405
|
+
// migration never falls through to the normal no-subcommand startup path. It
|
|
1406
|
+
// is the ONLY place credential migration lives — doctor stays strictly read-only.
|
|
1407
|
+
if (argv[0] === "credentials") {
|
|
1408
|
+
return runCredentialsCli(argv.slice(1));
|
|
1409
|
+
}
|
|
1410
|
+
// The `conductor` subcommand (BAPI-393) is the local event-ledger CLI for fast
|
|
1411
|
+
// hooks and operator diagnostics. It is a positional subcommand routed before
|
|
1412
|
+
// the --init / --upgrade flag guards and well before MCP server construction;
|
|
1413
|
+
// it talks ONLY to the local SQLite ledger and never starts the MCP server.
|
|
1414
|
+
if (argv[0] === "conductor") {
|
|
1415
|
+
return runConductorCli(argv.slice(1));
|
|
1416
|
+
}
|
|
1313
1417
|
// --init takes precedence over --upgrade; both are position-independent flags.
|
|
1314
1418
|
if (argv.includes("--init")) {
|
|
1315
1419
|
return runInitCli(cwd);
|
|
@@ -1335,6 +1439,28 @@ const server = new McpServer({
|
|
|
1335
1439
|
version: "1.0.0",
|
|
1336
1440
|
});
|
|
1337
1441
|
// ---------------------------------------------------------------------------
|
|
1442
|
+
// README resource
|
|
1443
|
+
// ---------------------------------------------------------------------------
|
|
1444
|
+
//
|
|
1445
|
+
// Expose the package README as a readable MCP resource so a connected agent can
|
|
1446
|
+
// discover the server's feature set without leaving the session. Without this,
|
|
1447
|
+
// `listMcpResources` returns nothing and the README is only visible on npm. The
|
|
1448
|
+
// markdown is bundled at build time (scripts/bundle-readme.js → readme.generated)
|
|
1449
|
+
// so there is no runtime file read.
|
|
1450
|
+
server.registerResource("readme", "bridge-api://readme", {
|
|
1451
|
+
title: "Bridge API MCP — README",
|
|
1452
|
+
description: "Overview and feature reference for the Bridge API MCP server (the same README published with the npm package).",
|
|
1453
|
+
mimeType: "text/markdown",
|
|
1454
|
+
}, async (uri) => ({
|
|
1455
|
+
contents: [
|
|
1456
|
+
{
|
|
1457
|
+
uri: uri.href,
|
|
1458
|
+
mimeType: "text/markdown",
|
|
1459
|
+
text: README,
|
|
1460
|
+
},
|
|
1461
|
+
],
|
|
1462
|
+
}));
|
|
1463
|
+
// ---------------------------------------------------------------------------
|
|
1338
1464
|
// Tool registration wrapper (BAPI-275)
|
|
1339
1465
|
// ---------------------------------------------------------------------------
|
|
1340
1466
|
//
|
|
@@ -1395,6 +1521,16 @@ const registerTool = ((name, config, handler) => {
|
|
|
1395
1521
|
}
|
|
1396
1522
|
return toolHandle;
|
|
1397
1523
|
});
|
|
1524
|
+
// ---------------------------------------------------------------------------
|
|
1525
|
+
// Conductor event-ledger tools (BAPI-393)
|
|
1526
|
+
// ---------------------------------------------------------------------------
|
|
1527
|
+
//
|
|
1528
|
+
// The conductor tools (emit_event, poll_events, wait_for_event,
|
|
1529
|
+
// get_supervisor_snapshot) operate against a LOCAL SQLite ledger at
|
|
1530
|
+
// ~/.config/bridge/events.db and intentionally do NOT route through the Bridge
|
|
1531
|
+
// API HTTP helpers. They are registered through the same `registerTool` wrapper
|
|
1532
|
+
// as every other tool so they share enable/disable + in-process dispatch.
|
|
1533
|
+
registerConductorTools(registerTool);
|
|
1398
1534
|
registerTool("ping", {
|
|
1399
1535
|
annotations: {
|
|
1400
1536
|
readOnlyHint: true,
|
|
@@ -1439,14 +1575,14 @@ registerTool("ping", {
|
|
|
1439
1575
|
});
|
|
1440
1576
|
registerTool("second_opinion", {
|
|
1441
1577
|
annotations: {
|
|
1442
|
-
readOnlyHint:
|
|
1578
|
+
readOnlyHint: false, // handler POSTs and creates a second_opinion_requests row every call
|
|
1443
1579
|
destructiveHint: false,
|
|
1444
|
-
idempotentHint:
|
|
1580
|
+
idempotentHint: false, // each call submits a fresh LLM request
|
|
1445
1581
|
openWorldHint: true,
|
|
1446
1582
|
},
|
|
1447
1583
|
description: "Get an IMMEDIATE, ad hoc independent critique or pushback on a plan, recommendation, or analysis you ALREADY HAVE in hand, from a different LLM model family. " +
|
|
1448
1584
|
"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. " +
|
|
1449
|
-
"This tool does NOT create or retrieve any Bridge artifact: it does not generate or fetch plans, ticket critiques, clarifying questions, architecture docs, deep research, brainstorms, or reimplementation context, and it
|
|
1585
|
+
"This tool does NOT create or retrieve any Bridge artifact: it does not generate or fetch plans, ticket critiques, clarifying questions, architecture docs, deep research, brainstorms, or reimplementation context, and it persists only an internal request-tracking row, no retrievable artifact. It simply returns the other model's reply to your prompt right now. " +
|
|
1450
1586
|
"If instead you want a Bridge ARTIFACT (a plan, critique, clarifying questions, architecture doc, reimplementation context, etc.) generated by a different provider, do NOT use this tool — call the matching `request_*` tool and set that tool's own `second_opinion` or `provider` parameter to route its generation to another provider. " +
|
|
1451
1587
|
"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'). " +
|
|
1452
1588
|
"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. " +
|
|
@@ -1467,7 +1603,10 @@ registerTool("second_opinion", {
|
|
|
1467
1603
|
.describe("Model tier within the chosen provider. CHEAP_MODEL for quick sanity checks, BASIC_MODEL for focused reviews, PREMIUM_MODEL for serious architectural pushback."),
|
|
1468
1604
|
},
|
|
1469
1605
|
}, async ({ prompt, provider, model }) => {
|
|
1470
|
-
|
|
1606
|
+
// The backend is async (submit -> poll -> result) so a slow second opinion
|
|
1607
|
+
// no longer exceeds the server's 30s request timeout (H12). This tool
|
|
1608
|
+
// absorbs the polling so the agent still gets the reply in a single call.
|
|
1609
|
+
const submitResp = await fetch(buildApiUrl("/llm/second-opinion"), {
|
|
1471
1610
|
method: "POST",
|
|
1472
1611
|
headers: await getPostHeaders(),
|
|
1473
1612
|
body: JSON.stringify({
|
|
@@ -1477,7 +1616,65 @@ registerTool("second_opinion", {
|
|
|
1477
1616
|
model,
|
|
1478
1617
|
}),
|
|
1479
1618
|
});
|
|
1480
|
-
|
|
1619
|
+
if (!submitResp.ok) {
|
|
1620
|
+
const text = await handleResponse(submitResp);
|
|
1621
|
+
return { content: [{ type: "text", text }] };
|
|
1622
|
+
}
|
|
1623
|
+
const submitBody = (await submitResp.json());
|
|
1624
|
+
const requestId = submitBody.request_id;
|
|
1625
|
+
if (typeof requestId !== "number") {
|
|
1626
|
+
return {
|
|
1627
|
+
content: [
|
|
1628
|
+
{
|
|
1629
|
+
type: "text",
|
|
1630
|
+
text: JSON.stringify({ error: "Second opinion submit response is missing request_id", status: 500 }),
|
|
1631
|
+
},
|
|
1632
|
+
],
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1635
|
+
const repoQuery = `repo_name=${encodeURIComponent(REPO_NAME)}`;
|
|
1636
|
+
const statusUrl = buildApiUrl(`/llm/second-opinion/${requestId}/status?${repoQuery}`);
|
|
1637
|
+
const resultUrl = buildApiUrl(`/llm/second-opinion/${requestId}/result?${repoQuery}`);
|
|
1638
|
+
// Poll /status until terminal. A second opinion is typically 20-90s but can
|
|
1639
|
+
// approach the server's 240s tool-loop deadline; start at 3s, back off to 8s
|
|
1640
|
+
// after 30s, cap at 300s. The server-side janitor fails truly-stuck rows
|
|
1641
|
+
// only after 15 min, so this cap always returns the recoverable message well
|
|
1642
|
+
// before a live row is touched.
|
|
1643
|
+
const startTime = Date.now();
|
|
1644
|
+
const timeoutMs = 300_000;
|
|
1645
|
+
let pollIntervalMs = 3_000;
|
|
1646
|
+
let finalStatus = "";
|
|
1647
|
+
while (true) {
|
|
1648
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
1649
|
+
if (Date.now() - startTime >= timeoutMs) {
|
|
1650
|
+
return {
|
|
1651
|
+
content: [
|
|
1652
|
+
{
|
|
1653
|
+
type: "text",
|
|
1654
|
+
text: `Second opinion is still processing after ${Math.round(timeoutMs / 1000)}s (request_id=${requestId}). The result is recoverable from the server via GET /llm/second-opinion/${requestId}/result?${repoQuery} once it finishes.`,
|
|
1655
|
+
},
|
|
1656
|
+
],
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
if (Date.now() - startTime > 30_000) {
|
|
1660
|
+
pollIntervalMs = 8_000;
|
|
1661
|
+
}
|
|
1662
|
+
const statusResp = await fetch(statusUrl, { headers: await getGetHeaders() });
|
|
1663
|
+
if (!statusResp.ok) {
|
|
1664
|
+
const text = await handleResponse(statusResp);
|
|
1665
|
+
return { content: [{ type: "text", text }] };
|
|
1666
|
+
}
|
|
1667
|
+
const statusBody = (await statusResp.json());
|
|
1668
|
+
finalStatus = typeof statusBody.status === "string" ? statusBody.status : "";
|
|
1669
|
+
if (finalStatus === "completed" || finalStatus === "failed") {
|
|
1670
|
+
break;
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
// Both terminal states are served by /result: completed returns the
|
|
1674
|
+
// {response, provider, tier, model} JSON; failed returns the sanitized 409
|
|
1675
|
+
// failure detail. handleResponse preserves the prior single-call output shape.
|
|
1676
|
+
const resultResp = await fetch(resultUrl, { headers: await getGetHeaders() });
|
|
1677
|
+
const text = await handleResponse(resultResp);
|
|
1481
1678
|
return { content: [{ type: "text", text }] };
|
|
1482
1679
|
});
|
|
1483
1680
|
registerTool("generate_image", {
|
|
@@ -1516,7 +1713,10 @@ registerTool("generate_image", {
|
|
|
1516
1713
|
.describe("Image dimensions. Defaults to '1024x1024'."),
|
|
1517
1714
|
},
|
|
1518
1715
|
}, async ({ prompt, provider, quality, size }) => {
|
|
1519
|
-
|
|
1716
|
+
// The backend is async (submit -> poll -> result) so a slow generation no
|
|
1717
|
+
// longer exceeds the server's 30s request timeout. This tool absorbs the
|
|
1718
|
+
// polling so the agent still gets the image back in a single call.
|
|
1719
|
+
const submitResp = await fetch(buildApiUrl("/llm/generate-image"), {
|
|
1520
1720
|
method: "POST",
|
|
1521
1721
|
headers: await getPostHeaders(),
|
|
1522
1722
|
body: JSON.stringify({
|
|
@@ -1527,11 +1727,71 @@ registerTool("generate_image", {
|
|
|
1527
1727
|
size,
|
|
1528
1728
|
}),
|
|
1529
1729
|
});
|
|
1530
|
-
if (!
|
|
1531
|
-
const text = await handleResponse(
|
|
1730
|
+
if (!submitResp.ok) {
|
|
1731
|
+
const text = await handleResponse(submitResp);
|
|
1732
|
+
return { content: [{ type: "text", text }] };
|
|
1733
|
+
}
|
|
1734
|
+
const submitBody = (await submitResp.json());
|
|
1735
|
+
const requestId = submitBody.request_id;
|
|
1736
|
+
if (typeof requestId !== "number") {
|
|
1737
|
+
return {
|
|
1738
|
+
content: [
|
|
1739
|
+
{
|
|
1740
|
+
type: "text",
|
|
1741
|
+
text: JSON.stringify({ error: "Image generation submit response is missing request_id", status: 500 }),
|
|
1742
|
+
},
|
|
1743
|
+
],
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
const repoQuery = `repo_name=${encodeURIComponent(REPO_NAME)}`;
|
|
1747
|
+
const statusUrl = buildApiUrl(`/llm/generate-image/${requestId}/status?${repoQuery}`);
|
|
1748
|
+
const resultUrl = buildApiUrl(`/llm/generate-image/${requestId}/result?${repoQuery}`);
|
|
1749
|
+
// Poll /status until terminal. Low quality is ~13s, but high quality can run
|
|
1750
|
+
// 2-4+ min; start snappy at 2s, back off to 5s after 20s, cap at 300s. The
|
|
1751
|
+
// server-side janitor fails truly-stuck rows only after 15 min, so this cap
|
|
1752
|
+
// always returns the recoverable message well before a live row is touched.
|
|
1753
|
+
const startTime = Date.now();
|
|
1754
|
+
const timeoutMs = 300_000;
|
|
1755
|
+
let pollIntervalMs = 2_000;
|
|
1756
|
+
let finalStatus = "";
|
|
1757
|
+
while (true) {
|
|
1758
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
1759
|
+
if (Date.now() - startTime >= timeoutMs) {
|
|
1760
|
+
return {
|
|
1761
|
+
content: [
|
|
1762
|
+
{
|
|
1763
|
+
type: "text",
|
|
1764
|
+
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.`,
|
|
1765
|
+
},
|
|
1766
|
+
],
|
|
1767
|
+
};
|
|
1768
|
+
}
|
|
1769
|
+
if (Date.now() - startTime > 20_000) {
|
|
1770
|
+
pollIntervalMs = 5_000;
|
|
1771
|
+
}
|
|
1772
|
+
const statusResp = await fetch(statusUrl, { headers: await getGetHeaders() });
|
|
1773
|
+
if (!statusResp.ok) {
|
|
1774
|
+
const text = await handleResponse(statusResp);
|
|
1775
|
+
return { content: [{ type: "text", text }] };
|
|
1776
|
+
}
|
|
1777
|
+
const statusBody = (await statusResp.json());
|
|
1778
|
+
finalStatus = typeof statusBody.status === "string" ? statusBody.status : "";
|
|
1779
|
+
if (finalStatus === "completed" || finalStatus === "failed") {
|
|
1780
|
+
break;
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
if (finalStatus === "failed") {
|
|
1784
|
+
// /result returns the sanitized 409 failure detail.
|
|
1785
|
+
const failResp = await fetch(resultUrl, { headers: await getGetHeaders() });
|
|
1786
|
+
const text = await handleResponse(failResp);
|
|
1787
|
+
return { content: [{ type: "text", text }] };
|
|
1788
|
+
}
|
|
1789
|
+
const resultResp = await fetch(resultUrl, { headers: await getGetHeaders() });
|
|
1790
|
+
if (!resultResp.ok) {
|
|
1791
|
+
const text = await handleResponse(resultResp);
|
|
1532
1792
|
return { content: [{ type: "text", text }] };
|
|
1533
1793
|
}
|
|
1534
|
-
const body = (await
|
|
1794
|
+
const body = (await resultResp.json());
|
|
1535
1795
|
const imageBase64 = body.image_base64;
|
|
1536
1796
|
if (typeof imageBase64 !== "string" || imageBase64.length === 0) {
|
|
1537
1797
|
return {
|
|
@@ -1806,11 +2066,10 @@ registerTool("get_plan", {
|
|
|
1806
2066
|
idempotentHint: true,
|
|
1807
2067
|
openWorldHint: true,
|
|
1808
2068
|
},
|
|
1809
|
-
description: "RETRIEVE an already-generated implementation plan for a Jira ticket. This tool only fetches an existing plan — it does NOT start or trigger plan generation. " +
|
|
2069
|
+
description: "RETRIEVE an already-generated implementation plan for a Jira ticket as markdown. This tool only fetches an existing plan — it does NOT start or trigger plan generation. " +
|
|
1810
2070
|
"If no plan exists yet (or you need a fresh one), call `request_plan_generation` first; it starts the async generation and this `get_plan` tool retrieves the result. " +
|
|
1811
|
-
"Returns the full plan as markdown
|
|
1812
|
-
"
|
|
1813
|
-
"Returns a 404 / not-found response when no plan is ready yet (the ticket may not have been processed by Bridge API) — that means generation has not run, not that this tool failed. " +
|
|
2071
|
+
"Returns the full plan as markdown verbatim — present it without summarizing. " +
|
|
2072
|
+
"Returns a 404 / not-found response when no plan is ready yet — that means generation has not run, not that this tool failed. " +
|
|
1814
2073
|
"Tip: call get_clarifying_questions for the same ticket to get the full context for implementation.",
|
|
1815
2074
|
inputSchema: {
|
|
1816
2075
|
ticket_number: z
|
|
@@ -1820,7 +2079,7 @@ registerTool("get_plan", {
|
|
|
1820
2079
|
.boolean()
|
|
1821
2080
|
.optional()
|
|
1822
2081
|
.default(true)
|
|
1823
|
-
.describe("Whether to save the plan to a local file
|
|
2082
|
+
.describe("Whether to save the retrieved plan to a local file. Saves to BAPI_DOCS_DIR/plans/{ticket}-plan.md. " +
|
|
1824
2083
|
"Defaults to true. Set to false to skip saving."),
|
|
1825
2084
|
},
|
|
1826
2085
|
}, async (args) => {
|
|
@@ -1852,6 +2111,32 @@ registerTool("get_architecture", {
|
|
|
1852
2111
|
}, async (args) => {
|
|
1853
2112
|
return getTicketArtifact("architecture", args);
|
|
1854
2113
|
});
|
|
2114
|
+
registerTool("get_prd", {
|
|
2115
|
+
annotations: {
|
|
2116
|
+
readOnlyHint: true,
|
|
2117
|
+
destructiveHint: false,
|
|
2118
|
+
idempotentHint: true,
|
|
2119
|
+
openWorldHint: true,
|
|
2120
|
+
},
|
|
2121
|
+
description: "RETRIEVE an already-generated Product Requirements Document (PRD) for a Jira ticket. This tool only fetches an existing PRD — it does NOT start or trigger generation. " +
|
|
2122
|
+
"If no PRD exists yet (or you need a fresh one), call `request_prd` first; it starts the async generation and this `get_prd` tool retrieves the result. " +
|
|
2123
|
+
"Returns the full PRD as markdown text — present it verbatim without summarizing. " +
|
|
2124
|
+
"The PRD is product/stakeholder-facing: problem framing, goals, success metrics, scope, and product requirements. " +
|
|
2125
|
+
"Returns a 404 / not-found response when no PRD is ready yet — that means generation has not run, not that this tool failed.",
|
|
2126
|
+
inputSchema: {
|
|
2127
|
+
ticket_number: z
|
|
2128
|
+
.string()
|
|
2129
|
+
.describe("Jira ticket key in PROJECT-NUMBER format (e.g. PROJ-123, BAPI-42)"),
|
|
2130
|
+
save_locally: z
|
|
2131
|
+
.boolean()
|
|
2132
|
+
.optional()
|
|
2133
|
+
.default(true)
|
|
2134
|
+
.describe("Whether to save the PRD to a local file in the BAPI_DOCS_DIR/prd/ directory. " +
|
|
2135
|
+
"Defaults to true. Set to false to skip saving."),
|
|
2136
|
+
},
|
|
2137
|
+
}, async (args) => {
|
|
2138
|
+
return getTicketArtifact("prd", args);
|
|
2139
|
+
});
|
|
1855
2140
|
registerTool("get_clarifying_questions", {
|
|
1856
2141
|
annotations: {
|
|
1857
2142
|
readOnlyHint: true,
|
|
@@ -2069,7 +2354,7 @@ registerTool("upload_attachment", {
|
|
|
2069
2354
|
"Optionally syncs the content to Bridge API's tickets_links table so retrieval endpoints " +
|
|
2070
2355
|
"(get_clarifying_questions, get_ticket_critique, get_plan) return the updated content without re-generation. " +
|
|
2071
2356
|
"Use link_type to specify which retrieval endpoint should serve this content. " +
|
|
2072
|
-
"Known link_type values: clarifying-questions.md, debugging-guidance.md, ticket-quality-critique.md, architecture-plan.md, fsd-plan.md.",
|
|
2357
|
+
"Known link_type values: clarifying-questions.md, debugging-guidance.md, ticket-quality-critique.md, architecture-plan.md, fsd-plan.md, prd-plan.md.",
|
|
2073
2358
|
inputSchema: {
|
|
2074
2359
|
ticket_number: z
|
|
2075
2360
|
.string()
|
|
@@ -2094,7 +2379,7 @@ registerTool("upload_attachment", {
|
|
|
2094
2379
|
.string()
|
|
2095
2380
|
.optional()
|
|
2096
2381
|
.describe("When provided, also syncs the content to Bridge API's tickets_links table. " +
|
|
2097
|
-
"Known values: clarifying-questions.md, debugging-guidance.md, ticket-quality-critique.md, architecture-plan.md. " +
|
|
2382
|
+
"Known values: clarifying-questions.md, debugging-guidance.md, ticket-quality-critique.md, architecture-plan.md, fsd-plan.md, prd-plan.md. " +
|
|
2098
2383
|
"Unknown values are accepted with a warning."),
|
|
2099
2384
|
replace_existing: z
|
|
2100
2385
|
.boolean()
|
|
@@ -2352,7 +2637,55 @@ registerTool("request_architecture", {
|
|
|
2352
2637
|
}, async (args) => {
|
|
2353
2638
|
return requestTicketArtifact("architecture", args);
|
|
2354
2639
|
});
|
|
2355
|
-
registerTool("
|
|
2640
|
+
registerTool("request_prd", {
|
|
2641
|
+
annotations: {
|
|
2642
|
+
readOnlyHint: false,
|
|
2643
|
+
destructiveHint: false,
|
|
2644
|
+
idempotentHint: false,
|
|
2645
|
+
openWorldHint: true,
|
|
2646
|
+
},
|
|
2647
|
+
description: "START (or refresh) async generation of a Product Requirements Document (PRD) for a Jira ticket. " +
|
|
2648
|
+
"This triggers an asynchronous background job — results are NOT immediate. " +
|
|
2649
|
+
"Processing typically takes 2-4 minutes depending on ticket complexity. " +
|
|
2650
|
+
"The matching get_prd tool retrieves the generated PRD later (call get_prd with the same ticket_number) — unless you set wait_for_result, in which case this call blocks and returns it directly. " +
|
|
2651
|
+
"Returns 202 if the request was accepted, 404 if the ticket does not exist in Jira, " +
|
|
2652
|
+
"or 403 if the API key is unauthorized. " +
|
|
2653
|
+
"Set wait_for_result to true to block until the result is ready (typically 2-4 minutes) instead of returning immediately.",
|
|
2654
|
+
inputSchema: {
|
|
2655
|
+
ticket_number: z
|
|
2656
|
+
.string()
|
|
2657
|
+
.describe("Jira ticket key in PROJECT-NUMBER format (e.g. PROJ-123) to generate a PRD for"),
|
|
2658
|
+
wait_for_result: z
|
|
2659
|
+
.boolean()
|
|
2660
|
+
.optional()
|
|
2661
|
+
.default(false)
|
|
2662
|
+
.describe("When true, the tool blocks and polls until the PRD is ready (typically 2-4 minutes), " +
|
|
2663
|
+
"then returns the full PRD content directly. When false (default), returns immediately " +
|
|
2664
|
+
"with a confirmation message — use get_prd later to retrieve results."),
|
|
2665
|
+
save_locally: z
|
|
2666
|
+
.boolean()
|
|
2667
|
+
.optional()
|
|
2668
|
+
.default(true)
|
|
2669
|
+
.describe("When wait_for_result is true, whether to save the PRD to a local file in the " +
|
|
2670
|
+
"BAPI_DOCS_DIR/prd/ directory. Defaults to true. Only takes effect when wait_for_result is true."),
|
|
2671
|
+
second_opinion: z
|
|
2672
|
+
.string()
|
|
2673
|
+
.optional()
|
|
2674
|
+
.describe("Provider routing override for THIS artifact-generation request " +
|
|
2675
|
+
"(e.g. 'anthropic', 'openai', 'gemini'). When set, the artifact is " +
|
|
2676
|
+
"generated by the named provider and, where supported, a cross-provider " +
|
|
2677
|
+
"second-opinion pass is applied to this request only. " +
|
|
2678
|
+
"This is NOT the standalone `second_opinion` tool — it does not return " +
|
|
2679
|
+
"an ad hoc critique; it only changes which provider produces this " +
|
|
2680
|
+
"request's artifact. Takes precedence over `provider` when both are set."),
|
|
2681
|
+
provider: z.string().optional().describe("Pure provider switch — use a specific LLM provider (openai, anthropic, gemini) without " +
|
|
2682
|
+
"triggering second-opinion semantics. If both provider and second_opinion are set, " +
|
|
2683
|
+
"second_opinion takes precedence."),
|
|
2684
|
+
},
|
|
2685
|
+
}, async (args) => {
|
|
2686
|
+
return requestTicketArtifact("prd", args);
|
|
2687
|
+
});
|
|
2688
|
+
registerTool("create_doc", {
|
|
2356
2689
|
annotations: {
|
|
2357
2690
|
readOnlyHint: false,
|
|
2358
2691
|
destructiveHint: false,
|
|
@@ -2360,27 +2693,28 @@ registerTool("request_design_doc", {
|
|
|
2360
2693
|
openWorldHint: true,
|
|
2361
2694
|
},
|
|
2362
2695
|
description: "START (or refresh) async generation of a design document for a Jira ticket, routed by doc_type. " +
|
|
2363
|
-
"Use doc_type 'tdd' for a Technical Design Document (architecture-focused, for engineers)
|
|
2364
|
-
"Functional Specification Document (product/functional-focused, for PMs, designers, and QA)
|
|
2696
|
+
"Use doc_type 'tdd' for a Technical Design Document (architecture-focused, for engineers), 'fsd' for a " +
|
|
2697
|
+
"Functional Specification Document (product/functional-focused, for PMs, designers, and QA), or 'prd' for a " +
|
|
2698
|
+
"Product Requirements Document (product-requirements-focused: problem, goals, and success metrics). " +
|
|
2365
2699
|
"This triggers an asynchronous background job — results are NOT immediate. " +
|
|
2366
2700
|
"Processing typically takes 2-4 minutes depending on ticket complexity. " +
|
|
2367
|
-
"The matching
|
|
2701
|
+
"The matching get_doc tool retrieves the generated document later (call get_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. " +
|
|
2368
2702
|
"Returns 202 if the request was accepted, 404 if the ticket does not exist in Jira, or 403 if the API key is unauthorized.",
|
|
2369
2703
|
inputSchema: {
|
|
2370
2704
|
ticket_number: z
|
|
2371
2705
|
.string()
|
|
2372
2706
|
.describe("Jira ticket key in PROJECT-NUMBER format (e.g. PROJ-123) to generate a design document for"),
|
|
2373
|
-
doc_type: z
|
|
2374
|
-
.
|
|
2375
|
-
|
|
2376
|
-
"'
|
|
2707
|
+
doc_type: z.enum(["tdd", "fsd", "prd"])
|
|
2708
|
+
.describe("Which design document to generate: 'tdd' (Technical Design Document, engineer audience), " +
|
|
2709
|
+
"'fsd' (Functional Specification Document, product/functional audience), or " +
|
|
2710
|
+
"'prd' (Product Requirements Document, product-requirements focused: problem, goals, success metrics)."),
|
|
2377
2711
|
wait_for_result: z
|
|
2378
2712
|
.boolean()
|
|
2379
2713
|
.optional()
|
|
2380
2714
|
.default(false)
|
|
2381
2715
|
.describe("When true, the tool blocks and polls until the document is ready (typically 2-4 minutes), " +
|
|
2382
2716
|
"then returns the full content directly. When false (default), returns immediately " +
|
|
2383
|
-
"with a confirmation message — use
|
|
2717
|
+
"with a confirmation message — use get_doc later to retrieve results."),
|
|
2384
2718
|
save_locally: z
|
|
2385
2719
|
.boolean()
|
|
2386
2720
|
.optional()
|
|
@@ -2402,7 +2736,7 @@ registerTool("request_design_doc", {
|
|
|
2402
2736
|
}, async (args) => {
|
|
2403
2737
|
return requestTicketArtifact(resolveDesignDocArtifactType(args.doc_type), args);
|
|
2404
2738
|
});
|
|
2405
|
-
registerTool("
|
|
2739
|
+
registerTool("get_doc", {
|
|
2406
2740
|
annotations: {
|
|
2407
2741
|
readOnlyHint: true,
|
|
2408
2742
|
destructiveHint: false,
|
|
@@ -2410,19 +2744,19 @@ registerTool("get_design_doc", {
|
|
|
2410
2744
|
openWorldHint: true,
|
|
2411
2745
|
},
|
|
2412
2746
|
description: "RETRIEVE an already-generated design document for a Jira ticket, routed by doc_type. " +
|
|
2413
|
-
"Use doc_type 'tdd' for the Technical Design Document
|
|
2747
|
+
"Use doc_type 'tdd' for the Technical Design Document, 'fsd' for the Functional Specification Document, or " +
|
|
2748
|
+
"'prd' for the Product Requirements Document. " +
|
|
2414
2749
|
"This tool only fetches an existing document — it does NOT start or trigger generation. " +
|
|
2415
|
-
"If no document exists yet (or you need a fresh one), call `
|
|
2750
|
+
"If no document exists yet (or you need a fresh one), call `create_doc` first with the same doc_type. " +
|
|
2416
2751
|
"Returns the full document as markdown text — present it verbatim without summarizing. " +
|
|
2417
2752
|
"Returns a 404 / not-found response when no document is ready yet — that means generation has not run, not that this tool failed.",
|
|
2418
2753
|
inputSchema: {
|
|
2419
2754
|
ticket_number: z
|
|
2420
2755
|
.string()
|
|
2421
2756
|
.describe("Jira ticket key in PROJECT-NUMBER format (e.g. PROJ-123, BAPI-42)"),
|
|
2422
|
-
doc_type: z
|
|
2423
|
-
.
|
|
2424
|
-
|
|
2425
|
-
"'fsd' (Functional Specification Document)."),
|
|
2757
|
+
doc_type: z.enum(["tdd", "fsd", "prd"])
|
|
2758
|
+
.describe("Which design document to retrieve: 'tdd' (Technical Design Document), " +
|
|
2759
|
+
"'fsd' (Functional Specification Document), or 'prd' (Product Requirements Document)."),
|
|
2426
2760
|
save_locally: z
|
|
2427
2761
|
.boolean()
|
|
2428
2762
|
.optional()
|
|
@@ -2855,7 +3189,7 @@ registerTool("resolve_target_status", {
|
|
|
2855
3189
|
// ---------------------------------------------------------------------------
|
|
2856
3190
|
const VALID_CONFIG_FIELDS = [
|
|
2857
3191
|
"review_instructions", "documentation_instructions", "architecture_instructions",
|
|
2858
|
-
"tdd_document_instructions", "fsd_document_instructions",
|
|
3192
|
+
"tdd_document_instructions", "fsd_document_instructions", "prd_document_instructions",
|
|
2859
3193
|
"unit_testing_instructions", "e2e_testing_instructions",
|
|
2860
3194
|
"unit_testing_stack", "e2e_testing_stack",
|
|
2861
3195
|
"frontend_correctness_standards", "backend_correctness_standards",
|
|
@@ -2864,6 +3198,7 @@ const VALID_CONFIG_FIELDS = [
|
|
|
2864
3198
|
"post_pr_target_status", "ci_check_config", "ci_followup_config",
|
|
2865
3199
|
"allow_mutating_smoke_ops",
|
|
2866
3200
|
"selected_mcp_slugs",
|
|
3201
|
+
"split_review_reorder_enabled",
|
|
2867
3202
|
"base_branch",
|
|
2868
3203
|
"difficulty_model_routing_enabled",
|
|
2869
3204
|
"difficulty_model_tier_overrides",
|
|
@@ -3038,7 +3373,7 @@ registerTool("update_config_field", {
|
|
|
3038
3373
|
}
|
|
3039
3374
|
// Scalar boolean config fields: reject file-path updates and normalize boolean
|
|
3040
3375
|
// true/false and string "true"/"false" to a real boolean before persisting.
|
|
3041
|
-
const BOOLEAN_CONFIG_FIELDS = ["allow_mutating_smoke_ops", "difficulty_model_routing_enabled"];
|
|
3376
|
+
const BOOLEAN_CONFIG_FIELDS = ["allow_mutating_smoke_ops", "difficulty_model_routing_enabled", "split_review_reorder_enabled"];
|
|
3042
3377
|
if (BOOLEAN_CONFIG_FIELDS.includes(field_name)) {
|
|
3043
3378
|
if (file_path) {
|
|
3044
3379
|
return {
|
|
@@ -3163,6 +3498,99 @@ registerTool("apply_install_manifest", {
|
|
|
3163
3498
|
const text = await handleResponse(resp);
|
|
3164
3499
|
return { content: [{ type: "text", text }] };
|
|
3165
3500
|
});
|
|
3501
|
+
registerTool("persist_routing_credential", {
|
|
3502
|
+
annotations: {
|
|
3503
|
+
readOnlyHint: false,
|
|
3504
|
+
destructiveHint: false,
|
|
3505
|
+
idempotentHint: true,
|
|
3506
|
+
openWorldHint: false,
|
|
3507
|
+
},
|
|
3508
|
+
description: "Persist the ALREADY-VALIDATED Bridge API key for this repo into the user-scoped credential " +
|
|
3509
|
+
"store (`~/.config/bridge/credentials.json`) under the target `bapi:<repo_name>`, so that " +
|
|
3510
|
+
"Bash-spawned CLI features such as `start-tickets` (a different runtime surface than the MCP " +
|
|
3511
|
+
"server) can resolve it for difficulty→model routing. This is the final stage of `/install-bridge`. " +
|
|
3512
|
+
"The key is resolved INSIDE the MCP server process (env-first, then the existing store) using the " +
|
|
3513
|
+
"provided `repo_name` as the store identity — it is NEVER passed as a tool argument. Existing " +
|
|
3514
|
+
"credentials are preserved; only `BAPI_API_KEY` for this repo is upserted. The response is " +
|
|
3515
|
+
"secret-free (it reports ok/action/target/path only) and never echoes the key value.",
|
|
3516
|
+
inputSchema: {
|
|
3517
|
+
repo_name: z
|
|
3518
|
+
.string()
|
|
3519
|
+
.describe("The repository name to store the routing credential under (target `bapi:<repo_name>`). " +
|
|
3520
|
+
"This is the ONLY input — do not pass the API key, a secret, or a token; the key is " +
|
|
3521
|
+
"resolved inside the MCP server process."),
|
|
3522
|
+
},
|
|
3523
|
+
}, async ({ repo_name }) => {
|
|
3524
|
+
const repoName = typeof repo_name === "string" ? repo_name.trim() : "";
|
|
3525
|
+
const deps = buildCredentialStoreWriteDeps();
|
|
3526
|
+
const storePath = getPrimaryCredentialStorePath(deps);
|
|
3527
|
+
if (repoName.length === 0) {
|
|
3528
|
+
return {
|
|
3529
|
+
content: [
|
|
3530
|
+
{
|
|
3531
|
+
type: "text",
|
|
3532
|
+
text: JSON.stringify({
|
|
3533
|
+
ok: false,
|
|
3534
|
+
message: "Cannot persist routing credential: repo_name is required. Pass the repo name " +
|
|
3535
|
+
"this install is configuring.",
|
|
3536
|
+
path: storePath,
|
|
3537
|
+
}),
|
|
3538
|
+
},
|
|
3539
|
+
],
|
|
3540
|
+
};
|
|
3541
|
+
}
|
|
3542
|
+
const target = `bapi:${repoName}`;
|
|
3543
|
+
const apiKey = await getResolvedApiKeyForRepo(repoName);
|
|
3544
|
+
if (apiKey.length === 0) {
|
|
3545
|
+
return {
|
|
3546
|
+
content: [
|
|
3547
|
+
{
|
|
3548
|
+
type: "text",
|
|
3549
|
+
text: JSON.stringify({
|
|
3550
|
+
ok: false,
|
|
3551
|
+
target,
|
|
3552
|
+
path: storePath,
|
|
3553
|
+
message: `No BAPI_API_KEY could be resolved for ${target}. Set BAPI_API_KEY in the ` +
|
|
3554
|
+
`environment (or add it under ${target} in ${storePath}) and rerun /install-bridge.`,
|
|
3555
|
+
}),
|
|
3556
|
+
},
|
|
3557
|
+
],
|
|
3558
|
+
};
|
|
3559
|
+
}
|
|
3560
|
+
const result = await upsertBapiCredential(repoName, apiKey, deps);
|
|
3561
|
+
if (!result.ok) {
|
|
3562
|
+
return {
|
|
3563
|
+
content: [
|
|
3564
|
+
{
|
|
3565
|
+
type: "text",
|
|
3566
|
+
text: JSON.stringify({
|
|
3567
|
+
ok: false,
|
|
3568
|
+
target: result.target,
|
|
3569
|
+
path: result.path,
|
|
3570
|
+
kind: result.kind,
|
|
3571
|
+
message: `Failed to persist routing credential for ${result.target}: ${result.error} ` +
|
|
3572
|
+
`You can rerun /install-bridge or migrate manually.`,
|
|
3573
|
+
}),
|
|
3574
|
+
},
|
|
3575
|
+
],
|
|
3576
|
+
};
|
|
3577
|
+
}
|
|
3578
|
+
return {
|
|
3579
|
+
content: [
|
|
3580
|
+
{
|
|
3581
|
+
type: "text",
|
|
3582
|
+
text: JSON.stringify({
|
|
3583
|
+
ok: true,
|
|
3584
|
+
action: result.action,
|
|
3585
|
+
target: result.target,
|
|
3586
|
+
path: result.path,
|
|
3587
|
+
migratedFallback: result.migratedFallback,
|
|
3588
|
+
message: `Stored routing credential for ${result.target} at ${result.path}.`,
|
|
3589
|
+
}),
|
|
3590
|
+
},
|
|
3591
|
+
],
|
|
3592
|
+
};
|
|
3593
|
+
});
|
|
3166
3594
|
function formatDeepResearchProviderReason(meta) {
|
|
3167
3595
|
if (!meta)
|
|
3168
3596
|
return "";
|
|
@@ -3482,21 +3910,23 @@ registerTool("request_brainstorm", {
|
|
|
3482
3910
|
openWorldHint: true,
|
|
3483
3911
|
},
|
|
3484
3912
|
description: "START an async brainstorm that fans out the task to two opinion-provider LLMs " +
|
|
3485
|
-
"(default: OpenAI + Gemini) and
|
|
3913
|
+
"(default: OpenAI + Gemini) and returns each provider's opinion directly (no synthesizer pass). " +
|
|
3486
3914
|
"Returns a brainstorm_id; the matching get_brainstorm tool retrieves the finished result later (unless you set wait_for_result). " +
|
|
3487
3915
|
"\n\n" +
|
|
3488
3916
|
"BEHAVIOR: By default, returns immediately with a brainstorm_id. Set wait_for_result=true " +
|
|
3489
3917
|
"to poll for terminal status (up to 15 minutes), then retrieve and optionally save the result. " +
|
|
3490
3918
|
"When save_locally=true (default), each provider's markdown is written with a semantic " +
|
|
3491
3919
|
"filename derived from task_description: " +
|
|
3492
|
-
"BAPI_DOCS_DIR/brainstorm/{slugified-task-description}-{short_brainstorm_id}-{provider}.md
|
|
3493
|
-
"and the synthesizer row follows the same pattern " +
|
|
3494
|
-
"({slugified-task-description}-{short_brainstorm_id}-synthesizer.md). " +
|
|
3920
|
+
"BAPI_DOCS_DIR/brainstorm/{slugified-task-description}-{short_brainstorm_id}-{provider}.md. " +
|
|
3495
3921
|
"(If task_description is empty or slugifies to nothing, it falls back to {brainstorm_id}-{provider}.md.) " +
|
|
3496
3922
|
"\n\n" +
|
|
3497
|
-
"
|
|
3498
|
-
"
|
|
3499
|
-
"
|
|
3923
|
+
"MODES: Set mode to pick the brainstorm style. 'technical' (default) runs the implementation/" +
|
|
3924
|
+
"architecture brainstorm. 'design' runs web-page/UI design ideation focused on visual appeal and " +
|
|
3925
|
+
"conversion. 'discovery' generates stakeholder discovery questions for early/vague tasks, grouped " +
|
|
3926
|
+
"into 'Technical Discovery Questions' and 'Business / Stakeholder Discovery Questions' and tagged " +
|
|
3927
|
+
"[HUMAN], [CODE], or [TICKET]; discovery needs no extra configuration. " +
|
|
3928
|
+
"The legacy boolean design=true still works and maps to mode='design'; prefer the explicit mode " +
|
|
3929
|
+
"selector for new callers.",
|
|
3500
3930
|
inputSchema: {
|
|
3501
3931
|
task_description: z.string().describe("Free-form description of the task to brainstorm about. Sent verbatim — " +
|
|
3502
3932
|
"this tool does NOT read task_description from a file."),
|
|
@@ -3504,7 +3934,8 @@ registerTool("request_brainstorm", {
|
|
|
3504
3934
|
ticket_number: z.string().optional().describe("Optional Jira ticket key (e.g. PROJ-123) to associate with the brainstorm. " +
|
|
3505
3935
|
"Ticket 1 only stores this for cross-reference — no Jira writes happen."),
|
|
3506
3936
|
providers: z.array(z.string()).optional().describe("Opinion-provider LLMs. Defaults to ['openai', 'gemini']. " +
|
|
3507
|
-
"A single-provider request
|
|
3937
|
+
"A single-provider request runs one opinion provider and returns that " +
|
|
3938
|
+
"provider's markdown directly."),
|
|
3508
3939
|
concerns: z.string().optional().describe("Optional caller-supplied concerns to surface to the brainstorm agents."),
|
|
3509
3940
|
wait_for_result: z.boolean().optional().describe("When true, polls until every row reaches a terminal status (max 15 minutes), " +
|
|
3510
3941
|
"then returns the full result envelope. When false (default), returns immediately."),
|
|
@@ -3512,11 +3943,17 @@ registerTool("request_brainstorm", {
|
|
|
3512
3943
|
"after the result is fetched. Request-time saves use semantic filenames derived from " +
|
|
3513
3944
|
"task_description (slugified) plus a short brainstorm-id segment."),
|
|
3514
3945
|
prior_brainstorm_id: z.string().optional().describe("Optional brainstorm_id from an earlier brainstorm to refine. " +
|
|
3515
|
-
"When provided, the
|
|
3516
|
-
"
|
|
3517
|
-
|
|
3946
|
+
"When provided, the prior brainstorm's completed opinion-provider " +
|
|
3947
|
+
"markdowns are concatenated and supplied as prior context."),
|
|
3948
|
+
mode: z.enum(["technical", "design", "discovery"]).optional().describe("Preferred brainstorm-mode selector for new callers. 'technical' (default) is the " +
|
|
3949
|
+
"implementation/architecture brainstorm; 'design' is web-page/UI visual-direction ideation; " +
|
|
3950
|
+
"'discovery' generates grouped technical and business/stakeholder discovery questions for " +
|
|
3951
|
+
"early/vague tasks. Takes precedence over the legacy boolean design field."),
|
|
3952
|
+
design: z.boolean().optional().describe("Legacy compatibility flag: set to true for web-page/UI design ideation focused on visual appeal " +
|
|
3953
|
+
"and conversion. New callers should use mode: \"design\" instead. Omit this field when not " +
|
|
3954
|
+
"requesting design mode; absent is treated as false."),
|
|
3518
3955
|
},
|
|
3519
|
-
}, async ({ task_description, repo_name, ticket_number, providers, concerns, wait_for_result, save_locally, prior_brainstorm_id, design, }) => {
|
|
3956
|
+
}, async ({ task_description, repo_name, ticket_number, providers, concerns, wait_for_result, save_locally, prior_brainstorm_id, mode, design, }) => {
|
|
3520
3957
|
const effectiveRepo = repo_name && repo_name.length > 0 ? repo_name : REPO_NAME;
|
|
3521
3958
|
const effectiveProviders = providers !== undefined ? providers : ["openai", "gemini"];
|
|
3522
3959
|
const shouldWait = wait_for_result === true;
|
|
@@ -3533,6 +3970,14 @@ registerTool("request_brainstorm", {
|
|
|
3533
3970
|
if (prior_brainstorm_id) {
|
|
3534
3971
|
submitPayload.prior_brainstorm_request_id = prior_brainstorm_id;
|
|
3535
3972
|
}
|
|
3973
|
+
// Forward `mode` only when the caller explicitly set one, mirroring the
|
|
3974
|
+
// legacy `design` handling below. Leaving it absent lets the backend apply
|
|
3975
|
+
// its own precedence (legacy `design` → prior-row inheritance → technical),
|
|
3976
|
+
// which is required for a design/discovery refinement to keep its mode.
|
|
3977
|
+
if (mode)
|
|
3978
|
+
submitPayload.mode = mode;
|
|
3979
|
+
// Preserve the legacy boolean mapping so a backend that only reads `design`
|
|
3980
|
+
// still behaves correctly.
|
|
3536
3981
|
if (design)
|
|
3537
3982
|
submitPayload.design = true;
|
|
3538
3983
|
const submitResp = await fetch(buildUrl("/brainstorms"), {
|
|
@@ -3544,11 +3989,13 @@ registerTool("request_brainstorm", {
|
|
|
3544
3989
|
const errorText = await handleResponse(submitResp);
|
|
3545
3990
|
return { content: [{ type: "text", text: errorText }] };
|
|
3546
3991
|
}
|
|
3992
|
+
// ``synthesizer_status`` is a temporary backend compatibility sentinel
|
|
3993
|
+
// (always "removed"); it is optional here and never drives control flow.
|
|
3547
3994
|
const submitBody = (await submitResp.json());
|
|
3548
3995
|
if (!shouldWait) {
|
|
3549
3996
|
const confirmation = `Brainstorm submitted (brainstorm_id: ${submitBody.brainstorm_id}). ` +
|
|
3550
3997
|
`Providers: ${submitBody.providers.join(", ")}. ` +
|
|
3551
|
-
`
|
|
3998
|
+
`Synthesis step: removed; provider opinions will be returned directly. ` +
|
|
3552
3999
|
`Use get_brainstorm with brainstorm_id ${submitBody.brainstorm_id} to retrieve results.`;
|
|
3553
4000
|
return { content: [{ type: "text", text: confirmation }] };
|
|
3554
4001
|
}
|
|
@@ -3591,9 +4038,9 @@ registerTool("get_brainstorm", {
|
|
|
3591
4038
|
},
|
|
3592
4039
|
description: "RETRIEVE the result envelope for a previously submitted brainstorm by brainstorm_id. This tool only fetches an existing/in-progress result — it does NOT start or trigger a new brainstorm. " +
|
|
3593
4040
|
"If you have not submitted a brainstorm yet (or you need a new one), call `request_brainstorm` first; it starts the async brainstorm and returns the brainstorm_id this `get_brainstorm` tool retrieves by. " +
|
|
3594
|
-
"Returns
|
|
4041
|
+
"Returns opinion-provider rows only, including error_kind for every row; a not-found / still-processing response means the brainstorm has not completed, not that this tool failed. " +
|
|
3595
4042
|
"When save_locally=true (default), writes each provider's markdown to " +
|
|
3596
|
-
"BAPI_DOCS_DIR/brainstorm/{brainstorm_id}-{provider}.md
|
|
4043
|
+
"BAPI_DOCS_DIR/brainstorm/{brainstorm_id}-{provider}.md. " +
|
|
3597
4044
|
"Retrieval uses this UUID-only filename (not the semantic task-description name that " +
|
|
3598
4045
|
"request_brainstorm uses) because the original task description is not available in the " +
|
|
3599
4046
|
"result envelope on retrieval. " +
|
|
@@ -3732,6 +4179,23 @@ const pollCiChecksTool = registerTool("poll_ci_checks", {
|
|
|
3732
4179
|
});
|
|
3733
4180
|
const resp = await fetch(url, { headers: await getGetHeaders() });
|
|
3734
4181
|
const text = await handleResponse(resp);
|
|
4182
|
+
// BAPI-395: opportunistically emit conductor PR/CI/gate events from the
|
|
4183
|
+
// already-fetched poll response. Fully best-effort — the producer validates
|
|
4184
|
+
// that commit_ref binds to the local PR head before emitting, and any failure
|
|
4185
|
+
// is swallowed so this NEVER changes the poll_ci_checks response body.
|
|
4186
|
+
try {
|
|
4187
|
+
const parsed = JSON.parse(text);
|
|
4188
|
+
const looksLikePollStatus = parsed !== null &&
|
|
4189
|
+
typeof parsed === "object" &&
|
|
4190
|
+
!("error" in parsed) &&
|
|
4191
|
+
(Array.isArray(parsed.checks) || typeof parsed.all_complete === "boolean");
|
|
4192
|
+
if (looksLikePollStatus) {
|
|
4193
|
+
void observePrCiFromPollResponse(commit_ref, parsed).catch(() => { });
|
|
4194
|
+
}
|
|
4195
|
+
}
|
|
4196
|
+
catch {
|
|
4197
|
+
/* non-JSON or non-poll response — nothing to produce */
|
|
4198
|
+
}
|
|
3735
4199
|
return { content: [{ type: "text", text }] };
|
|
3736
4200
|
});
|
|
3737
4201
|
// ---------------------------------------------------------------------------
|
|
@@ -3887,7 +4351,7 @@ registerTool("get_pipeline_recipe", {
|
|
|
3887
4351
|
if ("idea" in mergedVariables) {
|
|
3888
4352
|
mergedVariables.idea_hash = deriveIdeaHash(mergedVariables.idea);
|
|
3889
4353
|
}
|
|
3890
|
-
const recipe = resolveRecipe(pipelineDef, INSTRUCTIONS, mergedVariables, skip_steps, !!auto_approve);
|
|
4354
|
+
const recipe = resolveRecipe(pipelineDef, INSTRUCTIONS, mergedVariables, skip_steps, !!auto_approve, { includeUpgradeAdviceSurfacing: UPGRADE_ADVICE_SURFACING_ENABLED });
|
|
3891
4355
|
return {
|
|
3892
4356
|
content: [{
|
|
3893
4357
|
type: "text",
|
|
@@ -3929,6 +4393,7 @@ async function buildPipelineOrchestratorDeps() {
|
|
|
3929
4393
|
pipelines: PIPELINES,
|
|
3930
4394
|
instructions: INSTRUCTIONS,
|
|
3931
4395
|
toolHandlers: TOOL_HANDLERS,
|
|
4396
|
+
includeUpgradeAdviceSurfacing: UPGRADE_ADVICE_SURFACING_ENABLED,
|
|
3932
4397
|
};
|
|
3933
4398
|
}
|
|
3934
4399
|
// BAPI-326: dependency injection for the full-automation chain orchestrator.
|
|
@@ -3945,6 +4410,7 @@ async function buildChainOrchestratorDeps() {
|
|
|
3945
4410
|
chainRecipes: CHAIN_RECIPES,
|
|
3946
4411
|
instructions: INSTRUCTIONS,
|
|
3947
4412
|
toolHandlers: TOOL_HANDLERS,
|
|
4413
|
+
includeUpgradeAdviceSurfacing: UPGRADE_ADVICE_SURFACING_ENABLED,
|
|
3948
4414
|
};
|
|
3949
4415
|
}
|
|
3950
4416
|
registerTool("run_pipeline", {
|
|
@@ -4290,7 +4756,11 @@ registerTool("generate_decision_page", {
|
|
|
4290
4756
|
"original-question and closed-by-default codebase-evidence display, and an optional confirmed-" +
|
|
4291
4757
|
"improvements list. Presentation labels (title, intro, section/improvements headings) are " +
|
|
4292
4758
|
"overridable, and the output location under the docs directory is configurable, so automations " +
|
|
4293
|
-
"beyond ticket review can reuse it.
|
|
4759
|
+
"beyond ticket review can reuse it. Pass artifact_type=\"pre_ticket_planning\" to additionally " +
|
|
4760
|
+
"render read-only system_goals (business goal, desired end-state, system behavior, classified " +
|
|
4761
|
+
"NFRs) and a read-only implementation_order section for pre-ticket epic/task framing; open NFRs " +
|
|
4762
|
+
"still go in actionable_items so the human can decide them. The default artifact_type " +
|
|
4763
|
+
"\"review_decisions\" is unchanged. The user opens the HTML file in a browser, makes selections, " +
|
|
4294
4764
|
"and copies the resulting JSON output back to the agent.",
|
|
4295
4765
|
inputSchema: DecisionPageInputShape,
|
|
4296
4766
|
}, async (input) => {
|
|
@@ -4307,8 +4777,13 @@ registerTool("generate_decision_page", {
|
|
|
4307
4777
|
if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(input.ticket_key)) {
|
|
4308
4778
|
return validationError(`Invalid ticket_key "${input.ticket_key}": must start with a letter and contain only letters, digits, hyphens, or underscores.`);
|
|
4309
4779
|
}
|
|
4310
|
-
// No-decisions fast path: return structured response without writing a file
|
|
4311
|
-
|
|
4780
|
+
// No-decisions fast path: return structured response without writing a file.
|
|
4781
|
+
// pre_ticket_planning pages still render when they carry read-only goals or an
|
|
4782
|
+
// implementation order even with zero open NFRs, so only short-circuit when
|
|
4783
|
+
// there is genuinely nothing to show. review_decisions behavior is unchanged
|
|
4784
|
+
// (it never sets system_goals/implementation_order).
|
|
4785
|
+
const hasPlanningContent = input.system_goals !== undefined || (input.implementation_order?.length ?? 0) > 0;
|
|
4786
|
+
if (input.actionable_items.length === 0 && !hasPlanningContent) {
|
|
4312
4787
|
return {
|
|
4313
4788
|
content: [{
|
|
4314
4789
|
type: "text",
|
|
@@ -4350,10 +4825,27 @@ registerTool("generate_decision_page", {
|
|
|
4350
4825
|
if (!outputTarget.ok) {
|
|
4351
4826
|
return validationError(outputTarget.message);
|
|
4352
4827
|
}
|
|
4353
|
-
// Read design assets and base64-encode for embedding
|
|
4828
|
+
// Read design assets and base64-encode for embedding.
|
|
4829
|
+
// Prefer the project root (local dev); fall back to the installed package root
|
|
4830
|
+
// (consumer projects where design-assets/ isn't part of the project tree).
|
|
4354
4831
|
const projectRootForAssets = await getProjectRoot();
|
|
4355
|
-
const
|
|
4356
|
-
|
|
4832
|
+
const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../");
|
|
4833
|
+
let assetsDir;
|
|
4834
|
+
try {
|
|
4835
|
+
await stat(path.join(projectRootForAssets, "design-assets"));
|
|
4836
|
+
assetsDir = path.join(projectRootForAssets, "design-assets");
|
|
4837
|
+
}
|
|
4838
|
+
catch {
|
|
4839
|
+
assetsDir = path.join(pkgRoot, "design-assets");
|
|
4840
|
+
}
|
|
4841
|
+
let fontsDir;
|
|
4842
|
+
try {
|
|
4843
|
+
await stat(path.join(projectRootForAssets, "public", "fonts"));
|
|
4844
|
+
fontsDir = path.join(projectRootForAssets, "public", "fonts");
|
|
4845
|
+
}
|
|
4846
|
+
catch {
|
|
4847
|
+
fontsDir = path.join(pkgRoot, "public", "fonts");
|
|
4848
|
+
}
|
|
4357
4849
|
let faviconBase64 = "";
|
|
4358
4850
|
let logoBase64 = "";
|
|
4359
4851
|
try {
|
|
@@ -4384,8 +4876,11 @@ registerTool("generate_decision_page", {
|
|
|
4384
4876
|
text: JSON.stringify({
|
|
4385
4877
|
status: "decision_page_generated",
|
|
4386
4878
|
file_path: filePath,
|
|
4879
|
+
artifact_type: input.artifact_type,
|
|
4387
4880
|
actionable_items_count: input.actionable_items.length,
|
|
4388
4881
|
clear_improvements_count: input.clear_improvements.length,
|
|
4882
|
+
system_goals_nfr_count: input.system_goals?.nfrs?.length ?? 0,
|
|
4883
|
+
implementation_order_count: input.implementation_order?.length ?? 0,
|
|
4389
4884
|
}),
|
|
4390
4885
|
}],
|
|
4391
4886
|
};
|