@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.
@@ -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
- return { ...toMcpEpic(epic), criteria_count: criteriaCount };
27
+ const dependencies: string[] = [];
28
+ if (parsed.depends_on && parsed.depends_on.length > 0) {
29
+ for (const depRef of parsed.depends_on) {
30
+ await adapter.addDependency(epic.ref, depRef);
31
+ dependencies.push(depRef);
32
+ }
33
+ }
34
+
35
+ return { ...toMcpEpic(epic), criteria_count: criteriaCount, dependencies };
27
36
  }
28
37
 
29
38
  export const createEpicTool: ToolDefinition = {
30
39
  name: "create_epic",
31
40
  description:
32
- "Create a new Epic under a PRD. Required: prd_ref (e.g., 'FLUX-P1'), title. Optional: description, acceptance_criteria (string array). Returns {id, ref, title, status, criteria_count}. Status starts as PENDING.",
41
+ "Create a new Epic under a PRD. Required: prd_ref (e.g., 'FLUX-P1'), title. Optional: description, acceptance_criteria (string array), depends_on (array of Epic refs this Epic depends on, e.g., ['FLUX-E1']). Returns {id, ref, title, status, criteria_count, dependencies}. Status starts as PENDING.",
33
42
  inputSchema,
34
43
  handler,
35
44
  };
@@ -7,6 +7,7 @@ const inputSchema = z.object({
7
7
  title: z.string().min(1, "Title is required"),
8
8
  description: z.string().optional(),
9
9
  tag: z.string().optional(),
10
+ depends_on: z.array(z.string()).optional(),
10
11
  });
11
12
 
12
13
  async function handler(input: unknown) {
@@ -19,13 +20,21 @@ async function handler(input: unknown) {
19
20
  tag: parsed.tag,
20
21
  });
21
22
 
22
- return toMcpPrd(prd);
23
+ const dependencies: string[] = [];
24
+ if (parsed.depends_on && parsed.depends_on.length > 0) {
25
+ for (const depRef of parsed.depends_on) {
26
+ await adapter.addDependency(prd.ref, depRef);
27
+ dependencies.push(depRef);
28
+ }
29
+ }
30
+
31
+ return { ...toMcpPrd(prd), dependencies };
23
32
  }
24
33
 
25
34
  export const createPrdTool: ToolDefinition = {
26
35
  name: "create_prd",
27
36
  description:
28
- "Create a new PRD (Product Requirements Document). Required: title. Optional: description, tag. Returns the created PRD with {id, ref, title, description, status, tag}. Status starts as DRAFT.",
37
+ "Create a new PRD (Product Requirements Document). Required: title. Optional: description, tag, depends_on (array of PRD refs this PRD depends on, e.g., ['FLUX-P1', 'FLUX-P2']). Returns the created PRD with {id, ref, title, description, status, tag, dependencies}. Status starts as DRAFT.",
29
38
  inputSchema,
30
39
  handler,
31
40
  };
@@ -10,6 +10,7 @@ const inputSchema = z.object({
10
10
  description: z.string().optional(),
11
11
  priority: z.enum(["LOW", "MEDIUM", "HIGH"]).optional().default("MEDIUM"),
12
12
  acceptance_criteria: z.array(z.string()).optional(),
13
+ depends_on: z.array(z.string()).optional(),
13
14
  });
14
15
 
15
16
  async function handler(input: unknown) {
@@ -26,13 +27,21 @@ async function handler(input: unknown) {
26
27
 
27
28
  const criteriaCount = parsed.acceptance_criteria?.length || 0;
28
29
 
29
- return { ...toMcpTask(task), criteria_count: criteriaCount };
30
+ const dependencies: string[] = [];
31
+ if (parsed.depends_on && parsed.depends_on.length > 0) {
32
+ for (const depRef of parsed.depends_on) {
33
+ await adapter.addDependency(task.ref, depRef);
34
+ dependencies.push(depRef);
35
+ }
36
+ }
37
+
38
+ return { ...toMcpTask(task), criteria_count: criteriaCount, dependencies };
30
39
  }
31
40
 
32
41
  export const createTaskTool: ToolDefinition = {
33
42
  name: "create_task",
34
43
  description:
35
- "Create a new Task under an Epic. Required: epic_ref (e.g., 'FLUX-E1'), title. Optional: description, priority (LOW|MEDIUM|HIGH, default MEDIUM), acceptance_criteria (string array). Returns {id, ref, title, status, priority, criteria_count}.",
44
+ "Create a new Task under an Epic. Required: epic_ref (e.g., 'FLUX-E1'), title. Optional: description, priority (LOW|MEDIUM|HIGH, default MEDIUM), acceptance_criteria (string array), depends_on (array of Task refs this Task depends on, e.g., ['FLUX-T1']). Returns {id, ref, title, status, priority, criteria_count, dependencies}.",
36
45
  inputSchema,
37
46
  handler,
38
47
  };
@@ -41,7 +41,7 @@ async function removeDependencyHandler(input: unknown) {
41
41
  export const addDependencyTool: ToolDefinition = {
42
42
  name: "add_dependency",
43
43
  description:
44
- "Add a dependency between two entities of the same type. Required: ref (depends on depends_on_ref), depends_on_ref. Both must be Epics (FLUX-E*) or both Tasks (FLUX-T*). Validates no circular dependencies. Returns {success, ref, depends_on}.",
44
+ "Add a dependency between two entities of the same type. Required: ref (depends on depends_on_ref), depends_on_ref. Both must be PRDs (FLUX-P*), Epics (FLUX-E*), or Tasks (FLUX-T*). Validates no circular dependencies. Returns {success, ref, depends_on}.",
45
45
  inputSchema: addDependencySchema,
46
46
  handler: addDependencyHandler,
47
47
  };
@@ -49,7 +49,7 @@ export const addDependencyTool: ToolDefinition = {
49
49
  export const removeDependencyTool: ToolDefinition = {
50
50
  name: "remove_dependency",
51
51
  description:
52
- "Remove a dependency between two entities. Required: ref, depends_on_ref. Both must be same type (Epic or Task). Returns {success, ref, removed_dependency}.",
52
+ "Remove a dependency between two entities. Required: ref, depends_on_ref. Both must be same type (PRD, Epic, or Task). Returns {success, ref, removed_dependency}.",
53
53
  inputSchema: removeDependencySchema,
54
54
  handler: removeDependencyHandler,
55
55
  };
@@ -85,6 +85,10 @@ async function handler(input: unknown) {
85
85
  // Add available status transitions
86
86
  result.available_transitions = getValidTransitions(entityType, prd.status);
87
87
 
88
+ // Always include dependencies by default
89
+ const deps = await adapter.getDependencies(parsed.ref);
90
+ result.dependencies = deps;
91
+
88
92
  // Process includes
89
93
  for (const inc of includes) {
90
94
  if (!validIncludes.includes(inc)) continue;
@@ -166,6 +170,10 @@ async function handler(input: unknown) {
166
170
  result = { ...toMcpEpic(epic) };
167
171
  result.available_transitions = getValidTransitions(entityType, epic.status);
168
172
 
173
+ // Always include dependencies by default
174
+ const deps = await adapter.getDependencies(parsed.ref);
175
+ result.dependencies = deps;
176
+
169
177
  for (const inc of includes) {
170
178
  if (!validIncludes.includes(inc)) continue;
171
179
 
@@ -192,11 +200,6 @@ async function handler(input: unknown) {
192
200
  result.criteria_count = criteria.length;
193
201
  result.criteria_met = criteria.filter((c) => c.isMet).length;
194
202
  }
195
-
196
- if (inc === "dependencies") {
197
- const deps = await adapter.getDependencies(parsed.ref);
198
- result.dependencies = deps;
199
- }
200
203
  }
201
204
  } else {
202
205
  const task = await adapter.getTask(parsed.ref);
@@ -205,6 +208,10 @@ async function handler(input: unknown) {
205
208
  result = { ...toMcpTask(task) };
206
209
  result.available_transitions = getValidTransitions(entityType, task.status);
207
210
 
211
+ // Always include dependencies by default
212
+ const deps = await adapter.getDependencies(parsed.ref);
213
+ result.dependencies = deps;
214
+
208
215
  for (const inc of includes) {
209
216
  if (!validIncludes.includes(inc)) continue;
210
217
 
@@ -218,11 +225,6 @@ async function handler(input: unknown) {
218
225
  result.criteria_count = criteria.length;
219
226
  result.criteria_met = criteria.filter((c) => c.isMet).length;
220
227
  }
221
-
222
- if (inc === "dependencies") {
223
- const deps = await adapter.getDependencies(parsed.ref);
224
- result.dependencies = deps;
225
- }
226
228
  }
227
229
  }
228
230
 
@@ -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 message = `No Flux project found. Run init_project first or ensure you're in a directory with a .flux folder (or a subdirectory of one). Current directory: ${process.cwd()}, resolved project root: ${config.projectRoot}`;
64
- logger.error(message);
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(createError(message, "PROJECT_NOT_FOUND")),
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. Run get_project_context first to check.",
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 adapter.listEpics(
114
- { prdRef: prd.ref },
115
- { limit: 100 },
116
- );
118
+ const [prdDeps, epicsResult] = await Promise.all([
119
+ adapter.getDependencies(prd.ref),
120
+ adapter.listEpics({ prdRef: prd.ref }, { limit: 100 }),
121
+ ]);
117
122
 
118
123
  const epicsWithCounts: EpicForSummary[] = await Promise.all(
119
124
  epicsResult.items.map(async (epic) => {
120
- const tasksResult = await adapter.listTasks(
121
- { epicRef: epic.ref },
122
- { limit: 100 },
123
- );
125
+ const [epicDeps, tasksResult] = await Promise.all([
126
+ adapter.getDependencies(epic.ref),
127
+ adapter.listTasks({ epicRef: epic.ref }, { limit: 100 }),
128
+ ]);
124
129
  const completedCount = tasksResult.items.filter(
125
130
  (t) => t.status === "COMPLETED",
126
131
  ).length;
@@ -131,6 +136,7 @@ async function getSummaryData(adapter: Adapter): Promise<PrdForSummary[]> {
131
136
  status: epic.status,
132
137
  task_count: tasksResult.total,
133
138
  tasks_completed: completedCount,
139
+ dependencies: epicDeps,
134
140
  };
135
141
  }),
136
142
  );
@@ -140,6 +146,7 @@ async function getSummaryData(adapter: Adapter): Promise<PrdForSummary[]> {
140
146
  title: prd.title,
141
147
  status: prd.status,
142
148
  epics: epicsWithCounts,
149
+ dependencies: prdDeps,
143
150
  });
144
151
  }
145
152
 
@@ -156,27 +163,37 @@ async function getFullTreeData(adapter: Adapter): Promise<PrdForFull[]> {
156
163
  const result: PrdForFull[] = [];
157
164
 
158
165
  for (const prd of allPrds) {
159
- const epicsResult = await adapter.listEpics(
160
- { prdRef: prd.ref },
161
- { limit: 100 },
162
- );
166
+ const [prdDeps, epicsResult] = await Promise.all([
167
+ adapter.getDependencies(prd.ref),
168
+ adapter.listEpics({ prdRef: prd.ref }, { limit: 100 }),
169
+ ]);
163
170
 
164
171
  const epicsWithTasks: EpicForFull[] = await Promise.all(
165
172
  epicsResult.items.map(async (epic) => {
166
- const tasksResult = await adapter.listTasks(
167
- { epicRef: epic.ref },
168
- { limit: 100 },
173
+ const [epicDeps, tasksResult] = await Promise.all([
174
+ adapter.getDependencies(epic.ref),
175
+ adapter.listTasks({ epicRef: epic.ref }, { limit: 100 }),
176
+ ]);
177
+
178
+ // Get dependencies for each task
179
+ const tasksWithDeps: TaskForFull[] = await Promise.all(
180
+ tasksResult.items.map(async (t) => {
181
+ const taskDeps = await adapter.getDependencies(t.ref);
182
+ return {
183
+ ref: t.ref,
184
+ title: t.title,
185
+ status: t.status,
186
+ dependencies: taskDeps,
187
+ };
188
+ }),
169
189
  );
170
190
 
171
191
  return {
172
192
  ref: epic.ref,
173
193
  title: epic.title,
174
194
  status: epic.status,
175
- tasks: tasksResult.items.map((t) => ({
176
- ref: t.ref,
177
- title: t.title,
178
- status: t.status,
179
- })),
195
+ tasks: tasksWithDeps,
196
+ dependencies: epicDeps,
180
197
  };
181
198
  }),
182
199
  );
@@ -186,6 +203,7 @@ async function getFullTreeData(adapter: Adapter): Promise<PrdForFull[]> {
186
203
  title: prd.title,
187
204
  status: prd.status,
188
205
  epics: epicsWithTasks,
206
+ dependencies: prdDeps,
189
207
  });
190
208
  }
191
209
 
@@ -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
- lines.push(`${prd.ref} ${prd.title} ${badge}`);
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
- lines.push(` ${epicIcon} ${epic.ref} ${epic.title}`);
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