@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.
- package/README.md +3 -3
- package/agents/coder.md +150 -25
- package/commands/breakdown.md +47 -10
- package/commands/flux.md +92 -12
- package/commands/implement.md +166 -17
- package/commands/linear.md +6 -5
- package/commands/prd.md +996 -82
- package/manifest.json +2 -1
- package/package.json +4 -2
- package/skills/flux-orchestrator/SKILL.md +11 -3
- package/skills/prd-writer/SKILL.md +761 -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/index.ts +0 -2
- package/src/server/tools/__tests__/crud.test.ts +111 -1
- package/src/server/tools/__tests__/mcp-interface.test.ts +100 -9
- package/src/server/tools/__tests__/query.test.ts +73 -21
- 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/index.ts +53 -9
- package/src/server/tools/init-project.ts +1 -1
- 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/skills/prd-template/SKILL.md +0 -242
- package/src/server/tools/get-project-context.ts +0 -33
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);
|
package/src/server/index.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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("
|
|
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
|
|
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 () => ({
|
|
412
|
+
const handlerMock = mock(async () => ({ version: "1.0.0" }));
|
|
367
413
|
|
|
368
414
|
const testTool: ToolDefinition = {
|
|
369
|
-
name: "
|
|
370
|
-
description: "Get
|
|
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: "
|
|
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
|
-
|
|
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
|
};
|