@cliangdev/flux-plugin 0.2.0-dev.dc5e2c4 → 0.2.0-dev.f718bcf

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 (46) hide show
  1. package/README.md +3 -3
  2. package/agents/coder.md +150 -25
  3. package/commands/breakdown.md +47 -10
  4. package/commands/flux.md +92 -12
  5. package/commands/implement.md +166 -17
  6. package/commands/linear.md +6 -5
  7. package/commands/prd.md +996 -82
  8. package/manifest.json +2 -1
  9. package/package.json +4 -2
  10. package/skills/flux-orchestrator/SKILL.md +11 -3
  11. package/skills/prd-writer/SKILL.md +761 -0
  12. package/skills/ux-ui-design/SKILL.md +346 -0
  13. package/skills/ux-ui-design/references/design-tokens.md +359 -0
  14. package/src/dashboard/__tests__/api.test.ts +211 -0
  15. package/src/dashboard/browser.ts +35 -0
  16. package/src/dashboard/public/app.js +869 -0
  17. package/src/dashboard/public/index.html +90 -0
  18. package/src/dashboard/public/styles.css +807 -0
  19. package/src/dashboard/public/vendor/highlight.css +10 -0
  20. package/src/dashboard/public/vendor/highlight.min.js +8422 -0
  21. package/src/dashboard/public/vendor/marked.min.js +2210 -0
  22. package/src/dashboard/server.ts +296 -0
  23. package/src/dashboard/watchers.ts +83 -0
  24. package/src/server/adapters/__tests__/dependency-ops.test.ts +52 -18
  25. package/src/server/adapters/linear/adapter.ts +19 -14
  26. package/src/server/adapters/local-adapter.ts +48 -7
  27. package/src/server/db/__tests__/queries.test.ts +2 -1
  28. package/src/server/db/schema.ts +9 -0
  29. package/src/server/index.ts +0 -2
  30. package/src/server/tools/__tests__/crud.test.ts +111 -1
  31. package/src/server/tools/__tests__/mcp-interface.test.ts +100 -9
  32. package/src/server/tools/__tests__/query.test.ts +73 -21
  33. package/src/server/tools/__tests__/z-configure-linear.test.ts +1 -1
  34. package/src/server/tools/__tests__/z-get-linear-url.test.ts +1 -1
  35. package/src/server/tools/create-epic.ts +11 -2
  36. package/src/server/tools/create-prd.ts +11 -2
  37. package/src/server/tools/create-task.ts +11 -2
  38. package/src/server/tools/dependencies.ts +2 -2
  39. package/src/server/tools/get-entity.ts +12 -10
  40. package/src/server/tools/index.ts +53 -9
  41. package/src/server/tools/init-project.ts +1 -1
  42. package/src/server/tools/render-status.ts +38 -20
  43. package/src/status-line/__tests__/status-line.test.ts +1 -1
  44. package/src/utils/status-renderer.ts +32 -6
  45. package/skills/prd-template/SKILL.md +0 -242
  46. package/src/server/tools/get-project-context.ts +0 -33
@@ -77,6 +77,15 @@ CREATE TABLE IF NOT EXISTS task_dependencies (
77
77
  FOREIGN KEY (depends_on_task_id) REFERENCES tasks(id)
78
78
  );
79
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
+
80
89
  -- Indexes for common queries
81
90
  CREATE INDEX IF NOT EXISTS idx_prds_project ON prds(project_id);
82
91
  CREATE INDEX IF NOT EXISTS idx_prds_status ON prds(status);
@@ -11,7 +11,6 @@ import {
11
11
  createTaskTool,
12
12
  deleteEntityTool,
13
13
  getEntityTool,
14
- getProjectContextTool,
15
14
  getStatsTool,
16
15
  getVersionTool,
17
16
  initProjectTool,
@@ -47,7 +46,6 @@ const tools: ToolDefinition[] = [
47
46
  // Query tools
48
47
  getEntityTool,
49
48
  queryEntitiesTool,
50
- getProjectContextTool,
51
49
  initProjectTool,
52
50
  getStatsTool,
53
51
  getVersionTool,
@@ -3,7 +3,7 @@ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
 
5
5
  // Set up test environment BEFORE any imports
6
- const TEST_DIR = `/tmp/flux-test-${Date.now()}`;
6
+ const TEST_DIR = `/tmp/flux-test-${Date.now()}-${Math.random().toString(36).slice(2)}`;
7
7
  const FLUX_DIR = join(TEST_DIR, ".flux");
8
8
  process.env.FLUX_PROJECT_ROOT = TEST_DIR;
9
9
 
@@ -22,6 +22,7 @@ import { updateStatusTool } from "../update-status.js";
22
22
 
23
23
  describe("CRUD MCP Tools", () => {
24
24
  beforeEach(() => {
25
+ closeDb();
25
26
  config.clearCache();
26
27
  clearAdapterCache();
27
28
  process.env.FLUX_PROJECT_ROOT = TEST_DIR;
@@ -53,6 +54,7 @@ describe("CRUD MCP Tools", () => {
53
54
  expect(result.ref).toBeDefined();
54
55
  expect(result.status).toBe("DRAFT");
55
56
  expect(result.id).toBeDefined();
57
+ expect(result.dependencies).toEqual([]);
56
58
  });
57
59
 
58
60
  test("creates PRD with optional fields", async () => {
@@ -64,6 +66,17 @@ describe("CRUD MCP Tools", () => {
64
66
  expect(result.description).toBe("A description");
65
67
  expect(result.tag).toBe("mvp");
66
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
+ });
67
80
  });
68
81
 
69
82
  describe("create_epic", () => {
@@ -77,6 +90,7 @@ describe("CRUD MCP Tools", () => {
77
90
  expect(epic.title).toBe("Test Epic");
78
91
  expect(epic.ref).toBeDefined();
79
92
  expect(epic.status).toBe("PENDING");
93
+ expect(epic.dependencies).toEqual([]);
80
94
  });
81
95
 
82
96
  test("creates epic with acceptance criteria", async () => {
@@ -94,6 +108,25 @@ describe("CRUD MCP Tools", () => {
94
108
  createEpicTool.handler({ prd_ref: "INVALID-P999", title: "Test" }),
95
109
  ).rejects.toThrow("PRD not found");
96
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
+ });
97
130
  });
98
131
 
99
132
  describe("create_task", () => {
@@ -111,6 +144,7 @@ describe("CRUD MCP Tools", () => {
111
144
  expect(task.title).toBe("Test Task");
112
145
  expect(task.ref).toBeDefined();
113
146
  expect(task.priority).toBe("MEDIUM");
147
+ expect(task.dependencies).toEqual([]);
114
148
  });
115
149
 
116
150
  test("creates task with priority", async () => {
@@ -126,6 +160,29 @@ describe("CRUD MCP Tools", () => {
126
160
  })) as any;
127
161
  expect(task.priority).toBe("HIGH");
128
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
+ });
129
186
  });
130
187
 
131
188
  describe("update_entity", () => {
@@ -183,6 +240,19 @@ describe("CRUD MCP Tools", () => {
183
240
  const result = (await deleteEntityTool.handler({ ref: epic.ref })) as any;
184
241
  expect(result.cascade.tasks).toBe(2);
185
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
+ });
186
256
  });
187
257
 
188
258
  describe("dependencies", () => {
@@ -210,6 +280,26 @@ describe("CRUD MCP Tools", () => {
210
280
  expect(removeResult.success).toBe(true);
211
281
  });
212
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
+
213
303
  test("prevents self-dependency", async () => {
214
304
  const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
215
305
  const epic = (await createEpicTool.handler({
@@ -221,6 +311,26 @@ describe("CRUD MCP Tools", () => {
221
311
  addDependencyTool.handler({ ref: epic.ref, depends_on_ref: epic.ref }),
222
312
  ).rejects.toThrow("depend on itself");
223
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
+ });
224
334
  });
225
335
 
226
336
  describe("criteria", () => {
@@ -3,17 +3,23 @@ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
 
5
5
  // Set up test environment BEFORE any imports
6
- const TEST_DIR = `/tmp/flux-test-mcp-${Date.now()}`;
6
+ const TEST_DIR = `/tmp/flux-test-mcp-${Date.now()}-${Math.random().toString(36).slice(2)}`;
7
7
  const FLUX_DIR = join(TEST_DIR, ".flux");
8
8
  process.env.FLUX_PROJECT_ROOT = TEST_DIR;
9
9
 
10
10
  import { z } from "zod";
11
11
  import { config } from "../../config.js";
12
12
  import { closeDb, initDb } from "../../db/index.js";
13
- import { createError, registerTools, type ToolDefinition } from "../index.js";
13
+ import {
14
+ createError,
15
+ createProjectNotInitializedError,
16
+ registerTools,
17
+ type ToolDefinition,
18
+ } from "../index.js";
14
19
 
15
20
  describe("MCP Interface", () => {
16
21
  beforeEach(() => {
22
+ closeDb();
17
23
  config.clearCache();
18
24
  process.env.FLUX_PROJECT_ROOT = TEST_DIR;
19
25
 
@@ -44,6 +50,42 @@ describe("MCP Interface", () => {
44
50
  });
45
51
  });
46
52
 
53
+ describe("createProjectNotInitializedError", () => {
54
+ test("creates error with setup instructions", () => {
55
+ const error = createProjectNotInitializedError("/test/cwd", "/test/root");
56
+
57
+ expect(error.error).toBe(true);
58
+ expect(error.code).toBe("PROJECT_NOT_INITIALIZED");
59
+ expect(error.message).toContain("/test/cwd");
60
+ expect(error.message).toContain("/test/root");
61
+ expect(error.setup.instructions).toBeDefined();
62
+ expect(error.setup.options).toHaveLength(2);
63
+ });
64
+
65
+ test("includes command option for interactive setup", () => {
66
+ const error = createProjectNotInitializedError("/cwd", "/root");
67
+ const commandOption = error.setup.options.find(
68
+ (o) => o.method === "command",
69
+ );
70
+
71
+ expect(commandOption).toBeDefined();
72
+ expect(commandOption?.name).toBe("/flux");
73
+ expect(commandOption?.description).toContain("Interactive");
74
+ });
75
+
76
+ test("includes tool option with required params", () => {
77
+ const error = createProjectNotInitializedError("/cwd", "/root");
78
+ const toolOption = error.setup.options.find((o) => o.method === "tool");
79
+
80
+ expect(toolOption).toBeDefined();
81
+ expect(toolOption?.name).toBe("init_project");
82
+ expect(toolOption?.params).toBeDefined();
83
+ expect(toolOption?.params?.name).toBeDefined();
84
+ expect(toolOption?.params?.vision).toBeDefined();
85
+ expect(toolOption?.params?.adapter).toBeDefined();
86
+ });
87
+ });
88
+
47
89
  describe("registerTools", () => {
48
90
  test("sets up list tools handler", () => {
49
91
  const handlers: Record<string, Function> = {};
@@ -287,7 +329,7 @@ describe("Project Validation", () => {
287
329
  }
288
330
  });
289
331
 
290
- test("returns PROJECT_NOT_FOUND for tools requiring project", async () => {
332
+ test("returns PROJECT_NOT_INITIALIZED with setup instructions for tools requiring project", async () => {
291
333
  const handlers: Record<string, Function> = {};
292
334
  const mockServer = {
293
335
  setRequestHandler: mock((schema: any, handler: Function) => {
@@ -317,7 +359,11 @@ describe("Project Validation", () => {
317
359
  expect(result.isError).toBe(true);
318
360
  const parsedError = JSON.parse(result.content[0].text);
319
361
  expect(parsedError.error).toBe(true);
320
- expect(parsedError.code).toBe("PROJECT_NOT_FOUND");
362
+ expect(parsedError.code).toBe("PROJECT_NOT_INITIALIZED");
363
+ expect(parsedError.setup).toBeDefined();
364
+ expect(parsedError.setup.instructions).toBeDefined();
365
+ expect(parsedError.setup.options).toBeDefined();
366
+ expect(parsedError.setup.options.length).toBe(2);
321
367
  });
322
368
 
323
369
  test("allows init_project without existing project", async () => {
@@ -353,7 +399,7 @@ describe("Project Validation", () => {
353
399
  expect(handlerMock).toHaveBeenCalled();
354
400
  });
355
401
 
356
- test("allows get_project_context without existing project", async () => {
402
+ test("allows get_version without existing project", async () => {
357
403
  const handlers: Record<string, Function> = {};
358
404
  const mockServer = {
359
405
  setRequestHandler: mock((schema: any, handler: Function) => {
@@ -363,11 +409,11 @@ describe("Project Validation", () => {
363
409
  }),
364
410
  };
365
411
 
366
- const handlerMock = mock(async () => ({ initialized: false }));
412
+ const handlerMock = mock(async () => ({ version: "1.0.0" }));
367
413
 
368
414
  const testTool: ToolDefinition = {
369
- name: "get_project_context",
370
- description: "Get project context",
415
+ name: "get_version",
416
+ description: "Get version",
371
417
  inputSchema: z.object({}),
372
418
  handler: handlerMock,
373
419
  };
@@ -377,7 +423,7 @@ describe("Project Validation", () => {
377
423
  const callHandler = handlers.call;
378
424
  const result = await callHandler({
379
425
  params: {
380
- name: "get_project_context",
426
+ name: "get_version",
381
427
  arguments: {},
382
428
  },
383
429
  });
@@ -385,4 +431,49 @@ describe("Project Validation", () => {
385
431
  expect(result.isError).toBeUndefined();
386
432
  expect(handlerMock).toHaveBeenCalled();
387
433
  });
434
+
435
+ test("setup options have correct structure", async () => {
436
+ const handlers: Record<string, Function> = {};
437
+ const mockServer = {
438
+ setRequestHandler: mock((schema: any, handler: Function) => {
439
+ if (schema.shape?.method?.value === "tools/call") {
440
+ handlers.call = handler;
441
+ }
442
+ }),
443
+ };
444
+
445
+ const testTool: ToolDefinition = {
446
+ name: "query_entities",
447
+ description: "Query entities",
448
+ inputSchema: z.object({ type: z.string() }),
449
+ handler: async () => ({ items: [] }),
450
+ };
451
+
452
+ registerTools(mockServer as any, [testTool]);
453
+
454
+ const callHandler = handlers.call;
455
+ const result = await callHandler({
456
+ params: {
457
+ name: "query_entities",
458
+ arguments: { type: "prd" },
459
+ },
460
+ });
461
+
462
+ expect(result.isError).toBe(true);
463
+ const parsedError = JSON.parse(result.content[0].text);
464
+
465
+ const commandOption = parsedError.setup.options.find(
466
+ (o: any) => o.method === "command",
467
+ );
468
+ expect(commandOption.name).toBe("/flux");
469
+ expect(commandOption.description).toBeTruthy();
470
+
471
+ const toolOption = parsedError.setup.options.find(
472
+ (o: any) => o.method === "tool",
473
+ );
474
+ expect(toolOption.name).toBe("init_project");
475
+ expect(toolOption.params.name).toBeTruthy();
476
+ expect(toolOption.params.vision).toBeTruthy();
477
+ expect(toolOption.params.adapter).toBeTruthy();
478
+ });
388
479
  });
@@ -3,7 +3,7 @@ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
 
5
5
  // Set up test environment BEFORE any imports
6
- const TEST_DIR = `/tmp/flux-test-query-${Date.now()}`;
6
+ const TEST_DIR = `/tmp/flux-test-query-${Date.now()}-${Math.random().toString(36).slice(2)}`;
7
7
  const FLUX_DIR = join(TEST_DIR, ".flux");
8
8
  process.env.FLUX_PROJECT_ROOT = TEST_DIR;
9
9
 
@@ -16,13 +16,13 @@ import { createPrdTool } from "../create-prd.js";
16
16
  import { createTaskTool } from "../create-task.js";
17
17
  import { addDependencyTool } from "../dependencies.js";
18
18
  import { getEntityTool } from "../get-entity.js";
19
- import { getProjectContextTool } from "../get-project-context.js";
20
19
  import { getStatsTool } from "../get-stats.js";
21
20
  import { initProjectTool } from "../init-project.js";
22
21
  import { queryEntitiesTool } from "../query-entities.js";
23
22
 
24
23
  describe("Query MCP Tools", () => {
25
24
  beforeEach(() => {
25
+ closeDb();
26
26
  config.clearCache();
27
27
  clearAdapterCache();
28
28
  process.env.FLUX_PROJECT_ROOT = TEST_DIR;
@@ -115,6 +115,75 @@ describe("Query MCP Tools", () => {
115
115
  expect(result.dependencies[0]).toBe(epic1.ref);
116
116
  });
117
117
 
118
+ test("returns dependencies by default for PRD without include", async () => {
119
+ const prd1 = (await createPrdTool.handler({ title: "PRD 1" })) as any;
120
+ const prd2 = (await createPrdTool.handler({ title: "PRD 2" })) as any;
121
+ await addDependencyTool.handler({
122
+ ref: prd2.ref,
123
+ depends_on_ref: prd1.ref,
124
+ });
125
+
126
+ const result = (await getEntityTool.handler({
127
+ ref: prd2.ref,
128
+ })) as any;
129
+
130
+ expect(result.dependencies).toBeDefined();
131
+ expect(result.dependencies.length).toBe(1);
132
+ expect(result.dependencies[0]).toBe(prd1.ref);
133
+ });
134
+
135
+ test("returns dependencies by default for Epic without include", async () => {
136
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
137
+ const epic1 = (await createEpicTool.handler({
138
+ prd_ref: prd.ref,
139
+ title: "Epic 1",
140
+ })) as any;
141
+ const epic2 = (await createEpicTool.handler({
142
+ prd_ref: prd.ref,
143
+ title: "Epic 2",
144
+ })) as any;
145
+ await addDependencyTool.handler({
146
+ ref: epic2.ref,
147
+ depends_on_ref: epic1.ref,
148
+ });
149
+
150
+ const result = (await getEntityTool.handler({
151
+ ref: epic2.ref,
152
+ })) as any;
153
+
154
+ expect(result.dependencies).toBeDefined();
155
+ expect(result.dependencies.length).toBe(1);
156
+ expect(result.dependencies[0]).toBe(epic1.ref);
157
+ });
158
+
159
+ test("returns dependencies by default for Task without include", async () => {
160
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
161
+ const epic = (await createEpicTool.handler({
162
+ prd_ref: prd.ref,
163
+ title: "Test Epic",
164
+ })) as any;
165
+ const task1 = (await createTaskTool.handler({
166
+ epic_ref: epic.ref,
167
+ title: "Task 1",
168
+ })) as any;
169
+ const task2 = (await createTaskTool.handler({
170
+ epic_ref: epic.ref,
171
+ title: "Task 2",
172
+ })) as any;
173
+ await addDependencyTool.handler({
174
+ ref: task2.ref,
175
+ depends_on_ref: task1.ref,
176
+ });
177
+
178
+ const result = (await getEntityTool.handler({
179
+ ref: task2.ref,
180
+ })) as any;
181
+
182
+ expect(result.dependencies).toBeDefined();
183
+ expect(result.dependencies.length).toBe(1);
184
+ expect(result.dependencies[0]).toBe(task1.ref);
185
+ });
186
+
118
187
  test("throws error for invalid ref", async () => {
119
188
  await expect(
120
189
  getEntityTool.handler({ ref: "INVALID-P999" }),
@@ -242,24 +311,6 @@ describe("Query MCP Tools", () => {
242
311
  });
243
312
  });
244
313
 
245
- describe("get_project_context", () => {
246
- test("returns project context when initialized", async () => {
247
- const result = (await getProjectContextTool.handler({})) as any;
248
-
249
- expect(result.initialized).toBe(true);
250
- expect(result.name).toBe("test-project");
251
- expect(result.ref_prefix).toBe("TEST");
252
- });
253
-
254
- test("returns initialized false when no project", async () => {
255
- // Remove the project.json
256
- rmSync(join(FLUX_DIR, "project.json"));
257
-
258
- const result = (await getProjectContextTool.handler({})) as any;
259
- expect(result.initialized).toBe(false);
260
- });
261
- });
262
-
263
314
  describe("get_stats", () => {
264
315
  test("returns zeroes for empty project", async () => {
265
316
  const result = (await getStatsTool.handler({})) as any;
@@ -291,9 +342,10 @@ describe("Query MCP Tools", () => {
291
342
  });
292
343
 
293
344
  describe("init_project", () => {
294
- const INIT_TEST_DIR = `/tmp/flux-init-test-${Date.now()}`;
345
+ const INIT_TEST_DIR = `/tmp/flux-init-test-${Date.now()}-${Math.random().toString(36).slice(2)}`;
295
346
 
296
347
  beforeEach(() => {
348
+ closeDb();
297
349
  config.clearCache();
298
350
  clearAdapterCache();
299
351
  process.env.FLUX_PROJECT_ROOT = INIT_TEST_DIR;
@@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
2
  import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
 
5
- const TEST_DIR = `/tmp/flux-test-${Date.now()}`;
5
+ const TEST_DIR = `/tmp/flux-test-${Date.now()}-${Math.random().toString(36).slice(2)}`;
6
6
  const FLUX_DIR = join(TEST_DIR, ".flux");
7
7
  process.env.FLUX_PROJECT_ROOT = TEST_DIR;
8
8
 
@@ -3,7 +3,7 @@ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
 
5
5
  // Set up test environment BEFORE any imports
6
- const TEST_DIR = `/tmp/flux-test-${Date.now()}`;
6
+ const TEST_DIR = `/tmp/flux-test-${Date.now()}-${Math.random().toString(36).slice(2)}`;
7
7
  const FLUX_DIR = join(TEST_DIR, ".flux");
8
8
  process.env.FLUX_PROJECT_ROOT = TEST_DIR;
9
9
 
@@ -8,6 +8,7 @@ const inputSchema = z.object({
8
8
  title: z.string().min(1, "Title is required"),
9
9
  description: z.string().optional(),
10
10
  acceptance_criteria: z.array(z.string()).optional(),
11
+ depends_on: z.array(z.string()).optional(),
11
12
  });
12
13
 
13
14
  async function handler(input: unknown) {
@@ -23,13 +24,21 @@ async function handler(input: unknown) {
23
24
 
24
25
  const criteriaCount = parsed.acceptance_criteria?.length || 0;
25
26
 
26
- return { ...toMcpEpic(epic), criteria_count: criteriaCount };
27
+ const dependencies: string[] = [];
28
+ if (parsed.depends_on && parsed.depends_on.length > 0) {
29
+ for (const depRef of parsed.depends_on) {
30
+ await adapter.addDependency(epic.ref, depRef);
31
+ dependencies.push(depRef);
32
+ }
33
+ }
34
+
35
+ return { ...toMcpEpic(epic), criteria_count: criteriaCount, dependencies };
27
36
  }
28
37
 
29
38
  export const createEpicTool: ToolDefinition = {
30
39
  name: "create_epic",
31
40
  description:
32
- "Create a new Epic under a PRD. Required: prd_ref (e.g., 'FLUX-P1'), title. Optional: description, acceptance_criteria (string array). Returns {id, ref, title, status, criteria_count}. Status starts as PENDING.",
41
+ "Create a new Epic under a PRD. Required: prd_ref (e.g., 'FLUX-P1'), title. Optional: description, acceptance_criteria (string array), depends_on (array of Epic refs this Epic depends on, e.g., ['FLUX-E1']). Returns {id, ref, title, status, criteria_count, dependencies}. Status starts as PENDING.",
33
42
  inputSchema,
34
43
  handler,
35
44
  };
@@ -7,6 +7,7 @@ const inputSchema = z.object({
7
7
  title: z.string().min(1, "Title is required"),
8
8
  description: z.string().optional(),
9
9
  tag: z.string().optional(),
10
+ depends_on: z.array(z.string()).optional(),
10
11
  });
11
12
 
12
13
  async function handler(input: unknown) {
@@ -19,13 +20,21 @@ async function handler(input: unknown) {
19
20
  tag: parsed.tag,
20
21
  });
21
22
 
22
- return toMcpPrd(prd);
23
+ const dependencies: string[] = [];
24
+ if (parsed.depends_on && parsed.depends_on.length > 0) {
25
+ for (const depRef of parsed.depends_on) {
26
+ await adapter.addDependency(prd.ref, depRef);
27
+ dependencies.push(depRef);
28
+ }
29
+ }
30
+
31
+ return { ...toMcpPrd(prd), dependencies };
23
32
  }
24
33
 
25
34
  export const createPrdTool: ToolDefinition = {
26
35
  name: "create_prd",
27
36
  description:
28
- "Create a new PRD (Product Requirements Document). Required: title. Optional: description, tag. Returns the created PRD with {id, ref, title, description, status, tag}. Status starts as DRAFT.",
37
+ "Create a new PRD (Product Requirements Document). Required: title. Optional: description, tag, depends_on (array of PRD refs this PRD depends on, e.g., ['FLUX-P1', 'FLUX-P2']). Returns the created PRD with {id, ref, title, description, status, tag, dependencies}. Status starts as DRAFT.",
29
38
  inputSchema,
30
39
  handler,
31
40
  };
@@ -10,6 +10,7 @@ const inputSchema = z.object({
10
10
  description: z.string().optional(),
11
11
  priority: z.enum(["LOW", "MEDIUM", "HIGH"]).optional().default("MEDIUM"),
12
12
  acceptance_criteria: z.array(z.string()).optional(),
13
+ depends_on: z.array(z.string()).optional(),
13
14
  });
14
15
 
15
16
  async function handler(input: unknown) {
@@ -26,13 +27,21 @@ async function handler(input: unknown) {
26
27
 
27
28
  const criteriaCount = parsed.acceptance_criteria?.length || 0;
28
29
 
29
- return { ...toMcpTask(task), criteria_count: criteriaCount };
30
+ const dependencies: string[] = [];
31
+ if (parsed.depends_on && parsed.depends_on.length > 0) {
32
+ for (const depRef of parsed.depends_on) {
33
+ await adapter.addDependency(task.ref, depRef);
34
+ dependencies.push(depRef);
35
+ }
36
+ }
37
+
38
+ return { ...toMcpTask(task), criteria_count: criteriaCount, dependencies };
30
39
  }
31
40
 
32
41
  export const createTaskTool: ToolDefinition = {
33
42
  name: "create_task",
34
43
  description:
35
- "Create a new Task under an Epic. Required: epic_ref (e.g., 'FLUX-E1'), title. Optional: description, priority (LOW|MEDIUM|HIGH, default MEDIUM), acceptance_criteria (string array). Returns {id, ref, title, status, priority, criteria_count}.",
44
+ "Create a new Task under an Epic. Required: epic_ref (e.g., 'FLUX-E1'), title. Optional: description, priority (LOW|MEDIUM|HIGH, default MEDIUM), acceptance_criteria (string array), depends_on (array of Task refs this Task depends on, e.g., ['FLUX-T1']). Returns {id, ref, title, status, priority, criteria_count, dependencies}.",
36
45
  inputSchema,
37
46
  handler,
38
47
  };
@@ -41,7 +41,7 @@ async function removeDependencyHandler(input: unknown) {
41
41
  export const addDependencyTool: ToolDefinition = {
42
42
  name: "add_dependency",
43
43
  description:
44
- "Add a dependency between two entities of the same type. Required: ref (depends on depends_on_ref), depends_on_ref. Both must be Epics (FLUX-E*) or both Tasks (FLUX-T*). Validates no circular dependencies. Returns {success, ref, depends_on}.",
44
+ "Add a dependency between two entities of the same type. Required: ref (depends on depends_on_ref), depends_on_ref. Both must be PRDs (FLUX-P*), Epics (FLUX-E*), or Tasks (FLUX-T*). Validates no circular dependencies. Returns {success, ref, depends_on}.",
45
45
  inputSchema: addDependencySchema,
46
46
  handler: addDependencyHandler,
47
47
  };
@@ -49,7 +49,7 @@ export const addDependencyTool: ToolDefinition = {
49
49
  export const removeDependencyTool: ToolDefinition = {
50
50
  name: "remove_dependency",
51
51
  description:
52
- "Remove a dependency between two entities. Required: ref, depends_on_ref. Both must be same type (Epic or Task). Returns {success, ref, removed_dependency}.",
52
+ "Remove a dependency between two entities. Required: ref, depends_on_ref. Both must be same type (PRD, Epic, or Task). Returns {success, ref, removed_dependency}.",
53
53
  inputSchema: removeDependencySchema,
54
54
  handler: removeDependencyHandler,
55
55
  };