@cliangdev/flux-plugin 0.2.0-dev.e34d43b → 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 (37) hide show
  1. package/agents/coder.md +150 -25
  2. package/commands/breakdown.md +44 -7
  3. package/commands/implement.md +165 -15
  4. package/commands/prd.md +176 -1
  5. package/manifest.json +2 -1
  6. package/package.json +4 -2
  7. package/skills/prd-writer/SKILL.md +184 -0
  8. package/skills/ux-ui-design/SKILL.md +346 -0
  9. package/skills/ux-ui-design/references/design-tokens.md +359 -0
  10. package/src/dashboard/__tests__/api.test.ts +211 -0
  11. package/src/dashboard/browser.ts +35 -0
  12. package/src/dashboard/public/app.js +869 -0
  13. package/src/dashboard/public/index.html +90 -0
  14. package/src/dashboard/public/styles.css +807 -0
  15. package/src/dashboard/public/vendor/highlight.css +10 -0
  16. package/src/dashboard/public/vendor/highlight.min.js +8422 -0
  17. package/src/dashboard/public/vendor/marked.min.js +2210 -0
  18. package/src/dashboard/server.ts +296 -0
  19. package/src/dashboard/watchers.ts +83 -0
  20. package/src/server/adapters/__tests__/dependency-ops.test.ts +52 -18
  21. package/src/server/adapters/linear/adapter.ts +19 -14
  22. package/src/server/adapters/local-adapter.ts +48 -7
  23. package/src/server/db/__tests__/queries.test.ts +2 -1
  24. package/src/server/db/schema.ts +9 -0
  25. package/src/server/tools/__tests__/crud.test.ts +111 -1
  26. package/src/server/tools/__tests__/mcp-interface.test.ts +2 -1
  27. package/src/server/tools/__tests__/query.test.ts +73 -2
  28. package/src/server/tools/__tests__/z-configure-linear.test.ts +1 -1
  29. package/src/server/tools/__tests__/z-get-linear-url.test.ts +1 -1
  30. package/src/server/tools/create-epic.ts +11 -2
  31. package/src/server/tools/create-prd.ts +11 -2
  32. package/src/server/tools/create-task.ts +11 -2
  33. package/src/server/tools/dependencies.ts +2 -2
  34. package/src/server/tools/get-entity.ts +12 -10
  35. package/src/server/tools/render-status.ts +38 -20
  36. package/src/status-line/__tests__/status-line.test.ts +1 -1
  37. package/src/utils/status-renderer.ts +32 -6
@@ -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);
@@ -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,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-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
 
@@ -19,6 +19,7 @@ import {
19
19
 
20
20
  describe("MCP Interface", () => {
21
21
  beforeEach(() => {
22
+ closeDb();
22
23
  config.clearCache();
23
24
  process.env.FLUX_PROJECT_ROOT = TEST_DIR;
24
25
 
@@ -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
 
@@ -22,6 +22,7 @@ import { queryEntitiesTool } from "../query-entities.js";
22
22
 
23
23
  describe("Query MCP Tools", () => {
24
24
  beforeEach(() => {
25
+ closeDb();
25
26
  config.clearCache();
26
27
  clearAdapterCache();
27
28
  process.env.FLUX_PROJECT_ROOT = TEST_DIR;
@@ -114,6 +115,75 @@ describe("Query MCP Tools", () => {
114
115
  expect(result.dependencies[0]).toBe(epic1.ref);
115
116
  });
116
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
+
117
187
  test("throws error for invalid ref", async () => {
118
188
  await expect(
119
189
  getEntityTool.handler({ ref: "INVALID-P999" }),
@@ -272,9 +342,10 @@ describe("Query MCP Tools", () => {
272
342
  });
273
343
 
274
344
  describe("init_project", () => {
275
- 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)}`;
276
346
 
277
347
  beforeEach(() => {
348
+ closeDb();
278
349
  config.clearCache();
279
350
  clearAdapterCache();
280
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
  };
@@ -85,6 +85,10 @@ async function handler(input: unknown) {
85
85
  // Add available status transitions
86
86
  result.available_transitions = getValidTransitions(entityType, prd.status);
87
87
 
88
+ // Always include dependencies by default
89
+ const deps = await adapter.getDependencies(parsed.ref);
90
+ result.dependencies = deps;
91
+
88
92
  // Process includes
89
93
  for (const inc of includes) {
90
94
  if (!validIncludes.includes(inc)) continue;
@@ -166,6 +170,10 @@ async function handler(input: unknown) {
166
170
  result = { ...toMcpEpic(epic) };
167
171
  result.available_transitions = getValidTransitions(entityType, epic.status);
168
172
 
173
+ // Always include dependencies by default
174
+ const deps = await adapter.getDependencies(parsed.ref);
175
+ result.dependencies = deps;
176
+
169
177
  for (const inc of includes) {
170
178
  if (!validIncludes.includes(inc)) continue;
171
179
 
@@ -192,11 +200,6 @@ async function handler(input: unknown) {
192
200
  result.criteria_count = criteria.length;
193
201
  result.criteria_met = criteria.filter((c) => c.isMet).length;
194
202
  }
195
-
196
- if (inc === "dependencies") {
197
- const deps = await adapter.getDependencies(parsed.ref);
198
- result.dependencies = deps;
199
- }
200
203
  }
201
204
  } else {
202
205
  const task = await adapter.getTask(parsed.ref);
@@ -205,6 +208,10 @@ async function handler(input: unknown) {
205
208
  result = { ...toMcpTask(task) };
206
209
  result.available_transitions = getValidTransitions(entityType, task.status);
207
210
 
211
+ // Always include dependencies by default
212
+ const deps = await adapter.getDependencies(parsed.ref);
213
+ result.dependencies = deps;
214
+
208
215
  for (const inc of includes) {
209
216
  if (!validIncludes.includes(inc)) continue;
210
217
 
@@ -218,11 +225,6 @@ async function handler(input: unknown) {
218
225
  result.criteria_count = criteria.length;
219
226
  result.criteria_met = criteria.filter((c) => c.isMet).length;
220
227
  }
221
-
222
- if (inc === "dependencies") {
223
- const deps = await adapter.getDependencies(parsed.ref);
224
- result.dependencies = deps;
225
- }
226
228
  }
227
229
  }
228
230
 
@@ -23,6 +23,7 @@ interface EpicForSummary {
23
23
  status: string;
24
24
  task_count: number;
25
25
  tasks_completed: number;
26
+ dependencies: string[];
26
27
  }
27
28
 
28
29
  interface PrdForSummary {
@@ -30,12 +31,14 @@ interface PrdForSummary {
30
31
  title: string;
31
32
  status: string;
32
33
  epics: EpicForSummary[];
34
+ dependencies: string[];
33
35
  }
34
36
 
35
37
  interface TaskForFull {
36
38
  ref: string;
37
39
  title: string;
38
40
  status: string;
41
+ dependencies: string[];
39
42
  }
40
43
 
41
44
  interface EpicForFull {
@@ -43,6 +46,7 @@ interface EpicForFull {
43
46
  title: string;
44
47
  status: string;
45
48
  tasks: TaskForFull[];
49
+ dependencies: string[];
46
50
  }
47
51
 
48
52
  interface PrdForFull {
@@ -50,6 +54,7 @@ interface PrdForFull {
50
54
  title: string;
51
55
  status: string;
52
56
  epics: EpicForFull[];
57
+ dependencies: string[];
53
58
  }
54
59
 
55
60
  function getProjectInfo(): { name: string; ref_prefix: string } | null {
@@ -110,17 +115,17 @@ async function getSummaryData(adapter: Adapter): Promise<PrdForSummary[]> {
110
115
  const result: PrdForSummary[] = [];
111
116
 
112
117
  for (const prd of nonArchivedPrds) {
113
- const epicsResult = await adapter.listEpics(
114
- { prdRef: prd.ref },
115
- { limit: 100 },
116
- );
118
+ const [prdDeps, epicsResult] = await Promise.all([
119
+ adapter.getDependencies(prd.ref),
120
+ adapter.listEpics({ prdRef: prd.ref }, { limit: 100 }),
121
+ ]);
117
122
 
118
123
  const epicsWithCounts: EpicForSummary[] = await Promise.all(
119
124
  epicsResult.items.map(async (epic) => {
120
- const tasksResult = await adapter.listTasks(
121
- { epicRef: epic.ref },
122
- { limit: 100 },
123
- );
125
+ const [epicDeps, tasksResult] = await Promise.all([
126
+ adapter.getDependencies(epic.ref),
127
+ adapter.listTasks({ epicRef: epic.ref }, { limit: 100 }),
128
+ ]);
124
129
  const completedCount = tasksResult.items.filter(
125
130
  (t) => t.status === "COMPLETED",
126
131
  ).length;
@@ -131,6 +136,7 @@ async function getSummaryData(adapter: Adapter): Promise<PrdForSummary[]> {
131
136
  status: epic.status,
132
137
  task_count: tasksResult.total,
133
138
  tasks_completed: completedCount,
139
+ dependencies: epicDeps,
134
140
  };
135
141
  }),
136
142
  );
@@ -140,6 +146,7 @@ async function getSummaryData(adapter: Adapter): Promise<PrdForSummary[]> {
140
146
  title: prd.title,
141
147
  status: prd.status,
142
148
  epics: epicsWithCounts,
149
+ dependencies: prdDeps,
143
150
  });
144
151
  }
145
152
 
@@ -156,27 +163,37 @@ async function getFullTreeData(adapter: Adapter): Promise<PrdForFull[]> {
156
163
  const result: PrdForFull[] = [];
157
164
 
158
165
  for (const prd of allPrds) {
159
- const epicsResult = await adapter.listEpics(
160
- { prdRef: prd.ref },
161
- { limit: 100 },
162
- );
166
+ const [prdDeps, epicsResult] = await Promise.all([
167
+ adapter.getDependencies(prd.ref),
168
+ adapter.listEpics({ prdRef: prd.ref }, { limit: 100 }),
169
+ ]);
163
170
 
164
171
  const epicsWithTasks: EpicForFull[] = await Promise.all(
165
172
  epicsResult.items.map(async (epic) => {
166
- const tasksResult = await adapter.listTasks(
167
- { epicRef: epic.ref },
168
- { limit: 100 },
173
+ const [epicDeps, tasksResult] = await Promise.all([
174
+ adapter.getDependencies(epic.ref),
175
+ adapter.listTasks({ epicRef: epic.ref }, { limit: 100 }),
176
+ ]);
177
+
178
+ // Get dependencies for each task
179
+ const tasksWithDeps: TaskForFull[] = await Promise.all(
180
+ tasksResult.items.map(async (t) => {
181
+ const taskDeps = await adapter.getDependencies(t.ref);
182
+ return {
183
+ ref: t.ref,
184
+ title: t.title,
185
+ status: t.status,
186
+ dependencies: taskDeps,
187
+ };
188
+ }),
169
189
  );
170
190
 
171
191
  return {
172
192
  ref: epic.ref,
173
193
  title: epic.title,
174
194
  status: epic.status,
175
- tasks: tasksResult.items.map((t) => ({
176
- ref: t.ref,
177
- title: t.title,
178
- status: t.status,
179
- })),
195
+ tasks: tasksWithDeps,
196
+ dependencies: epicDeps,
180
197
  };
181
198
  }),
182
199
  );
@@ -186,6 +203,7 @@ async function getFullTreeData(adapter: Adapter): Promise<PrdForFull[]> {
186
203
  title: prd.title,
187
204
  status: prd.status,
188
205
  epics: epicsWithTasks,
206
+ dependencies: prdDeps,
189
207
  });
190
208
  }
191
209
 
@@ -31,7 +31,7 @@ function setupTestProject(testDir: string, projectName = "test-project") {
31
31
  }
32
32
 
33
33
  describe("status-line script", () => {
34
- const TEST_DIR = `/tmp/flux-status-line-test-${Date.now()}`;
34
+ const TEST_DIR = `/tmp/flux-status-line-test-${Date.now()}-${Math.random().toString(36).slice(2)}`;
35
35
 
36
36
  beforeEach(() => {
37
37
  if (existsSync(TEST_DIR)) {