@elixium.ai/mcp-server 0.4.0 → 0.6.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/README.md +25 -0
- package/dist/constants/featureConfigFallback.js +30 -0
- package/dist/constants/workflowModelLabels.js +59 -0
- package/dist/index.js +308 -130
- package/dist/stakeholders.js +72 -0
- package/dist/toolSchemas.js +245 -0
- package/package.json +8 -6
- package/dist/__tests__/board-isolation.test.js +0 -83
- package/dist/__tests__/create-board.test.js +0 -97
- package/dist/__tests__/fixtures/boards.js +0 -117
- package/dist/__tests__/list-boards.test.js +0 -54
- package/dist/__tests__/select-board.test.js +0 -74
package/dist/index.js
CHANGED
|
@@ -5,6 +5,10 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
|
5
5
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
6
6
|
import axios from "axios";
|
|
7
7
|
import * as http from "node:http";
|
|
8
|
+
import { CREATE_STORY_INPUT_SCHEMA, UPDATE_STORY_INPUT_SCHEMA, GET_STAKEHOLDERS_INPUT_SCHEMA, PROPOSE_FIELD_DRAFT_INPUT_SCHEMA, ENDORSE_PROPOSAL_INPUT_SCHEMA, REJECT_PROPOSAL_INPUT_SCHEMA, DRAFT_HYPOTHESIS_INPUT_SCHEMA, GET_TRUST_LEAKAGE_INPUT_SCHEMA, } from "./toolSchemas.js";
|
|
9
|
+
import { deriveStakeholders } from "./stakeholders.js";
|
|
10
|
+
import { formatWorkflowModelLine } from "./constants/workflowModelLabels.js";
|
|
11
|
+
import { MCP_FEATURE_CONFIG_ERROR_FALLBACK } from "./constants/featureConfigFallback.js";
|
|
8
12
|
const API_KEY = process.env.ELIXIUM_API_KEY;
|
|
9
13
|
const API_URL = process.env.ELIXIUM_API_URL || "https://elixium.ai/api";
|
|
10
14
|
const BOARD_SLUG = process.env.ELIXIUM_BOARD_SLUG;
|
|
@@ -311,25 +315,8 @@ const fetchFeatureConfig = async () => {
|
|
|
311
315
|
return response.data;
|
|
312
316
|
}
|
|
313
317
|
catch (error) {
|
|
314
|
-
console.error("Failed to fetch feature config,
|
|
315
|
-
return
|
|
316
|
-
features: {
|
|
317
|
-
balancedTeam: true,
|
|
318
|
-
learningLoop: true,
|
|
319
|
-
tddWorkflow: true,
|
|
320
|
-
aiTools: true,
|
|
321
|
-
teamDecisions: false,
|
|
322
|
-
ragKnowledgeBase: false,
|
|
323
|
-
},
|
|
324
|
-
source: {
|
|
325
|
-
balancedTeam: "error-fallback",
|
|
326
|
-
learningLoop: "error-fallback",
|
|
327
|
-
tddWorkflow: "error-fallback",
|
|
328
|
-
aiTools: "error-fallback",
|
|
329
|
-
teamDecisions: "error-fallback",
|
|
330
|
-
ragKnowledgeBase: "error-fallback",
|
|
331
|
-
},
|
|
332
|
-
};
|
|
318
|
+
console.error("Failed to fetch feature config, using error fallback:", error);
|
|
319
|
+
return MCP_FEATURE_CONFIG_ERROR_FALLBACK;
|
|
333
320
|
}
|
|
334
321
|
};
|
|
335
322
|
// Helper functions to check individual features
|
|
@@ -643,52 +630,8 @@ const createServer = () => {
|
|
|
643
630
|
},
|
|
644
631
|
{
|
|
645
632
|
name: "create_story",
|
|
646
|
-
description: "Create a
|
|
647
|
-
inputSchema:
|
|
648
|
-
type: "object",
|
|
649
|
-
properties: {
|
|
650
|
-
title: { type: "string", description: "Title of the story" },
|
|
651
|
-
description: {
|
|
652
|
-
type: "string",
|
|
653
|
-
description: "Description of the story",
|
|
654
|
-
},
|
|
655
|
-
acceptanceCriteria: {
|
|
656
|
-
type: "string",
|
|
657
|
-
description: "Acceptance criteria in Given/When/Then format",
|
|
658
|
-
},
|
|
659
|
-
lane: {
|
|
660
|
-
type: "string",
|
|
661
|
-
description: "Lane to add the story to (case-insensitive)",
|
|
662
|
-
enum: [
|
|
663
|
-
"Backlog",
|
|
664
|
-
"Icebox",
|
|
665
|
-
"Current",
|
|
666
|
-
"Done",
|
|
667
|
-
"BACKLOG",
|
|
668
|
-
"ICEBOX",
|
|
669
|
-
"CURRENT",
|
|
670
|
-
"DONE",
|
|
671
|
-
],
|
|
672
|
-
},
|
|
673
|
-
points: {
|
|
674
|
-
type: "number",
|
|
675
|
-
description: "Points (0, 1, 2, 3, 5, 8)",
|
|
676
|
-
},
|
|
677
|
-
requester: {
|
|
678
|
-
type: "string",
|
|
679
|
-
description: "Email of the person requesting this story. Defaults to ELIXIUM_USER_EMAIL env var or API key owner.",
|
|
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
|
-
},
|
|
689
|
-
},
|
|
690
|
-
required: ["title"],
|
|
691
|
-
},
|
|
633
|
+
description: "Create a story on the active board. New stories enter the Backlog lane and are attributed to the user bound to your API key. Stories you create without current board context risk duplicating in-flight work or contradicting recent decisions. Ensure you are grounded before writing — if you haven't grounded this session, call `get_board_context` (optionally with `intent` set to the user's request for similar-story matching). This is the normal path when a user asks you to draft a story; it is not a bypass of human review, which happens at downstream workflow gates. Accepts storyType (feature/bug/chore/platform) at create time. Use `propose_field_draft` for asynchronous suggestions on fields of an existing story.",
|
|
634
|
+
inputSchema: CREATE_STORY_INPUT_SCHEMA,
|
|
692
635
|
},
|
|
693
636
|
{
|
|
694
637
|
name: "get_iteration_context",
|
|
@@ -698,6 +641,19 @@ const createServer = () => {
|
|
|
698
641
|
properties: {},
|
|
699
642
|
},
|
|
700
643
|
},
|
|
644
|
+
{
|
|
645
|
+
name: "get_board_context",
|
|
646
|
+
description: "Get a snapshot of the workspace's board state for grounding before drafting or updating content. Returns time-balanced sections: inFlight (Current lane + in_progress epics), queued (Backlog + next/soon epics), icebox (Icebox + someday epics), recentlyCompleted (Done + archived epics), decisions, objectives, learnings, plus configuration (feature flags, workflow stages). Pass `intent` (free-form text of what you're about to do; the user's verbatim request is fine) to additionally receive `similar` — existing stories ranked by relevance. Stories you create or update without current board context risk duplicating in-flight work or contradicting recent decisions; ensure you are grounded before writing.",
|
|
647
|
+
inputSchema: {
|
|
648
|
+
type: "object",
|
|
649
|
+
properties: {
|
|
650
|
+
intent: {
|
|
651
|
+
type: "string",
|
|
652
|
+
description: "Optional free-form text describing what you're about to do (e.g., the user's verbatim request, or a short topic phrase). When provided, the response additionally includes a `similar` section ranked by relevance.",
|
|
653
|
+
},
|
|
654
|
+
},
|
|
655
|
+
},
|
|
656
|
+
},
|
|
701
657
|
{
|
|
702
658
|
name: "list_objectives",
|
|
703
659
|
description: "List objectives for the current workspace",
|
|
@@ -716,7 +672,7 @@ const createServer = () => {
|
|
|
716
672
|
},
|
|
717
673
|
{
|
|
718
674
|
name: "create_epic",
|
|
719
|
-
description: "Create
|
|
675
|
+
description: "Create an epic for the active board. Epics group related stories around an outcome hypothesis. Before creating, ensure you are grounded — call `get_board_context` to surface existing epics, workspace objectives, and recent decisions; duplicate or overlapping epics are easy to introduce without that context. Epics are attributed to the user bound to your API key. Use `update_epic` to revise an existing epic rather than duplicating.",
|
|
720
676
|
inputSchema: {
|
|
721
677
|
type: "object",
|
|
722
678
|
properties: {
|
|
@@ -741,7 +697,7 @@ const createServer = () => {
|
|
|
741
697
|
},
|
|
742
698
|
{
|
|
743
699
|
name: "update_epic",
|
|
744
|
-
description: "Update an epic
|
|
700
|
+
description: "Update an epic's title, description, stage, or outcome fields (hypothesis, success metrics, outcome status, target date). Before updating, ensure you are grounded — call `get_board_context` for current board state and recent decisions that may affect this epic. Updates are attributed to the user bound to your API key.",
|
|
745
701
|
inputSchema: {
|
|
746
702
|
type: "object",
|
|
747
703
|
properties: {
|
|
@@ -767,58 +723,38 @@ const createServer = () => {
|
|
|
767
723
|
},
|
|
768
724
|
{
|
|
769
725
|
name: "update_story",
|
|
770
|
-
description: "Update fields on an existing story",
|
|
771
|
-
inputSchema:
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
},
|
|
803
|
-
acceptanceCriteria: {
|
|
804
|
-
type: "string",
|
|
805
|
-
description: "Acceptance criteria in Given/When/Then format",
|
|
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
|
-
},
|
|
819
|
-
},
|
|
820
|
-
required: ["storyId"],
|
|
821
|
-
},
|
|
726
|
+
description: "Update fields on an existing story. Accepts Learning Loop fields (hypothesis, confidence_score, hidden_unknowns, risk_profile) and storyType in addition to the basics. Before updating, call `get_story` to inspect current state, and `get_board_context` if your update relates to a topic that may be governed by a recent team decision. Updates are attributed to the user bound to your API key.",
|
|
727
|
+
inputSchema: UPDATE_STORY_INPUT_SCHEMA,
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
name: "get_stakeholders",
|
|
731
|
+
description: "Derive the set of trust audiences this story needs to earn trust with. Returns an array of {audience, reason} entries based on the story's risk_profile, its linked epic's hypothesis/successMetrics, and the workspace's compliance frameworks. Pure derivation — no mutations, no AI calls.",
|
|
732
|
+
inputSchema: GET_STAKEHOLDERS_INPUT_SCHEMA,
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
name: "propose_field_draft",
|
|
736
|
+
description: "Submit an AI-drafted value for a Learning Loop field as a pending proposal. The draft does NOT change the story until a human endorses via endorse_proposal. Caller should disclose `provider` (LLM model) and `agent` (client+version) for trust attribution. Stories whose fields you propose without current board context risk contradicting recent decisions in this area. Ensure you are grounded before drafting — call `get_board_context` if you haven't grounded this session.",
|
|
737
|
+
inputSchema: PROPOSE_FIELD_DRAFT_INPUT_SCHEMA,
|
|
738
|
+
},
|
|
739
|
+
{
|
|
740
|
+
name: "endorse_proposal",
|
|
741
|
+
description: "Endorse a pending field proposal. Updates the underlying story field with the proposal's draft_value and emits a proposal.endorsed audit event. Only humans should endorse — the agent should never call this on its own draft.",
|
|
742
|
+
inputSchema: ENDORSE_PROPOSAL_INPUT_SCHEMA,
|
|
743
|
+
},
|
|
744
|
+
{
|
|
745
|
+
name: "reject_proposal",
|
|
746
|
+
description: "Reject a pending field proposal with a required reason. Story field is unchanged; reason is captured for agent learning + audit.",
|
|
747
|
+
inputSchema: REJECT_PROPOSAL_INPUT_SCHEMA,
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
name: "draft_hypothesis",
|
|
751
|
+
description: "Generate an AI-drafted hypothesis for the given story and submit it as a pending proposal. The backend calls the configured AI_PROVIDER, drafts the hypothesis text, and creates a proposal that a human must endorse via `endorse_proposal` before the story's hypothesis field is updated. The draft does not change the story until endorsed.",
|
|
752
|
+
inputSchema: DRAFT_HYPOTHESIS_INPUT_SCHEMA,
|
|
753
|
+
},
|
|
754
|
+
{
|
|
755
|
+
name: "get_trust_leakage",
|
|
756
|
+
description: "Return the tenant's off-system decision report: commits authored since `since` on the workspace's configured Git provider (GitHub, GitHub Enterprise, GitLab, or Gitea) whose messages do NOT reference any story on the board. Nudge signal, not a gate. Requires workspace_git_provider to be configured via POST /workspace/git-provider first.",
|
|
757
|
+
inputSchema: GET_TRUST_LEAKAGE_INPUT_SCHEMA,
|
|
822
758
|
},
|
|
823
759
|
{
|
|
824
760
|
name: "prepare_implementation",
|
|
@@ -938,7 +874,7 @@ const createServer = () => {
|
|
|
938
874
|
},
|
|
939
875
|
{
|
|
940
876
|
name: "propose_test_plan",
|
|
941
|
-
description: "Submit a test plan for human review. Sets workflow_stage to tests_proposed. Implementation is BLOCKED until human approves.",
|
|
877
|
+
description: "Submit a test plan for human review. Sets workflow_stage to tests_proposed. Implementation is BLOCKED until human approves. Before proposing, call `get_story` to inspect the story's hypothesis, hidden_unknowns, and risk_profile (the test plan should ground in those fields). Use `draft_test_plan` first for a scaffold seeded from the story's premortem assumptions.",
|
|
942
878
|
inputSchema: {
|
|
943
879
|
type: "object",
|
|
944
880
|
properties: {
|
|
@@ -959,6 +895,20 @@ const createServer = () => {
|
|
|
959
895
|
required: ["storyId", "testPlan"],
|
|
960
896
|
},
|
|
961
897
|
},
|
|
898
|
+
{
|
|
899
|
+
name: "draft_test_plan",
|
|
900
|
+
description: "Return a starter test plan scaffold seeded from the story's hidden_unknowns and hypothesis. The agent edits this scaffold and calls propose_test_plan with the final version. Makes Icebox premortem assumptions load-bearing at test authoring time.",
|
|
901
|
+
inputSchema: {
|
|
902
|
+
type: "object",
|
|
903
|
+
properties: {
|
|
904
|
+
storyId: {
|
|
905
|
+
type: "string",
|
|
906
|
+
description: "ID of the story to draft a test plan for",
|
|
907
|
+
},
|
|
908
|
+
},
|
|
909
|
+
required: ["storyId"],
|
|
910
|
+
},
|
|
911
|
+
},
|
|
962
912
|
{
|
|
963
913
|
name: "approve_tests",
|
|
964
914
|
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.",
|
|
@@ -1032,7 +982,7 @@ const createServer = () => {
|
|
|
1032
982
|
const learningLoopTools = learningLoopEnabled ? [
|
|
1033
983
|
{
|
|
1034
984
|
name: "create_hypothesis",
|
|
1035
|
-
description: "Create a new assumption/hypothesis in the Icebox for validation",
|
|
985
|
+
description: "Create a new assumption/hypothesis in the Icebox for validation. Before creating, ensure you are grounded — call `get_board_context` to check for existing related hypotheses and recent decisions in this area. The hypothesis is attributed to the user bound to your API key.",
|
|
1036
986
|
inputSchema: {
|
|
1037
987
|
type: "object",
|
|
1038
988
|
properties: {
|
|
@@ -1050,7 +1000,7 @@ const createServer = () => {
|
|
|
1050
1000
|
},
|
|
1051
1001
|
{
|
|
1052
1002
|
name: "record_learning",
|
|
1053
|
-
description: "Record a learning outcome for a completed story",
|
|
1003
|
+
description: "Record a learning outcome for a completed story (sets the story's outcome_summary field). The learning is visible in the workspace's recent-learnings surface. Before recording, call `get_story` to inspect the story's current state, and `get_board_context` to surface recently-recorded learnings on related stories that may already capture the same insight.",
|
|
1054
1004
|
inputSchema: {
|
|
1055
1005
|
type: "object",
|
|
1056
1006
|
properties: {
|
|
@@ -1068,7 +1018,7 @@ const createServer = () => {
|
|
|
1068
1018
|
const teamDecisionsTools = teamDecisionsEnabled ? [
|
|
1069
1019
|
{
|
|
1070
1020
|
name: "record_decision",
|
|
1071
|
-
description: "Record a team decision, meeting outcome, or architectural choice. These are shared across all team members' AI sessions.",
|
|
1021
|
+
description: "Record a team decision, meeting outcome, or architectural choice. These are shared across all team members' AI sessions. Before recording, ensure you are grounded — call `get_board_context` to check whether a recent decision already covers this area; duplicate or contradictory decisions are exactly what this tool exists to prevent.",
|
|
1072
1022
|
inputSchema: {
|
|
1073
1023
|
type: "object",
|
|
1074
1024
|
properties: {
|
|
@@ -1149,7 +1099,7 @@ const createServer = () => {
|
|
|
1149
1099
|
try {
|
|
1150
1100
|
const toolName = request.params.name;
|
|
1151
1101
|
// Check TDD workflow tools
|
|
1152
|
-
const tddWorkflowTools = ["start_story", "propose_test_plan", "get_test_plan", "submit_for_review", "review_pr"];
|
|
1102
|
+
const tddWorkflowTools = ["start_story", "propose_test_plan", "draft_test_plan", "get_test_plan", "submit_for_review", "review_pr"];
|
|
1153
1103
|
if (tddWorkflowTools.includes(toolName)) {
|
|
1154
1104
|
const enabled = await isTddWorkflowEnabled();
|
|
1155
1105
|
if (!enabled) {
|
|
@@ -1411,6 +1361,58 @@ ${config.infrastructureProfile?.provider ? `- Provider: ${config.infrastructureP
|
|
|
1411
1361
|
],
|
|
1412
1362
|
};
|
|
1413
1363
|
}
|
|
1364
|
+
case "get_board_context": {
|
|
1365
|
+
// Thin wrapper around GET /api/board-context. The backend composes
|
|
1366
|
+
// the response; this handler forwards the optional `intent` query
|
|
1367
|
+
// param and wraps the response in MCP content format.
|
|
1368
|
+
//
|
|
1369
|
+
// Note: the backend also supports a `since` parameter for conditional
|
|
1370
|
+
// return (sentinel when workspace state hasn't shifted), but it is
|
|
1371
|
+
// intentionally NOT exposed at the MCP layer until the
|
|
1372
|
+
// context_version invariant ships (separate follow-on story:
|
|
1373
|
+
// "Wire context_version invariant with chosen atomicity pattern").
|
|
1374
|
+
// Without per-write version bumps, the sentinel would be unreliable
|
|
1375
|
+
// (stale `unchanged: true` for state that did change).
|
|
1376
|
+
//
|
|
1377
|
+
// Graceful 404 fallback: if the backend is older than 2026-05-17
|
|
1378
|
+
// (or a self-hosted release that doesn't include this endpoint),
|
|
1379
|
+
// return a structured message pointing the agent at the existing
|
|
1380
|
+
// multi-tool orchestration path rather than throwing an opaque
|
|
1381
|
+
// error. Version skew between mcp-server and backend is the most
|
|
1382
|
+
// common failure mode for new MCP tools; surfacing it well here
|
|
1383
|
+
// is cheaper than every customer filing the same bug report.
|
|
1384
|
+
const args = request.params.arguments ?? {};
|
|
1385
|
+
const params = {};
|
|
1386
|
+
if (typeof args.intent === "string" && args.intent.trim()) {
|
|
1387
|
+
params.intent = args.intent.trim();
|
|
1388
|
+
}
|
|
1389
|
+
try {
|
|
1390
|
+
const response = await client.get("/board-context", { params });
|
|
1391
|
+
return {
|
|
1392
|
+
content: [
|
|
1393
|
+
{ type: "text", text: JSON.stringify(response.data, null, 2) },
|
|
1394
|
+
],
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
catch (err) {
|
|
1398
|
+
if (err?.response?.status === 404) {
|
|
1399
|
+
return {
|
|
1400
|
+
content: [
|
|
1401
|
+
{
|
|
1402
|
+
type: "text",
|
|
1403
|
+
text: JSON.stringify({
|
|
1404
|
+
error: "get_board_context is not available on this backend",
|
|
1405
|
+
reason: "The backend does not expose GET /api/board-context. This is likely a version-skew issue: the tool was added in mcp-server 0.6.0 (2026-05-17) and requires a backend release that includes the matching endpoint.",
|
|
1406
|
+
fallback: "Until the backend is updated, orchestrate grounding manually: call `get_iteration_context` (in-flight work), `list_epics` (queued and in-progress epics), `list_decisions` (recent team decisions in the workspace), and `get_feature_config` (workspace configuration). The trade-offs are more tool calls and no time-balanced sections or similar-story matching, but the agent still gets the underlying grounding data.",
|
|
1407
|
+
action: "Self-hosted: update the backend to a release that includes GET /api/board-context. SaaS (elixium.ai-hosted): contact support — the endpoint should already be live on hosted backends.",
|
|
1408
|
+
}, null, 2),
|
|
1409
|
+
},
|
|
1410
|
+
],
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
throw err;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1414
1416
|
case "create_hypothesis": {
|
|
1415
1417
|
const args = request.params.arguments;
|
|
1416
1418
|
const normalizedLane = await normalizeLane("Icebox");
|
|
@@ -1540,6 +1542,134 @@ ${config.infrastructureProfile?.provider ? `- Provider: ${config.infrastructureP
|
|
|
1540
1542
|
],
|
|
1541
1543
|
};
|
|
1542
1544
|
}
|
|
1545
|
+
case "get_stakeholders": {
|
|
1546
|
+
const args = request.params.arguments;
|
|
1547
|
+
const { storyId } = args;
|
|
1548
|
+
assertUUID(storyId, "storyId");
|
|
1549
|
+
// Fetch story + feature config in parallel; the story tells us
|
|
1550
|
+
// whether to fetch a linked epic.
|
|
1551
|
+
const [storyResponse, featureConfig] = await Promise.all([
|
|
1552
|
+
client.get(`/stories/${storyId}`),
|
|
1553
|
+
fetchFeatureConfig(),
|
|
1554
|
+
]);
|
|
1555
|
+
const story = storyResponse.data;
|
|
1556
|
+
// If the story is linked to an epic, fetch the epic too.
|
|
1557
|
+
let epic = null;
|
|
1558
|
+
if (story.epic_id) {
|
|
1559
|
+
try {
|
|
1560
|
+
const epicResponse = await client.get(`/epics/${story.epic_id}`);
|
|
1561
|
+
epic = epicResponse.data;
|
|
1562
|
+
}
|
|
1563
|
+
catch {
|
|
1564
|
+
// Epic fetch failure is non-fatal — derivation simply won't
|
|
1565
|
+
// add the `execs` audience in this case.
|
|
1566
|
+
epic = null;
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
// Read from canonical field name `complianceFrameworks` (per
|
|
1570
|
+
// types.ts InfrastructureProfile); fall back to `compliance` for
|
|
1571
|
+
// any shipped callers that use the older name. Values are
|
|
1572
|
+
// normalized case-insensitively in deriveStakeholders.
|
|
1573
|
+
const infra = featureConfig.infrastructureProfile ?? {};
|
|
1574
|
+
const workspaceCompliance = Array.isArray(infra.complianceFrameworks)
|
|
1575
|
+
? infra.complianceFrameworks
|
|
1576
|
+
: Array.isArray(infra.compliance)
|
|
1577
|
+
? infra.compliance
|
|
1578
|
+
: [];
|
|
1579
|
+
const stakeholders = deriveStakeholders({
|
|
1580
|
+
story: {
|
|
1581
|
+
story_type: story.story_type ?? null,
|
|
1582
|
+
title: story.title ?? null,
|
|
1583
|
+
risk_profile: story.risk_profile ?? null,
|
|
1584
|
+
epic_id: story.epic_id ?? null,
|
|
1585
|
+
},
|
|
1586
|
+
epic: epic
|
|
1587
|
+
? {
|
|
1588
|
+
hypothesis: epic.hypothesis ?? null,
|
|
1589
|
+
successMetrics: epic.successMetrics ?? null,
|
|
1590
|
+
}
|
|
1591
|
+
: null,
|
|
1592
|
+
workspaceCompliance,
|
|
1593
|
+
});
|
|
1594
|
+
const payload = {
|
|
1595
|
+
storyId: story.id,
|
|
1596
|
+
stakeholders,
|
|
1597
|
+
derived_from: {
|
|
1598
|
+
story_type: story.story_type ?? null,
|
|
1599
|
+
risk_profile: story.risk_profile ?? null,
|
|
1600
|
+
epic_id: story.epic_id ?? null,
|
|
1601
|
+
compliance_frameworks: workspaceCompliance,
|
|
1602
|
+
},
|
|
1603
|
+
};
|
|
1604
|
+
return {
|
|
1605
|
+
content: [
|
|
1606
|
+
{ type: "text", text: JSON.stringify(payload, null, 2) },
|
|
1607
|
+
],
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
case "propose_field_draft": {
|
|
1611
|
+
const args = request.params.arguments;
|
|
1612
|
+
const { storyId, fieldName, draftValue, provider, agent } = args;
|
|
1613
|
+
assertUUID(storyId, "storyId");
|
|
1614
|
+
const response = await client.post(`/stories/${storyId}/proposals`, {
|
|
1615
|
+
field_name: fieldName,
|
|
1616
|
+
draft_value: draftValue,
|
|
1617
|
+
...(provider ? { provider } : {}),
|
|
1618
|
+
...(agent ? { agent } : {}),
|
|
1619
|
+
});
|
|
1620
|
+
return {
|
|
1621
|
+
content: [
|
|
1622
|
+
{ type: "text", text: JSON.stringify(response.data, null, 2) },
|
|
1623
|
+
],
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
case "endorse_proposal": {
|
|
1627
|
+
const args = request.params.arguments;
|
|
1628
|
+
const { proposalId } = args;
|
|
1629
|
+
assertUUID(proposalId, "proposalId");
|
|
1630
|
+
const response = await client.post(`/proposals/${proposalId}/endorse`, {});
|
|
1631
|
+
return {
|
|
1632
|
+
content: [
|
|
1633
|
+
{ type: "text", text: JSON.stringify(response.data, null, 2) },
|
|
1634
|
+
],
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1637
|
+
case "reject_proposal": {
|
|
1638
|
+
const args = request.params.arguments;
|
|
1639
|
+
const { proposalId, reason } = args;
|
|
1640
|
+
assertUUID(proposalId, "proposalId");
|
|
1641
|
+
const response = await client.post(`/proposals/${proposalId}/reject`, { reason });
|
|
1642
|
+
return {
|
|
1643
|
+
content: [
|
|
1644
|
+
{ type: "text", text: JSON.stringify(response.data, null, 2) },
|
|
1645
|
+
],
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
case "draft_hypothesis": {
|
|
1649
|
+
const args = request.params.arguments;
|
|
1650
|
+
const { storyId, context } = args;
|
|
1651
|
+
assertUUID(storyId, "storyId");
|
|
1652
|
+
const response = await client.post(`/stories/${storyId}/proposals/draft`, { field_name: "hypothesis", ...(context ? { context } : {}) });
|
|
1653
|
+
return {
|
|
1654
|
+
content: [
|
|
1655
|
+
{ type: "text", text: JSON.stringify(response.data, null, 2) },
|
|
1656
|
+
],
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
case "get_trust_leakage": {
|
|
1660
|
+
const args = (request.params.arguments ?? {});
|
|
1661
|
+
const query = {};
|
|
1662
|
+
if (args.since)
|
|
1663
|
+
query.since = String(args.since);
|
|
1664
|
+
if (args.repo)
|
|
1665
|
+
query.repo = String(args.repo);
|
|
1666
|
+
const response = await client.get(`/trust-leakage`, { params: query });
|
|
1667
|
+
return {
|
|
1668
|
+
content: [
|
|
1669
|
+
{ type: "text", text: JSON.stringify(response.data, null, 2) },
|
|
1670
|
+
],
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1543
1673
|
case "prepare_implementation": {
|
|
1544
1674
|
const args = request.params.arguments;
|
|
1545
1675
|
const { storyId } = args;
|
|
@@ -1680,6 +1810,9 @@ Here’s the smallest change that will validate it:
|
|
|
1680
1810
|
const dorWarnings = formatDorWarnings(boardSettings, result.dorChecklist);
|
|
1681
1811
|
const isTrunk = result.trunkBased;
|
|
1682
1812
|
const isAutoMerge = result.autoMerge;
|
|
1813
|
+
const workflowModelLine = formatWorkflowModelLine({
|
|
1814
|
+
useDeliveredState: teamConfig.features.useDeliveredState,
|
|
1815
|
+
});
|
|
1683
1816
|
let formattedResult;
|
|
1684
1817
|
if (isTrunk && !isAutoMerge) {
|
|
1685
1818
|
formattedResult = `
|
|
@@ -1688,6 +1821,7 @@ Here’s the smallest change that will validate it:
|
|
|
1688
1821
|
**Mode:** Direct to main (trunk-based development)
|
|
1689
1822
|
**Feature Flag:** \`${result.featureFlagName || "none"}\`
|
|
1690
1823
|
**Workflow Stage:** ${result.workflow_stage}
|
|
1824
|
+
**Workflow Model:** ${workflowModelLine}
|
|
1691
1825
|
|
|
1692
1826
|
## Acceptance Criteria
|
|
1693
1827
|
${result.acceptance_criteria || "No specific AC provided."}
|
|
@@ -1714,6 +1848,7 @@ ${dorWarnings}`;
|
|
|
1714
1848
|
**Branch:** \`${result.branch}\`
|
|
1715
1849
|
**Feature Flag:** \`${result.featureFlagName || "none"}\`
|
|
1716
1850
|
**Workflow Stage:** ${result.workflow_stage}
|
|
1851
|
+
**Workflow Model:** ${workflowModelLine}
|
|
1717
1852
|
|
|
1718
1853
|
## Acceptance Criteria
|
|
1719
1854
|
${result.acceptance_criteria || "No specific AC provided."}
|
|
@@ -1738,6 +1873,7 @@ ${dorWarnings}`;
|
|
|
1738
1873
|
|
|
1739
1874
|
**Branch:** \`${result.branch}\`
|
|
1740
1875
|
**Workflow Stage:** ${result.workflow_stage}
|
|
1876
|
+
**Workflow Model:** ${workflowModelLine}
|
|
1741
1877
|
|
|
1742
1878
|
## Acceptance Criteria
|
|
1743
1879
|
${result.acceptance_criteria || "No specific AC provided."}
|
|
@@ -1844,6 +1980,30 @@ ${(result.test_file_paths || []).map((p) => `- \`${p}\``).join("\n") || "No test
|
|
|
1844
1980
|
${result.message}
|
|
1845
1981
|
|
|
1846
1982
|
> 🛑 **BLOCKED:** Implementation cannot proceed until a human approves this test plan.
|
|
1983
|
+
`;
|
|
1984
|
+
return {
|
|
1985
|
+
content: [{ type: "text", text: formattedResult.trim() }],
|
|
1986
|
+
};
|
|
1987
|
+
}
|
|
1988
|
+
case "draft_test_plan": {
|
|
1989
|
+
const args = request.params.arguments;
|
|
1990
|
+
const { storyId } = args;
|
|
1991
|
+
if (!storyId) {
|
|
1992
|
+
throw new Error("storyId is required");
|
|
1993
|
+
}
|
|
1994
|
+
assertUUID(storyId, "storyId");
|
|
1995
|
+
const response = await client.get(`/stories/${storyId}/draft-test-plan`);
|
|
1996
|
+
const result = response.data;
|
|
1997
|
+
const formattedResult = `
|
|
1998
|
+
# Drafted Test Plan
|
|
1999
|
+
|
|
2000
|
+
**Story ID:** ${storyId}
|
|
2001
|
+
|
|
2002
|
+
> This is a scaffold seeded from the story's \`hidden_unknowns\` and \`hypothesis\`. Edit freely, then call \`propose_test_plan\` with the final version.
|
|
2003
|
+
|
|
2004
|
+
---
|
|
2005
|
+
|
|
2006
|
+
${result.testPlan}
|
|
1847
2007
|
`;
|
|
1848
2008
|
return {
|
|
1849
2009
|
content: [{ type: "text", text: formattedResult.trim() }],
|
|
@@ -1880,13 +2040,20 @@ ${result.message}
|
|
|
1880
2040
|
throw new Error("storyId is required");
|
|
1881
2041
|
}
|
|
1882
2042
|
assertUUID(storyId, "storyId");
|
|
1883
|
-
const response = await
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
2043
|
+
const [response, submitFeatureConfig] = await Promise.all([
|
|
2044
|
+
client.post(`/stories/${storyId}/submit-review`, {
|
|
2045
|
+
commitHash,
|
|
2046
|
+
testResults,
|
|
2047
|
+
implementationNotes,
|
|
2048
|
+
}),
|
|
2049
|
+
fetchFeatureConfig(),
|
|
2050
|
+
]);
|
|
1888
2051
|
const result = response.data;
|
|
1889
2052
|
const isAutoMerge = result.autoMerge;
|
|
2053
|
+
const submitWorkflowModelLine = formatWorkflowModelLine({
|
|
2054
|
+
useDeliveredState: submitFeatureConfig.features.useDeliveredState,
|
|
2055
|
+
state: result.state,
|
|
2056
|
+
});
|
|
1890
2057
|
let autoMergeSection = "";
|
|
1891
2058
|
if (isAutoMerge && result.autoMergeInstructions) {
|
|
1892
2059
|
const instr = result.autoMergeInstructions;
|
|
@@ -1920,6 +2087,7 @@ git branch -d ${instr.sourceBranch}
|
|
|
1920
2087
|
**Status:** ${result.status}
|
|
1921
2088
|
**Workflow Stage:** ${result.workflow_stage}
|
|
1922
2089
|
**State:** ${result.state}
|
|
2090
|
+
**Workflow Model:** ${submitWorkflowModelLine}
|
|
1923
2091
|
${result.trunkBased ? `**Mode:** Trunk-based${isAutoMerge ? " + Auto-merge" : ""}` : ""}
|
|
1924
2092
|
${result.featureFlagName ? `**Feature Flag:** \`${result.featureFlagName}\`` : ""}
|
|
1925
2093
|
|
|
@@ -2335,7 +2503,17 @@ ${depList || "No dependencies detected."}
|
|
|
2335
2503
|
if (error.response?.status) {
|
|
2336
2504
|
errorText += ` (HTTP ${error.response.status})`;
|
|
2337
2505
|
}
|
|
2338
|
-
|
|
2506
|
+
// Enhanced 403 handling for RBAC and API key scope errors
|
|
2507
|
+
if (error.response?.status === 403 && error.response?.data) {
|
|
2508
|
+
const data = error.response.data;
|
|
2509
|
+
if (data.required && data.currentScopes) {
|
|
2510
|
+
errorText = `🔑 API key missing scope: ${data.required}\n\nYour key has scopes: [${data.currentScopes.join(", ")}]\nRegenerate your API key with the required scope to use this tool.\n\nIn Elixium → Integrations → Generate API Key (with scopes)`;
|
|
2511
|
+
}
|
|
2512
|
+
else if (data.required && data.currentLevel) {
|
|
2513
|
+
errorText = `🔒 Insufficient permissions: ${data.required}\n\nYour access level is "${data.currentLevel}". This operation requires a higher access level.\nContact your workspace owner to update your role.`;
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
else if (error.response?.data) {
|
|
2339
2517
|
const data = error.response.data;
|
|
2340
2518
|
const detail = typeof data === "string" ? data : JSON.stringify(data);
|
|
2341
2519
|
errorText += `\nDetails: ${detail}`;
|