@bridge_gpt/mcp-server 0.2.2 → 0.2.4
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 +554 -66
- 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 +1 -1
- 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 +682 -81
- 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 +17 -5
- 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 +17 -9
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";
|
|
@@ -27,13 +32,18 @@ import { checkForUpdate } from "./update-check.js";
|
|
|
27
32
|
import { reconstructAgentMarkdown, translateAgentToCopilot } from "./agent-utils.js";
|
|
28
33
|
import { resolveRecipe, loadCustomPipelines } from "./pipeline-utils.js";
|
|
29
34
|
import { runStartTicketsCli } from "./start-tickets.js";
|
|
35
|
+
import { runReviewTicketsCli } from "./review-tickets.js";
|
|
30
36
|
import { runDoctorCli } from "./doctor.js";
|
|
31
37
|
import { runScheduleRunCli } from "./schedule-run.js";
|
|
32
38
|
import { runMcpInvokeCli } from "./mcp-invoke.js";
|
|
33
39
|
import { runAgentCapabilitiesCli } from "./agent-capabilities/cli.js";
|
|
34
40
|
import { validateRepoName } from "./bridge-config.js";
|
|
35
41
|
import { ensureGitignored as ensureGitignoredShared } from "./git-ignore-utils.js";
|
|
36
|
-
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";
|
|
37
47
|
import { generateDecisionPageHtml } from "./decision-page-template.js";
|
|
38
48
|
import { DecisionPageInputShape } from "./decision-page-schema.js";
|
|
39
49
|
import { slugify, saveBrainstormResultsToDir, } from "./brainstorm-files.js";
|
|
@@ -48,6 +58,25 @@ let userPipelineKeys = new Set();
|
|
|
48
58
|
// ---------------------------------------------------------------------------
|
|
49
59
|
const BASE_URL = process.env.BAPI_BASE_URL ?? "https://bridgegpt-api.com";
|
|
50
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);
|
|
51
80
|
// ---------------------------------------------------------------------------
|
|
52
81
|
// Resolved-once credential + path accessors (BAPI-338)
|
|
53
82
|
//
|
|
@@ -86,6 +115,45 @@ async function getResolvedApiKey() {
|
|
|
86
115
|
}
|
|
87
116
|
return resolvedApiKeyPromise;
|
|
88
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
|
+
}
|
|
89
157
|
/** GET auth headers, with the API key resolved once via {@link getResolvedApiKey}. */
|
|
90
158
|
async function getGetHeaders() {
|
|
91
159
|
return {
|
|
@@ -458,6 +526,78 @@ async function resolveTextOrFile(textValue, filePath, textLabel) {
|
|
|
458
526
|
}
|
|
459
527
|
return { ok: true, text: resolvedText, note };
|
|
460
528
|
}
|
|
529
|
+
const BINARY_EXTENSIONS = new Set([
|
|
530
|
+
".png", ".jpg", ".jpeg", ".gif", ".webp", ".heic", ".heif",
|
|
531
|
+
".bmp", ".tiff", ".pdf", ".zip", ".docx", ".xlsx", ".pptx",
|
|
532
|
+
]);
|
|
533
|
+
async function resolveUploadAttachment(textValue, filePath, textLabel) {
|
|
534
|
+
if (!filePath && !textValue) {
|
|
535
|
+
return {
|
|
536
|
+
ok: false,
|
|
537
|
+
errorResponse: {
|
|
538
|
+
content: [{
|
|
539
|
+
type: "text",
|
|
540
|
+
text: JSON.stringify({ error: "BAD_REQUEST", status: 400, message: `Either ${textLabel} or file_path must be provided.` }),
|
|
541
|
+
}],
|
|
542
|
+
},
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
if (!filePath) {
|
|
546
|
+
return { ok: true, text: textValue, encoding: undefined, note: "" };
|
|
547
|
+
}
|
|
548
|
+
try {
|
|
549
|
+
const fileStat = await stat(filePath);
|
|
550
|
+
if (fileStat.size > 10 * 1024 * 1024) { // mirrors MAX_ATTACHMENT_SIZE in api/library/jira/jira_lib.py
|
|
551
|
+
return {
|
|
552
|
+
ok: false,
|
|
553
|
+
errorResponse: {
|
|
554
|
+
content: [{
|
|
555
|
+
type: "text",
|
|
556
|
+
text: JSON.stringify({ error: "BAD_REQUEST", status: 400, message: `File at ${filePath} exceeds 10MB size limit.` }),
|
|
557
|
+
}],
|
|
558
|
+
},
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
562
|
+
const buf = await readFile(filePath);
|
|
563
|
+
let isBinary = BINARY_EXTENSIONS.has(ext);
|
|
564
|
+
if (!isBinary) {
|
|
565
|
+
try {
|
|
566
|
+
new TextDecoder("utf-8", { fatal: true }).decode(buf);
|
|
567
|
+
}
|
|
568
|
+
catch {
|
|
569
|
+
isBinary = true;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
const note = textValue ? `\n\nNote: Both file_path and ${textLabel} were provided. file_path content was used.` : "";
|
|
573
|
+
if (isBinary) {
|
|
574
|
+
return { ok: true, text: buf.toString("base64"), encoding: "base64", note };
|
|
575
|
+
}
|
|
576
|
+
if (buf.length > 1_048_576) {
|
|
577
|
+
return {
|
|
578
|
+
ok: false,
|
|
579
|
+
errorResponse: {
|
|
580
|
+
content: [{
|
|
581
|
+
type: "text",
|
|
582
|
+
text: JSON.stringify({ error: "BAD_REQUEST", status: 400, message: `Text file at ${filePath} exceeds 1MB size limit.` }),
|
|
583
|
+
}],
|
|
584
|
+
},
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
return { ok: true, text: buf.toString("utf8"), encoding: undefined, note };
|
|
588
|
+
}
|
|
589
|
+
catch (err) {
|
|
590
|
+
return {
|
|
591
|
+
ok: false,
|
|
592
|
+
errorResponse: {
|
|
593
|
+
content: [{
|
|
594
|
+
type: "text",
|
|
595
|
+
text: JSON.stringify({ error: "BAD_REQUEST", status: 400, message: `Error reading file at ${filePath}: ${err instanceof Error ? err.message : String(err)}` }),
|
|
596
|
+
}],
|
|
597
|
+
},
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
}
|
|
461
601
|
async function pollForResult(getUrl, timeoutMs, label) {
|
|
462
602
|
const startTime = Date.now();
|
|
463
603
|
let pollIntervalMs = 15_000;
|
|
@@ -507,7 +647,7 @@ const TICKET_ARTIFACTS = {
|
|
|
507
647
|
requestErrorPrefix: "Failed to request architecture generation: ",
|
|
508
648
|
confirmationText: (n) => `Architecture generation requested for ${n}. ` +
|
|
509
649
|
`Processing typically takes 2-4 minutes. ` +
|
|
510
|
-
`Use get_architecture with ticket_number "${n}" to retrieve the architecture plan once processing completes.`,
|
|
650
|
+
`Use get_architecture with ticket_number "${n}" (or get_doc with doc_type "tdd") to retrieve the architecture plan once processing completes.`,
|
|
511
651
|
pollLabel: (n) => `Architecture generation for ${n}`,
|
|
512
652
|
},
|
|
513
653
|
fsd: {
|
|
@@ -519,9 +659,21 @@ const TICKET_ARTIFACTS = {
|
|
|
519
659
|
requestErrorPrefix: "Failed to request FSD generation: ",
|
|
520
660
|
confirmationText: (n) => `FSD generation requested for ${n}. ` +
|
|
521
661
|
`Processing typically takes 2-4 minutes. ` +
|
|
522
|
-
`Use
|
|
662
|
+
`Use get_doc with ticket_number "${n}" and doc_type "fsd" to retrieve the functional specification document once processing completes.`,
|
|
523
663
|
pollLabel: (n) => `FSD generation for ${n}`,
|
|
524
664
|
},
|
|
665
|
+
prd: {
|
|
666
|
+
kind: "single",
|
|
667
|
+
generateEndpoint: (n) => `/ticket/${encodeURIComponent(n)}/generate-prd`,
|
|
668
|
+
getEndpoint: (n) => `/ticket/${encodeURIComponent(n)}/prd`,
|
|
669
|
+
saveSubdir: "prd",
|
|
670
|
+
filename: (n) => `${n}-prd-plan.md`,
|
|
671
|
+
requestErrorPrefix: "Failed to request PRD generation: ",
|
|
672
|
+
confirmationText: (n) => `PRD generation requested for ${n}. ` +
|
|
673
|
+
`Processing typically takes 2-4 minutes. ` +
|
|
674
|
+
`Use get_prd with ticket_number "${n}" (or get_doc with doc_type "prd") to retrieve the PRD once processing completes.`,
|
|
675
|
+
pollLabel: (n) => `PRD generation for ${n}`,
|
|
676
|
+
},
|
|
525
677
|
clarifying_questions: {
|
|
526
678
|
kind: "single",
|
|
527
679
|
generateEndpoint: (n) => `/ticket/${encodeURIComponent(n)}/generate-clarifying-questions`,
|
|
@@ -598,6 +750,7 @@ async function getTicketArtifactDocsPath(subdir) {
|
|
|
598
750
|
return getDocsPath("architecture");
|
|
599
751
|
case "fsd":
|
|
600
752
|
return getDocsPath("fsd");
|
|
753
|
+
case "prd": return getDocsPath("prd");
|
|
601
754
|
case "clarifying-questions":
|
|
602
755
|
return getDocsPath("clarifying-questions");
|
|
603
756
|
case "ticket-critiques":
|
|
@@ -610,11 +763,15 @@ async function getTicketArtifactDocsPath(subdir) {
|
|
|
610
763
|
// `tdd` reuses the existing "architecture" artifact (the TDD/architecture
|
|
611
764
|
// document) WITHOUT renaming it, preserving the back-compatible
|
|
612
765
|
// request_architecture / get_architecture tools. `fsd` routes to the new "fsd"
|
|
613
|
-
// artifact.
|
|
766
|
+
// artifact, and `prd` routes to the new "prd" artifact.
|
|
614
767
|
function resolveDesignDocArtifactType(docType) {
|
|
615
|
-
|
|
768
|
+
if (docType === "fsd")
|
|
769
|
+
return "fsd";
|
|
770
|
+
if (docType === "prd")
|
|
771
|
+
return "prd";
|
|
772
|
+
return "architecture";
|
|
616
773
|
}
|
|
617
|
-
// Shared request flow for the
|
|
774
|
+
// Shared request flow for the six single-artifact request_* tools: POST the
|
|
618
775
|
// generate endpoint, return the per-tool error prefix on a non-OK POST, and on
|
|
619
776
|
// wait_for_result poll the get endpoint (900_000 ms) and optionally save.
|
|
620
777
|
async function requestTicketArtifact(type, args) {
|
|
@@ -647,7 +804,7 @@ async function requestTicketArtifact(type, args) {
|
|
|
647
804
|
content: [{ type: "text", text: config.confirmationText(args.ticket_number) }],
|
|
648
805
|
};
|
|
649
806
|
}
|
|
650
|
-
// Shared GET/save flow for the
|
|
807
|
+
// Shared GET/save flow for the six single-artifact get_* tools. Normal gets
|
|
651
808
|
// save only on `resp.ok && save_locally`. reimplement_context is asymmetric: it
|
|
652
809
|
// short-circuits a 404 with a custom NOT_FOUND envelope (without leaking the
|
|
653
810
|
// backend body) and otherwise saves on ANY non-404 response when save_locally.
|
|
@@ -1286,6 +1443,9 @@ async function dispatchCliSubcommand(argv) {
|
|
|
1286
1443
|
if (argv[0] === "start-tickets") {
|
|
1287
1444
|
return runStartTicketsCli(argv.slice(1));
|
|
1288
1445
|
}
|
|
1446
|
+
if (argv[0] === "review-tickets") {
|
|
1447
|
+
return runReviewTicketsCli(argv.slice(1));
|
|
1448
|
+
}
|
|
1289
1449
|
// The internal `mcp-invoke` worktree shim (BAPI-337) is a positional
|
|
1290
1450
|
// subcommand routed before the flag guards and well before MCP server
|
|
1291
1451
|
// construction: it resolves identity/credentials from `--project-root` and
|
|
@@ -1311,6 +1471,21 @@ async function dispatchCliSubcommand(argv) {
|
|
|
1311
1471
|
if (argv[0] === "agent-capabilities") {
|
|
1312
1472
|
return runAgentCapabilitiesCli(argv.slice(1));
|
|
1313
1473
|
}
|
|
1474
|
+
// The `credentials` subcommand (BAPI-377) hosts the consent-gated agent-config
|
|
1475
|
+
// credential migration. It is a positional subcommand routed before the
|
|
1476
|
+
// --init / --upgrade flag guards and well before MCP server construction so
|
|
1477
|
+
// migration never falls through to the normal no-subcommand startup path. It
|
|
1478
|
+
// is the ONLY place credential migration lives — doctor stays strictly read-only.
|
|
1479
|
+
if (argv[0] === "credentials") {
|
|
1480
|
+
return runCredentialsCli(argv.slice(1));
|
|
1481
|
+
}
|
|
1482
|
+
// The `conductor` subcommand (BAPI-393) is the local event-ledger CLI for fast
|
|
1483
|
+
// hooks and operator diagnostics. It is a positional subcommand routed before
|
|
1484
|
+
// the --init / --upgrade flag guards and well before MCP server construction;
|
|
1485
|
+
// it talks ONLY to the local SQLite ledger and never starts the MCP server.
|
|
1486
|
+
if (argv[0] === "conductor") {
|
|
1487
|
+
return runConductorCli(argv.slice(1));
|
|
1488
|
+
}
|
|
1314
1489
|
// --init takes precedence over --upgrade; both are position-independent flags.
|
|
1315
1490
|
if (argv.includes("--init")) {
|
|
1316
1491
|
return runInitCli(cwd);
|
|
@@ -1418,6 +1593,16 @@ const registerTool = ((name, config, handler) => {
|
|
|
1418
1593
|
}
|
|
1419
1594
|
return toolHandle;
|
|
1420
1595
|
});
|
|
1596
|
+
// ---------------------------------------------------------------------------
|
|
1597
|
+
// Conductor event-ledger tools (BAPI-393)
|
|
1598
|
+
// ---------------------------------------------------------------------------
|
|
1599
|
+
//
|
|
1600
|
+
// The conductor tools (emit_event, poll_events, wait_for_event,
|
|
1601
|
+
// get_supervisor_snapshot) operate against a LOCAL SQLite ledger at
|
|
1602
|
+
// ~/.config/bridge/events.db and intentionally do NOT route through the Bridge
|
|
1603
|
+
// API HTTP helpers. They are registered through the same `registerTool` wrapper
|
|
1604
|
+
// as every other tool so they share enable/disable + in-process dispatch.
|
|
1605
|
+
registerConductorTools(registerTool);
|
|
1421
1606
|
registerTool("ping", {
|
|
1422
1607
|
annotations: {
|
|
1423
1608
|
readOnlyHint: true,
|
|
@@ -1462,14 +1647,14 @@ registerTool("ping", {
|
|
|
1462
1647
|
});
|
|
1463
1648
|
registerTool("second_opinion", {
|
|
1464
1649
|
annotations: {
|
|
1465
|
-
readOnlyHint:
|
|
1650
|
+
readOnlyHint: false, // handler POSTs and creates a second_opinion_requests row every call
|
|
1466
1651
|
destructiveHint: false,
|
|
1467
|
-
idempotentHint:
|
|
1652
|
+
idempotentHint: false, // each call submits a fresh LLM request
|
|
1468
1653
|
openWorldHint: true,
|
|
1469
1654
|
},
|
|
1470
1655
|
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. " +
|
|
1471
1656
|
"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. " +
|
|
1472
|
-
"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
|
|
1657
|
+
"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. " +
|
|
1473
1658
|
"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. " +
|
|
1474
1659
|
"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'). " +
|
|
1475
1660
|
"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. " +
|
|
@@ -1490,7 +1675,10 @@ registerTool("second_opinion", {
|
|
|
1490
1675
|
.describe("Model tier within the chosen provider. CHEAP_MODEL for quick sanity checks, BASIC_MODEL for focused reviews, PREMIUM_MODEL for serious architectural pushback."),
|
|
1491
1676
|
},
|
|
1492
1677
|
}, async ({ prompt, provider, model }) => {
|
|
1493
|
-
|
|
1678
|
+
// The backend is async (submit -> poll -> result) so a slow second opinion
|
|
1679
|
+
// no longer exceeds the server's 30s request timeout (H12). This tool
|
|
1680
|
+
// absorbs the polling so the agent still gets the reply in a single call.
|
|
1681
|
+
const submitResp = await fetch(buildApiUrl("/llm/second-opinion"), {
|
|
1494
1682
|
method: "POST",
|
|
1495
1683
|
headers: await getPostHeaders(),
|
|
1496
1684
|
body: JSON.stringify({
|
|
@@ -1500,7 +1688,65 @@ registerTool("second_opinion", {
|
|
|
1500
1688
|
model,
|
|
1501
1689
|
}),
|
|
1502
1690
|
});
|
|
1503
|
-
|
|
1691
|
+
if (!submitResp.ok) {
|
|
1692
|
+
const text = await handleResponse(submitResp);
|
|
1693
|
+
return { content: [{ type: "text", text }] };
|
|
1694
|
+
}
|
|
1695
|
+
const submitBody = (await submitResp.json());
|
|
1696
|
+
const requestId = submitBody.request_id;
|
|
1697
|
+
if (typeof requestId !== "number") {
|
|
1698
|
+
return {
|
|
1699
|
+
content: [
|
|
1700
|
+
{
|
|
1701
|
+
type: "text",
|
|
1702
|
+
text: JSON.stringify({ error: "Second opinion submit response is missing request_id", status: 500 }),
|
|
1703
|
+
},
|
|
1704
|
+
],
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
const repoQuery = `repo_name=${encodeURIComponent(REPO_NAME)}`;
|
|
1708
|
+
const statusUrl = buildApiUrl(`/llm/second-opinion/${requestId}/status?${repoQuery}`);
|
|
1709
|
+
const resultUrl = buildApiUrl(`/llm/second-opinion/${requestId}/result?${repoQuery}`);
|
|
1710
|
+
// Poll /status until terminal. A second opinion is typically 20-90s but can
|
|
1711
|
+
// approach the server's 240s tool-loop deadline; start at 3s, back off to 8s
|
|
1712
|
+
// after 30s, cap at 300s. The server-side janitor fails truly-stuck rows
|
|
1713
|
+
// only after 15 min, so this cap always returns the recoverable message well
|
|
1714
|
+
// before a live row is touched.
|
|
1715
|
+
const startTime = Date.now();
|
|
1716
|
+
const timeoutMs = 300_000;
|
|
1717
|
+
let pollIntervalMs = 3_000;
|
|
1718
|
+
let finalStatus = "";
|
|
1719
|
+
while (true) {
|
|
1720
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
1721
|
+
if (Date.now() - startTime >= timeoutMs) {
|
|
1722
|
+
return {
|
|
1723
|
+
content: [
|
|
1724
|
+
{
|
|
1725
|
+
type: "text",
|
|
1726
|
+
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.`,
|
|
1727
|
+
},
|
|
1728
|
+
],
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
if (Date.now() - startTime > 30_000) {
|
|
1732
|
+
pollIntervalMs = 8_000;
|
|
1733
|
+
}
|
|
1734
|
+
const statusResp = await fetch(statusUrl, { headers: await getGetHeaders() });
|
|
1735
|
+
if (!statusResp.ok) {
|
|
1736
|
+
const text = await handleResponse(statusResp);
|
|
1737
|
+
return { content: [{ type: "text", text }] };
|
|
1738
|
+
}
|
|
1739
|
+
const statusBody = (await statusResp.json());
|
|
1740
|
+
finalStatus = typeof statusBody.status === "string" ? statusBody.status : "";
|
|
1741
|
+
if (finalStatus === "completed" || finalStatus === "failed") {
|
|
1742
|
+
break;
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
// Both terminal states are served by /result: completed returns the
|
|
1746
|
+
// {response, provider, tier, model} JSON; failed returns the sanitized 409
|
|
1747
|
+
// failure detail. handleResponse preserves the prior single-call output shape.
|
|
1748
|
+
const resultResp = await fetch(resultUrl, { headers: await getGetHeaders() });
|
|
1749
|
+
const text = await handleResponse(resultResp);
|
|
1504
1750
|
return { content: [{ type: "text", text }] };
|
|
1505
1751
|
});
|
|
1506
1752
|
registerTool("generate_image", {
|
|
@@ -1892,11 +2138,10 @@ registerTool("get_plan", {
|
|
|
1892
2138
|
idempotentHint: true,
|
|
1893
2139
|
openWorldHint: true,
|
|
1894
2140
|
},
|
|
1895
|
-
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. " +
|
|
2141
|
+
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. " +
|
|
1896
2142
|
"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. " +
|
|
1897
|
-
"Returns the full plan as markdown
|
|
1898
|
-
"
|
|
1899
|
-
"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. " +
|
|
2143
|
+
"Returns the full plan as markdown verbatim — present it without summarizing. " +
|
|
2144
|
+
"Returns a 404 / not-found response when no plan is ready yet — that means generation has not run, not that this tool failed. " +
|
|
1900
2145
|
"Tip: call get_clarifying_questions for the same ticket to get the full context for implementation.",
|
|
1901
2146
|
inputSchema: {
|
|
1902
2147
|
ticket_number: z
|
|
@@ -1906,7 +2151,7 @@ registerTool("get_plan", {
|
|
|
1906
2151
|
.boolean()
|
|
1907
2152
|
.optional()
|
|
1908
2153
|
.default(true)
|
|
1909
|
-
.describe("Whether to save the plan to a local file
|
|
2154
|
+
.describe("Whether to save the retrieved plan to a local file. Saves to BAPI_DOCS_DIR/plans/{ticket}-plan.md. " +
|
|
1910
2155
|
"Defaults to true. Set to false to skip saving."),
|
|
1911
2156
|
},
|
|
1912
2157
|
}, async (args) => {
|
|
@@ -1938,6 +2183,32 @@ registerTool("get_architecture", {
|
|
|
1938
2183
|
}, async (args) => {
|
|
1939
2184
|
return getTicketArtifact("architecture", args);
|
|
1940
2185
|
});
|
|
2186
|
+
registerTool("get_prd", {
|
|
2187
|
+
annotations: {
|
|
2188
|
+
readOnlyHint: true,
|
|
2189
|
+
destructiveHint: false,
|
|
2190
|
+
idempotentHint: true,
|
|
2191
|
+
openWorldHint: true,
|
|
2192
|
+
},
|
|
2193
|
+
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. " +
|
|
2194
|
+
"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. " +
|
|
2195
|
+
"Returns the full PRD as markdown text — present it verbatim without summarizing. " +
|
|
2196
|
+
"The PRD is product/stakeholder-facing: problem framing, goals, success metrics, scope, and product requirements. " +
|
|
2197
|
+
"Returns a 404 / not-found response when no PRD is ready yet — that means generation has not run, not that this tool failed.",
|
|
2198
|
+
inputSchema: {
|
|
2199
|
+
ticket_number: z
|
|
2200
|
+
.string()
|
|
2201
|
+
.describe("Jira ticket key in PROJECT-NUMBER format (e.g. PROJ-123, BAPI-42)"),
|
|
2202
|
+
save_locally: z
|
|
2203
|
+
.boolean()
|
|
2204
|
+
.optional()
|
|
2205
|
+
.default(true)
|
|
2206
|
+
.describe("Whether to save the PRD to a local file in the BAPI_DOCS_DIR/prd/ directory. " +
|
|
2207
|
+
"Defaults to true. Set to false to skip saving."),
|
|
2208
|
+
},
|
|
2209
|
+
}, async (args) => {
|
|
2210
|
+
return getTicketArtifact("prd", args);
|
|
2211
|
+
});
|
|
1941
2212
|
registerTool("get_clarifying_questions", {
|
|
1942
2213
|
annotations: {
|
|
1943
2214
|
readOnlyHint: true,
|
|
@@ -2151,11 +2422,14 @@ registerTool("upload_attachment", {
|
|
|
2151
2422
|
openWorldHint: true,
|
|
2152
2423
|
},
|
|
2153
2424
|
description: "Upload a local file as an attachment to a Jira ticket. " +
|
|
2154
|
-
"Supports
|
|
2155
|
-
"
|
|
2425
|
+
"Supports binary files up to `10 MB` and text files up to `1 MB`. " +
|
|
2426
|
+
"Binary files are auto-detected (extension list or UTF-8 probe) and sent as base64. " +
|
|
2427
|
+
"Optionally syncs text content to Bridge API's tickets_links table so retrieval endpoints " +
|
|
2156
2428
|
"(get_clarifying_questions, get_ticket_critique, get_plan) return the updated content without re-generation. " +
|
|
2157
2429
|
"Use link_type to specify which retrieval endpoint should serve this content. " +
|
|
2158
|
-
"Known link_type values: clarifying-questions.md, debugging-guidance.md, ticket-quality-critique.md, architecture-plan.md, fsd-plan.md."
|
|
2430
|
+
"Known link_type values: clarifying-questions.md, debugging-guidance.md, ticket-quality-critique.md, architecture-plan.md, fsd-plan.md, prd-plan.md. " +
|
|
2431
|
+
"Note: link_type is text-only; cannot be used with base64 uploads. " +
|
|
2432
|
+
"⚠️ SECURITY: Binary bytes cannot be secret-redacted by automated text scrubbers; review sensitive visual content before upload.",
|
|
2159
2433
|
inputSchema: {
|
|
2160
2434
|
ticket_number: z
|
|
2161
2435
|
.string()
|
|
@@ -2164,13 +2438,13 @@ registerTool("upload_attachment", {
|
|
|
2164
2438
|
.string()
|
|
2165
2439
|
.optional()
|
|
2166
2440
|
.describe("Path to a local file to upload as an attachment. " +
|
|
2167
|
-
"
|
|
2441
|
+
"Binary files (images, PDFs, etc.) are supported up to `10 MB`; text files up to `1 MB`. " +
|
|
2168
2442
|
"If both file_path and content are provided, file_path takes precedence."),
|
|
2169
2443
|
content: z
|
|
2170
2444
|
.string()
|
|
2171
2445
|
.max(1_048_576)
|
|
2172
2446
|
.optional()
|
|
2173
|
-
.describe("Inline text content to upload (max
|
|
2447
|
+
.describe("Inline text content to upload (max `1 MB`). Optional if file_path is provided."),
|
|
2174
2448
|
file_name: z
|
|
2175
2449
|
.string()
|
|
2176
2450
|
.optional()
|
|
@@ -2180,8 +2454,9 @@ registerTool("upload_attachment", {
|
|
|
2180
2454
|
.string()
|
|
2181
2455
|
.optional()
|
|
2182
2456
|
.describe("When provided, also syncs the content to Bridge API's tickets_links table. " +
|
|
2183
|
-
"Known values: clarifying-questions.md, debugging-guidance.md, ticket-quality-critique.md, architecture-plan.md. " +
|
|
2184
|
-
"Unknown values are accepted with a warning."
|
|
2457
|
+
"Known values: clarifying-questions.md, debugging-guidance.md, ticket-quality-critique.md, architecture-plan.md, fsd-plan.md, prd-plan.md. " +
|
|
2458
|
+
"Unknown values are accepted with a warning. " +
|
|
2459
|
+
"Cannot be used with binary file uploads."),
|
|
2185
2460
|
replace_existing: z
|
|
2186
2461
|
.boolean()
|
|
2187
2462
|
.optional()
|
|
@@ -2190,7 +2465,7 @@ registerTool("upload_attachment", {
|
|
|
2190
2465
|
"Set to false to allow duplicate filenames."),
|
|
2191
2466
|
},
|
|
2192
2467
|
}, async ({ ticket_number, file_path, content, file_name, link_type, replace_existing }) => {
|
|
2193
|
-
const resolved = await
|
|
2468
|
+
const resolved = await resolveUploadAttachment(content, file_path, "content");
|
|
2194
2469
|
if (!resolved.ok)
|
|
2195
2470
|
return resolved.errorResponse;
|
|
2196
2471
|
const derivedFileName = file_name
|
|
@@ -2201,6 +2476,9 @@ registerTool("upload_attachment", {
|
|
|
2201
2476
|
file_name: derivedFileName,
|
|
2202
2477
|
replace_existing,
|
|
2203
2478
|
};
|
|
2479
|
+
if (resolved.encoding) {
|
|
2480
|
+
payload.encoding = resolved.encoding;
|
|
2481
|
+
}
|
|
2204
2482
|
if (link_type) {
|
|
2205
2483
|
payload.link_type = link_type;
|
|
2206
2484
|
}
|
|
@@ -2438,7 +2716,55 @@ registerTool("request_architecture", {
|
|
|
2438
2716
|
}, async (args) => {
|
|
2439
2717
|
return requestTicketArtifact("architecture", args);
|
|
2440
2718
|
});
|
|
2441
|
-
registerTool("
|
|
2719
|
+
registerTool("request_prd", {
|
|
2720
|
+
annotations: {
|
|
2721
|
+
readOnlyHint: false,
|
|
2722
|
+
destructiveHint: false,
|
|
2723
|
+
idempotentHint: false,
|
|
2724
|
+
openWorldHint: true,
|
|
2725
|
+
},
|
|
2726
|
+
description: "START (or refresh) async generation of a Product Requirements Document (PRD) for a Jira ticket. " +
|
|
2727
|
+
"This triggers an asynchronous background job — results are NOT immediate. " +
|
|
2728
|
+
"Processing typically takes 2-4 minutes depending on ticket complexity. " +
|
|
2729
|
+
"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. " +
|
|
2730
|
+
"Returns 202 if the request was accepted, 404 if the ticket does not exist in Jira, " +
|
|
2731
|
+
"or 403 if the API key is unauthorized. " +
|
|
2732
|
+
"Set wait_for_result to true to block until the result is ready (typically 2-4 minutes) instead of returning immediately.",
|
|
2733
|
+
inputSchema: {
|
|
2734
|
+
ticket_number: z
|
|
2735
|
+
.string()
|
|
2736
|
+
.describe("Jira ticket key in PROJECT-NUMBER format (e.g. PROJ-123) to generate a PRD for"),
|
|
2737
|
+
wait_for_result: z
|
|
2738
|
+
.boolean()
|
|
2739
|
+
.optional()
|
|
2740
|
+
.default(false)
|
|
2741
|
+
.describe("When true, the tool blocks and polls until the PRD is ready (typically 2-4 minutes), " +
|
|
2742
|
+
"then returns the full PRD content directly. When false (default), returns immediately " +
|
|
2743
|
+
"with a confirmation message — use get_prd later to retrieve results."),
|
|
2744
|
+
save_locally: z
|
|
2745
|
+
.boolean()
|
|
2746
|
+
.optional()
|
|
2747
|
+
.default(true)
|
|
2748
|
+
.describe("When wait_for_result is true, whether to save the PRD to a local file in the " +
|
|
2749
|
+
"BAPI_DOCS_DIR/prd/ directory. Defaults to true. Only takes effect when wait_for_result is true."),
|
|
2750
|
+
second_opinion: z
|
|
2751
|
+
.string()
|
|
2752
|
+
.optional()
|
|
2753
|
+
.describe("Provider routing override for THIS artifact-generation request " +
|
|
2754
|
+
"(e.g. 'anthropic', 'openai', 'gemini'). When set, the artifact is " +
|
|
2755
|
+
"generated by the named provider and, where supported, a cross-provider " +
|
|
2756
|
+
"second-opinion pass is applied to this request only. " +
|
|
2757
|
+
"This is NOT the standalone `second_opinion` tool — it does not return " +
|
|
2758
|
+
"an ad hoc critique; it only changes which provider produces this " +
|
|
2759
|
+
"request's artifact. Takes precedence over `provider` when both are set."),
|
|
2760
|
+
provider: z.string().optional().describe("Pure provider switch — use a specific LLM provider (openai, anthropic, gemini) without " +
|
|
2761
|
+
"triggering second-opinion semantics. If both provider and second_opinion are set, " +
|
|
2762
|
+
"second_opinion takes precedence."),
|
|
2763
|
+
},
|
|
2764
|
+
}, async (args) => {
|
|
2765
|
+
return requestTicketArtifact("prd", args);
|
|
2766
|
+
});
|
|
2767
|
+
registerTool("create_doc", {
|
|
2442
2768
|
annotations: {
|
|
2443
2769
|
readOnlyHint: false,
|
|
2444
2770
|
destructiveHint: false,
|
|
@@ -2446,27 +2772,28 @@ registerTool("request_design_doc", {
|
|
|
2446
2772
|
openWorldHint: true,
|
|
2447
2773
|
},
|
|
2448
2774
|
description: "START (or refresh) async generation of a design document for a Jira ticket, routed by doc_type. " +
|
|
2449
|
-
"Use doc_type 'tdd' for a Technical Design Document (architecture-focused, for engineers)
|
|
2450
|
-
"Functional Specification Document (product/functional-focused, for PMs, designers, and QA)
|
|
2775
|
+
"Use doc_type 'tdd' for a Technical Design Document (architecture-focused, for engineers), 'fsd' for a " +
|
|
2776
|
+
"Functional Specification Document (product/functional-focused, for PMs, designers, and QA), or 'prd' for a " +
|
|
2777
|
+
"Product Requirements Document (product-requirements-focused: problem, goals, and success metrics). " +
|
|
2451
2778
|
"This triggers an asynchronous background job — results are NOT immediate. " +
|
|
2452
2779
|
"Processing typically takes 2-4 minutes depending on ticket complexity. " +
|
|
2453
|
-
"The matching
|
|
2780
|
+
"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. " +
|
|
2454
2781
|
"Returns 202 if the request was accepted, 404 if the ticket does not exist in Jira, or 403 if the API key is unauthorized.",
|
|
2455
2782
|
inputSchema: {
|
|
2456
2783
|
ticket_number: z
|
|
2457
2784
|
.string()
|
|
2458
2785
|
.describe("Jira ticket key in PROJECT-NUMBER format (e.g. PROJ-123) to generate a design document for"),
|
|
2459
|
-
doc_type: z
|
|
2460
|
-
.
|
|
2461
|
-
|
|
2462
|
-
"'
|
|
2786
|
+
doc_type: z.enum(["tdd", "fsd", "prd"])
|
|
2787
|
+
.describe("Which design document to generate: 'tdd' (Technical Design Document, engineer audience), " +
|
|
2788
|
+
"'fsd' (Functional Specification Document, product/functional audience), or " +
|
|
2789
|
+
"'prd' (Product Requirements Document, product-requirements focused: problem, goals, success metrics)."),
|
|
2463
2790
|
wait_for_result: z
|
|
2464
2791
|
.boolean()
|
|
2465
2792
|
.optional()
|
|
2466
2793
|
.default(false)
|
|
2467
2794
|
.describe("When true, the tool blocks and polls until the document is ready (typically 2-4 minutes), " +
|
|
2468
2795
|
"then returns the full content directly. When false (default), returns immediately " +
|
|
2469
|
-
"with a confirmation message — use
|
|
2796
|
+
"with a confirmation message — use get_doc later to retrieve results."),
|
|
2470
2797
|
save_locally: z
|
|
2471
2798
|
.boolean()
|
|
2472
2799
|
.optional()
|
|
@@ -2488,7 +2815,7 @@ registerTool("request_design_doc", {
|
|
|
2488
2815
|
}, async (args) => {
|
|
2489
2816
|
return requestTicketArtifact(resolveDesignDocArtifactType(args.doc_type), args);
|
|
2490
2817
|
});
|
|
2491
|
-
registerTool("
|
|
2818
|
+
registerTool("get_doc", {
|
|
2492
2819
|
annotations: {
|
|
2493
2820
|
readOnlyHint: true,
|
|
2494
2821
|
destructiveHint: false,
|
|
@@ -2496,19 +2823,19 @@ registerTool("get_design_doc", {
|
|
|
2496
2823
|
openWorldHint: true,
|
|
2497
2824
|
},
|
|
2498
2825
|
description: "RETRIEVE an already-generated design document for a Jira ticket, routed by doc_type. " +
|
|
2499
|
-
"Use doc_type 'tdd' for the Technical Design Document
|
|
2826
|
+
"Use doc_type 'tdd' for the Technical Design Document, 'fsd' for the Functional Specification Document, or " +
|
|
2827
|
+
"'prd' for the Product Requirements Document. " +
|
|
2500
2828
|
"This tool only fetches an existing document — it does NOT start or trigger generation. " +
|
|
2501
|
-
"If no document exists yet (or you need a fresh one), call `
|
|
2829
|
+
"If no document exists yet (or you need a fresh one), call `create_doc` first with the same doc_type. " +
|
|
2502
2830
|
"Returns the full document as markdown text — present it verbatim without summarizing. " +
|
|
2503
2831
|
"Returns a 404 / not-found response when no document is ready yet — that means generation has not run, not that this tool failed.",
|
|
2504
2832
|
inputSchema: {
|
|
2505
2833
|
ticket_number: z
|
|
2506
2834
|
.string()
|
|
2507
2835
|
.describe("Jira ticket key in PROJECT-NUMBER format (e.g. PROJ-123, BAPI-42)"),
|
|
2508
|
-
doc_type: z
|
|
2509
|
-
.
|
|
2510
|
-
|
|
2511
|
-
"'fsd' (Functional Specification Document)."),
|
|
2836
|
+
doc_type: z.enum(["tdd", "fsd", "prd"])
|
|
2837
|
+
.describe("Which design document to retrieve: 'tdd' (Technical Design Document), " +
|
|
2838
|
+
"'fsd' (Functional Specification Document), or 'prd' (Product Requirements Document)."),
|
|
2512
2839
|
save_locally: z
|
|
2513
2840
|
.boolean()
|
|
2514
2841
|
.optional()
|
|
@@ -2941,7 +3268,7 @@ registerTool("resolve_target_status", {
|
|
|
2941
3268
|
// ---------------------------------------------------------------------------
|
|
2942
3269
|
const VALID_CONFIG_FIELDS = [
|
|
2943
3270
|
"review_instructions", "documentation_instructions", "architecture_instructions",
|
|
2944
|
-
"tdd_document_instructions", "fsd_document_instructions",
|
|
3271
|
+
"tdd_document_instructions", "fsd_document_instructions", "prd_document_instructions",
|
|
2945
3272
|
"unit_testing_instructions", "e2e_testing_instructions",
|
|
2946
3273
|
"unit_testing_stack", "e2e_testing_stack",
|
|
2947
3274
|
"frontend_correctness_standards", "backend_correctness_standards",
|
|
@@ -2950,6 +3277,7 @@ const VALID_CONFIG_FIELDS = [
|
|
|
2950
3277
|
"post_pr_target_status", "ci_check_config", "ci_followup_config",
|
|
2951
3278
|
"allow_mutating_smoke_ops",
|
|
2952
3279
|
"selected_mcp_slugs",
|
|
3280
|
+
"split_review_reorder_enabled",
|
|
2953
3281
|
"base_branch",
|
|
2954
3282
|
"difficulty_model_routing_enabled",
|
|
2955
3283
|
"difficulty_model_tier_overrides",
|
|
@@ -3124,7 +3452,7 @@ registerTool("update_config_field", {
|
|
|
3124
3452
|
}
|
|
3125
3453
|
// Scalar boolean config fields: reject file-path updates and normalize boolean
|
|
3126
3454
|
// true/false and string "true"/"false" to a real boolean before persisting.
|
|
3127
|
-
const BOOLEAN_CONFIG_FIELDS = ["allow_mutating_smoke_ops", "difficulty_model_routing_enabled"];
|
|
3455
|
+
const BOOLEAN_CONFIG_FIELDS = ["allow_mutating_smoke_ops", "difficulty_model_routing_enabled", "split_review_reorder_enabled"];
|
|
3128
3456
|
if (BOOLEAN_CONFIG_FIELDS.includes(field_name)) {
|
|
3129
3457
|
if (file_path) {
|
|
3130
3458
|
return {
|
|
@@ -3249,6 +3577,99 @@ registerTool("apply_install_manifest", {
|
|
|
3249
3577
|
const text = await handleResponse(resp);
|
|
3250
3578
|
return { content: [{ type: "text", text }] };
|
|
3251
3579
|
});
|
|
3580
|
+
registerTool("persist_routing_credential", {
|
|
3581
|
+
annotations: {
|
|
3582
|
+
readOnlyHint: false,
|
|
3583
|
+
destructiveHint: false,
|
|
3584
|
+
idempotentHint: true,
|
|
3585
|
+
openWorldHint: false,
|
|
3586
|
+
},
|
|
3587
|
+
description: "Persist the ALREADY-VALIDATED Bridge API key for this repo into the user-scoped credential " +
|
|
3588
|
+
"store (`~/.config/bridge/credentials.json`) under the target `bapi:<repo_name>`, so that " +
|
|
3589
|
+
"Bash-spawned CLI features such as `start-tickets` (a different runtime surface than the MCP " +
|
|
3590
|
+
"server) can resolve it for difficulty→model routing. This is the final stage of `/install-bridge`. " +
|
|
3591
|
+
"The key is resolved INSIDE the MCP server process (env-first, then the existing store) using the " +
|
|
3592
|
+
"provided `repo_name` as the store identity — it is NEVER passed as a tool argument. Existing " +
|
|
3593
|
+
"credentials are preserved; only `BAPI_API_KEY` for this repo is upserted. The response is " +
|
|
3594
|
+
"secret-free (it reports ok/action/target/path only) and never echoes the key value.",
|
|
3595
|
+
inputSchema: {
|
|
3596
|
+
repo_name: z
|
|
3597
|
+
.string()
|
|
3598
|
+
.describe("The repository name to store the routing credential under (target `bapi:<repo_name>`). " +
|
|
3599
|
+
"This is the ONLY input — do not pass the API key, a secret, or a token; the key is " +
|
|
3600
|
+
"resolved inside the MCP server process."),
|
|
3601
|
+
},
|
|
3602
|
+
}, async ({ repo_name }) => {
|
|
3603
|
+
const repoName = typeof repo_name === "string" ? repo_name.trim() : "";
|
|
3604
|
+
const deps = buildCredentialStoreWriteDeps();
|
|
3605
|
+
const storePath = getPrimaryCredentialStorePath(deps);
|
|
3606
|
+
if (repoName.length === 0) {
|
|
3607
|
+
return {
|
|
3608
|
+
content: [
|
|
3609
|
+
{
|
|
3610
|
+
type: "text",
|
|
3611
|
+
text: JSON.stringify({
|
|
3612
|
+
ok: false,
|
|
3613
|
+
message: "Cannot persist routing credential: repo_name is required. Pass the repo name " +
|
|
3614
|
+
"this install is configuring.",
|
|
3615
|
+
path: storePath,
|
|
3616
|
+
}),
|
|
3617
|
+
},
|
|
3618
|
+
],
|
|
3619
|
+
};
|
|
3620
|
+
}
|
|
3621
|
+
const target = `bapi:${repoName}`;
|
|
3622
|
+
const apiKey = await getResolvedApiKeyForRepo(repoName);
|
|
3623
|
+
if (apiKey.length === 0) {
|
|
3624
|
+
return {
|
|
3625
|
+
content: [
|
|
3626
|
+
{
|
|
3627
|
+
type: "text",
|
|
3628
|
+
text: JSON.stringify({
|
|
3629
|
+
ok: false,
|
|
3630
|
+
target,
|
|
3631
|
+
path: storePath,
|
|
3632
|
+
message: `No BAPI_API_KEY could be resolved for ${target}. Set BAPI_API_KEY in the ` +
|
|
3633
|
+
`environment (or add it under ${target} in ${storePath}) and rerun /install-bridge.`,
|
|
3634
|
+
}),
|
|
3635
|
+
},
|
|
3636
|
+
],
|
|
3637
|
+
};
|
|
3638
|
+
}
|
|
3639
|
+
const result = await upsertBapiCredential(repoName, apiKey, deps);
|
|
3640
|
+
if (!result.ok) {
|
|
3641
|
+
return {
|
|
3642
|
+
content: [
|
|
3643
|
+
{
|
|
3644
|
+
type: "text",
|
|
3645
|
+
text: JSON.stringify({
|
|
3646
|
+
ok: false,
|
|
3647
|
+
target: result.target,
|
|
3648
|
+
path: result.path,
|
|
3649
|
+
kind: result.kind,
|
|
3650
|
+
message: `Failed to persist routing credential for ${result.target}: ${result.error} ` +
|
|
3651
|
+
`You can rerun /install-bridge or migrate manually.`,
|
|
3652
|
+
}),
|
|
3653
|
+
},
|
|
3654
|
+
],
|
|
3655
|
+
};
|
|
3656
|
+
}
|
|
3657
|
+
return {
|
|
3658
|
+
content: [
|
|
3659
|
+
{
|
|
3660
|
+
type: "text",
|
|
3661
|
+
text: JSON.stringify({
|
|
3662
|
+
ok: true,
|
|
3663
|
+
action: result.action,
|
|
3664
|
+
target: result.target,
|
|
3665
|
+
path: result.path,
|
|
3666
|
+
migratedFallback: result.migratedFallback,
|
|
3667
|
+
message: `Stored routing credential for ${result.target} at ${result.path}.`,
|
|
3668
|
+
}),
|
|
3669
|
+
},
|
|
3670
|
+
],
|
|
3671
|
+
};
|
|
3672
|
+
});
|
|
3252
3673
|
function formatDeepResearchProviderReason(meta) {
|
|
3253
3674
|
if (!meta)
|
|
3254
3675
|
return "";
|
|
@@ -3568,21 +3989,23 @@ registerTool("request_brainstorm", {
|
|
|
3568
3989
|
openWorldHint: true,
|
|
3569
3990
|
},
|
|
3570
3991
|
description: "START an async brainstorm that fans out the task to two opinion-provider LLMs " +
|
|
3571
|
-
"(default: OpenAI + Gemini) and
|
|
3992
|
+
"(default: OpenAI + Gemini) and returns each provider's opinion directly (no synthesizer pass). " +
|
|
3572
3993
|
"Returns a brainstorm_id; the matching get_brainstorm tool retrieves the finished result later (unless you set wait_for_result). " +
|
|
3573
3994
|
"\n\n" +
|
|
3574
3995
|
"BEHAVIOR: By default, returns immediately with a brainstorm_id. Set wait_for_result=true " +
|
|
3575
3996
|
"to poll for terminal status (up to 15 minutes), then retrieve and optionally save the result. " +
|
|
3576
3997
|
"When save_locally=true (default), each provider's markdown is written with a semantic " +
|
|
3577
3998
|
"filename derived from task_description: " +
|
|
3578
|
-
"BAPI_DOCS_DIR/brainstorm/{slugified-task-description}-{short_brainstorm_id}-{provider}.md
|
|
3579
|
-
"and the synthesizer row follows the same pattern " +
|
|
3580
|
-
"({slugified-task-description}-{short_brainstorm_id}-synthesizer.md). " +
|
|
3999
|
+
"BAPI_DOCS_DIR/brainstorm/{slugified-task-description}-{short_brainstorm_id}-{provider}.md. " +
|
|
3581
4000
|
"(If task_description is empty or slugifies to nothing, it falls back to {brainstorm_id}-{provider}.md.) " +
|
|
3582
4001
|
"\n\n" +
|
|
3583
|
-
"
|
|
3584
|
-
"
|
|
3585
|
-
"
|
|
4002
|
+
"MODES: Set mode to pick the brainstorm style. 'technical' (default) runs the implementation/" +
|
|
4003
|
+
"architecture brainstorm. 'design' runs web-page/UI design ideation focused on visual appeal and " +
|
|
4004
|
+
"conversion. 'discovery' generates stakeholder discovery questions for early/vague tasks, grouped " +
|
|
4005
|
+
"into 'Technical Discovery Questions' and 'Business / Stakeholder Discovery Questions' and tagged " +
|
|
4006
|
+
"[HUMAN], [CODE], or [TICKET]; discovery needs no extra configuration. " +
|
|
4007
|
+
"The legacy boolean design=true still works and maps to mode='design'; prefer the explicit mode " +
|
|
4008
|
+
"selector for new callers.",
|
|
3586
4009
|
inputSchema: {
|
|
3587
4010
|
task_description: z.string().describe("Free-form description of the task to brainstorm about. Sent verbatim — " +
|
|
3588
4011
|
"this tool does NOT read task_description from a file."),
|
|
@@ -3590,7 +4013,8 @@ registerTool("request_brainstorm", {
|
|
|
3590
4013
|
ticket_number: z.string().optional().describe("Optional Jira ticket key (e.g. PROJ-123) to associate with the brainstorm. " +
|
|
3591
4014
|
"Ticket 1 only stores this for cross-reference — no Jira writes happen."),
|
|
3592
4015
|
providers: z.array(z.string()).optional().describe("Opinion-provider LLMs. Defaults to ['openai', 'gemini']. " +
|
|
3593
|
-
"A single-provider request
|
|
4016
|
+
"A single-provider request runs one opinion provider and returns that " +
|
|
4017
|
+
"provider's markdown directly."),
|
|
3594
4018
|
concerns: z.string().optional().describe("Optional caller-supplied concerns to surface to the brainstorm agents."),
|
|
3595
4019
|
wait_for_result: z.boolean().optional().describe("When true, polls until every row reaches a terminal status (max 15 minutes), " +
|
|
3596
4020
|
"then returns the full result envelope. When false (default), returns immediately."),
|
|
@@ -3598,11 +4022,17 @@ registerTool("request_brainstorm", {
|
|
|
3598
4022
|
"after the result is fetched. Request-time saves use semantic filenames derived from " +
|
|
3599
4023
|
"task_description (slugified) plus a short brainstorm-id segment."),
|
|
3600
4024
|
prior_brainstorm_id: z.string().optional().describe("Optional brainstorm_id from an earlier brainstorm to refine. " +
|
|
3601
|
-
"When provided, the
|
|
3602
|
-
"
|
|
3603
|
-
|
|
4025
|
+
"When provided, the prior brainstorm's completed opinion-provider " +
|
|
4026
|
+
"markdowns are concatenated and supplied as prior context."),
|
|
4027
|
+
mode: z.enum(["technical", "design", "discovery"]).optional().describe("Preferred brainstorm-mode selector for new callers. 'technical' (default) is the " +
|
|
4028
|
+
"implementation/architecture brainstorm; 'design' is web-page/UI visual-direction ideation; " +
|
|
4029
|
+
"'discovery' generates grouped technical and business/stakeholder discovery questions for " +
|
|
4030
|
+
"early/vague tasks. Takes precedence over the legacy boolean design field."),
|
|
4031
|
+
design: z.boolean().optional().describe("Legacy compatibility flag: set to true for web-page/UI design ideation focused on visual appeal " +
|
|
4032
|
+
"and conversion. New callers should use mode: \"design\" instead. Omit this field when not " +
|
|
4033
|
+
"requesting design mode; absent is treated as false."),
|
|
3604
4034
|
},
|
|
3605
|
-
}, async ({ task_description, repo_name, ticket_number, providers, concerns, wait_for_result, save_locally, prior_brainstorm_id, design, }) => {
|
|
4035
|
+
}, async ({ task_description, repo_name, ticket_number, providers, concerns, wait_for_result, save_locally, prior_brainstorm_id, mode, design, }) => {
|
|
3606
4036
|
const effectiveRepo = repo_name && repo_name.length > 0 ? repo_name : REPO_NAME;
|
|
3607
4037
|
const effectiveProviders = providers !== undefined ? providers : ["openai", "gemini"];
|
|
3608
4038
|
const shouldWait = wait_for_result === true;
|
|
@@ -3619,6 +4049,14 @@ registerTool("request_brainstorm", {
|
|
|
3619
4049
|
if (prior_brainstorm_id) {
|
|
3620
4050
|
submitPayload.prior_brainstorm_request_id = prior_brainstorm_id;
|
|
3621
4051
|
}
|
|
4052
|
+
// Forward `mode` only when the caller explicitly set one, mirroring the
|
|
4053
|
+
// legacy `design` handling below. Leaving it absent lets the backend apply
|
|
4054
|
+
// its own precedence (legacy `design` → prior-row inheritance → technical),
|
|
4055
|
+
// which is required for a design/discovery refinement to keep its mode.
|
|
4056
|
+
if (mode)
|
|
4057
|
+
submitPayload.mode = mode;
|
|
4058
|
+
// Preserve the legacy boolean mapping so a backend that only reads `design`
|
|
4059
|
+
// still behaves correctly.
|
|
3622
4060
|
if (design)
|
|
3623
4061
|
submitPayload.design = true;
|
|
3624
4062
|
const submitResp = await fetch(buildUrl("/brainstorms"), {
|
|
@@ -3630,11 +4068,13 @@ registerTool("request_brainstorm", {
|
|
|
3630
4068
|
const errorText = await handleResponse(submitResp);
|
|
3631
4069
|
return { content: [{ type: "text", text: errorText }] };
|
|
3632
4070
|
}
|
|
4071
|
+
// ``synthesizer_status`` is a temporary backend compatibility sentinel
|
|
4072
|
+
// (always "removed"); it is optional here and never drives control flow.
|
|
3633
4073
|
const submitBody = (await submitResp.json());
|
|
3634
4074
|
if (!shouldWait) {
|
|
3635
4075
|
const confirmation = `Brainstorm submitted (brainstorm_id: ${submitBody.brainstorm_id}). ` +
|
|
3636
4076
|
`Providers: ${submitBody.providers.join(", ")}. ` +
|
|
3637
|
-
`
|
|
4077
|
+
`Synthesis step: removed; provider opinions will be returned directly. ` +
|
|
3638
4078
|
`Use get_brainstorm with brainstorm_id ${submitBody.brainstorm_id} to retrieve results.`;
|
|
3639
4079
|
return { content: [{ type: "text", text: confirmation }] };
|
|
3640
4080
|
}
|
|
@@ -3677,9 +4117,9 @@ registerTool("get_brainstorm", {
|
|
|
3677
4117
|
},
|
|
3678
4118
|
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. " +
|
|
3679
4119
|
"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. " +
|
|
3680
|
-
"Returns
|
|
4120
|
+
"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. " +
|
|
3681
4121
|
"When save_locally=true (default), writes each provider's markdown to " +
|
|
3682
|
-
"BAPI_DOCS_DIR/brainstorm/{brainstorm_id}-{provider}.md
|
|
4122
|
+
"BAPI_DOCS_DIR/brainstorm/{brainstorm_id}-{provider}.md. " +
|
|
3683
4123
|
"Retrieval uses this UUID-only filename (not the semantic task-description name that " +
|
|
3684
4124
|
"request_brainstorm uses) because the original task description is not available in the " +
|
|
3685
4125
|
"result envelope on retrieval. " +
|
|
@@ -3818,6 +4258,23 @@ const pollCiChecksTool = registerTool("poll_ci_checks", {
|
|
|
3818
4258
|
});
|
|
3819
4259
|
const resp = await fetch(url, { headers: await getGetHeaders() });
|
|
3820
4260
|
const text = await handleResponse(resp);
|
|
4261
|
+
// BAPI-395: opportunistically emit conductor PR/CI/gate events from the
|
|
4262
|
+
// already-fetched poll response. Fully best-effort — the producer validates
|
|
4263
|
+
// that commit_ref binds to the local PR head before emitting, and any failure
|
|
4264
|
+
// is swallowed so this NEVER changes the poll_ci_checks response body.
|
|
4265
|
+
try {
|
|
4266
|
+
const parsed = JSON.parse(text);
|
|
4267
|
+
const looksLikePollStatus = parsed !== null &&
|
|
4268
|
+
typeof parsed === "object" &&
|
|
4269
|
+
!("error" in parsed) &&
|
|
4270
|
+
(Array.isArray(parsed.checks) || typeof parsed.all_complete === "boolean");
|
|
4271
|
+
if (looksLikePollStatus) {
|
|
4272
|
+
void observePrCiFromPollResponse(commit_ref, parsed).catch(() => { });
|
|
4273
|
+
}
|
|
4274
|
+
}
|
|
4275
|
+
catch {
|
|
4276
|
+
/* non-JSON or non-poll response — nothing to produce */
|
|
4277
|
+
}
|
|
3821
4278
|
return { content: [{ type: "text", text }] };
|
|
3822
4279
|
});
|
|
3823
4280
|
// ---------------------------------------------------------------------------
|
|
@@ -3973,7 +4430,7 @@ registerTool("get_pipeline_recipe", {
|
|
|
3973
4430
|
if ("idea" in mergedVariables) {
|
|
3974
4431
|
mergedVariables.idea_hash = deriveIdeaHash(mergedVariables.idea);
|
|
3975
4432
|
}
|
|
3976
|
-
const recipe = resolveRecipe(pipelineDef, INSTRUCTIONS, mergedVariables, skip_steps, !!auto_approve);
|
|
4433
|
+
const recipe = resolveRecipe(pipelineDef, INSTRUCTIONS, mergedVariables, skip_steps, !!auto_approve, { includeUpgradeAdviceSurfacing: UPGRADE_ADVICE_SURFACING_ENABLED });
|
|
3977
4434
|
return {
|
|
3978
4435
|
content: [{
|
|
3979
4436
|
type: "text",
|
|
@@ -4015,6 +4472,7 @@ async function buildPipelineOrchestratorDeps() {
|
|
|
4015
4472
|
pipelines: PIPELINES,
|
|
4016
4473
|
instructions: INSTRUCTIONS,
|
|
4017
4474
|
toolHandlers: TOOL_HANDLERS,
|
|
4475
|
+
includeUpgradeAdviceSurfacing: UPGRADE_ADVICE_SURFACING_ENABLED,
|
|
4018
4476
|
};
|
|
4019
4477
|
}
|
|
4020
4478
|
// BAPI-326: dependency injection for the full-automation chain orchestrator.
|
|
@@ -4031,6 +4489,7 @@ async function buildChainOrchestratorDeps() {
|
|
|
4031
4489
|
chainRecipes: CHAIN_RECIPES,
|
|
4032
4490
|
instructions: INSTRUCTIONS,
|
|
4033
4491
|
toolHandlers: TOOL_HANDLERS,
|
|
4492
|
+
includeUpgradeAdviceSurfacing: UPGRADE_ADVICE_SURFACING_ENABLED,
|
|
4034
4493
|
};
|
|
4035
4494
|
}
|
|
4036
4495
|
registerTool("run_pipeline", {
|
|
@@ -4376,7 +4835,11 @@ registerTool("generate_decision_page", {
|
|
|
4376
4835
|
"original-question and closed-by-default codebase-evidence display, and an optional confirmed-" +
|
|
4377
4836
|
"improvements list. Presentation labels (title, intro, section/improvements headings) are " +
|
|
4378
4837
|
"overridable, and the output location under the docs directory is configurable, so automations " +
|
|
4379
|
-
"beyond ticket review can reuse it.
|
|
4838
|
+
"beyond ticket review can reuse it. Pass artifact_type=\"pre_ticket_planning\" to additionally " +
|
|
4839
|
+
"render read-only system_goals (business goal, desired end-state, system behavior, classified " +
|
|
4840
|
+
"NFRs) and a read-only implementation_order section for pre-ticket epic/task framing; open NFRs " +
|
|
4841
|
+
"still go in actionable_items so the human can decide them. The default artifact_type " +
|
|
4842
|
+
"\"review_decisions\" is unchanged. The user opens the HTML file in a browser, makes selections, " +
|
|
4380
4843
|
"and copies the resulting JSON output back to the agent.",
|
|
4381
4844
|
inputSchema: DecisionPageInputShape,
|
|
4382
4845
|
}, async (input) => {
|
|
@@ -4393,8 +4856,13 @@ registerTool("generate_decision_page", {
|
|
|
4393
4856
|
if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(input.ticket_key)) {
|
|
4394
4857
|
return validationError(`Invalid ticket_key "${input.ticket_key}": must start with a letter and contain only letters, digits, hyphens, or underscores.`);
|
|
4395
4858
|
}
|
|
4396
|
-
// No-decisions fast path: return structured response without writing a file
|
|
4397
|
-
|
|
4859
|
+
// No-decisions fast path: return structured response without writing a file.
|
|
4860
|
+
// pre_ticket_planning pages still render when they carry read-only goals or an
|
|
4861
|
+
// implementation order even with zero open NFRs, so only short-circuit when
|
|
4862
|
+
// there is genuinely nothing to show. review_decisions behavior is unchanged
|
|
4863
|
+
// (it never sets system_goals/implementation_order).
|
|
4864
|
+
const hasPlanningContent = input.system_goals !== undefined || (input.implementation_order?.length ?? 0) > 0;
|
|
4865
|
+
if (input.actionable_items.length === 0 && !hasPlanningContent) {
|
|
4398
4866
|
return {
|
|
4399
4867
|
content: [{
|
|
4400
4868
|
type: "text",
|
|
@@ -4436,10 +4904,27 @@ registerTool("generate_decision_page", {
|
|
|
4436
4904
|
if (!outputTarget.ok) {
|
|
4437
4905
|
return validationError(outputTarget.message);
|
|
4438
4906
|
}
|
|
4439
|
-
// Read design assets and base64-encode for embedding
|
|
4907
|
+
// Read design assets and base64-encode for embedding.
|
|
4908
|
+
// Prefer the project root (local dev); fall back to the installed package root
|
|
4909
|
+
// (consumer projects where design-assets/ isn't part of the project tree).
|
|
4440
4910
|
const projectRootForAssets = await getProjectRoot();
|
|
4441
|
-
const
|
|
4442
|
-
|
|
4911
|
+
const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../");
|
|
4912
|
+
let assetsDir;
|
|
4913
|
+
try {
|
|
4914
|
+
await stat(path.join(projectRootForAssets, "design-assets"));
|
|
4915
|
+
assetsDir = path.join(projectRootForAssets, "design-assets");
|
|
4916
|
+
}
|
|
4917
|
+
catch {
|
|
4918
|
+
assetsDir = path.join(pkgRoot, "design-assets");
|
|
4919
|
+
}
|
|
4920
|
+
let fontsDir;
|
|
4921
|
+
try {
|
|
4922
|
+
await stat(path.join(projectRootForAssets, "public", "fonts"));
|
|
4923
|
+
fontsDir = path.join(projectRootForAssets, "public", "fonts");
|
|
4924
|
+
}
|
|
4925
|
+
catch {
|
|
4926
|
+
fontsDir = path.join(pkgRoot, "public", "fonts");
|
|
4927
|
+
}
|
|
4443
4928
|
let faviconBase64 = "";
|
|
4444
4929
|
let logoBase64 = "";
|
|
4445
4930
|
try {
|
|
@@ -4470,8 +4955,11 @@ registerTool("generate_decision_page", {
|
|
|
4470
4955
|
text: JSON.stringify({
|
|
4471
4956
|
status: "decision_page_generated",
|
|
4472
4957
|
file_path: filePath,
|
|
4958
|
+
artifact_type: input.artifact_type,
|
|
4473
4959
|
actionable_items_count: input.actionable_items.length,
|
|
4474
4960
|
clear_improvements_count: input.clear_improvements.length,
|
|
4961
|
+
system_goals_nfr_count: input.system_goals?.nfrs?.length ?? 0,
|
|
4962
|
+
implementation_order_count: input.implementation_order?.length ?? 0,
|
|
4475
4963
|
}),
|
|
4476
4964
|
}],
|
|
4477
4965
|
};
|