@cliangdev/flux-plugin 0.2.0 → 0.3.0

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.
Files changed (108) hide show
  1. package/README.md +11 -7
  2. package/agents/coder.md +150 -25
  3. package/bin/install.cjs +171 -16
  4. package/commands/breakdown.md +47 -10
  5. package/commands/dashboard.md +29 -0
  6. package/commands/flux.md +92 -12
  7. package/commands/implement.md +166 -17
  8. package/commands/linear.md +6 -5
  9. package/commands/prd.md +996 -82
  10. package/manifest.json +2 -1
  11. package/package.json +9 -11
  12. package/skills/flux-orchestrator/SKILL.md +11 -3
  13. package/skills/prd-writer/SKILL.md +761 -0
  14. package/skills/ux-ui-design/SKILL.md +346 -0
  15. package/skills/ux-ui-design/references/design-tokens.md +359 -0
  16. package/src/__tests__/version.test.ts +37 -0
  17. package/src/adapters/local/.gitkeep +0 -0
  18. package/src/dashboard/__tests__/api.test.ts +211 -0
  19. package/src/dashboard/browser.ts +35 -0
  20. package/src/dashboard/public/app.js +869 -0
  21. package/src/dashboard/public/index.html +90 -0
  22. package/src/dashboard/public/styles.css +807 -0
  23. package/src/dashboard/public/vendor/highlight.css +10 -0
  24. package/src/dashboard/public/vendor/highlight.min.js +8422 -0
  25. package/src/dashboard/public/vendor/marked.min.js +2210 -0
  26. package/src/dashboard/server.ts +296 -0
  27. package/src/dashboard/watchers.ts +83 -0
  28. package/src/server/__tests__/config.test.ts +163 -0
  29. package/src/server/adapters/__tests__/a-client-linear.test.ts +197 -0
  30. package/src/server/adapters/__tests__/adapter-factory.test.ts +230 -0
  31. package/src/server/adapters/__tests__/dependency-ops.test.ts +429 -0
  32. package/src/server/adapters/__tests__/document-ops.test.ts +306 -0
  33. package/src/server/adapters/__tests__/linear-adapter.test.ts +91 -0
  34. package/src/server/adapters/__tests__/linear-config.test.ts +425 -0
  35. package/src/server/adapters/__tests__/linear-criteria-parser.test.ts +287 -0
  36. package/src/server/adapters/__tests__/linear-description-test.ts +238 -0
  37. package/src/server/adapters/__tests__/linear-epic-crud.test.ts +496 -0
  38. package/src/server/adapters/__tests__/linear-mappers-description.test.ts +276 -0
  39. package/src/server/adapters/__tests__/linear-mappers-epic.test.ts +294 -0
  40. package/src/server/adapters/__tests__/linear-mappers-prd.test.ts +300 -0
  41. package/src/server/adapters/__tests__/linear-mappers-task.test.ts +197 -0
  42. package/src/server/adapters/__tests__/linear-prd-crud.test.ts +620 -0
  43. package/src/server/adapters/__tests__/linear-stats.test.ts +450 -0
  44. package/src/server/adapters/__tests__/linear-task-crud.test.ts +534 -0
  45. package/src/server/adapters/__tests__/linear-types.test.ts +243 -0
  46. package/src/server/adapters/__tests__/status-ops.test.ts +441 -0
  47. package/src/server/adapters/factory.ts +90 -0
  48. package/src/server/adapters/index.ts +9 -0
  49. package/src/server/adapters/linear/adapter.ts +1141 -0
  50. package/src/server/adapters/linear/client.ts +169 -0
  51. package/src/server/adapters/linear/config.ts +152 -0
  52. package/src/server/adapters/linear/helpers/criteria-parser.ts +197 -0
  53. package/src/server/adapters/linear/helpers/index.ts +7 -0
  54. package/src/server/adapters/linear/index.ts +16 -0
  55. package/src/server/adapters/linear/mappers/description.ts +136 -0
  56. package/src/server/adapters/linear/mappers/epic.ts +81 -0
  57. package/src/server/adapters/linear/mappers/index.ts +27 -0
  58. package/src/server/adapters/linear/mappers/prd.ts +178 -0
  59. package/src/server/adapters/linear/mappers/task.ts +82 -0
  60. package/src/server/adapters/linear/types.ts +264 -0
  61. package/src/server/adapters/local-adapter.ts +1009 -0
  62. package/src/server/adapters/types.ts +293 -0
  63. package/src/server/config.ts +73 -0
  64. package/src/server/db/__tests__/queries.test.ts +473 -0
  65. package/src/server/db/ids.ts +17 -0
  66. package/src/server/db/index.ts +69 -0
  67. package/src/server/db/queries.ts +142 -0
  68. package/src/server/db/refs.ts +60 -0
  69. package/src/server/db/schema.ts +97 -0
  70. package/src/server/db/sqlite.ts +10 -0
  71. package/src/server/index.ts +81 -0
  72. package/src/server/tools/__tests__/crud.test.ts +411 -0
  73. package/src/server/tools/__tests__/get-version.test.ts +27 -0
  74. package/src/server/tools/__tests__/mcp-interface.test.ts +479 -0
  75. package/src/server/tools/__tests__/query.test.ts +405 -0
  76. package/src/server/tools/__tests__/z-configure-linear.test.ts +511 -0
  77. package/src/server/tools/__tests__/z-get-linear-url.test.ts +108 -0
  78. package/src/server/tools/configure-linear.ts +373 -0
  79. package/src/server/tools/create-epic.ts +44 -0
  80. package/src/server/tools/create-prd.ts +40 -0
  81. package/src/server/tools/create-task.ts +47 -0
  82. package/src/server/tools/criteria.ts +50 -0
  83. package/src/server/tools/delete-entity.ts +76 -0
  84. package/src/server/tools/dependencies.ts +55 -0
  85. package/src/server/tools/get-entity.ts +240 -0
  86. package/src/server/tools/get-linear-url.ts +28 -0
  87. package/src/server/tools/get-stats.ts +52 -0
  88. package/src/server/tools/get-version.ts +20 -0
  89. package/src/server/tools/index.ts +158 -0
  90. package/src/server/tools/init-project.ts +108 -0
  91. package/src/server/tools/query-entities.ts +167 -0
  92. package/src/server/tools/render-status.ts +219 -0
  93. package/src/server/tools/update-entity.ts +140 -0
  94. package/src/server/tools/update-status.ts +166 -0
  95. package/src/server/utils/__tests__/mcp-response.test.ts +331 -0
  96. package/src/server/utils/logger.ts +9 -0
  97. package/src/server/utils/mcp-response.ts +254 -0
  98. package/src/server/utils/status-transitions.ts +160 -0
  99. package/src/status-line/__tests__/status-line.test.ts +215 -0
  100. package/src/status-line/index.ts +147 -0
  101. package/src/utils/__tests__/chalk-import.test.ts +32 -0
  102. package/src/utils/__tests__/display.test.ts +97 -0
  103. package/src/utils/__tests__/status-renderer.test.ts +310 -0
  104. package/src/utils/display.ts +62 -0
  105. package/src/utils/status-renderer.ts +214 -0
  106. package/src/version.ts +5 -0
  107. package/dist/server/index.js +0 -87063
  108. package/skills/prd-template/SKILL.md +0 -242
@@ -0,0 +1,167 @@
1
+ import { z } from "zod";
2
+ import { getAdapter } from "../adapters/index.js";
3
+ import type { ToolDefinition } from "./index.js";
4
+
5
+ const inputSchema = z.object({
6
+ type: z.enum(["prd", "epic", "task"]),
7
+ status: z.string().optional(),
8
+ tag: z.string().optional(),
9
+ prd_ref: z.string().optional(),
10
+ epic_ref: z.string().optional(),
11
+ include: z.array(z.string()).optional(),
12
+ limit: z.number().min(1).max(100).optional().default(50),
13
+ offset: z.number().min(0).optional().default(0),
14
+ });
15
+
16
+ async function handler(input: unknown) {
17
+ const parsed = inputSchema.parse(input);
18
+ const adapter = getAdapter();
19
+ const includes = parsed.include || [];
20
+
21
+ if (parsed.type === "prd") {
22
+ const result = await adapter.listPrds(
23
+ {
24
+ status: parsed.status as
25
+ | "DRAFT"
26
+ | "PENDING_REVIEW"
27
+ | "REVIEWED"
28
+ | "APPROVED"
29
+ | "BREAKDOWN_READY"
30
+ | "COMPLETED"
31
+ | "ARCHIVED"
32
+ | undefined,
33
+ tag: parsed.tag,
34
+ },
35
+ { limit: parsed.limit, offset: parsed.offset },
36
+ );
37
+
38
+ const items = await Promise.all(
39
+ result.items.map(async (prd) => {
40
+ const minimal: Record<string, unknown> = {
41
+ ref: prd.ref,
42
+ title: prd.title,
43
+ description: prd.description,
44
+ status: prd.status,
45
+ tag: prd.tag,
46
+ folder_path: prd.folderPath,
47
+ };
48
+
49
+ if (includes.includes("epics")) {
50
+ const epics = await adapter.listEpics(
51
+ { prdRef: prd.ref },
52
+ { limit: 100 },
53
+ );
54
+ minimal.epic_count = epics.total;
55
+ }
56
+
57
+ return minimal;
58
+ }),
59
+ );
60
+
61
+ return {
62
+ items,
63
+ total: result.total,
64
+ limit: result.limit,
65
+ offset: result.offset,
66
+ has_more: result.hasMore,
67
+ };
68
+ }
69
+
70
+ if (parsed.type === "epic") {
71
+ const result = await adapter.listEpics(
72
+ {
73
+ status: parsed.status as
74
+ | "PENDING"
75
+ | "IN_PROGRESS"
76
+ | "COMPLETED"
77
+ | undefined,
78
+ prdRef: parsed.prd_ref,
79
+ },
80
+ { limit: parsed.limit, offset: parsed.offset },
81
+ );
82
+
83
+ const items = await Promise.all(
84
+ result.items.map(async (epic) => {
85
+ const minimal: Record<string, unknown> = {
86
+ ref: epic.ref,
87
+ title: epic.title,
88
+ description: epic.description,
89
+ status: epic.status,
90
+ };
91
+
92
+ if (includes.includes("criteria")) {
93
+ const criteria = await adapter.getCriteria(epic.ref);
94
+ minimal.criteria_count = criteria.length;
95
+ minimal.criteria_met = criteria.filter((c) => c.isMet).length;
96
+ }
97
+
98
+ if (includes.includes("tasks")) {
99
+ const tasks = await adapter.listTasks(
100
+ { epicRef: epic.ref },
101
+ { limit: 100 },
102
+ );
103
+ minimal.task_count = tasks.total;
104
+ }
105
+
106
+ return minimal;
107
+ }),
108
+ );
109
+
110
+ return {
111
+ items,
112
+ total: result.total,
113
+ limit: result.limit,
114
+ offset: result.offset,
115
+ has_more: result.hasMore,
116
+ };
117
+ }
118
+
119
+ // task type
120
+ const result = await adapter.listTasks(
121
+ {
122
+ status: parsed.status as
123
+ | "PENDING"
124
+ | "IN_PROGRESS"
125
+ | "COMPLETED"
126
+ | undefined,
127
+ epicRef: parsed.epic_ref,
128
+ },
129
+ { limit: parsed.limit, offset: parsed.offset },
130
+ );
131
+
132
+ const items = await Promise.all(
133
+ result.items.map(async (task) => {
134
+ const minimal: Record<string, unknown> = {
135
+ ref: task.ref,
136
+ title: task.title,
137
+ description: task.description,
138
+ status: task.status,
139
+ priority: task.priority,
140
+ };
141
+
142
+ if (includes.includes("criteria")) {
143
+ const criteria = await adapter.getCriteria(task.ref);
144
+ minimal.criteria_count = criteria.length;
145
+ minimal.criteria_met = criteria.filter((c) => c.isMet).length;
146
+ }
147
+
148
+ return minimal;
149
+ }),
150
+ );
151
+
152
+ return {
153
+ items,
154
+ total: result.total,
155
+ limit: result.limit,
156
+ offset: result.offset,
157
+ has_more: result.hasMore,
158
+ };
159
+ }
160
+
161
+ export const queryEntitiesTool: ToolDefinition = {
162
+ name: "query_entities",
163
+ description:
164
+ "Query entities with filters and pagination. Required: type (prd|epic|task). Optional: status, tag (PRD only), prd_ref (Epic filter), epic_ref (Task filter), include (['epics','tasks','criteria'] for counts only), limit (1-100, default 50), offset (default 0). Returns minimal data: {items: [{ref, title, description, status, ...counts}], total, limit, offset, has_more}. Description provides context to decide if full read is needed. Use get_entity for more details, Read tool for full PRD content.",
165
+ inputSchema,
166
+ handler,
167
+ };
@@ -0,0 +1,219 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { z } from "zod";
3
+ import {
4
+ renderFullTreeView,
5
+ renderSummaryView,
6
+ } from "../../utils/status-renderer.js";
7
+ import { VERSION } from "../../version.js";
8
+ import { getAdapter } from "../adapters/index.js";
9
+ import { config } from "../config.js";
10
+ import { computeStats } from "./get-stats.js";
11
+ import type { ToolDefinition } from "./index.js";
12
+
13
+ const inputSchema = z.object({
14
+ view: z
15
+ .enum(["summary", "full"])
16
+ .default("summary")
17
+ .describe("View type: 'summary' (default) or 'full' tree view"),
18
+ });
19
+
20
+ interface EpicForSummary {
21
+ ref: string;
22
+ title: string;
23
+ status: string;
24
+ task_count: number;
25
+ tasks_completed: number;
26
+ dependencies: string[];
27
+ }
28
+
29
+ interface PrdForSummary {
30
+ ref: string;
31
+ title: string;
32
+ status: string;
33
+ epics: EpicForSummary[];
34
+ dependencies: string[];
35
+ }
36
+
37
+ interface TaskForFull {
38
+ ref: string;
39
+ title: string;
40
+ status: string;
41
+ dependencies: string[];
42
+ }
43
+
44
+ interface EpicForFull {
45
+ ref: string;
46
+ title: string;
47
+ status: string;
48
+ tasks: TaskForFull[];
49
+ dependencies: string[];
50
+ }
51
+
52
+ interface PrdForFull {
53
+ ref: string;
54
+ title: string;
55
+ status: string;
56
+ epics: EpicForFull[];
57
+ dependencies: string[];
58
+ }
59
+
60
+ function getProjectInfo(): { name: string; ref_prefix: string } | null {
61
+ if (!existsSync(config.projectJsonPath)) {
62
+ return null;
63
+ }
64
+ try {
65
+ const content = readFileSync(config.projectJsonPath, "utf-8");
66
+ const project = JSON.parse(content);
67
+ return {
68
+ name: project.name || "Unnamed Project",
69
+ ref_prefix: project.ref_prefix || "FLUX",
70
+ };
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+
76
+ async function handler(input: unknown) {
77
+ const parsed = inputSchema.parse(input);
78
+ const adapter = getAdapter();
79
+
80
+ const project = getProjectInfo();
81
+ if (!project) {
82
+ return { error: true, message: "No project found", code: "NO_PROJECT" };
83
+ }
84
+
85
+ const stats = await computeStats();
86
+
87
+ if (parsed.view === "full") {
88
+ const prds = await getFullTreeData(adapter);
89
+ return { output: renderFullTreeView(prds) };
90
+ }
91
+
92
+ const prds = await getSummaryData(adapter);
93
+ const output = renderSummaryView(
94
+ {
95
+ name: project.name,
96
+ ref_prefix: project.ref_prefix,
97
+ },
98
+ stats,
99
+ prds,
100
+ VERSION,
101
+ );
102
+
103
+ return { output };
104
+ }
105
+
106
+ type Adapter = ReturnType<typeof getAdapter>;
107
+
108
+ async function getSummaryData(adapter: Adapter): Promise<PrdForSummary[]> {
109
+ // Get all PRDs except archived
110
+ const prdsResult = await adapter.listPrds({}, { limit: 100 });
111
+ const nonArchivedPrds = prdsResult.items.filter(
112
+ (prd) => prd.status !== "ARCHIVED",
113
+ );
114
+
115
+ const result: PrdForSummary[] = [];
116
+
117
+ for (const prd of nonArchivedPrds) {
118
+ const [prdDeps, epicsResult] = await Promise.all([
119
+ adapter.getDependencies(prd.ref),
120
+ adapter.listEpics({ prdRef: prd.ref }, { limit: 100 }),
121
+ ]);
122
+
123
+ const epicsWithCounts: EpicForSummary[] = await Promise.all(
124
+ epicsResult.items.map(async (epic) => {
125
+ const [epicDeps, tasksResult] = await Promise.all([
126
+ adapter.getDependencies(epic.ref),
127
+ adapter.listTasks({ epicRef: epic.ref }, { limit: 100 }),
128
+ ]);
129
+ const completedCount = tasksResult.items.filter(
130
+ (t) => t.status === "COMPLETED",
131
+ ).length;
132
+
133
+ return {
134
+ ref: epic.ref,
135
+ title: epic.title,
136
+ status: epic.status,
137
+ task_count: tasksResult.total,
138
+ tasks_completed: completedCount,
139
+ dependencies: epicDeps,
140
+ };
141
+ }),
142
+ );
143
+
144
+ result.push({
145
+ ref: prd.ref,
146
+ title: prd.title,
147
+ status: prd.status,
148
+ epics: epicsWithCounts,
149
+ dependencies: prdDeps,
150
+ });
151
+ }
152
+
153
+ return result;
154
+ }
155
+
156
+ async function getFullTreeData(adapter: Adapter): Promise<PrdForFull[]> {
157
+ // Get all PRDs except archived
158
+ const prdsResult = await adapter.listPrds({}, { limit: 100 });
159
+ const allPrds = prdsResult.items
160
+ .filter((prd) => prd.status !== "ARCHIVED")
161
+ .sort((a, b) => a.ref.localeCompare(b.ref));
162
+
163
+ const result: PrdForFull[] = [];
164
+
165
+ for (const prd of allPrds) {
166
+ const [prdDeps, epicsResult] = await Promise.all([
167
+ adapter.getDependencies(prd.ref),
168
+ adapter.listEpics({ prdRef: prd.ref }, { limit: 100 }),
169
+ ]);
170
+
171
+ const epicsWithTasks: EpicForFull[] = await Promise.all(
172
+ epicsResult.items.map(async (epic) => {
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
+ }),
189
+ );
190
+
191
+ return {
192
+ ref: epic.ref,
193
+ title: epic.title,
194
+ status: epic.status,
195
+ tasks: tasksWithDeps,
196
+ dependencies: epicDeps,
197
+ };
198
+ }),
199
+ );
200
+
201
+ result.push({
202
+ ref: prd.ref,
203
+ title: prd.title,
204
+ status: prd.status,
205
+ epics: epicsWithTasks,
206
+ dependencies: prdDeps,
207
+ });
208
+ }
209
+
210
+ return result;
211
+ }
212
+
213
+ export const renderStatusTool: ToolDefinition = {
214
+ name: "render_status",
215
+ description:
216
+ "Render project status with ANSI colors. Optional: view ('summary' or 'full', default 'summary'). Returns {output: string} with pre-formatted terminal output including progress bars, status icons, and colors. Display the output directly to user.",
217
+ inputSchema,
218
+ handler,
219
+ };
@@ -0,0 +1,140 @@
1
+ import { z } from "zod";
2
+ import { getAdapter } from "../adapters/index.js";
3
+ import type {
4
+ EpicStatus,
5
+ PrdStatus,
6
+ Priority,
7
+ TaskStatus,
8
+ } from "../adapters/types.js";
9
+ import { toMcpEpic, toMcpPrd, toMcpTask } from "../utils/mcp-response.js";
10
+ import type { ToolDefinition } from "./index.js";
11
+
12
+ const inputSchema = z.object({
13
+ ref: z.string().min(1, "Entity reference is required"),
14
+ fields: z.record(z.string(), z.unknown()),
15
+ });
16
+
17
+ // Allowed fields per entity type (snake_case as received from MCP)
18
+ const ALLOWED_FIELDS: Record<string, string[]> = {
19
+ P: ["title", "description", "status", "tag", "folder_path"],
20
+ E: ["title", "description", "status"],
21
+ T: ["title", "description", "status", "priority"],
22
+ };
23
+
24
+ function getEntityType(ref: string): "P" | "E" | "T" | null {
25
+ // Try Flux-style ref format first (e.g., FLUX-P1, FLUX-E2, FLUX-T3)
26
+ const match = ref.match(/-([PET])\d+$/);
27
+ if (match) {
28
+ return match[1] as "P" | "E" | "T";
29
+ }
30
+ // For other ref formats (e.g., Linear: CAL-10), return null
31
+ return null;
32
+ }
33
+
34
+ async function handler(input: unknown) {
35
+ const parsed = inputSchema.parse(input);
36
+ const adapter = getAdapter();
37
+
38
+ let entityType = getEntityType(parsed.ref);
39
+
40
+ // If ref format is unknown (e.g., Linear refs), try each entity type
41
+ if (!entityType) {
42
+ const prd = await adapter.getPrd(parsed.ref);
43
+ if (prd) {
44
+ entityType = "P";
45
+ } else {
46
+ const epic = await adapter.getEpic(parsed.ref);
47
+ if (epic) {
48
+ entityType = "E";
49
+ } else {
50
+ const task = await adapter.getTask(parsed.ref);
51
+ if (task) {
52
+ entityType = "T";
53
+ } else {
54
+ throw new Error(`Entity not found: ${parsed.ref}`);
55
+ }
56
+ }
57
+ }
58
+ }
59
+
60
+ const allowedFields = ALLOWED_FIELDS[entityType];
61
+
62
+ // Filter to allowed fields only
63
+ const validFields: Record<string, unknown> = {};
64
+ for (const [key, value] of Object.entries(parsed.fields)) {
65
+ if (allowedFields.includes(key)) {
66
+ validFields[key] = value;
67
+ }
68
+ }
69
+
70
+ if (Object.keys(validFields).length === 0) {
71
+ throw new Error(
72
+ `No valid fields to update. Allowed: ${allowedFields.join(", ")}`,
73
+ );
74
+ }
75
+
76
+ // Convert to adapter format and call appropriate update method
77
+ if (entityType === "P") {
78
+ const updateInput: {
79
+ title?: string;
80
+ description?: string;
81
+ status?: PrdStatus;
82
+ tag?: string;
83
+ folderPath?: string;
84
+ } = {};
85
+ if (validFields.title !== undefined)
86
+ updateInput.title = validFields.title as string;
87
+ if (validFields.description !== undefined)
88
+ updateInput.description = validFields.description as string;
89
+ if (validFields.status !== undefined)
90
+ updateInput.status = validFields.status as PrdStatus;
91
+ if (validFields.tag !== undefined)
92
+ updateInput.tag = validFields.tag as string;
93
+ if (validFields.folder_path !== undefined)
94
+ updateInput.folderPath = validFields.folder_path as string;
95
+
96
+ const prd = await adapter.updatePrd(parsed.ref, updateInput);
97
+ return toMcpPrd(prd);
98
+ } else if (entityType === "E") {
99
+ const updateInput: {
100
+ title?: string;
101
+ description?: string;
102
+ status?: EpicStatus;
103
+ } = {};
104
+ if (validFields.title !== undefined)
105
+ updateInput.title = validFields.title as string;
106
+ if (validFields.description !== undefined)
107
+ updateInput.description = validFields.description as string;
108
+ if (validFields.status !== undefined)
109
+ updateInput.status = validFields.status as EpicStatus;
110
+
111
+ const epic = await adapter.updateEpic(parsed.ref, updateInput);
112
+ return toMcpEpic(epic);
113
+ } else {
114
+ const updateInput: {
115
+ title?: string;
116
+ description?: string;
117
+ status?: TaskStatus;
118
+ priority?: Priority;
119
+ } = {};
120
+ if (validFields.title !== undefined)
121
+ updateInput.title = validFields.title as string;
122
+ if (validFields.description !== undefined)
123
+ updateInput.description = validFields.description as string;
124
+ if (validFields.status !== undefined)
125
+ updateInput.status = validFields.status as TaskStatus;
126
+ if (validFields.priority !== undefined)
127
+ updateInput.priority = validFields.priority as Priority;
128
+
129
+ const task = await adapter.updateTask(parsed.ref, updateInput);
130
+ return toMcpTask(task);
131
+ }
132
+ }
133
+
134
+ export const updateEntityTool: ToolDefinition = {
135
+ name: "update_entity",
136
+ description:
137
+ "Update fields on any entity by reference. Required: ref (e.g., 'FLUX-P1'), fields object. Allowed fields - PRD: title, description, status, tag, folder_path. Epic: title, description, status. Task: title, description, status, priority. Returns updated entity.",
138
+ inputSchema,
139
+ handler,
140
+ };
@@ -0,0 +1,166 @@
1
+ import { z } from "zod";
2
+ import { getAdapter } from "../adapters/index.js";
3
+ import type { EpicStatus, PrdStatus, TaskStatus } from "../adapters/types.js";
4
+ import { toMcpEpic, toMcpPrd, toMcpTask } from "../utils/mcp-response.js";
5
+ import {
6
+ type EntityType,
7
+ getTransitionErrorMessage,
8
+ getValidTransitions,
9
+ isValidTransition,
10
+ } from "../utils/status-transitions.js";
11
+ import type { ToolDefinition } from "./index.js";
12
+
13
+ const PRD_STATUSES = [
14
+ "DRAFT",
15
+ "PENDING_REVIEW",
16
+ "REVIEWED",
17
+ "APPROVED",
18
+ "BREAKDOWN_READY",
19
+ "COMPLETED",
20
+ "ARCHIVED",
21
+ ] as const;
22
+ const EPIC_STATUSES = ["PENDING", "IN_PROGRESS", "COMPLETED"] as const;
23
+ const TASK_STATUSES = ["PENDING", "IN_PROGRESS", "COMPLETED"] as const;
24
+
25
+ const inputSchema = z.object({
26
+ ref: z.string().min(1, "Entity reference is required"),
27
+ status: z.string().min(1, "Status is required"),
28
+ });
29
+
30
+ function getEntityTypeFromRef(
31
+ ref: string,
32
+ ): { type: EntityType; code: string } | null {
33
+ // Try Flux-style ref format first (e.g., FLUX-P1, FLUX-E2, FLUX-T3)
34
+ const match = ref.match(/-([PET])\d+$/);
35
+ if (match) {
36
+ const code = match[1];
37
+ const type: EntityType =
38
+ code === "P" ? "prd" : code === "E" ? "epic" : "task";
39
+ return { type, code };
40
+ }
41
+ // For other ref formats (e.g., Linear: CAL-10), return null
42
+ return null;
43
+ }
44
+
45
+ function getValidStatuses(code: string): readonly string[] {
46
+ switch (code) {
47
+ case "P":
48
+ return PRD_STATUSES;
49
+ case "E":
50
+ return EPIC_STATUSES;
51
+ case "T":
52
+ return TASK_STATUSES;
53
+ default:
54
+ throw new Error(`Unknown entity type: ${code}`);
55
+ }
56
+ }
57
+
58
+ async function handler(input: unknown) {
59
+ const parsed = inputSchema.parse(input);
60
+ const adapter = getAdapter();
61
+
62
+ const typeInfo = getEntityTypeFromRef(parsed.ref);
63
+ let entityType: EntityType;
64
+ let code: string;
65
+ let currentStatus = "";
66
+
67
+ // If ref format is unknown (e.g., Linear refs), try each entity type
68
+ if (typeInfo) {
69
+ entityType = typeInfo.type;
70
+ code = typeInfo.code;
71
+ } else {
72
+ // Try PRD first, then Epic, then Task
73
+ const prd = await adapter.getPrd(parsed.ref);
74
+ if (prd) {
75
+ entityType = "prd";
76
+ code = "P";
77
+ currentStatus = prd.status;
78
+ } else {
79
+ const epic = await adapter.getEpic(parsed.ref);
80
+ if (epic) {
81
+ entityType = "epic";
82
+ code = "E";
83
+ currentStatus = epic.status;
84
+ } else {
85
+ const task = await adapter.getTask(parsed.ref);
86
+ if (task) {
87
+ entityType = "task";
88
+ code = "T";
89
+ currentStatus = task.status;
90
+ } else {
91
+ throw new Error(`Entity not found: ${parsed.ref}`);
92
+ }
93
+ }
94
+ }
95
+ }
96
+
97
+ const validStatuses = getValidStatuses(code);
98
+
99
+ // Validate status value
100
+ if (!validStatuses.includes(parsed.status)) {
101
+ throw new Error(
102
+ `Invalid status '${parsed.status}' for ${entityType}. Valid: ${validStatuses.join(", ")}`,
103
+ );
104
+ }
105
+
106
+ // Get current entity status if not already set (for Flux-style refs)
107
+ if (typeInfo) {
108
+ if (code === "P") {
109
+ const prd = await adapter.getPrd(parsed.ref);
110
+ if (!prd) throw new Error(`Entity not found: ${parsed.ref}`);
111
+ currentStatus = prd.status;
112
+ } else if (code === "E") {
113
+ const epic = await adapter.getEpic(parsed.ref);
114
+ if (!epic) throw new Error(`Entity not found: ${parsed.ref}`);
115
+ currentStatus = epic.status;
116
+ } else {
117
+ const task = await adapter.getTask(parsed.ref);
118
+ if (!task) throw new Error(`Entity not found: ${parsed.ref}`);
119
+ currentStatus = task.status;
120
+ }
121
+ }
122
+
123
+ // Validate status transition
124
+ if (!isValidTransition(entityType, currentStatus, parsed.status)) {
125
+ throw new Error(
126
+ getTransitionErrorMessage(entityType, currentStatus, parsed.status),
127
+ );
128
+ }
129
+
130
+ // Update status using the appropriate adapter method
131
+ const availableTransitions = getValidTransitions(entityType, parsed.status);
132
+
133
+ if (code === "P") {
134
+ const prd = await adapter.updatePrd(parsed.ref, {
135
+ status: parsed.status as PrdStatus,
136
+ });
137
+ return {
138
+ ...toMcpPrd(prd),
139
+ available_transitions: availableTransitions,
140
+ };
141
+ } else if (code === "E") {
142
+ const epic = await adapter.updateEpic(parsed.ref, {
143
+ status: parsed.status as EpicStatus,
144
+ });
145
+ return {
146
+ ...toMcpEpic(epic),
147
+ available_transitions: availableTransitions,
148
+ };
149
+ } else {
150
+ const task = await adapter.updateTask(parsed.ref, {
151
+ status: parsed.status as TaskStatus,
152
+ });
153
+ return {
154
+ ...toMcpTask(task),
155
+ available_transitions: availableTransitions,
156
+ };
157
+ }
158
+ }
159
+
160
+ export const updateStatusTool: ToolDefinition = {
161
+ name: "update_status",
162
+ description:
163
+ "Update status of any entity with transition validation. Required: ref, status. Valid statuses - PRD: DRAFT|PENDING_REVIEW|REVIEWED|APPROVED|BREAKDOWN_READY|COMPLETED|ARCHIVED. Epic/Task: PENDING|IN_PROGRESS|COMPLETED. Returns updated entity with available_transitions. Validates transitions (e.g., PRD cannot skip from DRAFT to APPROVED).",
164
+ inputSchema,
165
+ handler,
166
+ };