@cliangdev/flux-plugin 0.2.0-dev.4f12f3f → 0.2.0-dev.71255d1
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 +44 -7
- package/commands/implement.md +165 -15
- package/commands/prd.md +996 -82
- package/manifest.json +2 -1
- package/package.json +1 -1
- package/skills/flux-orchestrator/SKILL.md +9 -1
- 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/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 +40 -5
- package/src/server/db/schema.ts +9 -0
- package/src/server/tools/__tests__/crud.test.ts +109 -0
- package/src/server/tools/__tests__/query.test.ts +69 -0
- 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/utils/status-renderer.ts +32 -6
- package/skills/prd-template/SKILL.md +0 -242
|
@@ -194,8 +194,12 @@ describe("LinearAdapter - Dependency Operations", () => {
|
|
|
194
194
|
const mockRelation = {
|
|
195
195
|
id: "relation_1",
|
|
196
196
|
type: "blocks",
|
|
197
|
-
|
|
198
|
-
|
|
197
|
+
// Linear SDK returns promises for lazy-loaded related objects
|
|
198
|
+
issue: Promise.resolve({ id: "issue_epic_1", identifier: "ENG-42" }),
|
|
199
|
+
relatedIssue: Promise.resolve({
|
|
200
|
+
id: "issue_epic_2",
|
|
201
|
+
identifier: "ENG-43",
|
|
202
|
+
}),
|
|
199
203
|
delete: mockDelete,
|
|
200
204
|
};
|
|
201
205
|
|
|
@@ -208,7 +212,7 @@ describe("LinearAdapter - Dependency Operations", () => {
|
|
|
208
212
|
id: "issue_epic_2",
|
|
209
213
|
identifier: "ENG-43",
|
|
210
214
|
_raw: {
|
|
211
|
-
|
|
215
|
+
inverseRelations: mock(async () => ({
|
|
212
216
|
nodes: [mockRelation],
|
|
213
217
|
})),
|
|
214
218
|
},
|
|
@@ -235,7 +239,7 @@ describe("LinearAdapter - Dependency Operations", () => {
|
|
|
235
239
|
id: "issue_epic_1",
|
|
236
240
|
identifier: "ENG-42",
|
|
237
241
|
_raw: {
|
|
238
|
-
|
|
242
|
+
inverseRelations: mock(async () => ({ nodes: [] })),
|
|
239
243
|
},
|
|
240
244
|
});
|
|
241
245
|
|
|
@@ -265,7 +269,7 @@ describe("LinearAdapter - Dependency Operations", () => {
|
|
|
265
269
|
id: "issue_epic_2",
|
|
266
270
|
identifier: "ENG-43",
|
|
267
271
|
_raw: {
|
|
268
|
-
|
|
272
|
+
inverseRelations: mock(async () => ({
|
|
269
273
|
nodes: [], // No relations
|
|
270
274
|
})),
|
|
271
275
|
},
|
|
@@ -294,19 +298,31 @@ describe("LinearAdapter - Dependency Operations", () => {
|
|
|
294
298
|
id: "issue_epic_1",
|
|
295
299
|
identifier: "ENG-43",
|
|
296
300
|
_raw: {
|
|
297
|
-
|
|
301
|
+
inverseRelations: mock(async () => ({
|
|
298
302
|
nodes: [
|
|
299
303
|
{
|
|
300
304
|
id: "relation_1",
|
|
301
305
|
type: "blocks",
|
|
302
|
-
issue: {
|
|
303
|
-
|
|
306
|
+
issue: Promise.resolve({
|
|
307
|
+
id: "issue_epic_2",
|
|
308
|
+
identifier: "ENG-42",
|
|
309
|
+
}),
|
|
310
|
+
relatedIssue: Promise.resolve({
|
|
311
|
+
id: "issue_epic_1",
|
|
312
|
+
identifier: "ENG-43",
|
|
313
|
+
}),
|
|
304
314
|
},
|
|
305
315
|
{
|
|
306
316
|
id: "relation_2",
|
|
307
317
|
type: "blocks",
|
|
308
|
-
issue: {
|
|
309
|
-
|
|
318
|
+
issue: Promise.resolve({
|
|
319
|
+
id: "issue_epic_3",
|
|
320
|
+
identifier: "ENG-44",
|
|
321
|
+
}),
|
|
322
|
+
relatedIssue: Promise.resolve({
|
|
323
|
+
id: "issue_epic_1",
|
|
324
|
+
identifier: "ENG-43",
|
|
325
|
+
}),
|
|
310
326
|
},
|
|
311
327
|
],
|
|
312
328
|
})),
|
|
@@ -328,7 +344,7 @@ describe("LinearAdapter - Dependency Operations", () => {
|
|
|
328
344
|
id: "issue_epic_1",
|
|
329
345
|
identifier: "ENG-42",
|
|
330
346
|
_raw: {
|
|
331
|
-
|
|
347
|
+
inverseRelations: mock(async () => ({
|
|
332
348
|
nodes: [],
|
|
333
349
|
})),
|
|
334
350
|
},
|
|
@@ -349,25 +365,43 @@ describe("LinearAdapter - Dependency Operations", () => {
|
|
|
349
365
|
id: "issue_epic_1",
|
|
350
366
|
identifier: "ENG-43",
|
|
351
367
|
_raw: {
|
|
352
|
-
|
|
368
|
+
inverseRelations: mock(async () => ({
|
|
353
369
|
nodes: [
|
|
354
370
|
{
|
|
355
371
|
id: "relation_1",
|
|
356
372
|
type: "blocks",
|
|
357
|
-
issue: {
|
|
358
|
-
|
|
373
|
+
issue: Promise.resolve({
|
|
374
|
+
id: "issue_epic_2",
|
|
375
|
+
identifier: "ENG-42",
|
|
376
|
+
}),
|
|
377
|
+
relatedIssue: Promise.resolve({
|
|
378
|
+
id: "issue_epic_1",
|
|
379
|
+
identifier: "ENG-43",
|
|
380
|
+
}),
|
|
359
381
|
},
|
|
360
382
|
{
|
|
361
383
|
id: "relation_2",
|
|
362
384
|
type: "duplicate",
|
|
363
|
-
issue: {
|
|
364
|
-
|
|
385
|
+
issue: Promise.resolve({
|
|
386
|
+
id: "issue_epic_3",
|
|
387
|
+
identifier: "ENG-44",
|
|
388
|
+
}),
|
|
389
|
+
relatedIssue: Promise.resolve({
|
|
390
|
+
id: "issue_epic_1",
|
|
391
|
+
identifier: "ENG-43",
|
|
392
|
+
}),
|
|
365
393
|
},
|
|
366
394
|
{
|
|
367
395
|
id: "relation_3",
|
|
368
396
|
type: "related",
|
|
369
|
-
issue: {
|
|
370
|
-
|
|
397
|
+
issue: Promise.resolve({
|
|
398
|
+
id: "issue_epic_4",
|
|
399
|
+
identifier: "ENG-45",
|
|
400
|
+
}),
|
|
401
|
+
relatedIssue: Promise.resolve({
|
|
402
|
+
id: "issue_epic_1",
|
|
403
|
+
identifier: "ENG-43",
|
|
404
|
+
}),
|
|
371
405
|
},
|
|
372
406
|
],
|
|
373
407
|
})),
|
|
@@ -887,16 +887,22 @@ export class LinearAdapter implements BackendAdapter {
|
|
|
887
887
|
if (!blockedIssue) throw new Error(`Issue not found: ${ref}`);
|
|
888
888
|
if (!blockerIssue) throw new Error(`Issue not found: ${dependsOnRef}`);
|
|
889
889
|
|
|
890
|
+
// Use inverseRelations to find relations where blockedIssue is the target
|
|
890
891
|
const relations = await this.client.execute<any>(() =>
|
|
891
|
-
blockedIssue._raw.
|
|
892
|
+
blockedIssue._raw.inverseRelations(),
|
|
892
893
|
);
|
|
893
894
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
895
|
+
// Find the relation to delete - need to await rel.issue since it's lazy-loaded
|
|
896
|
+
let relationToDelete: any = null;
|
|
897
|
+
for (const rel of relations.nodes) {
|
|
898
|
+
if (rel.type === "blocks") {
|
|
899
|
+
const relIssue = await rel.issue;
|
|
900
|
+
if (relIssue?.id === blockerIssue.id) {
|
|
901
|
+
relationToDelete = rel;
|
|
902
|
+
break;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
900
906
|
|
|
901
907
|
if (!relationToDelete) {
|
|
902
908
|
throw new Error(
|
|
@@ -911,18 +917,17 @@ export class LinearAdapter implements BackendAdapter {
|
|
|
911
917
|
const issue = await this.fetchIssue(ref);
|
|
912
918
|
if (!issue) throw new Error(`Issue not found: ${ref}`);
|
|
913
919
|
|
|
920
|
+
// Use inverseRelations to get relations where this issue is the target (relatedIssueId)
|
|
921
|
+
// This finds issues that block this one
|
|
914
922
|
const relations = await this.client.execute<any>(() =>
|
|
915
|
-
issue._raw.
|
|
923
|
+
issue._raw.inverseRelations(),
|
|
916
924
|
);
|
|
917
925
|
|
|
918
926
|
const blockingRefs: string[] = [];
|
|
919
927
|
for (const rel of relations.nodes) {
|
|
920
|
-
if (
|
|
921
|
-
|
|
922
|
-
rel.
|
|
923
|
-
(rel.relatedIssue as any).id === issue.id
|
|
924
|
-
) {
|
|
925
|
-
const blockerIssue = rel.issue as any;
|
|
928
|
+
if (rel.type === "blocks") {
|
|
929
|
+
// Linear SDK returns lazy-loaded promises for related objects
|
|
930
|
+
const blockerIssue = await rel.issue;
|
|
926
931
|
if (blockerIssue?.identifier) {
|
|
927
932
|
blockingRefs.push(blockerIssue.identifier);
|
|
928
933
|
}
|
|
@@ -312,6 +312,14 @@ export class LocalAdapter implements BackendAdapter {
|
|
|
312
312
|
cascade.epics++;
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
+
// Delete PRD dependencies
|
|
316
|
+
const deletedDeps = db
|
|
317
|
+
.query(
|
|
318
|
+
"DELETE FROM prd_dependencies WHERE prd_id = ? OR depends_on_prd_id = ?",
|
|
319
|
+
)
|
|
320
|
+
.run(prd.id, prd.id);
|
|
321
|
+
cascade.dependencies += deletedDeps.changes;
|
|
322
|
+
|
|
315
323
|
remove(db, "prds", prd.id);
|
|
316
324
|
|
|
317
325
|
return { deleted: ref, cascade };
|
|
@@ -710,7 +718,15 @@ export class LocalAdapter implements BackendAdapter {
|
|
|
710
718
|
throw new Error("Dependencies must be between entities of the same type");
|
|
711
719
|
}
|
|
712
720
|
|
|
713
|
-
if (entityType === "
|
|
721
|
+
if (entityType === "P") {
|
|
722
|
+
const prd = findByRef<PrdRow>(db, "prds", ref);
|
|
723
|
+
const dependsOnPrd = findByRef<PrdRow>(db, "prds", dependsOnRef);
|
|
724
|
+
if (!prd || !dependsOnPrd) throw new Error("PRD not found");
|
|
725
|
+
|
|
726
|
+
db.query(
|
|
727
|
+
"INSERT OR IGNORE INTO prd_dependencies (prd_id, depends_on_prd_id) VALUES (?, ?)",
|
|
728
|
+
).run(prd.id, dependsOnPrd.id);
|
|
729
|
+
} else if (entityType === "E") {
|
|
714
730
|
const epic = findByRef<EpicRow>(db, "epics", ref);
|
|
715
731
|
const dependsOnEpic = findByRef<EpicRow>(db, "epics", dependsOnRef);
|
|
716
732
|
if (!epic || !dependsOnEpic) throw new Error("Epic not found");
|
|
@@ -726,8 +742,6 @@ export class LocalAdapter implements BackendAdapter {
|
|
|
726
742
|
db.query(
|
|
727
743
|
"INSERT OR IGNORE INTO task_dependencies (task_id, depends_on_task_id) VALUES (?, ?)",
|
|
728
744
|
).run(task.id, dependsOnTask.id);
|
|
729
|
-
} else {
|
|
730
|
-
throw new Error("Dependencies can only be added to Epics or Tasks");
|
|
731
745
|
}
|
|
732
746
|
}
|
|
733
747
|
|
|
@@ -735,7 +749,15 @@ export class LocalAdapter implements BackendAdapter {
|
|
|
735
749
|
const db = getDb();
|
|
736
750
|
const entityType = getEntityType(ref);
|
|
737
751
|
|
|
738
|
-
if (entityType === "
|
|
752
|
+
if (entityType === "P") {
|
|
753
|
+
const prd = findByRef<PrdRow>(db, "prds", ref);
|
|
754
|
+
const dependsOnPrd = findByRef<PrdRow>(db, "prds", dependsOnRef);
|
|
755
|
+
if (!prd || !dependsOnPrd) throw new Error("PRD not found");
|
|
756
|
+
|
|
757
|
+
db.query(
|
|
758
|
+
"DELETE FROM prd_dependencies WHERE prd_id = ? AND depends_on_prd_id = ?",
|
|
759
|
+
).run(prd.id, dependsOnPrd.id);
|
|
760
|
+
} else if (entityType === "E") {
|
|
739
761
|
const epic = findByRef<EpicRow>(db, "epics", ref);
|
|
740
762
|
const dependsOnEpic = findByRef<EpicRow>(db, "epics", dependsOnRef);
|
|
741
763
|
if (!epic || !dependsOnEpic) throw new Error("Epic not found");
|
|
@@ -758,7 +780,20 @@ export class LocalAdapter implements BackendAdapter {
|
|
|
758
780
|
const db = getDb();
|
|
759
781
|
const entityType = getEntityType(ref);
|
|
760
782
|
|
|
761
|
-
if (entityType === "
|
|
783
|
+
if (entityType === "P") {
|
|
784
|
+
const prd = findByRef<PrdRow>(db, "prds", ref);
|
|
785
|
+
if (!prd) return [];
|
|
786
|
+
|
|
787
|
+
const deps = db
|
|
788
|
+
.query(
|
|
789
|
+
`SELECT p.ref FROM prd_dependencies pd
|
|
790
|
+
JOIN prds p ON pd.depends_on_prd_id = p.id
|
|
791
|
+
WHERE pd.prd_id = ?`,
|
|
792
|
+
)
|
|
793
|
+
.all(prd.id) as { ref: string }[];
|
|
794
|
+
|
|
795
|
+
return deps.map((d) => d.ref);
|
|
796
|
+
} else if (entityType === "E") {
|
|
762
797
|
const epic = findByRef<EpicRow>(db, "epics", ref);
|
|
763
798
|
if (!epic) return [];
|
|
764
799
|
|
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);
|
|
@@ -53,6 +53,7 @@ describe("CRUD MCP Tools", () => {
|
|
|
53
53
|
expect(result.ref).toBeDefined();
|
|
54
54
|
expect(result.status).toBe("DRAFT");
|
|
55
55
|
expect(result.id).toBeDefined();
|
|
56
|
+
expect(result.dependencies).toEqual([]);
|
|
56
57
|
});
|
|
57
58
|
|
|
58
59
|
test("creates PRD with optional fields", async () => {
|
|
@@ -64,6 +65,17 @@ describe("CRUD MCP Tools", () => {
|
|
|
64
65
|
expect(result.description).toBe("A description");
|
|
65
66
|
expect(result.tag).toBe("mvp");
|
|
66
67
|
});
|
|
68
|
+
|
|
69
|
+
test("creates PRD with depends_on", async () => {
|
|
70
|
+
const prd1 = (await createPrdTool.handler({ title: "PRD 1" })) as any;
|
|
71
|
+
const prd2 = (await createPrdTool.handler({ title: "PRD 2" })) as any;
|
|
72
|
+
const prd3 = (await createPrdTool.handler({
|
|
73
|
+
title: "PRD 3",
|
|
74
|
+
depends_on: [prd1.ref, prd2.ref],
|
|
75
|
+
})) as any;
|
|
76
|
+
|
|
77
|
+
expect(prd3.dependencies).toEqual([prd1.ref, prd2.ref]);
|
|
78
|
+
});
|
|
67
79
|
});
|
|
68
80
|
|
|
69
81
|
describe("create_epic", () => {
|
|
@@ -77,6 +89,7 @@ describe("CRUD MCP Tools", () => {
|
|
|
77
89
|
expect(epic.title).toBe("Test Epic");
|
|
78
90
|
expect(epic.ref).toBeDefined();
|
|
79
91
|
expect(epic.status).toBe("PENDING");
|
|
92
|
+
expect(epic.dependencies).toEqual([]);
|
|
80
93
|
});
|
|
81
94
|
|
|
82
95
|
test("creates epic with acceptance criteria", async () => {
|
|
@@ -94,6 +107,25 @@ describe("CRUD MCP Tools", () => {
|
|
|
94
107
|
createEpicTool.handler({ prd_ref: "INVALID-P999", title: "Test" }),
|
|
95
108
|
).rejects.toThrow("PRD not found");
|
|
96
109
|
});
|
|
110
|
+
|
|
111
|
+
test("creates epic with depends_on", async () => {
|
|
112
|
+
const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
|
|
113
|
+
const epic1 = (await createEpicTool.handler({
|
|
114
|
+
prd_ref: prd.ref,
|
|
115
|
+
title: "Epic 1",
|
|
116
|
+
})) as any;
|
|
117
|
+
const epic2 = (await createEpicTool.handler({
|
|
118
|
+
prd_ref: prd.ref,
|
|
119
|
+
title: "Epic 2",
|
|
120
|
+
})) as any;
|
|
121
|
+
const epic3 = (await createEpicTool.handler({
|
|
122
|
+
prd_ref: prd.ref,
|
|
123
|
+
title: "Epic 3",
|
|
124
|
+
depends_on: [epic1.ref, epic2.ref],
|
|
125
|
+
})) as any;
|
|
126
|
+
|
|
127
|
+
expect(epic3.dependencies).toEqual([epic1.ref, epic2.ref]);
|
|
128
|
+
});
|
|
97
129
|
});
|
|
98
130
|
|
|
99
131
|
describe("create_task", () => {
|
|
@@ -111,6 +143,7 @@ describe("CRUD MCP Tools", () => {
|
|
|
111
143
|
expect(task.title).toBe("Test Task");
|
|
112
144
|
expect(task.ref).toBeDefined();
|
|
113
145
|
expect(task.priority).toBe("MEDIUM");
|
|
146
|
+
expect(task.dependencies).toEqual([]);
|
|
114
147
|
});
|
|
115
148
|
|
|
116
149
|
test("creates task with priority", async () => {
|
|
@@ -126,6 +159,29 @@ describe("CRUD MCP Tools", () => {
|
|
|
126
159
|
})) as any;
|
|
127
160
|
expect(task.priority).toBe("HIGH");
|
|
128
161
|
});
|
|
162
|
+
|
|
163
|
+
test("creates task with depends_on", async () => {
|
|
164
|
+
const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
|
|
165
|
+
const epic = (await createEpicTool.handler({
|
|
166
|
+
prd_ref: prd.ref,
|
|
167
|
+
title: "Test Epic",
|
|
168
|
+
})) as any;
|
|
169
|
+
const task1 = (await createTaskTool.handler({
|
|
170
|
+
epic_ref: epic.ref,
|
|
171
|
+
title: "Task 1",
|
|
172
|
+
})) as any;
|
|
173
|
+
const task2 = (await createTaskTool.handler({
|
|
174
|
+
epic_ref: epic.ref,
|
|
175
|
+
title: "Task 2",
|
|
176
|
+
})) as any;
|
|
177
|
+
const task3 = (await createTaskTool.handler({
|
|
178
|
+
epic_ref: epic.ref,
|
|
179
|
+
title: "Task 3",
|
|
180
|
+
depends_on: [task1.ref, task2.ref],
|
|
181
|
+
})) as any;
|
|
182
|
+
|
|
183
|
+
expect(task3.dependencies).toEqual([task1.ref, task2.ref]);
|
|
184
|
+
});
|
|
129
185
|
});
|
|
130
186
|
|
|
131
187
|
describe("update_entity", () => {
|
|
@@ -183,6 +239,19 @@ describe("CRUD MCP Tools", () => {
|
|
|
183
239
|
const result = (await deleteEntityTool.handler({ ref: epic.ref })) as any;
|
|
184
240
|
expect(result.cascade.tasks).toBe(2);
|
|
185
241
|
});
|
|
242
|
+
|
|
243
|
+
test("cascade deletes PRD with dependencies", async () => {
|
|
244
|
+
const prd1 = (await createPrdTool.handler({ title: "PRD 1" })) as any;
|
|
245
|
+
const prd2 = (await createPrdTool.handler({ title: "PRD 2" })) as any;
|
|
246
|
+
await addDependencyTool.handler({
|
|
247
|
+
ref: prd2.ref,
|
|
248
|
+
depends_on_ref: prd1.ref,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const result = (await deleteEntityTool.handler({ ref: prd1.ref })) as any;
|
|
252
|
+
expect(result.deleted).toBe(prd1.ref);
|
|
253
|
+
expect(result.cascade.dependencies).toBe(1);
|
|
254
|
+
});
|
|
186
255
|
});
|
|
187
256
|
|
|
188
257
|
describe("dependencies", () => {
|
|
@@ -210,6 +279,26 @@ describe("CRUD MCP Tools", () => {
|
|
|
210
279
|
expect(removeResult.success).toBe(true);
|
|
211
280
|
});
|
|
212
281
|
|
|
282
|
+
test("adds and removes PRD dependency", async () => {
|
|
283
|
+
const prd1 = (await createPrdTool.handler({ title: "PRD 1" })) as any;
|
|
284
|
+
const prd2 = (await createPrdTool.handler({ title: "PRD 2" })) as any;
|
|
285
|
+
|
|
286
|
+
const addResult = (await addDependencyTool.handler({
|
|
287
|
+
ref: prd2.ref,
|
|
288
|
+
depends_on_ref: prd1.ref,
|
|
289
|
+
})) as any;
|
|
290
|
+
expect(addResult.success).toBe(true);
|
|
291
|
+
expect(addResult.ref).toBe(prd2.ref);
|
|
292
|
+
expect(addResult.depends_on).toBe(prd1.ref);
|
|
293
|
+
|
|
294
|
+
const removeResult = (await removeDependencyTool.handler({
|
|
295
|
+
ref: prd2.ref,
|
|
296
|
+
depends_on_ref: prd1.ref,
|
|
297
|
+
})) as any;
|
|
298
|
+
expect(removeResult.success).toBe(true);
|
|
299
|
+
expect(removeResult.removed_dependency).toBe(prd1.ref);
|
|
300
|
+
});
|
|
301
|
+
|
|
213
302
|
test("prevents self-dependency", async () => {
|
|
214
303
|
const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
|
|
215
304
|
const epic = (await createEpicTool.handler({
|
|
@@ -221,6 +310,26 @@ describe("CRUD MCP Tools", () => {
|
|
|
221
310
|
addDependencyTool.handler({ ref: epic.ref, depends_on_ref: epic.ref }),
|
|
222
311
|
).rejects.toThrow("depend on itself");
|
|
223
312
|
});
|
|
313
|
+
|
|
314
|
+
test("prevents PRD self-dependency", async () => {
|
|
315
|
+
const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
|
|
316
|
+
|
|
317
|
+
await expect(
|
|
318
|
+
addDependencyTool.handler({ ref: prd.ref, depends_on_ref: prd.ref }),
|
|
319
|
+
).rejects.toThrow("depend on itself");
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test("prevents cross-type dependencies between PRD and Epic", async () => {
|
|
323
|
+
const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
|
|
324
|
+
const epic = (await createEpicTool.handler({
|
|
325
|
+
prd_ref: prd.ref,
|
|
326
|
+
title: "Epic",
|
|
327
|
+
})) as any;
|
|
328
|
+
|
|
329
|
+
await expect(
|
|
330
|
+
addDependencyTool.handler({ ref: prd.ref, depends_on_ref: epic.ref }),
|
|
331
|
+
).rejects.toThrow("same type");
|
|
332
|
+
});
|
|
224
333
|
});
|
|
225
334
|
|
|
226
335
|
describe("criteria", () => {
|
|
@@ -114,6 +114,75 @@ describe("Query MCP Tools", () => {
|
|
|
114
114
|
expect(result.dependencies[0]).toBe(epic1.ref);
|
|
115
115
|
});
|
|
116
116
|
|
|
117
|
+
test("returns dependencies by default for PRD without include", async () => {
|
|
118
|
+
const prd1 = (await createPrdTool.handler({ title: "PRD 1" })) as any;
|
|
119
|
+
const prd2 = (await createPrdTool.handler({ title: "PRD 2" })) as any;
|
|
120
|
+
await addDependencyTool.handler({
|
|
121
|
+
ref: prd2.ref,
|
|
122
|
+
depends_on_ref: prd1.ref,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const result = (await getEntityTool.handler({
|
|
126
|
+
ref: prd2.ref,
|
|
127
|
+
})) as any;
|
|
128
|
+
|
|
129
|
+
expect(result.dependencies).toBeDefined();
|
|
130
|
+
expect(result.dependencies.length).toBe(1);
|
|
131
|
+
expect(result.dependencies[0]).toBe(prd1.ref);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("returns dependencies by default for Epic without include", async () => {
|
|
135
|
+
const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
|
|
136
|
+
const epic1 = (await createEpicTool.handler({
|
|
137
|
+
prd_ref: prd.ref,
|
|
138
|
+
title: "Epic 1",
|
|
139
|
+
})) as any;
|
|
140
|
+
const epic2 = (await createEpicTool.handler({
|
|
141
|
+
prd_ref: prd.ref,
|
|
142
|
+
title: "Epic 2",
|
|
143
|
+
})) as any;
|
|
144
|
+
await addDependencyTool.handler({
|
|
145
|
+
ref: epic2.ref,
|
|
146
|
+
depends_on_ref: epic1.ref,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const result = (await getEntityTool.handler({
|
|
150
|
+
ref: epic2.ref,
|
|
151
|
+
})) as any;
|
|
152
|
+
|
|
153
|
+
expect(result.dependencies).toBeDefined();
|
|
154
|
+
expect(result.dependencies.length).toBe(1);
|
|
155
|
+
expect(result.dependencies[0]).toBe(epic1.ref);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("returns dependencies by default for Task without include", async () => {
|
|
159
|
+
const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
|
|
160
|
+
const epic = (await createEpicTool.handler({
|
|
161
|
+
prd_ref: prd.ref,
|
|
162
|
+
title: "Test Epic",
|
|
163
|
+
})) as any;
|
|
164
|
+
const task1 = (await createTaskTool.handler({
|
|
165
|
+
epic_ref: epic.ref,
|
|
166
|
+
title: "Task 1",
|
|
167
|
+
})) as any;
|
|
168
|
+
const task2 = (await createTaskTool.handler({
|
|
169
|
+
epic_ref: epic.ref,
|
|
170
|
+
title: "Task 2",
|
|
171
|
+
})) as any;
|
|
172
|
+
await addDependencyTool.handler({
|
|
173
|
+
ref: task2.ref,
|
|
174
|
+
depends_on_ref: task1.ref,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const result = (await getEntityTool.handler({
|
|
178
|
+
ref: task2.ref,
|
|
179
|
+
})) as any;
|
|
180
|
+
|
|
181
|
+
expect(result.dependencies).toBeDefined();
|
|
182
|
+
expect(result.dependencies.length).toBe(1);
|
|
183
|
+
expect(result.dependencies[0]).toBe(task1.ref);
|
|
184
|
+
});
|
|
185
|
+
|
|
117
186
|
test("throws error for invalid ref", async () => {
|
|
118
187
|
await expect(
|
|
119
188
|
getEntityTool.handler({ ref: "INVALID-P999" }),
|
|
@@ -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
|
};
|