@elixium.ai/mcp-server 0.3.3 → 0.4.0
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/dist/__tests__/board-isolation.test.js +83 -0
- package/dist/__tests__/create-board.test.js +97 -0
- package/dist/__tests__/fixtures/boards.js +117 -0
- package/dist/__tests__/list-boards.test.js +54 -0
- package/dist/__tests__/select-board.test.js +74 -0
- package/dist/board-context.js +111 -0
- package/dist/index.js +752 -10
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -44,6 +44,7 @@ const SSE_PATH = ensurePath(getArgValue("--sse-path") ?? process.env.ELIXIUM_MCP
|
|
|
44
44
|
const MESSAGE_PATH = ensurePath(getArgValue("--message-path") ??
|
|
45
45
|
process.env.ELIXIUM_MCP_MESSAGE_PATH ??
|
|
46
46
|
"/message", "/message");
|
|
47
|
+
import { listBoards, createBoard, selectBoard, getActiveBoardSlug, getRuntimeBoardId, } from "./board-context.js";
|
|
47
48
|
import * as fs from "fs";
|
|
48
49
|
import * as path from "path";
|
|
49
50
|
import { fileURLToPath } from "url";
|
|
@@ -98,6 +99,119 @@ const client = axios.create({
|
|
|
98
99
|
...(BOARD_SLUG ? { "x-board-slug": BOARD_SLUG } : {}),
|
|
99
100
|
},
|
|
100
101
|
});
|
|
102
|
+
// ── Contract Scanning for prepare_implementation ──
|
|
103
|
+
const CONTRACT_STOP_WORDS = new Set([
|
|
104
|
+
"the", "a", "an", "is", "are", "was", "were", "be", "been", "being",
|
|
105
|
+
"have", "has", "had", "do", "does", "did", "will", "would", "could",
|
|
106
|
+
"should", "may", "might", "shall", "can", "for", "and", "nor", "but",
|
|
107
|
+
"or", "yet", "so", "in", "on", "at", "to", "of", "by", "with", "from",
|
|
108
|
+
"up", "about", "into", "through", "during", "before", "after", "above",
|
|
109
|
+
"below", "between", "out", "off", "over", "under", "again", "further",
|
|
110
|
+
"then", "once", "here", "there", "when", "where", "why", "how", "all",
|
|
111
|
+
"each", "every", "both", "few", "more", "most", "other", "some", "such",
|
|
112
|
+
"no", "not", "only", "own", "same", "than", "too", "very", "just",
|
|
113
|
+
"because", "as", "until", "while", "that", "this", "these", "those",
|
|
114
|
+
"it", "its", "we", "they", "add", "update", "fix", "implement", "create",
|
|
115
|
+
"new", "remove", "change", "make", "use", "set", "get", "check", "ensure",
|
|
116
|
+
"story", "feature", "bug", "chore", "task",
|
|
117
|
+
]);
|
|
118
|
+
function extractKeywords(title, description = "") {
|
|
119
|
+
const text = `${title} ${description}`.toLowerCase();
|
|
120
|
+
const words = text
|
|
121
|
+
.replace(/[^a-z0-9\s-]/g, " ")
|
|
122
|
+
.split(/\s+/)
|
|
123
|
+
.filter((w) => w.length > 2 && !CONTRACT_STOP_WORDS.has(w));
|
|
124
|
+
return [...new Set(words)];
|
|
125
|
+
}
|
|
126
|
+
function categorizeMatch(content) {
|
|
127
|
+
if (/\b(create|generate|produce|emit|publish|write|return|send|build|insert)\b/i.test(content))
|
|
128
|
+
return "producer";
|
|
129
|
+
if (/\b(consume|read|validate|verify|import|parse|receive|fetch|load|decode|check)\b/i.test(content))
|
|
130
|
+
return "consumer";
|
|
131
|
+
return "reference";
|
|
132
|
+
}
|
|
133
|
+
function scanCodebaseForContracts(keywords, cwd) {
|
|
134
|
+
if (keywords.length === 0)
|
|
135
|
+
return [];
|
|
136
|
+
const matches = [];
|
|
137
|
+
const searchDirs = ["app", "backend/api/src", "mcp-server/src", "components"].map(d => path.join(cwd, d));
|
|
138
|
+
const extensions = new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
139
|
+
const maxMatches = 50;
|
|
140
|
+
function searchDir(dir) {
|
|
141
|
+
if (matches.length >= maxMatches)
|
|
142
|
+
return;
|
|
143
|
+
try {
|
|
144
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
145
|
+
for (const entry of entries) {
|
|
146
|
+
if (matches.length >= maxMatches)
|
|
147
|
+
return;
|
|
148
|
+
const fullPath = path.join(dir, entry.name);
|
|
149
|
+
if (entry.isDirectory()) {
|
|
150
|
+
if (entry.name === "node_modules" || entry.name === ".next" || entry.name === "dist")
|
|
151
|
+
continue;
|
|
152
|
+
searchDir(fullPath);
|
|
153
|
+
}
|
|
154
|
+
else if (extensions.has(path.extname(entry.name))) {
|
|
155
|
+
try {
|
|
156
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
157
|
+
const lines = content.split("\n");
|
|
158
|
+
for (let i = 0; i < lines.length && matches.length < maxMatches; i++) {
|
|
159
|
+
const line = lines[i];
|
|
160
|
+
const lineLower = line.toLowerCase();
|
|
161
|
+
if (keywords.some((kw) => lineLower.includes(kw))) {
|
|
162
|
+
const trimmed = line.trim();
|
|
163
|
+
if (trimmed.length > 0 && !trimmed.startsWith("//") && !trimmed.startsWith("*")) {
|
|
164
|
+
matches.push({
|
|
165
|
+
file: path.relative(cwd, fullPath),
|
|
166
|
+
line: i + 1,
|
|
167
|
+
content: trimmed.substring(0, 200),
|
|
168
|
+
role: categorizeMatch(trimmed),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch { /* skip unreadable files */ }
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch { /* skip inaccessible dirs */ }
|
|
179
|
+
}
|
|
180
|
+
for (const dir of searchDirs) {
|
|
181
|
+
if (fs.existsSync(dir))
|
|
182
|
+
searchDir(dir);
|
|
183
|
+
}
|
|
184
|
+
return matches;
|
|
185
|
+
}
|
|
186
|
+
function formatContractsSection(matches) {
|
|
187
|
+
if (matches.length === 0) {
|
|
188
|
+
return "\n## Integration Contracts\nNo downstream consumers detected.\n";
|
|
189
|
+
}
|
|
190
|
+
const producers = matches.filter((m) => m.role === "producer");
|
|
191
|
+
const consumers = matches.filter((m) => m.role === "consumer");
|
|
192
|
+
const references = matches.filter((m) => m.role === "reference");
|
|
193
|
+
let section = "\n## Integration Contracts\n\nThis story touches systems with upstream/downstream dependencies:\n";
|
|
194
|
+
if (producers.length > 0) {
|
|
195
|
+
section += "\n**Produces (writes/creates/returns):**\n";
|
|
196
|
+
for (const p of producers.slice(0, 10)) {
|
|
197
|
+
section += `- \`${p.file}:${p.line}\` — ${p.content}\n`;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (consumers.length > 0) {
|
|
201
|
+
section += "\n**Consumed by (reads/validates/imports):**\n";
|
|
202
|
+
for (const c of consumers.slice(0, 10)) {
|
|
203
|
+
section += `- \`${c.file}:${c.line}\` — ${c.content}\n`;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (references.length > 0 && producers.length + consumers.length < 5) {
|
|
207
|
+
section += "\n**Other references:**\n";
|
|
208
|
+
for (const r of references.slice(0, 5)) {
|
|
209
|
+
section += `- \`${r.file}:${r.line}\` — ${r.content}\n`;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
section += "\n> **Note:** This section is informational. Review these contracts before writing your test plan.\n";
|
|
213
|
+
return section;
|
|
214
|
+
}
|
|
101
215
|
const LANE_TITLE = {
|
|
102
216
|
backlog: "Backlog",
|
|
103
217
|
icebox: "Icebox",
|
|
@@ -153,6 +267,10 @@ const normalizeBoardSlug = (value) => {
|
|
|
153
267
|
let cachedBoardId = null;
|
|
154
268
|
let cachedBoardSlug = null;
|
|
155
269
|
const resolveBoardId = async () => {
|
|
270
|
+
// Prefer runtime selection from select_board tool
|
|
271
|
+
const runtimeId = getRuntimeBoardId();
|
|
272
|
+
if (runtimeId)
|
|
273
|
+
return runtimeId;
|
|
156
274
|
const slug = normalizeBoardSlug(BOARD_SLUG);
|
|
157
275
|
if (!slug)
|
|
158
276
|
return null;
|
|
@@ -200,12 +318,16 @@ const fetchFeatureConfig = async () => {
|
|
|
200
318
|
learningLoop: true,
|
|
201
319
|
tddWorkflow: true,
|
|
202
320
|
aiTools: true,
|
|
321
|
+
teamDecisions: false,
|
|
322
|
+
ragKnowledgeBase: false,
|
|
203
323
|
},
|
|
204
324
|
source: {
|
|
205
325
|
balancedTeam: "error-fallback",
|
|
206
326
|
learningLoop: "error-fallback",
|
|
207
327
|
tddWorkflow: "error-fallback",
|
|
208
328
|
aiTools: "error-fallback",
|
|
329
|
+
teamDecisions: "error-fallback",
|
|
330
|
+
ragKnowledgeBase: "error-fallback",
|
|
209
331
|
},
|
|
210
332
|
};
|
|
211
333
|
}
|
|
@@ -219,6 +341,10 @@ const isLearningLoopEnabled = async () => {
|
|
|
219
341
|
const config = await fetchFeatureConfig();
|
|
220
342
|
return config.features.learningLoop;
|
|
221
343
|
};
|
|
344
|
+
const isTeamDecisionsEnabled = async () => {
|
|
345
|
+
const config = await fetchFeatureConfig();
|
|
346
|
+
return config.features.teamDecisions;
|
|
347
|
+
};
|
|
222
348
|
const isAiToolsEnabled = async () => {
|
|
223
349
|
const config = await fetchFeatureConfig();
|
|
224
350
|
return config.features.aiTools;
|
|
@@ -328,7 +454,7 @@ const formatDorWarnings = (boardSettings, dorChecklist) => {
|
|
|
328
454
|
};
|
|
329
455
|
const fetchStories = async () => {
|
|
330
456
|
const boardId = await resolveBoardId();
|
|
331
|
-
const slug = normalizeBoardSlug(BOARD_SLUG);
|
|
457
|
+
const slug = getActiveBoardSlug(BOARD_SLUG) ?? normalizeBoardSlug(BOARD_SLUG);
|
|
332
458
|
const isMainBoard = slug === "main";
|
|
333
459
|
// For the "main" board, fetch ALL stories (no boardId filter) then filter
|
|
334
460
|
// client-side. This matches the frontend behavior: legacy stories have
|
|
@@ -346,7 +472,7 @@ const fetchStories = async () => {
|
|
|
346
472
|
};
|
|
347
473
|
const fetchEpics = async () => {
|
|
348
474
|
const boardId = await resolveBoardId();
|
|
349
|
-
const slug = normalizeBoardSlug(BOARD_SLUG);
|
|
475
|
+
const slug = getActiveBoardSlug(BOARD_SLUG) ?? normalizeBoardSlug(BOARD_SLUG);
|
|
350
476
|
const isMainBoard = slug === "main";
|
|
351
477
|
const response = await client.get("/epics", {
|
|
352
478
|
params: boardId && !isMainBoard ? { boardId } : undefined,
|
|
@@ -423,7 +549,58 @@ const createServer = () => {
|
|
|
423
549
|
const featureConfig = await fetchFeatureConfig();
|
|
424
550
|
const tddEnabled = featureConfig.features.tddWorkflow;
|
|
425
551
|
const learningLoopEnabled = featureConfig.features.learningLoop;
|
|
552
|
+
const teamDecisionsEnabled = featureConfig.features.teamDecisions;
|
|
553
|
+
const ragKnowledgeBaseEnabled = featureConfig.features.ragKnowledgeBase;
|
|
426
554
|
const baseTools = [
|
|
555
|
+
{
|
|
556
|
+
name: "list_boards",
|
|
557
|
+
description: "List all boards in the workspace. Returns id, name, slug, and story count for each board. Marks the currently selected board.",
|
|
558
|
+
inputSchema: {
|
|
559
|
+
type: "object",
|
|
560
|
+
properties: {
|
|
561
|
+
includeArchived: {
|
|
562
|
+
type: "boolean",
|
|
563
|
+
description: "Include archived boards (default false)",
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
name: "create_board",
|
|
570
|
+
description: "Create a new board in the workspace. Auto-generates slug from name if not provided. Max 3 active boards per tenant.",
|
|
571
|
+
inputSchema: {
|
|
572
|
+
type: "object",
|
|
573
|
+
properties: {
|
|
574
|
+
name: {
|
|
575
|
+
type: "string",
|
|
576
|
+
description: "Name for the new board",
|
|
577
|
+
},
|
|
578
|
+
description: {
|
|
579
|
+
type: "string",
|
|
580
|
+
description: "Optional description",
|
|
581
|
+
},
|
|
582
|
+
slug: {
|
|
583
|
+
type: "string",
|
|
584
|
+
description: "Optional custom slug (auto-generated from name if omitted)",
|
|
585
|
+
},
|
|
586
|
+
},
|
|
587
|
+
required: ["name"],
|
|
588
|
+
},
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
name: "select_board",
|
|
592
|
+
description: "Switch the active board context. All subsequent tool calls (list_stories, create_story, list_epics, etc.) will operate against the selected board. Overrides ELIXIUM_BOARD_SLUG env var for this session.",
|
|
593
|
+
inputSchema: {
|
|
594
|
+
type: "object",
|
|
595
|
+
properties: {
|
|
596
|
+
slug: {
|
|
597
|
+
type: "string",
|
|
598
|
+
description: "Slug of the board to select (case-insensitive)",
|
|
599
|
+
},
|
|
600
|
+
},
|
|
601
|
+
required: ["slug"],
|
|
602
|
+
},
|
|
603
|
+
},
|
|
427
604
|
{
|
|
428
605
|
name: "get_feature_config",
|
|
429
606
|
description: "Get the feature configuration for this board/workspace. Returns enabled features, team profile, and smart defaults context.",
|
|
@@ -434,10 +611,34 @@ const createServer = () => {
|
|
|
434
611
|
},
|
|
435
612
|
{
|
|
436
613
|
name: "list_stories",
|
|
437
|
-
description: "List
|
|
614
|
+
description: "List stories on the Elixium board. Returns compact summaries (id, title, lane, points, owners, epic, state). Use lane filter to reduce results. Defaults to 25 stories max.",
|
|
438
615
|
inputSchema: {
|
|
439
616
|
type: "object",
|
|
440
|
-
properties: {
|
|
617
|
+
properties: {
|
|
618
|
+
lane: {
|
|
619
|
+
type: "string",
|
|
620
|
+
description: "Filter by lane (case-insensitive). Omit to list all lanes.",
|
|
621
|
+
enum: ["Current", "Backlog", "Icebox", "Done"],
|
|
622
|
+
},
|
|
623
|
+
limit: {
|
|
624
|
+
type: "number",
|
|
625
|
+
description: "Max stories to return (default 25, max 100)",
|
|
626
|
+
},
|
|
627
|
+
},
|
|
628
|
+
},
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
name: "get_story",
|
|
632
|
+
description: "Get full details for a single story by UUID. Returns all fields including description, acceptance criteria, tasks, labels, metrics, and workflow state.",
|
|
633
|
+
inputSchema: {
|
|
634
|
+
type: "object",
|
|
635
|
+
properties: {
|
|
636
|
+
id: {
|
|
637
|
+
type: "string",
|
|
638
|
+
description: "The full UUID of the story (from list_stories output)",
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
required: ["id"],
|
|
441
642
|
},
|
|
442
643
|
},
|
|
443
644
|
{
|
|
@@ -477,6 +678,14 @@ const createServer = () => {
|
|
|
477
678
|
type: "string",
|
|
478
679
|
description: "Email of the person requesting this story. Defaults to ELIXIUM_USER_EMAIL env var or API key owner.",
|
|
479
680
|
},
|
|
681
|
+
epicId: {
|
|
682
|
+
type: "string",
|
|
683
|
+
description: "Epic ID to link this story to (optional). Use list_epics to find epic IDs.",
|
|
684
|
+
},
|
|
685
|
+
testPlan: {
|
|
686
|
+
type: "string",
|
|
687
|
+
description: "Markdown test plan describing test strategy (optional). Sets the test_plan field without changing workflow_stage.",
|
|
688
|
+
},
|
|
480
689
|
},
|
|
481
690
|
required: ["title"],
|
|
482
691
|
},
|
|
@@ -595,6 +804,18 @@ const createServer = () => {
|
|
|
595
804
|
type: "string",
|
|
596
805
|
description: "Acceptance criteria in Given/When/Then format",
|
|
597
806
|
},
|
|
807
|
+
sortOrder: {
|
|
808
|
+
type: "number",
|
|
809
|
+
description: "Sort order within the lane (lower = higher priority)",
|
|
810
|
+
},
|
|
811
|
+
epicId: {
|
|
812
|
+
type: "string",
|
|
813
|
+
description: "Epic ID to link this story to. Set to empty string to unlink.",
|
|
814
|
+
},
|
|
815
|
+
testPlan: {
|
|
816
|
+
type: "string",
|
|
817
|
+
description: "Markdown test plan. Updates test_plan field without changing workflow_stage.",
|
|
818
|
+
},
|
|
598
819
|
},
|
|
599
820
|
required: ["storyId"],
|
|
600
821
|
},
|
|
@@ -658,6 +879,25 @@ const createServer = () => {
|
|
|
658
879
|
required: ["epicId"],
|
|
659
880
|
},
|
|
660
881
|
},
|
|
882
|
+
{
|
|
883
|
+
name: "prioritize_epic",
|
|
884
|
+
description: "AI-powered dependency analysis and story prioritization for an epic. Analyzes stories, proposes optimal execution order based on dependencies, and optionally applies the reorder. Uses the same server-side endpoint as the UI.",
|
|
885
|
+
inputSchema: {
|
|
886
|
+
type: "object",
|
|
887
|
+
properties: {
|
|
888
|
+
epicId: {
|
|
889
|
+
type: "string",
|
|
890
|
+
description: "ID of the epic to prioritize",
|
|
891
|
+
},
|
|
892
|
+
mode: {
|
|
893
|
+
type: "string",
|
|
894
|
+
description: "analyze (preview proposed order) or apply (commit the reorder)",
|
|
895
|
+
enum: ["analyze", "apply"],
|
|
896
|
+
},
|
|
897
|
+
},
|
|
898
|
+
required: ["epicId"],
|
|
899
|
+
},
|
|
900
|
+
},
|
|
661
901
|
];
|
|
662
902
|
const tddTools = tddEnabled ? [
|
|
663
903
|
// TDD Workflow Tools
|
|
@@ -684,6 +924,14 @@ const createServer = () => {
|
|
|
684
924
|
type: "boolean",
|
|
685
925
|
description: "If true with trunkBased, creates a short-lived branch that auto-merges to main on submit_for_review. Branch is deleted after merge.",
|
|
686
926
|
},
|
|
927
|
+
skipGates: {
|
|
928
|
+
type: "boolean",
|
|
929
|
+
description: "If true, bypasses the test plan approval gate. For hotfixes/emergencies only. Bypass is recorded in the story audit trail.",
|
|
930
|
+
},
|
|
931
|
+
skipReason: {
|
|
932
|
+
type: "string",
|
|
933
|
+
description: "Reason for bypassing gates (e.g., 'hotfix for production outage'). Recorded in the audit trail.",
|
|
934
|
+
},
|
|
687
935
|
},
|
|
688
936
|
required: ["storyId"],
|
|
689
937
|
},
|
|
@@ -725,6 +973,20 @@ const createServer = () => {
|
|
|
725
973
|
required: ["storyId"],
|
|
726
974
|
},
|
|
727
975
|
},
|
|
976
|
+
{
|
|
977
|
+
name: "get_test_plan",
|
|
978
|
+
description: "Retrieve and display the full TDD test plan for a story. Shows the plan text, workflow stage, approval status, and test file paths.",
|
|
979
|
+
inputSchema: {
|
|
980
|
+
type: "object",
|
|
981
|
+
properties: {
|
|
982
|
+
storyId: {
|
|
983
|
+
type: "string",
|
|
984
|
+
description: "ID of the story whose test plan to display",
|
|
985
|
+
},
|
|
986
|
+
},
|
|
987
|
+
required: ["storyId"],
|
|
988
|
+
},
|
|
989
|
+
},
|
|
728
990
|
{
|
|
729
991
|
name: "submit_for_review",
|
|
730
992
|
description: "Submit implementation for human review. Only works if tests are approved. Sets state to finished.",
|
|
@@ -802,14 +1064,92 @@ const createServer = () => {
|
|
|
802
1064
|
},
|
|
803
1065
|
},
|
|
804
1066
|
] : [];
|
|
805
|
-
|
|
1067
|
+
// Team Decisions tools (conditional on feature flag)
|
|
1068
|
+
const teamDecisionsTools = teamDecisionsEnabled ? [
|
|
1069
|
+
{
|
|
1070
|
+
name: "record_decision",
|
|
1071
|
+
description: "Record a team decision, meeting outcome, or architectural choice. These are shared across all team members' AI sessions.",
|
|
1072
|
+
inputSchema: {
|
|
1073
|
+
type: "object",
|
|
1074
|
+
properties: {
|
|
1075
|
+
title: { type: "string", description: "Short title for the decision" },
|
|
1076
|
+
content: { type: "string", description: "Full description of the decision, context, and rationale" },
|
|
1077
|
+
category: {
|
|
1078
|
+
type: "string",
|
|
1079
|
+
description: "Category (e.g., architecture, meeting-note, convention, tooling, decision, general)",
|
|
1080
|
+
},
|
|
1081
|
+
tags: {
|
|
1082
|
+
type: "array",
|
|
1083
|
+
items: { type: "string" },
|
|
1084
|
+
description: "Tags for filtering (e.g., ['auth', 'self-hosted', 'phase-2'])",
|
|
1085
|
+
},
|
|
1086
|
+
storyId: { type: "string", description: "Optional story UUID to link this decision to" },
|
|
1087
|
+
epicId: { type: "string", description: "Optional epic UUID to link this decision to" },
|
|
1088
|
+
},
|
|
1089
|
+
required: ["title", "content"],
|
|
1090
|
+
},
|
|
1091
|
+
},
|
|
1092
|
+
{
|
|
1093
|
+
name: "list_decisions",
|
|
1094
|
+
description: "List recent team decisions. Filter by category, tag, or date. Use this to check what the team has decided recently.",
|
|
1095
|
+
inputSchema: {
|
|
1096
|
+
type: "object",
|
|
1097
|
+
properties: {
|
|
1098
|
+
category: { type: "string", description: "Filter by category" },
|
|
1099
|
+
tag: { type: "string", description: "Filter by tag" },
|
|
1100
|
+
since: { type: "string", description: "ISO date string — only return decisions after this date" },
|
|
1101
|
+
limit: { type: "number", description: "Max results to return (default 20)" },
|
|
1102
|
+
},
|
|
1103
|
+
},
|
|
1104
|
+
},
|
|
1105
|
+
{
|
|
1106
|
+
name: "search_decisions",
|
|
1107
|
+
description: "Full-text search across all team decisions. Use this when looking for decisions related to a specific topic or keyword.",
|
|
1108
|
+
inputSchema: {
|
|
1109
|
+
type: "object",
|
|
1110
|
+
properties: {
|
|
1111
|
+
query: { type: "string", description: "Search query (keywords)" },
|
|
1112
|
+
category: { type: "string", description: "Optional category filter" },
|
|
1113
|
+
limit: { type: "number", description: "Max results to return (default 10)" },
|
|
1114
|
+
},
|
|
1115
|
+
required: ["query"],
|
|
1116
|
+
},
|
|
1117
|
+
},
|
|
1118
|
+
] : [];
|
|
1119
|
+
// RAG Knowledge Base tools (conditional on feature flag)
|
|
1120
|
+
const ragTools = ragKnowledgeBaseEnabled ? [
|
|
1121
|
+
{
|
|
1122
|
+
name: "search_knowledge",
|
|
1123
|
+
description: "Semantic search across team knowledge base (documents, wikis, runbooks, etc). Returns relevant chunks with source attribution.",
|
|
1124
|
+
inputSchema: {
|
|
1125
|
+
type: "object",
|
|
1126
|
+
properties: {
|
|
1127
|
+
query: { type: "string", description: "Search query (natural language)" },
|
|
1128
|
+
category: { type: "string", description: "Optional source type filter (e.g., wiki, runbook, adr, confluence)" },
|
|
1129
|
+
limit: { type: "number", description: "Max results to return (default 10)" },
|
|
1130
|
+
},
|
|
1131
|
+
required: ["query"],
|
|
1132
|
+
},
|
|
1133
|
+
},
|
|
1134
|
+
{
|
|
1135
|
+
name: "list_knowledge_sources",
|
|
1136
|
+
description: "List all knowledge sources indexed in the RAG knowledge base, grouped by source type.",
|
|
1137
|
+
inputSchema: {
|
|
1138
|
+
type: "object",
|
|
1139
|
+
properties: {
|
|
1140
|
+
source_type: { type: "string", description: "Optional filter by source type" },
|
|
1141
|
+
},
|
|
1142
|
+
},
|
|
1143
|
+
},
|
|
1144
|
+
] : [];
|
|
1145
|
+
return { tools: [...baseTools, ...tddTools, ...learningLoopTools, ...teamDecisionsTools, ...ragTools] };
|
|
806
1146
|
});
|
|
807
1147
|
// Handle Requests
|
|
808
1148
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
809
1149
|
try {
|
|
810
1150
|
const toolName = request.params.name;
|
|
811
1151
|
// Check TDD workflow tools
|
|
812
|
-
const tddWorkflowTools = ["start_story", "propose_test_plan", "submit_for_review", "review_pr"];
|
|
1152
|
+
const tddWorkflowTools = ["start_story", "propose_test_plan", "get_test_plan", "submit_for_review", "review_pr"];
|
|
813
1153
|
if (tddWorkflowTools.includes(toolName)) {
|
|
814
1154
|
const enabled = await isTddWorkflowEnabled();
|
|
815
1155
|
if (!enabled) {
|
|
@@ -844,7 +1184,87 @@ const createServer = () => {
|
|
|
844
1184
|
};
|
|
845
1185
|
}
|
|
846
1186
|
}
|
|
1187
|
+
// Check Team Decisions tools
|
|
1188
|
+
const teamDecisionsToolNames = ["record_decision", "list_decisions", "search_decisions"];
|
|
1189
|
+
if (teamDecisionsToolNames.includes(toolName)) {
|
|
1190
|
+
const enabled = await isTeamDecisionsEnabled();
|
|
1191
|
+
if (!enabled) {
|
|
1192
|
+
return {
|
|
1193
|
+
content: [{
|
|
1194
|
+
type: "text",
|
|
1195
|
+
text: JSON.stringify({
|
|
1196
|
+
error: "Team Decisions is disabled",
|
|
1197
|
+
message: "The Team Decisions feature is currently disabled for this board. This feature enables shared institutional memory across all team members' AI sessions.",
|
|
1198
|
+
help: "Team Decisions can be enabled in workspace settings or per-board settings."
|
|
1199
|
+
}, null, 2)
|
|
1200
|
+
}],
|
|
1201
|
+
isError: true,
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
// Check RAG Knowledge Base tools
|
|
1206
|
+
const ragToolNames = ["search_knowledge", "list_knowledge_sources"];
|
|
1207
|
+
if (ragToolNames.includes(toolName)) {
|
|
1208
|
+
const config = await fetchFeatureConfig();
|
|
1209
|
+
if (!config.features.ragKnowledgeBase) {
|
|
1210
|
+
return {
|
|
1211
|
+
content: [{
|
|
1212
|
+
type: "text",
|
|
1213
|
+
text: JSON.stringify({
|
|
1214
|
+
error: "RAG Knowledge Base is disabled",
|
|
1215
|
+
message: "The RAG Knowledge Base feature is currently disabled for this board. This feature enables semantic search across team knowledge sources.",
|
|
1216
|
+
help: "RAG Knowledge Base can be enabled in workspace settings or per-board settings."
|
|
1217
|
+
}, null, 2)
|
|
1218
|
+
}],
|
|
1219
|
+
isError: true,
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
847
1223
|
switch (toolName) {
|
|
1224
|
+
case "list_boards": {
|
|
1225
|
+
const args = request.params.arguments;
|
|
1226
|
+
const result = await listBoards(client, {
|
|
1227
|
+
includeArchived: args?.includeArchived,
|
|
1228
|
+
});
|
|
1229
|
+
return {
|
|
1230
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
case "create_board": {
|
|
1234
|
+
const args = request.params.arguments;
|
|
1235
|
+
const board = await createBoard(client, {
|
|
1236
|
+
name: args.name,
|
|
1237
|
+
description: args.description,
|
|
1238
|
+
slug: args.slug,
|
|
1239
|
+
});
|
|
1240
|
+
// Invalidate feature config cache since board list changed
|
|
1241
|
+
cachedFeatureConfig = null;
|
|
1242
|
+
return {
|
|
1243
|
+
content: [
|
|
1244
|
+
{
|
|
1245
|
+
type: "text",
|
|
1246
|
+
text: `# Board Created\n\n- **Name:** ${board.name}\n- **Slug:** ${board.slug}\n- **ID:** ${board.id}\n\n> Use \`select_board\` with slug "${board.slug}" to switch to this board.`,
|
|
1247
|
+
},
|
|
1248
|
+
],
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
case "select_board": {
|
|
1252
|
+
const args = request.params.arguments;
|
|
1253
|
+
const board = await selectBoard(client, args.slug);
|
|
1254
|
+
// Invalidate caches so subsequent calls use new board context
|
|
1255
|
+
cachedFeatureConfig = null;
|
|
1256
|
+
cachedBoardId = null;
|
|
1257
|
+
cachedBoardSlug = null;
|
|
1258
|
+
cachedLaneStyle = null;
|
|
1259
|
+
return {
|
|
1260
|
+
content: [
|
|
1261
|
+
{
|
|
1262
|
+
type: "text",
|
|
1263
|
+
text: `# Board Selected\n\n- **Name:** ${board.name}\n- **Slug:** ${board.slug}\n- **ID:** ${board.id}\n\nAll subsequent tool calls will operate against this board.`,
|
|
1264
|
+
},
|
|
1265
|
+
],
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
848
1268
|
case "get_feature_config": {
|
|
849
1269
|
const config = await fetchFeatureConfig();
|
|
850
1270
|
const formattedConfig = `
|
|
@@ -855,6 +1275,8 @@ ${config.features.balancedTeam ? "✅" : "❌"} **Balanced Team** - Design URLs,
|
|
|
855
1275
|
${config.features.learningLoop ? "✅" : "❌"} **Learning Loop** - Hypothesis tracking, risk profiles, confidence scores
|
|
856
1276
|
${config.features.tddWorkflow ? "✅" : "❌"} **TDD Workflow** - Test-driven development enforcement
|
|
857
1277
|
${config.features.aiTools ? "✅" : "❌"} **AI Tools** - AI-powered suggestions and analysis
|
|
1278
|
+
${config.features.teamDecisions ? "✅" : "❌"} **Team Decisions** - Shared institutional memory across AI sessions
|
|
1279
|
+
${config.features.ragKnowledgeBase ? "✅" : "❌"} **RAG Knowledge Base** - Semantic search across team knowledge sources
|
|
858
1280
|
|
|
859
1281
|
## Team Profile
|
|
860
1282
|
${config.teamProfile ? `
|
|
@@ -870,6 +1292,8 @@ ${config.teamProfile ? `
|
|
|
870
1292
|
- Learning Loop: ${config.source.learningLoop}
|
|
871
1293
|
- TDD Workflow: ${config.source.tddWorkflow}
|
|
872
1294
|
- AI Tools: ${config.source.aiTools}
|
|
1295
|
+
- Team Decisions: ${config.source.teamDecisions}
|
|
1296
|
+
- RAG Knowledge Base: ${config.source.ragKnowledgeBase}
|
|
873
1297
|
|
|
874
1298
|
## Branching Strategy Defaults
|
|
875
1299
|
${config.branchingDefaults ? `- Trunk-Based: ${config.branchingDefaults.trunkBased ? "Yes" : "No"}
|
|
@@ -890,10 +1314,49 @@ ${config.infrastructureProfile?.provider ? `- Provider: ${config.infrastructureP
|
|
|
890
1314
|
};
|
|
891
1315
|
}
|
|
892
1316
|
case "list_stories": {
|
|
893
|
-
const
|
|
1317
|
+
const args = request.params.arguments;
|
|
1318
|
+
let stories = await fetchStories();
|
|
1319
|
+
// Filter by lane if specified
|
|
1320
|
+
if (args?.lane) {
|
|
1321
|
+
const targetLane = args.lane.toLowerCase();
|
|
1322
|
+
stories = stories.filter((s) => (s.lane || "").toLowerCase() === targetLane);
|
|
1323
|
+
}
|
|
1324
|
+
// Apply limit (default 25, max 100)
|
|
1325
|
+
const limit = Math.min(Math.max(args?.limit || 25, 1), 100);
|
|
1326
|
+
const total = stories.length;
|
|
1327
|
+
stories = stories.slice(0, limit);
|
|
1328
|
+
// Return compact summaries to fit LLM context
|
|
1329
|
+
const compact = stories.map((s) => ({
|
|
1330
|
+
id: s.id,
|
|
1331
|
+
title: s.title,
|
|
1332
|
+
lane: s.lane,
|
|
1333
|
+
state: s.state,
|
|
1334
|
+
points: s.points,
|
|
1335
|
+
owners: s.owners,
|
|
1336
|
+
labels: s.labels,
|
|
1337
|
+
epicId: s.epicId,
|
|
1338
|
+
storyType: s.storyType || s.story_type,
|
|
1339
|
+
createdAt: s.createdAt || s.created_at,
|
|
1340
|
+
}));
|
|
1341
|
+
const result = {
|
|
1342
|
+
total,
|
|
1343
|
+
showing: compact.length,
|
|
1344
|
+
...(args?.lane ? { lane: args.lane } : {}),
|
|
1345
|
+
stories: compact,
|
|
1346
|
+
};
|
|
894
1347
|
return {
|
|
895
1348
|
content: [
|
|
896
|
-
{ type: "text", text: JSON.stringify(
|
|
1349
|
+
{ type: "text", text: JSON.stringify(result, null, 2) },
|
|
1350
|
+
],
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
case "get_story": {
|
|
1354
|
+
const args = request.params.arguments;
|
|
1355
|
+
assertUUID(args.id, "story id");
|
|
1356
|
+
const response = await client.get(`/stories/${args.id}`);
|
|
1357
|
+
return {
|
|
1358
|
+
content: [
|
|
1359
|
+
{ type: "text", text: JSON.stringify(response.data, null, 2) },
|
|
897
1360
|
],
|
|
898
1361
|
};
|
|
899
1362
|
}
|
|
@@ -1088,6 +1551,32 @@ ${config.infrastructureProfile?.provider ? `- Provider: ${config.infrastructureP
|
|
|
1088
1551
|
fetchBoardSettings(),
|
|
1089
1552
|
]);
|
|
1090
1553
|
const story = storyResponse.data;
|
|
1554
|
+
// Auto-search team decisions if feature flag is enabled (graceful degradation)
|
|
1555
|
+
let relevantDecisions = [];
|
|
1556
|
+
if (teamConfig.features.teamDecisions) {
|
|
1557
|
+
try {
|
|
1558
|
+
const decisionsResponse = await client.get("/decisions/search", {
|
|
1559
|
+
params: { q: story.title, limit: 5 },
|
|
1560
|
+
});
|
|
1561
|
+
relevantDecisions = decisionsResponse.data?.decisions || [];
|
|
1562
|
+
}
|
|
1563
|
+
catch {
|
|
1564
|
+
// Silently omit — decisions search failure should not break the brief
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
// Auto-search knowledge base if feature flag is enabled (graceful degradation)
|
|
1568
|
+
let relatedKnowledge = [];
|
|
1569
|
+
if (teamConfig.features.ragKnowledgeBase) {
|
|
1570
|
+
try {
|
|
1571
|
+
const knowledgeResponse = await client.get("/knowledge/search", {
|
|
1572
|
+
params: { q: story.title, limit: 5 },
|
|
1573
|
+
});
|
|
1574
|
+
relatedKnowledge = knowledgeResponse.data?.results || [];
|
|
1575
|
+
}
|
|
1576
|
+
catch {
|
|
1577
|
+
// Silently omit — knowledge search failure should not break the brief
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1091
1580
|
// Validation & Guardrails
|
|
1092
1581
|
const storyLane = typeof story.lane === "string" ? story.lane.trim().toLowerCase() : "";
|
|
1093
1582
|
const statusWarning = storyLane !== "current"
|
|
@@ -1114,6 +1603,35 @@ ${config.infrastructureProfile?.provider ? `- Provider: ${config.infrastructureP
|
|
|
1114
1603
|
r.concerns.map((c) => `- **${c.severity}:** ${c.description}`).join("\n") + "\n";
|
|
1115
1604
|
}
|
|
1116
1605
|
}
|
|
1606
|
+
// Format relevant team decisions section (only if matches exist)
|
|
1607
|
+
let decisionsSection = "";
|
|
1608
|
+
if (relevantDecisions.length > 0) {
|
|
1609
|
+
const formatted = relevantDecisions.map((d) => {
|
|
1610
|
+
const date = d.created_at ? new Date(d.created_at).toISOString().split("T")[0] : "unknown";
|
|
1611
|
+
return `- [${date}] **${d.title}** (${d.category || "general"})\n ${d.content}`;
|
|
1612
|
+
}).join("\n");
|
|
1613
|
+
decisionsSection = `\n## Relevant Team Decisions\n${formatted}\n`;
|
|
1614
|
+
}
|
|
1615
|
+
// Format related knowledge section (only if matches exist)
|
|
1616
|
+
let knowledgeSection = "";
|
|
1617
|
+
if (relatedKnowledge.length > 0) {
|
|
1618
|
+
const formatted = relatedKnowledge.map((k) => {
|
|
1619
|
+
const relevance = k.relevance ? ` (${(k.relevance * 100).toFixed(0)}% match)` : "";
|
|
1620
|
+
return `- **${k.source_title}** [${k.source_type}]${relevance}\n ${k.content}`;
|
|
1621
|
+
}).join("\n");
|
|
1622
|
+
knowledgeSection = `\n## Related Knowledge\n${formatted}\n`;
|
|
1623
|
+
}
|
|
1624
|
+
// Scan codebase for integration contracts (non-blocking)
|
|
1625
|
+
let contractsSection = "";
|
|
1626
|
+
try {
|
|
1627
|
+
const keywords = extractKeywords(story.title, story.description || "");
|
|
1628
|
+
const cwd = process.cwd();
|
|
1629
|
+
const contractMatches = scanCodebaseForContracts(keywords, cwd);
|
|
1630
|
+
contractsSection = formatContractsSection(contractMatches);
|
|
1631
|
+
}
|
|
1632
|
+
catch {
|
|
1633
|
+
contractsSection = "\n## Integration Contracts\nContract scanning unavailable.\n";
|
|
1634
|
+
}
|
|
1117
1635
|
const formattedBrief = `
|
|
1118
1636
|
# Implementation Brief: ${story.title}
|
|
1119
1637
|
|
|
@@ -1126,7 +1644,7 @@ ${acceptanceCriteria}
|
|
|
1126
1644
|
## Assumptions
|
|
1127
1645
|
Here are the assumptions I think we’re testing:
|
|
1128
1646
|
${assumptions}
|
|
1129
|
-
|
|
1647
|
+
${decisionsSection}${knowledgeSection}${contractsSection}
|
|
1130
1648
|
${formatTeamContext(teamConfig)}
|
|
1131
1649
|
${formatDorDodSection(boardSettings, story.dorChecklist, story.dodChecklist)}
|
|
1132
1650
|
## Proposal
|
|
@@ -1140,7 +1658,7 @@ Here’s the smallest change that will validate it:
|
|
|
1140
1658
|
// TDD Workflow Handlers
|
|
1141
1659
|
case "start_story": {
|
|
1142
1660
|
const args = request.params.arguments;
|
|
1143
|
-
const { storyId, branchPrefix, trunkBased, autoMerge } = args;
|
|
1661
|
+
const { storyId, branchPrefix, trunkBased, autoMerge, skipGates, skipReason } = args;
|
|
1144
1662
|
if (!storyId) {
|
|
1145
1663
|
throw new Error("storyId is required");
|
|
1146
1664
|
}
|
|
@@ -1151,6 +1669,8 @@ Here’s the smallest change that will validate it:
|
|
|
1151
1669
|
branchPrefix: branchPrefix || "feat",
|
|
1152
1670
|
trunkBased,
|
|
1153
1671
|
autoMerge,
|
|
1672
|
+
...(skipGates && { skipGates: true }),
|
|
1673
|
+
...(skipReason && { skipReason }),
|
|
1154
1674
|
}),
|
|
1155
1675
|
fetchFeatureConfig(),
|
|
1156
1676
|
fetchBoardSettings(),
|
|
@@ -1234,6 +1754,56 @@ ${dorWarnings}`;
|
|
|
1234
1754
|
content: [{ type: "text", text: formattedResult.trim() }],
|
|
1235
1755
|
};
|
|
1236
1756
|
}
|
|
1757
|
+
case "get_test_plan": {
|
|
1758
|
+
const args = request.params.arguments;
|
|
1759
|
+
const { storyId } = args;
|
|
1760
|
+
if (!storyId) {
|
|
1761
|
+
throw new Error("storyId is required");
|
|
1762
|
+
}
|
|
1763
|
+
assertUUID(storyId, "storyId");
|
|
1764
|
+
const response = await client.get(`/stories/${storyId}`);
|
|
1765
|
+
const story = response.data;
|
|
1766
|
+
const testPlan = story.test_plan || story.testPlan;
|
|
1767
|
+
const workflowStage = story.workflow_stage || story.workflowStage;
|
|
1768
|
+
const testFilePaths = story.test_file_paths || story.testFilePaths || [];
|
|
1769
|
+
const feedback = story.test_plan_feedback || story.testPlanFeedback;
|
|
1770
|
+
const title = story.title || "Untitled Story";
|
|
1771
|
+
if (!testPlan) {
|
|
1772
|
+
return {
|
|
1773
|
+
content: [
|
|
1774
|
+
{
|
|
1775
|
+
type: "text",
|
|
1776
|
+
text: `# No Test Plan\n\n**Story:** ${title}\n**Story ID:** ${storyId}\n**Workflow Stage:** ${workflowStage || "none"}\n\nThis story does not have a test plan yet. Use \`propose_test_plan\` to submit one.`,
|
|
1777
|
+
},
|
|
1778
|
+
],
|
|
1779
|
+
};
|
|
1780
|
+
}
|
|
1781
|
+
const stageLabel = workflowStage === "tests_approved"
|
|
1782
|
+
? "Approved"
|
|
1783
|
+
: workflowStage === "tests_proposed"
|
|
1784
|
+
? "Proposed — Awaiting Approval"
|
|
1785
|
+
: workflowStage === "tests_revision_requested"
|
|
1786
|
+
? "Revision Requested"
|
|
1787
|
+
: workflowStage || "unknown";
|
|
1788
|
+
const feedbackSection = feedback
|
|
1789
|
+
? `\n## Revision Feedback\n> ${feedback.split("\n").join("\n> ")}\n`
|
|
1790
|
+
: "";
|
|
1791
|
+
const formattedResult = `# Test Plan: ${title}
|
|
1792
|
+
|
|
1793
|
+
**Story ID:** ${storyId}
|
|
1794
|
+
**Workflow Stage:** ${workflowStage}
|
|
1795
|
+
**Status:** ${stageLabel}
|
|
1796
|
+
|
|
1797
|
+
## Test Plan
|
|
1798
|
+
${testPlan}
|
|
1799
|
+
${feedbackSection}
|
|
1800
|
+
## Test Files
|
|
1801
|
+
${testFilePaths.length > 0 ? testFilePaths.map((p) => `- \`${p}\``).join("\n") : "No test files specified"}
|
|
1802
|
+
`;
|
|
1803
|
+
return {
|
|
1804
|
+
content: [{ type: "text", text: formattedResult.trim() }],
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1237
1807
|
case "propose_test_plan": {
|
|
1238
1808
|
const args = request.params.arguments;
|
|
1239
1809
|
const { storyId, testPlan, testFilePaths } = args;
|
|
@@ -1540,6 +2110,49 @@ ${categoryBreakdown}
|
|
|
1540
2110
|
${unestimated.length === 0
|
|
1541
2111
|
? "All stories have been estimated!"
|
|
1542
2112
|
: unestimated.map((s) => `- ${s.title} (${s.id})`).join("\n")}
|
|
2113
|
+
`;
|
|
2114
|
+
return {
|
|
2115
|
+
content: [{ type: "text", text: formattedResult.trim() }],
|
|
2116
|
+
};
|
|
2117
|
+
}
|
|
2118
|
+
case "prioritize_epic": {
|
|
2119
|
+
const args = request.params.arguments;
|
|
2120
|
+
const { epicId, mode = "analyze" } = args;
|
|
2121
|
+
if (!epicId)
|
|
2122
|
+
throw new Error("epicId is required");
|
|
2123
|
+
assertUUID(epicId, "epicId");
|
|
2124
|
+
const res = await client.post(`/prioritize-epic/${epicId}`, { mode });
|
|
2125
|
+
if (mode === "apply") {
|
|
2126
|
+
return {
|
|
2127
|
+
content: [{
|
|
2128
|
+
type: "text",
|
|
2129
|
+
text: `Prioritization applied to epic. ${res.data.updatedCount} stories reordered.`,
|
|
2130
|
+
}],
|
|
2131
|
+
};
|
|
2132
|
+
}
|
|
2133
|
+
// Analyze mode — format the preview
|
|
2134
|
+
const data = res.data;
|
|
2135
|
+
const orderList = (data.proposedOrder || [])
|
|
2136
|
+
.map((item, i) => `${i + 1}. **${item.storyTitle}** — ${item.reason}`)
|
|
2137
|
+
.join("\n");
|
|
2138
|
+
const depList = (data.dependencies || [])
|
|
2139
|
+
.map((dep) => `- ${dep.storyTitle} is blocked by ${dep.blockedByStoryTitle}: ${dep.reason}`)
|
|
2140
|
+
.join("\n");
|
|
2141
|
+
const circularWarning = data.circularDependencies?.length > 0
|
|
2142
|
+
? `\n## Circular Dependencies Detected\n${data.circularDependencies.map((cd) => `- ${cd.description}`).join("\n")}\n`
|
|
2143
|
+
: "";
|
|
2144
|
+
const formattedResult = `
|
|
2145
|
+
# AI Prioritization Preview
|
|
2146
|
+
|
|
2147
|
+
${data.summary}
|
|
2148
|
+
${circularWarning}
|
|
2149
|
+
## Proposed Order (do first → do last)
|
|
2150
|
+
${orderList}
|
|
2151
|
+
|
|
2152
|
+
## Dependencies
|
|
2153
|
+
${depList || "No dependencies detected."}
|
|
2154
|
+
|
|
2155
|
+
> To apply this order, call \`prioritize_epic\` with mode: "apply"
|
|
1543
2156
|
`;
|
|
1544
2157
|
return {
|
|
1545
2158
|
content: [{ type: "text", text: formattedResult.trim() }],
|
|
@@ -1580,6 +2193,135 @@ ${unestimated.length === 0
|
|
|
1580
2193
|
content: [{ type: "text", text: sections.join("\n") }],
|
|
1581
2194
|
};
|
|
1582
2195
|
}
|
|
2196
|
+
// Team Decisions Handlers
|
|
2197
|
+
case "record_decision": {
|
|
2198
|
+
const args = request.params.arguments;
|
|
2199
|
+
const { title, content, category, tags, storyId, epicId } = args;
|
|
2200
|
+
if (!title || !content) {
|
|
2201
|
+
throw new Error("title and content are required");
|
|
2202
|
+
}
|
|
2203
|
+
if (storyId)
|
|
2204
|
+
assertUUID(storyId, "storyId");
|
|
2205
|
+
if (epicId)
|
|
2206
|
+
assertUUID(epicId, "epicId");
|
|
2207
|
+
const response = await client.post("/decisions", {
|
|
2208
|
+
title,
|
|
2209
|
+
content,
|
|
2210
|
+
category,
|
|
2211
|
+
tags,
|
|
2212
|
+
storyId,
|
|
2213
|
+
epicId,
|
|
2214
|
+
});
|
|
2215
|
+
const decision = response.data;
|
|
2216
|
+
const confirmText = `✅ Decision recorded: "${decision.title || title}"
|
|
2217
|
+
- **Category:** ${decision.category || category || "general"}
|
|
2218
|
+
- **Tags:** ${(decision.tags || tags || []).join(", ") || "none"}
|
|
2219
|
+
- **ID:** ${decision.id}`;
|
|
2220
|
+
return {
|
|
2221
|
+
content: [{ type: "text", text: confirmText }],
|
|
2222
|
+
};
|
|
2223
|
+
}
|
|
2224
|
+
case "list_decisions": {
|
|
2225
|
+
const args = request.params.arguments;
|
|
2226
|
+
const { category, tag, since, limit } = args || {};
|
|
2227
|
+
const params = {};
|
|
2228
|
+
if (category)
|
|
2229
|
+
params.category = category;
|
|
2230
|
+
if (tag)
|
|
2231
|
+
params.tag = tag;
|
|
2232
|
+
if (since)
|
|
2233
|
+
params.since = since;
|
|
2234
|
+
if (limit)
|
|
2235
|
+
params.limit = String(limit);
|
|
2236
|
+
const response = await client.get("/decisions", { params });
|
|
2237
|
+
const { decisions, total } = response.data;
|
|
2238
|
+
if (!decisions || decisions.length === 0) {
|
|
2239
|
+
return {
|
|
2240
|
+
content: [{ type: "text", text: "No team decisions found matching the filters." }],
|
|
2241
|
+
};
|
|
2242
|
+
}
|
|
2243
|
+
const formatted = decisions.map((d) => {
|
|
2244
|
+
const date = d.created_at ? new Date(d.created_at).toISOString().split("T")[0] : "unknown";
|
|
2245
|
+
const tagStr = d.tags?.length ? ` [${d.tags.join(", ")}]` : "";
|
|
2246
|
+
return `### [${date}] ${d.title} (${d.category || "general"})${tagStr}\n${d.content}`;
|
|
2247
|
+
}).join("\n\n---\n\n");
|
|
2248
|
+
const header = `# Team Decisions (${decisions.length} of ${total})\n\n`;
|
|
2249
|
+
return {
|
|
2250
|
+
content: [{ type: "text", text: header + formatted }],
|
|
2251
|
+
};
|
|
2252
|
+
}
|
|
2253
|
+
case "search_decisions": {
|
|
2254
|
+
const args = request.params.arguments;
|
|
2255
|
+
const { query, category, limit } = args || {};
|
|
2256
|
+
if (!query) {
|
|
2257
|
+
throw new Error("query is required");
|
|
2258
|
+
}
|
|
2259
|
+
const params = { q: query };
|
|
2260
|
+
if (category)
|
|
2261
|
+
params.category = category;
|
|
2262
|
+
if (limit)
|
|
2263
|
+
params.limit = String(limit);
|
|
2264
|
+
const response = await client.get("/decisions/search", { params });
|
|
2265
|
+
const { decisions } = response.data;
|
|
2266
|
+
if (!decisions || decisions.length === 0) {
|
|
2267
|
+
return {
|
|
2268
|
+
content: [{ type: "text", text: `No team decisions found matching "${query}".` }],
|
|
2269
|
+
};
|
|
2270
|
+
}
|
|
2271
|
+
const formatted = decisions.map((d) => {
|
|
2272
|
+
const date = d.created_at ? new Date(d.created_at).toISOString().split("T")[0] : "unknown";
|
|
2273
|
+
const tagStr = d.tags?.length ? ` [${d.tags.join(", ")}]` : "";
|
|
2274
|
+
return `### [${date}] ${d.title} (${d.category || "general"})${tagStr}\n${d.content}`;
|
|
2275
|
+
}).join("\n\n---\n\n");
|
|
2276
|
+
const header = `# Search Results for "${query}" (${decisions.length} matches)\n\n`;
|
|
2277
|
+
return {
|
|
2278
|
+
content: [{ type: "text", text: header + formatted }],
|
|
2279
|
+
};
|
|
2280
|
+
}
|
|
2281
|
+
case "search_knowledge": {
|
|
2282
|
+
const args = request.params.arguments;
|
|
2283
|
+
const { query, category, limit } = args || {};
|
|
2284
|
+
if (!query) {
|
|
2285
|
+
throw new Error("query is required");
|
|
2286
|
+
}
|
|
2287
|
+
const params = { q: query };
|
|
2288
|
+
if (category)
|
|
2289
|
+
params.category = category;
|
|
2290
|
+
if (limit)
|
|
2291
|
+
params.limit = String(limit);
|
|
2292
|
+
const response = await client.get("/knowledge/search", { params });
|
|
2293
|
+
const { results } = response.data;
|
|
2294
|
+
if (!results || results.length === 0) {
|
|
2295
|
+
return {
|
|
2296
|
+
content: [{ type: "text", text: `No knowledge results found matching "${query}".` }],
|
|
2297
|
+
};
|
|
2298
|
+
}
|
|
2299
|
+
const formatted = results.map((r) => {
|
|
2300
|
+
const relevance = r.relevance ? `(${(r.relevance * 100).toFixed(1)}% relevant)` : "";
|
|
2301
|
+
return `### ${r.source_title} [${r.source_type}] ${relevance}\n${r.content}`;
|
|
2302
|
+
}).join("\n\n---\n\n");
|
|
2303
|
+
const header = `# Knowledge Search Results for "${query}" (${results.length} matches)\n\n`;
|
|
2304
|
+
return {
|
|
2305
|
+
content: [{ type: "text", text: header + formatted }],
|
|
2306
|
+
};
|
|
2307
|
+
}
|
|
2308
|
+
case "list_knowledge_sources": {
|
|
2309
|
+
const args = request.params.arguments;
|
|
2310
|
+
const params = {};
|
|
2311
|
+
if (args?.source_type)
|
|
2312
|
+
params.source_type = args.source_type;
|
|
2313
|
+
const response = await client.get("/knowledge/sources", { params });
|
|
2314
|
+
const { sources } = response.data;
|
|
2315
|
+
if (!sources || sources.length === 0) {
|
|
2316
|
+
return {
|
|
2317
|
+
content: [{ type: "text", text: "No knowledge sources are currently indexed." }],
|
|
2318
|
+
};
|
|
2319
|
+
}
|
|
2320
|
+
const formatted = sources.map((s) => `- **${s.source_title}** (${s.source_type}) — ${s.chunk_count || 0} chunks`).join("\n");
|
|
2321
|
+
return {
|
|
2322
|
+
content: [{ type: "text", text: `# Knowledge Sources\n\n${formatted}` }],
|
|
2323
|
+
};
|
|
2324
|
+
}
|
|
1583
2325
|
default:
|
|
1584
2326
|
throw new Error("Unknown tool");
|
|
1585
2327
|
}
|