@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.
- package/agents/coder.md +150 -25
- package/commands/breakdown.md +44 -7
- package/commands/implement.md +165 -15
- package/commands/prd.md +176 -1
- package/manifest.json +2 -1
- package/package.json +4 -2
- package/skills/prd-writer/SKILL.md +184 -0
- package/skills/ux-ui-design/SKILL.md +346 -0
- package/skills/ux-ui-design/references/design-tokens.md +359 -0
- package/src/dashboard/__tests__/api.test.ts +211 -0
- package/src/dashboard/browser.ts +35 -0
- package/src/dashboard/public/app.js +869 -0
- package/src/dashboard/public/index.html +90 -0
- package/src/dashboard/public/styles.css +807 -0
- package/src/dashboard/public/vendor/highlight.css +10 -0
- package/src/dashboard/public/vendor/highlight.min.js +8422 -0
- package/src/dashboard/public/vendor/marked.min.js +2210 -0
- package/src/dashboard/server.ts +296 -0
- package/src/dashboard/watchers.ts +83 -0
- package/src/server/adapters/__tests__/dependency-ops.test.ts +52 -18
- package/src/server/adapters/linear/adapter.ts +19 -14
- package/src/server/adapters/local-adapter.ts +48 -7
- package/src/server/db/__tests__/queries.test.ts +2 -1
- package/src/server/db/schema.ts +9 -0
- package/src/server/tools/__tests__/crud.test.ts +111 -1
- package/src/server/tools/__tests__/mcp-interface.test.ts +2 -1
- package/src/server/tools/__tests__/query.test.ts +73 -2
- package/src/server/tools/__tests__/z-configure-linear.test.ts +1 -1
- package/src/server/tools/__tests__/z-get-linear-url.test.ts +1 -1
- package/src/server/tools/create-epic.ts +11 -2
- package/src/server/tools/create-prd.ts +11 -2
- package/src/server/tools/create-task.ts +11 -2
- package/src/server/tools/dependencies.ts +2 -2
- package/src/server/tools/get-entity.ts +12 -10
- package/src/server/tools/render-status.ts +38 -20
- package/src/status-line/__tests__/status-line.test.ts +1 -1
- package/src/utils/status-renderer.ts +32 -6
package/src/server/db/schema.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
114
|
-
|
|
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
|
|
121
|
-
|
|
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
|
|
160
|
-
|
|
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
|
|
167
|
-
|
|
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:
|
|
176
|
-
|
|
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)) {
|