@cliangdev/flux-plugin 0.2.0 → 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.
Files changed (108) hide show
  1. package/README.md +11 -7
  2. package/agents/coder.md +150 -25
  3. package/bin/install.cjs +171 -16
  4. package/commands/breakdown.md +47 -10
  5. package/commands/dashboard.md +29 -0
  6. package/commands/flux.md +92 -12
  7. package/commands/implement.md +166 -17
  8. package/commands/linear.md +6 -5
  9. package/commands/prd.md +996 -82
  10. package/manifest.json +2 -1
  11. package/package.json +9 -11
  12. package/skills/flux-orchestrator/SKILL.md +11 -3
  13. package/skills/prd-writer/SKILL.md +761 -0
  14. package/skills/ux-ui-design/SKILL.md +346 -0
  15. package/skills/ux-ui-design/references/design-tokens.md +359 -0
  16. package/src/__tests__/version.test.ts +37 -0
  17. package/src/adapters/local/.gitkeep +0 -0
  18. package/src/dashboard/__tests__/api.test.ts +211 -0
  19. package/src/dashboard/browser.ts +35 -0
  20. package/src/dashboard/public/app.js +869 -0
  21. package/src/dashboard/public/index.html +90 -0
  22. package/src/dashboard/public/styles.css +807 -0
  23. package/src/dashboard/public/vendor/highlight.css +10 -0
  24. package/src/dashboard/public/vendor/highlight.min.js +8422 -0
  25. package/src/dashboard/public/vendor/marked.min.js +2210 -0
  26. package/src/dashboard/server.ts +296 -0
  27. package/src/dashboard/watchers.ts +83 -0
  28. package/src/server/__tests__/config.test.ts +163 -0
  29. package/src/server/adapters/__tests__/a-client-linear.test.ts +197 -0
  30. package/src/server/adapters/__tests__/adapter-factory.test.ts +230 -0
  31. package/src/server/adapters/__tests__/dependency-ops.test.ts +429 -0
  32. package/src/server/adapters/__tests__/document-ops.test.ts +306 -0
  33. package/src/server/adapters/__tests__/linear-adapter.test.ts +91 -0
  34. package/src/server/adapters/__tests__/linear-config.test.ts +425 -0
  35. package/src/server/adapters/__tests__/linear-criteria-parser.test.ts +287 -0
  36. package/src/server/adapters/__tests__/linear-description-test.ts +238 -0
  37. package/src/server/adapters/__tests__/linear-epic-crud.test.ts +496 -0
  38. package/src/server/adapters/__tests__/linear-mappers-description.test.ts +276 -0
  39. package/src/server/adapters/__tests__/linear-mappers-epic.test.ts +294 -0
  40. package/src/server/adapters/__tests__/linear-mappers-prd.test.ts +300 -0
  41. package/src/server/adapters/__tests__/linear-mappers-task.test.ts +197 -0
  42. package/src/server/adapters/__tests__/linear-prd-crud.test.ts +620 -0
  43. package/src/server/adapters/__tests__/linear-stats.test.ts +450 -0
  44. package/src/server/adapters/__tests__/linear-task-crud.test.ts +534 -0
  45. package/src/server/adapters/__tests__/linear-types.test.ts +243 -0
  46. package/src/server/adapters/__tests__/status-ops.test.ts +441 -0
  47. package/src/server/adapters/factory.ts +90 -0
  48. package/src/server/adapters/index.ts +9 -0
  49. package/src/server/adapters/linear/adapter.ts +1141 -0
  50. package/src/server/adapters/linear/client.ts +169 -0
  51. package/src/server/adapters/linear/config.ts +152 -0
  52. package/src/server/adapters/linear/helpers/criteria-parser.ts +197 -0
  53. package/src/server/adapters/linear/helpers/index.ts +7 -0
  54. package/src/server/adapters/linear/index.ts +16 -0
  55. package/src/server/adapters/linear/mappers/description.ts +136 -0
  56. package/src/server/adapters/linear/mappers/epic.ts +81 -0
  57. package/src/server/adapters/linear/mappers/index.ts +27 -0
  58. package/src/server/adapters/linear/mappers/prd.ts +178 -0
  59. package/src/server/adapters/linear/mappers/task.ts +82 -0
  60. package/src/server/adapters/linear/types.ts +264 -0
  61. package/src/server/adapters/local-adapter.ts +1009 -0
  62. package/src/server/adapters/types.ts +293 -0
  63. package/src/server/config.ts +73 -0
  64. package/src/server/db/__tests__/queries.test.ts +473 -0
  65. package/src/server/db/ids.ts +17 -0
  66. package/src/server/db/index.ts +69 -0
  67. package/src/server/db/queries.ts +142 -0
  68. package/src/server/db/refs.ts +60 -0
  69. package/src/server/db/schema.ts +97 -0
  70. package/src/server/db/sqlite.ts +10 -0
  71. package/src/server/index.ts +81 -0
  72. package/src/server/tools/__tests__/crud.test.ts +411 -0
  73. package/src/server/tools/__tests__/get-version.test.ts +27 -0
  74. package/src/server/tools/__tests__/mcp-interface.test.ts +479 -0
  75. package/src/server/tools/__tests__/query.test.ts +405 -0
  76. package/src/server/tools/__tests__/z-configure-linear.test.ts +511 -0
  77. package/src/server/tools/__tests__/z-get-linear-url.test.ts +108 -0
  78. package/src/server/tools/configure-linear.ts +373 -0
  79. package/src/server/tools/create-epic.ts +44 -0
  80. package/src/server/tools/create-prd.ts +40 -0
  81. package/src/server/tools/create-task.ts +47 -0
  82. package/src/server/tools/criteria.ts +50 -0
  83. package/src/server/tools/delete-entity.ts +76 -0
  84. package/src/server/tools/dependencies.ts +55 -0
  85. package/src/server/tools/get-entity.ts +240 -0
  86. package/src/server/tools/get-linear-url.ts +28 -0
  87. package/src/server/tools/get-stats.ts +52 -0
  88. package/src/server/tools/get-version.ts +20 -0
  89. package/src/server/tools/index.ts +158 -0
  90. package/src/server/tools/init-project.ts +108 -0
  91. package/src/server/tools/query-entities.ts +167 -0
  92. package/src/server/tools/render-status.ts +219 -0
  93. package/src/server/tools/update-entity.ts +140 -0
  94. package/src/server/tools/update-status.ts +166 -0
  95. package/src/server/utils/__tests__/mcp-response.test.ts +331 -0
  96. package/src/server/utils/logger.ts +9 -0
  97. package/src/server/utils/mcp-response.ts +254 -0
  98. package/src/server/utils/status-transitions.ts +160 -0
  99. package/src/status-line/__tests__/status-line.test.ts +215 -0
  100. package/src/status-line/index.ts +147 -0
  101. package/src/utils/__tests__/chalk-import.test.ts +32 -0
  102. package/src/utils/__tests__/display.test.ts +97 -0
  103. package/src/utils/__tests__/status-renderer.test.ts +310 -0
  104. package/src/utils/display.ts +62 -0
  105. package/src/utils/status-renderer.ts +214 -0
  106. package/src/version.ts +5 -0
  107. package/dist/server/index.js +0 -87063
  108. package/skills/prd-template/SKILL.md +0 -242
@@ -0,0 +1,60 @@
1
+ import type { Database } from "./sqlite.js";
2
+
3
+ /**
4
+ * Generate a project prefix from project name.
5
+ * Uses acronym for multi-word names, or truncated uppercase for single words.
6
+ *
7
+ * @example
8
+ * generatePrefix("my-saas-app") // -> "MSA"
9
+ * generatePrefix("flux") // -> "FLUX"
10
+ * generatePrefix("my app") // -> "MA"
11
+ */
12
+ export function generatePrefix(projectName: string): string {
13
+ const words = projectName
14
+ .replace(/[^a-zA-Z0-9]/g, " ")
15
+ .split(/\s+/)
16
+ .filter((w) => w.length > 0);
17
+
18
+ if (words.length >= 2) {
19
+ return words
20
+ .map((w) => w[0])
21
+ .join("")
22
+ .toUpperCase()
23
+ .slice(0, 10);
24
+ }
25
+ return projectName
26
+ .replace(/[^a-zA-Z0-9]/g, "")
27
+ .toUpperCase()
28
+ .slice(0, 10);
29
+ }
30
+
31
+ /**
32
+ * Generate a sequential reference for an entity.
33
+ * Refs follow pattern: PREFIX-TYPE{NUMBER}
34
+ *
35
+ * @param db - Database instance
36
+ * @param type - Entity type: "P" (PRD), "E" (Epic), "T" (Task)
37
+ * @param prefix - Project prefix
38
+ * @returns Sequential ref like "MSA-P1", "MSA-E1", "MSA-T1"
39
+ */
40
+ export function generateRef(
41
+ db: Database,
42
+ type: "P" | "E" | "T",
43
+ prefix: string,
44
+ ): string {
45
+ const table = type === "P" ? "prds" : type === "E" ? "epics" : "tasks";
46
+ const pattern = `${prefix}-${type}%`;
47
+ const result = db
48
+ .query(
49
+ `SELECT ref FROM ${table} WHERE ref LIKE ? ORDER BY CAST(SUBSTR(ref, LENGTH(?) + 1) AS INTEGER) DESC LIMIT 1`,
50
+ )
51
+ .get(pattern, `${prefix}-${type}`) as { ref: string } | null;
52
+
53
+ if (!result) {
54
+ return `${prefix}-${type}1`;
55
+ }
56
+
57
+ const match = result.ref.match(new RegExp(`${prefix}-${type}(\\d+)$`));
58
+ const maxNum = match ? parseInt(match[1], 10) : 0;
59
+ return `${prefix}-${type}${maxNum + 1}`;
60
+ }
@@ -0,0 +1,97 @@
1
+ export const SCHEMA = `
2
+ -- Projects table (for multi-project support later)
3
+ CREATE TABLE IF NOT EXISTS projects (
4
+ id TEXT PRIMARY KEY,
5
+ name TEXT NOT NULL,
6
+ ref_prefix TEXT NOT NULL,
7
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
8
+ );
9
+
10
+ -- PRDs
11
+ CREATE TABLE IF NOT EXISTS prds (
12
+ id TEXT PRIMARY KEY,
13
+ project_id TEXT NOT NULL,
14
+ ref TEXT UNIQUE NOT NULL,
15
+ title TEXT NOT NULL,
16
+ description TEXT,
17
+ status TEXT DEFAULT 'DRAFT',
18
+ tag TEXT,
19
+ folder_path TEXT,
20
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
21
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
22
+ FOREIGN KEY (project_id) REFERENCES projects(id)
23
+ );
24
+
25
+ -- Epics
26
+ CREATE TABLE IF NOT EXISTS epics (
27
+ id TEXT PRIMARY KEY,
28
+ prd_id TEXT NOT NULL,
29
+ ref TEXT UNIQUE NOT NULL,
30
+ title TEXT NOT NULL,
31
+ description TEXT,
32
+ status TEXT DEFAULT 'PENDING',
33
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
34
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
35
+ FOREIGN KEY (prd_id) REFERENCES prds(id)
36
+ );
37
+
38
+ -- Tasks
39
+ CREATE TABLE IF NOT EXISTS tasks (
40
+ id TEXT PRIMARY KEY,
41
+ epic_id TEXT NOT NULL,
42
+ ref TEXT UNIQUE NOT NULL,
43
+ title TEXT NOT NULL,
44
+ description TEXT,
45
+ status TEXT DEFAULT 'PENDING',
46
+ priority TEXT DEFAULT 'MEDIUM',
47
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
48
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
49
+ FOREIGN KEY (epic_id) REFERENCES epics(id)
50
+ );
51
+
52
+ -- Acceptance Criteria (for both epics and tasks)
53
+ CREATE TABLE IF NOT EXISTS acceptance_criteria (
54
+ id TEXT PRIMARY KEY,
55
+ parent_type TEXT NOT NULL,
56
+ parent_id TEXT NOT NULL,
57
+ criteria TEXT NOT NULL,
58
+ is_met BOOLEAN DEFAULT FALSE,
59
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
60
+ );
61
+
62
+ -- Epic Dependencies
63
+ CREATE TABLE IF NOT EXISTS epic_dependencies (
64
+ epic_id TEXT NOT NULL,
65
+ depends_on_epic_id TEXT NOT NULL,
66
+ PRIMARY KEY (epic_id, depends_on_epic_id),
67
+ FOREIGN KEY (epic_id) REFERENCES epics(id),
68
+ FOREIGN KEY (depends_on_epic_id) REFERENCES epics(id)
69
+ );
70
+
71
+ -- Task Dependencies
72
+ CREATE TABLE IF NOT EXISTS task_dependencies (
73
+ task_id TEXT NOT NULL,
74
+ depends_on_task_id TEXT NOT NULL,
75
+ PRIMARY KEY (task_id, depends_on_task_id),
76
+ FOREIGN KEY (task_id) REFERENCES tasks(id),
77
+ FOREIGN KEY (depends_on_task_id) REFERENCES tasks(id)
78
+ );
79
+
80
+ -- PRD Dependencies
81
+ CREATE TABLE IF NOT EXISTS prd_dependencies (
82
+ prd_id TEXT NOT NULL,
83
+ depends_on_prd_id TEXT NOT NULL,
84
+ PRIMARY KEY (prd_id, depends_on_prd_id),
85
+ FOREIGN KEY (prd_id) REFERENCES prds(id),
86
+ FOREIGN KEY (depends_on_prd_id) REFERENCES prds(id)
87
+ );
88
+
89
+ -- Indexes for common queries
90
+ CREATE INDEX IF NOT EXISTS idx_prds_project ON prds(project_id);
91
+ CREATE INDEX IF NOT EXISTS idx_prds_status ON prds(status);
92
+ CREATE INDEX IF NOT EXISTS idx_epics_prd ON epics(prd_id);
93
+ CREATE INDEX IF NOT EXISTS idx_epics_status ON epics(status);
94
+ CREATE INDEX IF NOT EXISTS idx_tasks_epic ON tasks(epic_id);
95
+ CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
96
+ CREATE INDEX IF NOT EXISTS idx_criteria_parent ON acceptance_criteria(parent_type, parent_id);
97
+ `;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * SQLite adapter using bun:sqlite.
3
+ * This plugin requires Bun runtime.
4
+ */
5
+
6
+ import type { SQLQueryBindings } from "bun:sqlite";
7
+ import { Database } from "bun:sqlite";
8
+
9
+ export { Database };
10
+ export type { SQLQueryBindings };
@@ -0,0 +1,81 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { config } from "./config.js";
4
+ import { fluxProjectExists, initDb } from "./db/index.js";
5
+ import {
6
+ addCriteriaTool,
7
+ addDependencyTool,
8
+ configureLinearTool,
9
+ createEpicTool,
10
+ createPrdTool,
11
+ createTaskTool,
12
+ deleteEntityTool,
13
+ getEntityTool,
14
+ getStatsTool,
15
+ getVersionTool,
16
+ initProjectTool,
17
+ markCriteriaMetTool,
18
+ queryEntitiesTool,
19
+ registerTools,
20
+ removeDependencyTool,
21
+ renderStatusTool,
22
+ type ToolDefinition,
23
+ updateEntityTool,
24
+ updateStatusTool,
25
+ } from "./tools/index.js";
26
+ import { logger } from "./utils/logger.js";
27
+
28
+ const server = new Server(
29
+ { name: "flux", version: "0.1.0" },
30
+ { capabilities: { tools: {} } },
31
+ );
32
+
33
+ // All tools
34
+ const tools: ToolDefinition[] = [
35
+ // CRUD tools
36
+ createPrdTool,
37
+ createEpicTool,
38
+ createTaskTool,
39
+ updateEntityTool,
40
+ deleteEntityTool,
41
+ addDependencyTool,
42
+ removeDependencyTool,
43
+ addCriteriaTool,
44
+ markCriteriaMetTool,
45
+ updateStatusTool,
46
+ // Query tools
47
+ getEntityTool,
48
+ queryEntitiesTool,
49
+ initProjectTool,
50
+ getStatsTool,
51
+ getVersionTool,
52
+ // Configuration tools
53
+ configureLinearTool,
54
+ // Display tools
55
+ renderStatusTool,
56
+ ];
57
+
58
+ registerTools(server, tools);
59
+
60
+ async function main() {
61
+ logger.info("Flux MCP server starting...");
62
+ logger.info(` Project root: ${config.projectRoot}`);
63
+ logger.info(` Flux path: ${config.fluxPath}`);
64
+
65
+ // Initialize database if flux project exists
66
+ if (fluxProjectExists()) {
67
+ logger.info("Flux project found, initializing database...");
68
+ initDb();
69
+ } else {
70
+ logger.info("No flux project found. Use init_project to create one.");
71
+ }
72
+
73
+ const transport = new StdioServerTransport();
74
+ await server.connect(transport);
75
+ logger.info("Flux MCP server running");
76
+ }
77
+
78
+ main().catch((err) => {
79
+ logger.error("Server failed to start", err);
80
+ process.exit(1);
81
+ });
@@ -0,0 +1,411 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+
5
+ // Set up test environment BEFORE any imports
6
+ const TEST_DIR = `/tmp/flux-test-${Date.now()}-${Math.random().toString(36).slice(2)}`;
7
+ const FLUX_DIR = join(TEST_DIR, ".flux");
8
+ process.env.FLUX_PROJECT_ROOT = TEST_DIR;
9
+
10
+ // Now import modules
11
+ import { clearAdapterCache } from "../../adapters/index.js";
12
+ import { config } from "../../config.js";
13
+ import { closeDb, initDb } from "../../db/index.js";
14
+ import { createEpicTool } from "../create-epic.js";
15
+ import { createPrdTool } from "../create-prd.js";
16
+ import { createTaskTool } from "../create-task.js";
17
+ import { addCriteriaTool, markCriteriaMetTool } from "../criteria.js";
18
+ import { deleteEntityTool } from "../delete-entity.js";
19
+ import { addDependencyTool, removeDependencyTool } from "../dependencies.js";
20
+ import { updateEntityTool } from "../update-entity.js";
21
+ import { updateStatusTool } from "../update-status.js";
22
+
23
+ describe("CRUD MCP Tools", () => {
24
+ beforeEach(() => {
25
+ closeDb();
26
+ config.clearCache();
27
+ clearAdapterCache();
28
+ process.env.FLUX_PROJECT_ROOT = TEST_DIR;
29
+
30
+ mkdirSync(FLUX_DIR, { recursive: true });
31
+ writeFileSync(
32
+ join(FLUX_DIR, "project.json"),
33
+ JSON.stringify({ name: "test-project", ref_prefix: "TEST" }),
34
+ );
35
+ initDb();
36
+ });
37
+
38
+ afterEach(() => {
39
+ closeDb();
40
+ clearAdapterCache();
41
+ config.clearCache();
42
+ if (existsSync(TEST_DIR)) {
43
+ rmSync(TEST_DIR, { recursive: true, force: true });
44
+ }
45
+ });
46
+
47
+ describe("create_prd", () => {
48
+ test("creates PRD with required fields", async () => {
49
+ const result = (await createPrdTool.handler({
50
+ title: "Test PRD",
51
+ })) as any;
52
+ expect(result).toBeDefined();
53
+ expect(result.title).toBe("Test PRD");
54
+ expect(result.ref).toBeDefined();
55
+ expect(result.status).toBe("DRAFT");
56
+ expect(result.id).toBeDefined();
57
+ expect(result.dependencies).toEqual([]);
58
+ });
59
+
60
+ test("creates PRD with optional fields", async () => {
61
+ const result = (await createPrdTool.handler({
62
+ title: "Test PRD",
63
+ description: "A description",
64
+ tag: "mvp",
65
+ })) as any;
66
+ expect(result.description).toBe("A description");
67
+ expect(result.tag).toBe("mvp");
68
+ });
69
+
70
+ test("creates PRD with depends_on", async () => {
71
+ const prd1 = (await createPrdTool.handler({ title: "PRD 1" })) as any;
72
+ const prd2 = (await createPrdTool.handler({ title: "PRD 2" })) as any;
73
+ const prd3 = (await createPrdTool.handler({
74
+ title: "PRD 3",
75
+ depends_on: [prd1.ref, prd2.ref],
76
+ })) as any;
77
+
78
+ expect(prd3.dependencies).toEqual([prd1.ref, prd2.ref]);
79
+ });
80
+ });
81
+
82
+ describe("create_epic", () => {
83
+ test("creates epic linked to PRD", async () => {
84
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
85
+ const epic = (await createEpicTool.handler({
86
+ prd_ref: prd.ref,
87
+ title: "Test Epic",
88
+ })) as any;
89
+
90
+ expect(epic.title).toBe("Test Epic");
91
+ expect(epic.ref).toBeDefined();
92
+ expect(epic.status).toBe("PENDING");
93
+ expect(epic.dependencies).toEqual([]);
94
+ });
95
+
96
+ test("creates epic with acceptance criteria", async () => {
97
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
98
+ const epic = (await createEpicTool.handler({
99
+ prd_ref: prd.ref,
100
+ title: "Test Epic",
101
+ acceptance_criteria: ["Criterion 1", "Criterion 2"],
102
+ })) as any;
103
+ expect(epic.criteria_count).toBe(2);
104
+ });
105
+
106
+ test("fails with invalid PRD ref", async () => {
107
+ await expect(
108
+ createEpicTool.handler({ prd_ref: "INVALID-P999", title: "Test" }),
109
+ ).rejects.toThrow("PRD not found");
110
+ });
111
+
112
+ test("creates epic with depends_on", async () => {
113
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
114
+ const epic1 = (await createEpicTool.handler({
115
+ prd_ref: prd.ref,
116
+ title: "Epic 1",
117
+ })) as any;
118
+ const epic2 = (await createEpicTool.handler({
119
+ prd_ref: prd.ref,
120
+ title: "Epic 2",
121
+ })) as any;
122
+ const epic3 = (await createEpicTool.handler({
123
+ prd_ref: prd.ref,
124
+ title: "Epic 3",
125
+ depends_on: [epic1.ref, epic2.ref],
126
+ })) as any;
127
+
128
+ expect(epic3.dependencies).toEqual([epic1.ref, epic2.ref]);
129
+ });
130
+ });
131
+
132
+ describe("create_task", () => {
133
+ test("creates task linked to epic", async () => {
134
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
135
+ const epic = (await createEpicTool.handler({
136
+ prd_ref: prd.ref,
137
+ title: "Test Epic",
138
+ })) as any;
139
+ const task = (await createTaskTool.handler({
140
+ epic_ref: epic.ref,
141
+ title: "Test Task",
142
+ })) as any;
143
+
144
+ expect(task.title).toBe("Test Task");
145
+ expect(task.ref).toBeDefined();
146
+ expect(task.priority).toBe("MEDIUM");
147
+ expect(task.dependencies).toEqual([]);
148
+ });
149
+
150
+ test("creates task with priority", async () => {
151
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
152
+ const epic = (await createEpicTool.handler({
153
+ prd_ref: prd.ref,
154
+ title: "Test Epic",
155
+ })) as any;
156
+ const task = (await createTaskTool.handler({
157
+ epic_ref: epic.ref,
158
+ title: "Test Task",
159
+ priority: "HIGH",
160
+ })) as any;
161
+ expect(task.priority).toBe("HIGH");
162
+ });
163
+
164
+ test("creates task with depends_on", async () => {
165
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
166
+ const epic = (await createEpicTool.handler({
167
+ prd_ref: prd.ref,
168
+ title: "Test Epic",
169
+ })) as any;
170
+ const task1 = (await createTaskTool.handler({
171
+ epic_ref: epic.ref,
172
+ title: "Task 1",
173
+ })) as any;
174
+ const task2 = (await createTaskTool.handler({
175
+ epic_ref: epic.ref,
176
+ title: "Task 2",
177
+ })) as any;
178
+ const task3 = (await createTaskTool.handler({
179
+ epic_ref: epic.ref,
180
+ title: "Task 3",
181
+ depends_on: [task1.ref, task2.ref],
182
+ })) as any;
183
+
184
+ expect(task3.dependencies).toEqual([task1.ref, task2.ref]);
185
+ });
186
+ });
187
+
188
+ describe("update_entity", () => {
189
+ test("updates PRD fields", async () => {
190
+ const prd = (await createPrdTool.handler({ title: "Original" })) as any;
191
+ const updated = (await updateEntityTool.handler({
192
+ ref: prd.ref,
193
+ fields: { title: "Updated", status: "APPROVED" },
194
+ })) as any;
195
+
196
+ expect(updated.title).toBe("Updated");
197
+ expect(updated.status).toBe("APPROVED");
198
+ });
199
+
200
+ test("updates epic status", async () => {
201
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
202
+ const epic = (await createEpicTool.handler({
203
+ prd_ref: prd.ref,
204
+ title: "Test Epic",
205
+ })) as any;
206
+ const updated = (await updateEntityTool.handler({
207
+ ref: epic.ref,
208
+ fields: { status: "IN_PROGRESS" },
209
+ })) as any;
210
+
211
+ expect(updated.status).toBe("IN_PROGRESS");
212
+ });
213
+ });
214
+
215
+ describe("delete_entity", () => {
216
+ test("deletes task", async () => {
217
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
218
+ const epic = (await createEpicTool.handler({
219
+ prd_ref: prd.ref,
220
+ title: "Test Epic",
221
+ })) as any;
222
+ const task = (await createTaskTool.handler({
223
+ epic_ref: epic.ref,
224
+ title: "Test Task",
225
+ })) as any;
226
+
227
+ const result = (await deleteEntityTool.handler({ ref: task.ref })) as any;
228
+ expect(result.deleted).toBe(task.ref);
229
+ });
230
+
231
+ test("cascade deletes epic with tasks", async () => {
232
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
233
+ const epic = (await createEpicTool.handler({
234
+ prd_ref: prd.ref,
235
+ title: "Test Epic",
236
+ })) as any;
237
+ await createTaskTool.handler({ epic_ref: epic.ref, title: "Task 1" });
238
+ await createTaskTool.handler({ epic_ref: epic.ref, title: "Task 2" });
239
+
240
+ const result = (await deleteEntityTool.handler({ ref: epic.ref })) as any;
241
+ expect(result.cascade.tasks).toBe(2);
242
+ });
243
+
244
+ test("cascade deletes PRD with dependencies", async () => {
245
+ const prd1 = (await createPrdTool.handler({ title: "PRD 1" })) as any;
246
+ const prd2 = (await createPrdTool.handler({ title: "PRD 2" })) as any;
247
+ await addDependencyTool.handler({
248
+ ref: prd2.ref,
249
+ depends_on_ref: prd1.ref,
250
+ });
251
+
252
+ const result = (await deleteEntityTool.handler({ ref: prd1.ref })) as any;
253
+ expect(result.deleted).toBe(prd1.ref);
254
+ expect(result.cascade.dependencies).toBe(1);
255
+ });
256
+ });
257
+
258
+ describe("dependencies", () => {
259
+ test("adds and removes epic dependency", async () => {
260
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
261
+ const epic1 = (await createEpicTool.handler({
262
+ prd_ref: prd.ref,
263
+ title: "Epic 1",
264
+ })) as any;
265
+ const epic2 = (await createEpicTool.handler({
266
+ prd_ref: prd.ref,
267
+ title: "Epic 2",
268
+ })) as any;
269
+
270
+ const addResult = (await addDependencyTool.handler({
271
+ ref: epic2.ref,
272
+ depends_on_ref: epic1.ref,
273
+ })) as any;
274
+ expect(addResult.success).toBe(true);
275
+
276
+ const removeResult = (await removeDependencyTool.handler({
277
+ ref: epic2.ref,
278
+ depends_on_ref: epic1.ref,
279
+ })) as any;
280
+ expect(removeResult.success).toBe(true);
281
+ });
282
+
283
+ test("adds and removes PRD dependency", async () => {
284
+ const prd1 = (await createPrdTool.handler({ title: "PRD 1" })) as any;
285
+ const prd2 = (await createPrdTool.handler({ title: "PRD 2" })) as any;
286
+
287
+ const addResult = (await addDependencyTool.handler({
288
+ ref: prd2.ref,
289
+ depends_on_ref: prd1.ref,
290
+ })) as any;
291
+ expect(addResult.success).toBe(true);
292
+ expect(addResult.ref).toBe(prd2.ref);
293
+ expect(addResult.depends_on).toBe(prd1.ref);
294
+
295
+ const removeResult = (await removeDependencyTool.handler({
296
+ ref: prd2.ref,
297
+ depends_on_ref: prd1.ref,
298
+ })) as any;
299
+ expect(removeResult.success).toBe(true);
300
+ expect(removeResult.removed_dependency).toBe(prd1.ref);
301
+ });
302
+
303
+ test("prevents self-dependency", async () => {
304
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
305
+ const epic = (await createEpicTool.handler({
306
+ prd_ref: prd.ref,
307
+ title: "Epic",
308
+ })) as any;
309
+
310
+ await expect(
311
+ addDependencyTool.handler({ ref: epic.ref, depends_on_ref: epic.ref }),
312
+ ).rejects.toThrow("depend on itself");
313
+ });
314
+
315
+ test("prevents PRD self-dependency", async () => {
316
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
317
+
318
+ await expect(
319
+ addDependencyTool.handler({ ref: prd.ref, depends_on_ref: prd.ref }),
320
+ ).rejects.toThrow("depend on itself");
321
+ });
322
+
323
+ test("prevents cross-type dependencies between PRD and Epic", async () => {
324
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
325
+ const epic = (await createEpicTool.handler({
326
+ prd_ref: prd.ref,
327
+ title: "Epic",
328
+ })) as any;
329
+
330
+ await expect(
331
+ addDependencyTool.handler({ ref: prd.ref, depends_on_ref: epic.ref }),
332
+ ).rejects.toThrow("same type");
333
+ });
334
+ });
335
+
336
+ describe("criteria", () => {
337
+ test("adds and marks criterion", async () => {
338
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
339
+ const epic = (await createEpicTool.handler({
340
+ prd_ref: prd.ref,
341
+ title: "Test Epic",
342
+ })) as any;
343
+
344
+ const criterion = (await addCriteriaTool.handler({
345
+ parent_ref: epic.ref,
346
+ criteria: "Test criterion",
347
+ })) as any;
348
+ expect(criterion.criteria).toBe("Test criterion");
349
+ expect(criterion.is_met).toBe(false);
350
+
351
+ const updated = (await markCriteriaMetTool.handler({
352
+ criteria_id: criterion.id,
353
+ })) as any;
354
+ expect(updated.is_met).toBe(true);
355
+ });
356
+ });
357
+
358
+ describe("update_status", () => {
359
+ test("updates PRD status with valid transition", async () => {
360
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
361
+ // DRAFT → PENDING_REVIEW is a valid transition
362
+ const updated = (await updateStatusTool.handler({
363
+ ref: prd.ref,
364
+ status: "PENDING_REVIEW",
365
+ })) as any;
366
+ expect(updated.status).toBe("PENDING_REVIEW");
367
+ expect(updated.available_transitions).toContain("REVIEWED");
368
+ });
369
+
370
+ test("rejects invalid status for entity type", async () => {
371
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
372
+ await expect(
373
+ updateStatusTool.handler({ ref: prd.ref, status: "IN_PROGRESS" }),
374
+ ).rejects.toThrow("Invalid status");
375
+ });
376
+
377
+ test("rejects invalid status transition", async () => {
378
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
379
+ // DRAFT → APPROVED is not a valid transition (must go through PENDING_REVIEW first)
380
+ await expect(
381
+ updateStatusTool.handler({ ref: prd.ref, status: "APPROVED" }),
382
+ ).rejects.toThrow("Invalid transition");
383
+ });
384
+
385
+ test("updates task status with valid transitions", async () => {
386
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
387
+ const epic = (await createEpicTool.handler({
388
+ prd_ref: prd.ref,
389
+ title: "Test Epic",
390
+ })) as any;
391
+ const task = (await createTaskTool.handler({
392
+ epic_ref: epic.ref,
393
+ title: "Test Task",
394
+ })) as any;
395
+
396
+ // PENDING → IN_PROGRESS is valid
397
+ const inProgress = (await updateStatusTool.handler({
398
+ ref: task.ref,
399
+ status: "IN_PROGRESS",
400
+ })) as any;
401
+ expect(inProgress.status).toBe("IN_PROGRESS");
402
+
403
+ // IN_PROGRESS → COMPLETED is valid
404
+ const completed = (await updateStatusTool.handler({
405
+ ref: task.ref,
406
+ status: "COMPLETED",
407
+ })) as any;
408
+ expect(completed.status).toBe("COMPLETED");
409
+ });
410
+ });
411
+ });
@@ -0,0 +1,27 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { VERSION } from "../../../version.js";
3
+ import { getVersionTool } from "../get-version.js";
4
+
5
+ describe("get_version", () => {
6
+ test("returns version and name", async () => {
7
+ const result = (await getVersionTool.handler({})) as any;
8
+
9
+ expect(result.version).toBe(VERSION);
10
+ expect(result.name).toBe("flux-plugin");
11
+ });
12
+
13
+ test("requires no input parameters", async () => {
14
+ // Should work with empty object
15
+ const result = (await getVersionTool.handler({})) as any;
16
+ expect(result).toBeDefined();
17
+ expect(result.version).toBeDefined();
18
+ expect(result.name).toBeDefined();
19
+ });
20
+
21
+ test("has correct tool definition", () => {
22
+ expect(getVersionTool.name).toBe("get_version");
23
+ expect(getVersionTool.description).toContain("version");
24
+ expect(getVersionTool.inputSchema).toBeDefined();
25
+ expect(getVersionTool.handler).toBeDefined();
26
+ });
27
+ });