@elixium.ai/mcp-server 0.2.1 → 0.3.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 +314 -18
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10,6 +10,14 @@ const API_URL = process.env.ELIXIUM_API_URL || "https://elixium.ai/api";
|
|
|
10
10
|
const BOARD_SLUG = process.env.ELIXIUM_BOARD_SLUG;
|
|
11
11
|
const LANE_STYLE_ENV = process.env.ELIXIUM_LANE_STYLE;
|
|
12
12
|
const USER_EMAIL = process.env.ELIXIUM_USER_EMAIL; // Optional: Override requester email for stories
|
|
13
|
+
// UUID v4 format validation — prevents 500s from partial/truncated IDs
|
|
14
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
15
|
+
function assertUUID(value, label) {
|
|
16
|
+
if (!UUID_RE.test(value)) {
|
|
17
|
+
throw new Error(`Invalid ${label}: "${value}" is not a valid UUID. ` +
|
|
18
|
+
`Expected format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (full UUID from list_stories/list_epics).`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
13
21
|
const CLI_ARGS = process.argv.slice(2);
|
|
14
22
|
const hasArg = (flag) => CLI_ARGS.includes(flag);
|
|
15
23
|
const getArgValue = (flag) => {
|
|
@@ -219,6 +227,105 @@ const getTeamProfile = async () => {
|
|
|
219
227
|
const config = await fetchFeatureConfig();
|
|
220
228
|
return config.teamProfile;
|
|
221
229
|
};
|
|
230
|
+
const formatTeamContext = (config) => {
|
|
231
|
+
const features = config.features;
|
|
232
|
+
const tp = config.teamProfile;
|
|
233
|
+
const bd = config.branchingDefaults;
|
|
234
|
+
const infra = config.infrastructureProfile;
|
|
235
|
+
return `
|
|
236
|
+
## Team Context
|
|
237
|
+
|
|
238
|
+
### Enabled Features
|
|
239
|
+
${features.balancedTeam ? "✅" : "❌"} Balanced Team
|
|
240
|
+
${features.learningLoop ? "✅" : "❌"} Learning Loop
|
|
241
|
+
${features.tddWorkflow ? "✅" : "❌"} TDD Workflow
|
|
242
|
+
${features.aiTools ? "✅" : "❌"} AI Tools
|
|
243
|
+
|
|
244
|
+
### Team Profile
|
|
245
|
+
${tp ? `- Team Size: ${tp.teamSize || "Not set"}
|
|
246
|
+
- Has Designer: ${tp.hasDesigner ? "Yes" : "No"}
|
|
247
|
+
- Has Product Manager: ${tp.hasProductManager ? "Yes" : "No"}
|
|
248
|
+
- Has QA: ${tp.hasQA ? "Yes" : "No"}
|
|
249
|
+
- Development Approach: ${tp.developmentApproach || "Not set"}` : "Team profile not configured. Set up in Settings → Profile."}
|
|
250
|
+
|
|
251
|
+
### Branching Strategy
|
|
252
|
+
- Trunk-Based: ${bd?.trunkBased ? "Yes" : "No"}
|
|
253
|
+
- Auto-Merge: ${bd?.autoMerge ? "Yes" : "No"}
|
|
254
|
+
- Source: ${bd?.source || "default"}
|
|
255
|
+
${infra?.provider ? `
|
|
256
|
+
### Infrastructure
|
|
257
|
+
- Provider: ${infra.provider.toUpperCase()}
|
|
258
|
+
- Regions: ${infra.regions?.join(", ") || "Not set"}
|
|
259
|
+
- Compliance: ${infra.complianceFrameworks?.map((f) => f.toUpperCase()).join(", ") || "None"}` : ""}
|
|
260
|
+
`.trim();
|
|
261
|
+
};
|
|
262
|
+
const fetchBoardSettings = async () => {
|
|
263
|
+
try {
|
|
264
|
+
const boardId = await resolveBoardId();
|
|
265
|
+
if (!boardId)
|
|
266
|
+
return undefined;
|
|
267
|
+
const response = await client.get("/boards");
|
|
268
|
+
const boards = Array.isArray(response.data) ? response.data : [];
|
|
269
|
+
const board = boards.find((b) => b.id === boardId);
|
|
270
|
+
return board?.settings;
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
const formatDorDodSection = (boardSettings, dorChecklist, dodChecklist) => {
|
|
277
|
+
if (!boardSettings)
|
|
278
|
+
return "";
|
|
279
|
+
const sections = [];
|
|
280
|
+
const dorConfig = boardSettings.definitionOfReady;
|
|
281
|
+
if (dorConfig?.enabled && dorConfig.items?.length > 0) {
|
|
282
|
+
const cl = dorChecklist || {};
|
|
283
|
+
const lines = dorConfig.items.map((item) => {
|
|
284
|
+
const met = cl[item.id] === true;
|
|
285
|
+
return `- ${met ? "✅" : "❌"} ${item.label}`;
|
|
286
|
+
});
|
|
287
|
+
const unmetItems = dorConfig.items.filter((item) => cl[item.id] !== true);
|
|
288
|
+
const met = dorConfig.items.length - unmetItems.length;
|
|
289
|
+
sections.push(`### Definition of Ready (${met}/${dorConfig.items.length})`);
|
|
290
|
+
sections.push(lines.join("\n"));
|
|
291
|
+
if (unmetItems.length > 0) {
|
|
292
|
+
const unmetLabels = unmetItems.map((i) => i.label).join(", ");
|
|
293
|
+
sections.push(`\n> **DoR unmet:** ${unmetLabels}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const dodConfig = boardSettings.definitionOfDone;
|
|
297
|
+
if (dodConfig?.enabled && dodConfig.items?.length > 0) {
|
|
298
|
+
const cl = dodChecklist || {};
|
|
299
|
+
const lines = dodConfig.items.map((item) => {
|
|
300
|
+
const met = cl[item.id] === true;
|
|
301
|
+
return `- ${met ? "✅" : "❌"} ${item.label}`;
|
|
302
|
+
});
|
|
303
|
+
const unmetItems = dodConfig.items.filter((item) => cl[item.id] !== true);
|
|
304
|
+
const met = dodConfig.items.length - unmetItems.length;
|
|
305
|
+
sections.push(`### Definition of Done (${met}/${dodConfig.items.length})`);
|
|
306
|
+
sections.push(lines.join("\n"));
|
|
307
|
+
if (unmetItems.length > 0) {
|
|
308
|
+
const unmetLabels = unmetItems.map((i) => i.label).join(", ");
|
|
309
|
+
sections.push(`\n> **DoD unmet:** ${unmetLabels}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (sections.length === 0)
|
|
313
|
+
return "";
|
|
314
|
+
return `\n## Checklists\n${sections.join("\n")}\n`;
|
|
315
|
+
};
|
|
316
|
+
const formatDorWarnings = (boardSettings, dorChecklist) => {
|
|
317
|
+
if (!boardSettings)
|
|
318
|
+
return "";
|
|
319
|
+
const dorConfig = boardSettings.definitionOfReady;
|
|
320
|
+
if (!dorConfig?.enabled || dorConfig.enforcement !== "warn")
|
|
321
|
+
return "";
|
|
322
|
+
const cl = dorChecklist || {};
|
|
323
|
+
const unmetItems = (dorConfig.items || []).filter((item) => cl[item.id] !== true);
|
|
324
|
+
if (unmetItems.length === 0)
|
|
325
|
+
return "";
|
|
326
|
+
const warningLines = unmetItems.map((i) => `- ⚠️ DoR not met: ${i.label}`).join("\n");
|
|
327
|
+
return `\n## ⚠️ DoR Warnings\n${warningLines}\n\nConsider addressing these before starting implementation.\n`;
|
|
328
|
+
};
|
|
222
329
|
const fetchStories = async () => {
|
|
223
330
|
const boardId = await resolveBoardId();
|
|
224
331
|
const slug = normalizeBoardSlug(BOARD_SLUG);
|
|
@@ -604,6 +711,20 @@ const createServer = () => {
|
|
|
604
711
|
required: ["storyId", "testPlan"],
|
|
605
712
|
},
|
|
606
713
|
},
|
|
714
|
+
{
|
|
715
|
+
name: "approve_tests",
|
|
716
|
+
description: "Approve a proposed test plan so implementation can proceed. Transitions workflow from tests_proposed to tests_approved. This is the human approval gate in the TDD workflow.",
|
|
717
|
+
inputSchema: {
|
|
718
|
+
type: "object",
|
|
719
|
+
properties: {
|
|
720
|
+
storyId: {
|
|
721
|
+
type: "string",
|
|
722
|
+
description: "ID of the story whose test plan to approve",
|
|
723
|
+
},
|
|
724
|
+
},
|
|
725
|
+
required: ["storyId"],
|
|
726
|
+
},
|
|
727
|
+
},
|
|
607
728
|
{
|
|
608
729
|
name: "submit_for_review",
|
|
609
730
|
description: "Submit implementation for human review. Only works if tests are approved. Sets state to finished.",
|
|
@@ -630,6 +751,20 @@ const createServer = () => {
|
|
|
630
751
|
required: ["storyId"],
|
|
631
752
|
},
|
|
632
753
|
},
|
|
754
|
+
{
|
|
755
|
+
name: "review_pr",
|
|
756
|
+
description: "Run AI review on a story's PR diff against acceptance criteria and DoD. Fetches diff, runs AI validation, posts findings as GitHub PR comment, and stores results on the story.",
|
|
757
|
+
inputSchema: {
|
|
758
|
+
type: "object",
|
|
759
|
+
properties: {
|
|
760
|
+
storyId: {
|
|
761
|
+
type: "string",
|
|
762
|
+
description: "ID of the story to review",
|
|
763
|
+
},
|
|
764
|
+
},
|
|
765
|
+
required: ["storyId"],
|
|
766
|
+
},
|
|
767
|
+
},
|
|
633
768
|
] : [];
|
|
634
769
|
// Learning Loop tools (conditional on feature flag)
|
|
635
770
|
const learningLoopTools = learningLoopEnabled ? [
|
|
@@ -674,7 +809,7 @@ const createServer = () => {
|
|
|
674
809
|
try {
|
|
675
810
|
const toolName = request.params.name;
|
|
676
811
|
// Check TDD workflow tools
|
|
677
|
-
const tddWorkflowTools = ["start_story", "propose_test_plan", "submit_for_review"];
|
|
812
|
+
const tddWorkflowTools = ["start_story", "propose_test_plan", "submit_for_review", "review_pr"];
|
|
678
813
|
if (tddWorkflowTools.includes(toolName)) {
|
|
679
814
|
const enabled = await isTddWorkflowEnabled();
|
|
680
815
|
if (!enabled) {
|
|
@@ -836,6 +971,7 @@ ${config.infrastructureProfile?.provider ? `- Provider: ${config.infrastructureP
|
|
|
836
971
|
case "record_learning": {
|
|
837
972
|
const args = request.params.arguments;
|
|
838
973
|
const { storyId, outcome_summary } = args;
|
|
974
|
+
assertUUID(storyId, "storyId");
|
|
839
975
|
const response = await client.patch(`/stories/${storyId}`, {
|
|
840
976
|
outcome_summary,
|
|
841
977
|
});
|
|
@@ -851,6 +987,7 @@ ${config.infrastructureProfile?.provider ? `- Provider: ${config.infrastructureP
|
|
|
851
987
|
if (!storyId) {
|
|
852
988
|
throw new Error("storyId is required");
|
|
853
989
|
}
|
|
990
|
+
assertUUID(storyId, "storyId");
|
|
854
991
|
// Guardrail: Block AI from setting accepted/rejected states (human-in-the-loop)
|
|
855
992
|
const blockedStates = ["accepted", "rejected"];
|
|
856
993
|
if (state && blockedStates.includes(state.toLowerCase())) {
|
|
@@ -862,11 +999,38 @@ ${config.infrastructureProfile?.provider ? `- Provider: ${config.infrastructureP
|
|
|
862
999
|
...(state ? { state } : {}),
|
|
863
1000
|
...(normalizedLane ? { lane: normalizedLane } : {}),
|
|
864
1001
|
}).filter(([, value]) => value !== undefined));
|
|
865
|
-
|
|
1002
|
+
let response;
|
|
1003
|
+
try {
|
|
1004
|
+
response = await client.patch(`/stories/${storyId}`, payload);
|
|
1005
|
+
}
|
|
1006
|
+
catch (err) {
|
|
1007
|
+
// DoR/DoD enforcement: surface 422 with unmet criteria to agent
|
|
1008
|
+
if (err.response?.status === 422 && err.response?.data?.unmetCriteria) {
|
|
1009
|
+
const errorMsg = err.response.data.error || "Checklist not met";
|
|
1010
|
+
const unmet = err.response.data.unmetCriteria;
|
|
1011
|
+
const unmetList = unmet.map((c) => `- ❌ ${c.label}`).join("\n");
|
|
1012
|
+
const isDod = errorMsg.includes("Done");
|
|
1013
|
+
const checklistField = isDod ? "dodChecklist" : "dorChecklist";
|
|
1014
|
+
return {
|
|
1015
|
+
content: [
|
|
1016
|
+
{
|
|
1017
|
+
type: "text",
|
|
1018
|
+
text: `# ⛔ ${errorMsg}\n\nCannot move story. The following criteria are unmet:\n\n${unmetList}\n\n**Action:** Update the story's \`${checklistField}\` to mark items as met, or ask the team to review the requirements in Board Settings.`,
|
|
1019
|
+
},
|
|
1020
|
+
],
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
throw err;
|
|
1024
|
+
}
|
|
1025
|
+
const data = response.data;
|
|
1026
|
+
let resultText = JSON.stringify(data, null, 2);
|
|
1027
|
+
// Surface DoR warnings to agent
|
|
1028
|
+
if (data.warnings && data.warnings.length > 0) {
|
|
1029
|
+
const warningList = data.warnings.map((w) => `- ⚠️ ${w}`).join("\n");
|
|
1030
|
+
resultText += `\n\n---\n# ⚠️ DoR Warnings\n\nStory moved to Current, but the following DoR items are unmet:\n\n${warningList}\n\nConsider addressing these before starting implementation.`;
|
|
1031
|
+
}
|
|
866
1032
|
return {
|
|
867
|
-
content: [
|
|
868
|
-
{ type: "text", text: JSON.stringify(response.data, null, 2) },
|
|
869
|
-
],
|
|
1033
|
+
content: [{ type: "text", text: resultText }],
|
|
870
1034
|
};
|
|
871
1035
|
}
|
|
872
1036
|
case "list_objectives": {
|
|
@@ -904,6 +1068,7 @@ ${config.infrastructureProfile?.provider ? `- Provider: ${config.infrastructureP
|
|
|
904
1068
|
if (!epicId) {
|
|
905
1069
|
throw new Error("epicId is required");
|
|
906
1070
|
}
|
|
1071
|
+
assertUUID(epicId, "epicId");
|
|
907
1072
|
const payload = Object.fromEntries(Object.entries(rest).filter(([, value]) => value !== undefined));
|
|
908
1073
|
const response = await client.patch(`/epics/${epicId}`, payload);
|
|
909
1074
|
return {
|
|
@@ -915,7 +1080,13 @@ ${config.infrastructureProfile?.provider ? `- Provider: ${config.infrastructureP
|
|
|
915
1080
|
case "prepare_implementation": {
|
|
916
1081
|
const args = request.params.arguments;
|
|
917
1082
|
const { storyId } = args;
|
|
918
|
-
|
|
1083
|
+
assertUUID(storyId, "storyId");
|
|
1084
|
+
// Fetch story, team context, and board settings in parallel
|
|
1085
|
+
const [storyResponse, teamConfig, boardSettings] = await Promise.all([
|
|
1086
|
+
client.get(`/stories/${storyId}`),
|
|
1087
|
+
fetchFeatureConfig(),
|
|
1088
|
+
fetchBoardSettings(),
|
|
1089
|
+
]);
|
|
919
1090
|
const story = storyResponse.data;
|
|
920
1091
|
// Validation & Guardrails
|
|
921
1092
|
const storyLane = typeof story.lane === "string" ? story.lane.trim().toLowerCase() : "";
|
|
@@ -931,11 +1102,23 @@ ${config.infrastructureProfile?.provider ? `- Provider: ${config.infrastructureP
|
|
|
931
1102
|
story.learningGoals ||
|
|
932
1103
|
story.hypothesis ||
|
|
933
1104
|
"No specific learning goals identified.";
|
|
1105
|
+
const revisionFeedbackSection = story.test_plan_feedback
|
|
1106
|
+
? `\n## Test Plan Revision Feedback\n> ${story.test_plan_feedback.split("\n").join("\n> ")}\n\n> **Action:** Revise your test plan based on this feedback and call \`propose_test_plan\` again.\n`
|
|
1107
|
+
: "";
|
|
1108
|
+
let aiReviewSection = "";
|
|
1109
|
+
if (story.ai_review) {
|
|
1110
|
+
const r = story.ai_review;
|
|
1111
|
+
aiReviewSection = `\n## Previous AI Review\n> ${r.summary || "Review completed."}\n`;
|
|
1112
|
+
if (r.concerns?.length > 0) {
|
|
1113
|
+
aiReviewSection += "\n**Concerns to address:**\n" +
|
|
1114
|
+
r.concerns.map((c) => `- **${c.severity}:** ${c.description}`).join("\n") + "\n";
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
934
1117
|
const formattedBrief = `
|
|
935
1118
|
# Implementation Brief: ${story.title}
|
|
936
1119
|
|
|
937
1120
|
${statusWarning}
|
|
938
|
-
|
|
1121
|
+
${revisionFeedbackSection}${aiReviewSection}
|
|
939
1122
|
## Acceptance Criteria
|
|
940
1123
|
Here’s the acceptance criteria I’m going to satisfy:
|
|
941
1124
|
${acceptanceCriteria}
|
|
@@ -944,6 +1127,8 @@ ${acceptanceCriteria}
|
|
|
944
1127
|
Here are the assumptions I think we’re testing:
|
|
945
1128
|
${assumptions}
|
|
946
1129
|
|
|
1130
|
+
${formatTeamContext(teamConfig)}
|
|
1131
|
+
${formatDorDodSection(boardSettings, story.dorChecklist, story.dodChecklist)}
|
|
947
1132
|
## Proposal
|
|
948
1133
|
Here’s the smallest change that will validate it:
|
|
949
1134
|
[Agent should fill this in based on the context above]
|
|
@@ -959,12 +1144,20 @@ Here’s the smallest change that will validate it:
|
|
|
959
1144
|
if (!storyId) {
|
|
960
1145
|
throw new Error("storyId is required");
|
|
961
1146
|
}
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1147
|
+
assertUUID(storyId, "storyId");
|
|
1148
|
+
// Fetch story start, team config, and board settings in parallel
|
|
1149
|
+
const [response, teamConfig, boardSettings] = await Promise.all([
|
|
1150
|
+
client.post(`/stories/${storyId}/start`, {
|
|
1151
|
+
branchPrefix: branchPrefix || "feat",
|
|
1152
|
+
trunkBased,
|
|
1153
|
+
autoMerge,
|
|
1154
|
+
}),
|
|
1155
|
+
fetchFeatureConfig(),
|
|
1156
|
+
fetchBoardSettings(),
|
|
1157
|
+
]);
|
|
967
1158
|
const result = response.data;
|
|
1159
|
+
const teamContext = formatTeamContext(teamConfig);
|
|
1160
|
+
const dorWarnings = formatDorWarnings(boardSettings, result.dorChecklist);
|
|
968
1161
|
const isTrunk = result.trunkBased;
|
|
969
1162
|
const isAutoMerge = result.autoMerge;
|
|
970
1163
|
let formattedResult;
|
|
@@ -989,7 +1182,9 @@ ${result.acceptance_criteria || "No specific AC provided."}
|
|
|
989
1182
|
|
|
990
1183
|
> **Feature Flag:** Wrap new behavior with \`${result.featureFlagName}\` for safe isolation.
|
|
991
1184
|
> **TDD Workflow:** Write tests first, then call \`propose_test_plan\` before implementing.
|
|
992
|
-
|
|
1185
|
+
|
|
1186
|
+
${teamContext}
|
|
1187
|
+
${dorWarnings}`;
|
|
993
1188
|
}
|
|
994
1189
|
else if (isTrunk && isAutoMerge) {
|
|
995
1190
|
formattedResult = `
|
|
@@ -1013,7 +1208,9 @@ ${result.acceptance_criteria || "No specific AC provided."}
|
|
|
1013
1208
|
|
|
1014
1209
|
> **Feature Flag:** Wrap new behavior with \`${result.featureFlagName}\` for safe isolation.
|
|
1015
1210
|
> **TDD Workflow:** Write tests first, then call \`propose_test_plan\` before implementing.
|
|
1016
|
-
|
|
1211
|
+
|
|
1212
|
+
${teamContext}
|
|
1213
|
+
${dorWarnings}`;
|
|
1017
1214
|
}
|
|
1018
1215
|
else {
|
|
1019
1216
|
formattedResult = `
|
|
@@ -1029,7 +1226,9 @@ ${result.acceptance_criteria || "No specific AC provided."}
|
|
|
1029
1226
|
${result.workflow_reminder}
|
|
1030
1227
|
|
|
1031
1228
|
> **TDD Workflow:** Write tests first, then call \`propose_test_plan\` before implementing.
|
|
1032
|
-
|
|
1229
|
+
|
|
1230
|
+
${teamContext}
|
|
1231
|
+
${dorWarnings}`;
|
|
1033
1232
|
}
|
|
1034
1233
|
return {
|
|
1035
1234
|
content: [{ type: "text", text: formattedResult.trim() }],
|
|
@@ -1041,20 +1240,33 @@ ${result.workflow_reminder}
|
|
|
1041
1240
|
if (!storyId) {
|
|
1042
1241
|
throw new Error("storyId is required");
|
|
1043
1242
|
}
|
|
1243
|
+
assertUUID(storyId, "storyId");
|
|
1044
1244
|
if (!testPlan) {
|
|
1045
1245
|
throw new Error("testPlan is required");
|
|
1046
1246
|
}
|
|
1247
|
+
// Fetch story to check for previous revision feedback
|
|
1248
|
+
let previousFeedback = null;
|
|
1249
|
+
try {
|
|
1250
|
+
const storyResponse = await client.get(`/stories/${storyId}`);
|
|
1251
|
+
previousFeedback = storyResponse.data?.test_plan_feedback || null;
|
|
1252
|
+
}
|
|
1253
|
+
catch (_) {
|
|
1254
|
+
// Non-blocking — proceed without feedback context
|
|
1255
|
+
}
|
|
1047
1256
|
const response = await client.post(`/stories/${storyId}/propose-tests`, {
|
|
1048
1257
|
testPlan,
|
|
1049
1258
|
testFilePaths,
|
|
1050
1259
|
});
|
|
1051
1260
|
const result = response.data;
|
|
1261
|
+
const feedbackSection = previousFeedback
|
|
1262
|
+
? `\n## Previous Revision Feedback\n> ${previousFeedback.split("\n").join("\n> ")}\n`
|
|
1263
|
+
: "";
|
|
1052
1264
|
const formattedResult = `
|
|
1053
1265
|
# Test Plan Proposed
|
|
1054
1266
|
|
|
1055
1267
|
**Story ID:** ${storyId}
|
|
1056
1268
|
**Workflow Stage:** ${result.workflow_stage}
|
|
1057
|
-
|
|
1269
|
+
${feedbackSection}
|
|
1058
1270
|
## Test Files
|
|
1059
1271
|
${(result.test_file_paths || []).map((p) => `- \`${p}\``).join("\n") || "No test files specified"}
|
|
1060
1272
|
|
|
@@ -1062,6 +1274,30 @@ ${(result.test_file_paths || []).map((p) => `- \`${p}\``).join("\n") || "No test
|
|
|
1062
1274
|
${result.message}
|
|
1063
1275
|
|
|
1064
1276
|
> 🛑 **BLOCKED:** Implementation cannot proceed until a human approves this test plan.
|
|
1277
|
+
`;
|
|
1278
|
+
return {
|
|
1279
|
+
content: [{ type: "text", text: formattedResult.trim() }],
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
case "approve_tests": {
|
|
1283
|
+
const args = request.params.arguments;
|
|
1284
|
+
const { storyId } = args;
|
|
1285
|
+
if (!storyId) {
|
|
1286
|
+
throw new Error("storyId is required");
|
|
1287
|
+
}
|
|
1288
|
+
assertUUID(storyId, "storyId");
|
|
1289
|
+
const response = await client.post(`/stories/${storyId}/approve-tests`);
|
|
1290
|
+
const result = response.data;
|
|
1291
|
+
const formattedResult = `
|
|
1292
|
+
# Test Plan Approved
|
|
1293
|
+
|
|
1294
|
+
**Story ID:** ${storyId}
|
|
1295
|
+
**Workflow Stage:** ${result.workflow_stage}
|
|
1296
|
+
|
|
1297
|
+
## Status
|
|
1298
|
+
${result.message}
|
|
1299
|
+
|
|
1300
|
+
> ✅ **Approved:** Agent may now proceed with implementation.
|
|
1065
1301
|
`;
|
|
1066
1302
|
return {
|
|
1067
1303
|
content: [{ type: "text", text: formattedResult.trim() }],
|
|
@@ -1073,6 +1309,7 @@ ${result.message}
|
|
|
1073
1309
|
if (!storyId) {
|
|
1074
1310
|
throw new Error("storyId is required");
|
|
1075
1311
|
}
|
|
1312
|
+
assertUUID(storyId, "storyId");
|
|
1076
1313
|
const response = await client.post(`/stories/${storyId}/submit-review`, {
|
|
1077
1314
|
commitHash,
|
|
1078
1315
|
testResults,
|
|
@@ -1098,6 +1335,14 @@ git branch -d ${instr.sourceBranch}
|
|
|
1098
1335
|
\`\`\`
|
|
1099
1336
|
`;
|
|
1100
1337
|
}
|
|
1338
|
+
// Format PR section if PR was created or failed
|
|
1339
|
+
let prSection = "";
|
|
1340
|
+
if (result.pr_url && result.pr_number) {
|
|
1341
|
+
prSection = `\n## Pull Request\n**PR:** [#${result.pr_number}](${result.pr_url})\n**Status:** ${result.pr_state || "open"}\n> DoD checklist and acceptance criteria included in PR body.\n`;
|
|
1342
|
+
}
|
|
1343
|
+
else if (result.pr_error) {
|
|
1344
|
+
prSection = `\n## Pull Request\n> PR creation failed: ${result.pr_error}\n> Workflow proceeded successfully — create PR manually if needed.\n`;
|
|
1345
|
+
}
|
|
1101
1346
|
const formattedResult = `
|
|
1102
1347
|
# Implementation Submitted for Review
|
|
1103
1348
|
|
|
@@ -1113,7 +1358,7 @@ ${result.featureFlagName ? `**Feature Flag:** \`${result.featureFlagName}\`` : "
|
|
|
1113
1358
|
|
|
1114
1359
|
## Commits
|
|
1115
1360
|
${(result.commit_hashes || []).map((h) => `- \`${h}\``).join("\n") || "No commits recorded"}
|
|
1116
|
-
${autoMergeSection}
|
|
1361
|
+
${autoMergeSection}${prSection}
|
|
1117
1362
|
## Next Step
|
|
1118
1363
|
${result.next_step}
|
|
1119
1364
|
|
|
@@ -1125,12 +1370,53 @@ ${result.next_step}
|
|
|
1125
1370
|
content: [{ type: "text", text: formattedResult.trim() }],
|
|
1126
1371
|
};
|
|
1127
1372
|
}
|
|
1373
|
+
case "review_pr": {
|
|
1374
|
+
const args = request.params.arguments;
|
|
1375
|
+
const { storyId } = args;
|
|
1376
|
+
if (!storyId)
|
|
1377
|
+
throw new Error("storyId is required");
|
|
1378
|
+
assertUUID(storyId, "storyId");
|
|
1379
|
+
const reviewResponse = await client.post(`/stories/${storyId}/ai-review`);
|
|
1380
|
+
const reviewResult = reviewResponse.data;
|
|
1381
|
+
const review = reviewResult.ai_review;
|
|
1382
|
+
let acSection = "";
|
|
1383
|
+
if (review?.acCoverage?.length > 0) {
|
|
1384
|
+
acSection = "\n## Acceptance Criteria Coverage\n" +
|
|
1385
|
+
review.acCoverage.map((ac) => {
|
|
1386
|
+
const icon = ac.status === "met" ? "+" : ac.status === "partial" ? "~" : "-";
|
|
1387
|
+
return `[${icon}] **${ac.status.toUpperCase()}:** ${ac.criterion}\n ${ac.evidence}`;
|
|
1388
|
+
}).join("\n");
|
|
1389
|
+
}
|
|
1390
|
+
let dodSection = "";
|
|
1391
|
+
if (review?.dodValidation?.length > 0) {
|
|
1392
|
+
dodSection = "\n\n## Definition of Done Validation\n" +
|
|
1393
|
+
review.dodValidation.map((d) => `[${d.aiVerified ? "x" : " "}] ${d.label} — ${d.note}`).join("\n");
|
|
1394
|
+
}
|
|
1395
|
+
let concernsSection = "";
|
|
1396
|
+
if (review?.concerns?.length > 0) {
|
|
1397
|
+
concernsSection = "\n\n## Concerns\n" +
|
|
1398
|
+
review.concerns.map((c) => `- **${c.severity}:** ${c.description}${c.file ? ` (${c.file}${c.line ? `:${c.line}` : ""})` : ""}`).join("\n");
|
|
1399
|
+
}
|
|
1400
|
+
const formattedReview = `
|
|
1401
|
+
# AI Review — Story ${storyId.substring(0, 8)}
|
|
1402
|
+
|
|
1403
|
+
${review?.summary || "Review completed."}
|
|
1404
|
+
${acSection}${dodSection}${concernsSection}
|
|
1405
|
+
|
|
1406
|
+
---
|
|
1407
|
+
_Generated by Elixium AI Review Intelligence_
|
|
1408
|
+
`.trim();
|
|
1409
|
+
return {
|
|
1410
|
+
content: [{ type: "text", text: formattedReview }],
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1128
1413
|
case "estimate_cost": {
|
|
1129
1414
|
const args = request.params.arguments;
|
|
1130
1415
|
const { storyId, constraints } = args;
|
|
1131
1416
|
let { provider } = args;
|
|
1132
1417
|
if (!storyId)
|
|
1133
1418
|
throw new Error("storyId is required");
|
|
1419
|
+
assertUUID(storyId, "storyId");
|
|
1134
1420
|
// Default provider from infrastructure profile if not explicitly provided
|
|
1135
1421
|
if (!provider) {
|
|
1136
1422
|
const costConfig = await fetchFeatureConfig();
|
|
@@ -1200,6 +1486,7 @@ ${(estimate.assumptions || []).map((a) => `- ${a}`).join("\n")}
|
|
|
1200
1486
|
const { epicId } = args;
|
|
1201
1487
|
if (!epicId)
|
|
1202
1488
|
throw new Error("epicId is required");
|
|
1489
|
+
assertUUID(epicId, "epicId");
|
|
1203
1490
|
// Fetch all stories and filter by epicId
|
|
1204
1491
|
const allStories = await fetchStories();
|
|
1205
1492
|
const epicStories = allStories.filter((s) => s.epicId === epicId);
|
|
@@ -1302,11 +1589,20 @@ ${unestimated.length === 0
|
|
|
1302
1589
|
if (error.response) {
|
|
1303
1590
|
console.error("Response data:", error.response.data);
|
|
1304
1591
|
}
|
|
1592
|
+
let errorText = `Error: ${error.message}`;
|
|
1593
|
+
if (error.response?.status) {
|
|
1594
|
+
errorText += ` (HTTP ${error.response.status})`;
|
|
1595
|
+
}
|
|
1596
|
+
if (error.response?.data) {
|
|
1597
|
+
const data = error.response.data;
|
|
1598
|
+
const detail = typeof data === "string" ? data : JSON.stringify(data);
|
|
1599
|
+
errorText += `\nDetails: ${detail}`;
|
|
1600
|
+
}
|
|
1305
1601
|
return {
|
|
1306
1602
|
content: [
|
|
1307
1603
|
{
|
|
1308
1604
|
type: "text",
|
|
1309
|
-
text:
|
|
1605
|
+
text: errorText,
|
|
1310
1606
|
},
|
|
1311
1607
|
],
|
|
1312
1608
|
isError: true,
|