@cliangdev/flux-plugin 0.1.0-dev.588ae42 → 0.2.0-dev.2b9c207
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 +1 -1
- package/package.json +1 -1
- package/skills/flux-orchestrator/SKILL.md +11 -3
- package/skills/prd-writer/SKILL.md +761 -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/index.ts +0 -2
- package/src/server/tools/__tests__/crud.test.ts +109 -0
- package/src/server/tools/__tests__/mcp-interface.test.ts +98 -8
- package/src/server/tools/__tests__/query.test.ts +69 -19
- 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/utils/status-renderer.ts +32 -6
- package/skills/prd-template/SKILL.md +0 -242
- package/src/server/tools/get-project-context.ts +0 -33
|
@@ -16,7 +16,6 @@ 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";
|
|
@@ -115,6 +114,75 @@ describe("Query MCP Tools", () => {
|
|
|
115
114
|
expect(result.dependencies[0]).toBe(epic1.ref);
|
|
116
115
|
});
|
|
117
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
|
+
|
|
118
186
|
test("throws error for invalid ref", async () => {
|
|
119
187
|
await expect(
|
|
120
188
|
getEntityTool.handler({ ref: "INVALID-P999" }),
|
|
@@ -242,24 +310,6 @@ describe("Query MCP Tools", () => {
|
|
|
242
310
|
});
|
|
243
311
|
});
|
|
244
312
|
|
|
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
313
|
describe("get_stats", () => {
|
|
264
314
|
test("returns zeroes for empty project", async () => {
|
|
265
315
|
const result = (await getStatsTool.handler({})) as any;
|
|
@@ -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
|
|
|
@@ -7,11 +7,7 @@ import { z } from "zod";
|
|
|
7
7
|
import { config } from "../config.js";
|
|
8
8
|
import { logger } from "../utils/logger.js";
|
|
9
9
|
|
|
10
|
-
const TOOLS_WITHOUT_PROJECT = [
|
|
11
|
-
"init_project",
|
|
12
|
-
"get_project_context",
|
|
13
|
-
"get_version",
|
|
14
|
-
];
|
|
10
|
+
const TOOLS_WITHOUT_PROJECT = ["init_project", "get_version"];
|
|
15
11
|
|
|
16
12
|
export interface ToolDefinition {
|
|
17
13
|
name: string;
|
|
@@ -26,10 +22,56 @@ export interface ToolError {
|
|
|
26
22
|
code: string;
|
|
27
23
|
}
|
|
28
24
|
|
|
25
|
+
export interface ProjectNotInitializedError extends ToolError {
|
|
26
|
+
code: "PROJECT_NOT_INITIALIZED";
|
|
27
|
+
setup: {
|
|
28
|
+
instructions: string;
|
|
29
|
+
options: Array<{
|
|
30
|
+
method: "command" | "tool";
|
|
31
|
+
name: string;
|
|
32
|
+
description: string;
|
|
33
|
+
params?: Record<string, string>;
|
|
34
|
+
}>;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
29
38
|
export function createError(message: string, code: string): ToolError {
|
|
30
39
|
return { error: true, message, code };
|
|
31
40
|
}
|
|
32
41
|
|
|
42
|
+
export function createProjectNotInitializedError(
|
|
43
|
+
cwd: string,
|
|
44
|
+
projectRoot: string,
|
|
45
|
+
): ProjectNotInitializedError {
|
|
46
|
+
return {
|
|
47
|
+
error: true,
|
|
48
|
+
code: "PROJECT_NOT_INITIALIZED",
|
|
49
|
+
message: `No Flux project found. Current directory: ${cwd}, resolved project root: ${projectRoot}`,
|
|
50
|
+
setup: {
|
|
51
|
+
instructions:
|
|
52
|
+
"Initialize a Flux project before using Flux tools. Use one of the following options:",
|
|
53
|
+
options: [
|
|
54
|
+
{
|
|
55
|
+
method: "command",
|
|
56
|
+
name: "/flux",
|
|
57
|
+
description:
|
|
58
|
+
"Interactive setup with guided prompts (recommended for first-time setup)",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
method: "tool",
|
|
62
|
+
name: "init_project",
|
|
63
|
+
description: "Direct initialization via MCP tool",
|
|
64
|
+
params: {
|
|
65
|
+
name: "Project name (required)",
|
|
66
|
+
vision: "Brief project description (required)",
|
|
67
|
+
adapter: "local | linear (default: local)",
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
33
75
|
export function registerTools(server: Server, tools: ToolDefinition[]) {
|
|
34
76
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
35
77
|
tools: tools.map((t) => ({
|
|
@@ -60,13 +102,16 @@ export function registerTools(server: Server, tools: ToolDefinition[]) {
|
|
|
60
102
|
}
|
|
61
103
|
|
|
62
104
|
if (!TOOLS_WITHOUT_PROJECT.includes(toolName) && !config.projectExists) {
|
|
63
|
-
const
|
|
64
|
-
|
|
105
|
+
const error = createProjectNotInitializedError(
|
|
106
|
+
process.cwd(),
|
|
107
|
+
config.projectRoot,
|
|
108
|
+
);
|
|
109
|
+
logger.error(error.message);
|
|
65
110
|
return {
|
|
66
111
|
content: [
|
|
67
112
|
{
|
|
68
113
|
type: "text",
|
|
69
|
-
text: JSON.stringify(
|
|
114
|
+
text: JSON.stringify(error, null, 2),
|
|
70
115
|
},
|
|
71
116
|
],
|
|
72
117
|
isError: true,
|
|
@@ -104,7 +149,6 @@ export { deleteEntityTool } from "./delete-entity.js";
|
|
|
104
149
|
export { addDependencyTool, removeDependencyTool } from "./dependencies.js";
|
|
105
150
|
export { getEntityTool } from "./get-entity.js";
|
|
106
151
|
export { getLinearUrlTool } from "./get-linear-url.js";
|
|
107
|
-
export { getProjectContextTool } from "./get-project-context.js";
|
|
108
152
|
export { getStatsTool } from "./get-stats.js";
|
|
109
153
|
export { getVersionTool } from "./get-version.js";
|
|
110
154
|
export { initProjectTool } from "./init-project.js";
|
|
@@ -102,7 +102,7 @@ async function handler(input: unknown) {
|
|
|
102
102
|
export const initProjectTool: ToolDefinition = {
|
|
103
103
|
name: "init_project",
|
|
104
104
|
description:
|
|
105
|
-
"Initialize a new Flux project. Required: name, vision. Optional: adapter (local|specflux|linear|notion, default 'local'). Creates .flux/ directory with project.json and SQLite database. Returns {success, project, message}. Fails if .flux/ already exists.
|
|
105
|
+
"Initialize a new Flux project. Required: name, vision. Optional: adapter (local|specflux|linear|notion, default 'local'). Creates .flux/ directory with project.json and SQLite database. Returns {success, project, message}. Fails if .flux/ already exists.",
|
|
106
106
|
inputSchema,
|
|
107
107
|
handler,
|
|
108
108
|
};
|
|
@@ -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
|
|
|
@@ -34,12 +34,14 @@ interface Epic {
|
|
|
34
34
|
status: string;
|
|
35
35
|
task_count: number;
|
|
36
36
|
tasks_completed: number;
|
|
37
|
+
dependencies?: string[];
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
interface Task {
|
|
40
41
|
ref: string;
|
|
41
42
|
title: string;
|
|
42
43
|
status: string;
|
|
44
|
+
dependencies?: string[];
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
interface EpicWithTasks {
|
|
@@ -47,6 +49,7 @@ interface EpicWithTasks {
|
|
|
47
49
|
title: string;
|
|
48
50
|
status: string;
|
|
49
51
|
tasks: Task[];
|
|
52
|
+
dependencies?: string[];
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
interface Prd {
|
|
@@ -54,6 +57,7 @@ interface Prd {
|
|
|
54
57
|
title: string;
|
|
55
58
|
status: string;
|
|
56
59
|
epics: Epic[];
|
|
60
|
+
dependencies?: string[];
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
interface PrdWithTasks {
|
|
@@ -61,6 +65,7 @@ interface PrdWithTasks {
|
|
|
61
65
|
title: string;
|
|
62
66
|
status: string;
|
|
63
67
|
epics: EpicWithTasks[];
|
|
68
|
+
dependencies?: string[];
|
|
64
69
|
}
|
|
65
70
|
|
|
66
71
|
const STATUS_ORDER: Record<string, number> = {
|
|
@@ -117,13 +122,22 @@ export function renderSummaryView(
|
|
|
117
122
|
|
|
118
123
|
for (const prd of sortedPrds) {
|
|
119
124
|
const badge = getStatusBadge(prd.status);
|
|
120
|
-
|
|
125
|
+
const prdBlocked = prd.dependencies && prd.dependencies.length > 0;
|
|
126
|
+
const prdBlockedBy = prdBlocked
|
|
127
|
+
? ` ⚠️ blocked by: ${prd.dependencies?.join(", ")}`
|
|
128
|
+
: "";
|
|
129
|
+
lines.push(`${prd.ref} ${prd.title} ${badge}${prdBlockedBy}`);
|
|
121
130
|
|
|
122
131
|
for (const epic of prd.epics) {
|
|
123
132
|
const icon = getStatusIcon(epic.status);
|
|
133
|
+
const epicBlocked = epic.dependencies && epic.dependencies.length > 0;
|
|
134
|
+
const epicBlockedBy = epicBlocked
|
|
135
|
+
? ` ⚠️ blocked by: ${epic.dependencies?.join(", ")}`
|
|
136
|
+
: "";
|
|
137
|
+
|
|
124
138
|
if (epic.task_count === 0) {
|
|
125
139
|
lines.push(
|
|
126
|
-
` ${icon} ${epic.ref} ${epic.title} ·········· (no tasks)`,
|
|
140
|
+
` ${icon} ${epic.ref} ${epic.title} ·········· (no tasks)${epicBlockedBy}`,
|
|
127
141
|
);
|
|
128
142
|
} else {
|
|
129
143
|
const epicProgress = renderProgressBar(
|
|
@@ -131,7 +145,7 @@ export function renderSummaryView(
|
|
|
131
145
|
10,
|
|
132
146
|
);
|
|
133
147
|
lines.push(
|
|
134
|
-
` ${icon} ${epic.ref} ${epic.title} ${epicProgress} ${epic.tasks_completed}/${epic.task_count}`,
|
|
148
|
+
` ${icon} ${epic.ref} ${epic.title} ${epicProgress} ${epic.tasks_completed}/${epic.task_count}${epicBlockedBy}`,
|
|
135
149
|
);
|
|
136
150
|
}
|
|
137
151
|
}
|
|
@@ -157,23 +171,35 @@ export function renderFullTreeView(prds: PrdWithTasks[]): string {
|
|
|
157
171
|
|
|
158
172
|
for (const prd of sortedPrds) {
|
|
159
173
|
const prdBadge = getStatusBadge(prd.status);
|
|
174
|
+
const prdBlocked = prd.dependencies && prd.dependencies.length > 0;
|
|
175
|
+
const prdBlockedBy = prdBlocked
|
|
176
|
+
? ` ⚠️ blocked by: ${prd.dependencies?.join(", ")}`
|
|
177
|
+
: "";
|
|
160
178
|
lines.push(`${prd.ref} ${prd.title}`);
|
|
161
|
-
lines.push(`${prdBadge}`);
|
|
179
|
+
lines.push(`${prdBadge}${prdBlockedBy}`);
|
|
162
180
|
lines.push("");
|
|
163
181
|
|
|
164
182
|
for (const epic of prd.epics) {
|
|
165
183
|
const epicIcon = getStatusIcon(epic.status);
|
|
166
|
-
|
|
184
|
+
const epicBlocked = epic.dependencies && epic.dependencies.length > 0;
|
|
185
|
+
const epicBlockedBy = epicBlocked
|
|
186
|
+
? ` ⚠️ blocked by: ${epic.dependencies?.join(", ")}`
|
|
187
|
+
: "";
|
|
188
|
+
lines.push(` ${epicIcon} ${epic.ref} ${epic.title}${epicBlockedBy}`);
|
|
167
189
|
|
|
168
190
|
if (epic.tasks.length > 0) {
|
|
169
191
|
lines.push(` ┌${"─".repeat(50)}┐`);
|
|
170
192
|
|
|
171
193
|
for (const task of epic.tasks) {
|
|
172
194
|
const taskIcon = getStatusIcon(task.status);
|
|
195
|
+
const taskBlocked = task.dependencies && task.dependencies.length > 0;
|
|
196
|
+
const taskBlockedBy = taskBlocked
|
|
197
|
+
? ` ⚠️ blocked by: ${task.dependencies?.join(", ")}`
|
|
198
|
+
: "";
|
|
173
199
|
const currentMarker =
|
|
174
200
|
task.status === "IN_PROGRESS" ? " ← CURRENT" : "";
|
|
175
201
|
lines.push(
|
|
176
|
-
` │ ${taskIcon} ${task.ref} ${task.title}${currentMarker}`,
|
|
202
|
+
` │ ${taskIcon} ${task.ref} ${task.title}${currentMarker}${taskBlockedBy}`,
|
|
177
203
|
);
|
|
178
204
|
}
|
|
179
205
|
|