@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/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 all stories on the Elixium board",
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
- return { tools: [...baseTools, ...tddTools, ...learningLoopTools] };
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 stories = await fetchStories();
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(stories, null, 2) },
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
  }