@task-mcp/shared 1.0.29 → 1.0.30

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 (68) hide show
  1. package/dist/schemas/inbox.d.ts +2 -2
  2. package/dist/schemas/response-format.d.ts +11 -0
  3. package/dist/schemas/response-format.d.ts.map +1 -1
  4. package/dist/schemas/response-format.js.map +1 -1
  5. package/dist/schemas/session.d.ts +12 -12
  6. package/dist/schemas/state.d.ts +2 -2
  7. package/dist/schemas/task.d.ts +9 -0
  8. package/dist/schemas/task.d.ts.map +1 -1
  9. package/dist/schemas/task.js +3 -0
  10. package/dist/schemas/task.js.map +1 -1
  11. package/dist/utils/clustering.d.ts +60 -0
  12. package/dist/utils/clustering.d.ts.map +1 -0
  13. package/dist/utils/clustering.js +283 -0
  14. package/dist/utils/clustering.js.map +1 -0
  15. package/dist/utils/clustering.test.d.ts +2 -0
  16. package/dist/utils/clustering.test.d.ts.map +1 -0
  17. package/dist/utils/clustering.test.js +237 -0
  18. package/dist/utils/clustering.test.js.map +1 -0
  19. package/dist/utils/env.d.ts +24 -0
  20. package/dist/utils/env.d.ts.map +1 -0
  21. package/dist/utils/env.js +40 -0
  22. package/dist/utils/env.js.map +1 -0
  23. package/dist/utils/hierarchy.d.ts.map +1 -1
  24. package/dist/utils/hierarchy.js +7 -3
  25. package/dist/utils/hierarchy.js.map +1 -1
  26. package/dist/utils/index.d.ts +4 -1
  27. package/dist/utils/index.d.ts.map +1 -1
  28. package/dist/utils/index.js +7 -1
  29. package/dist/utils/index.js.map +1 -1
  30. package/dist/utils/intent-extractor.d.ts +30 -0
  31. package/dist/utils/intent-extractor.d.ts.map +1 -0
  32. package/dist/utils/intent-extractor.js +135 -0
  33. package/dist/utils/intent-extractor.js.map +1 -0
  34. package/dist/utils/intent-extractor.test.d.ts +2 -0
  35. package/dist/utils/intent-extractor.test.d.ts.map +1 -0
  36. package/dist/utils/intent-extractor.test.js +69 -0
  37. package/dist/utils/intent-extractor.test.js.map +1 -0
  38. package/dist/utils/natural-language.d.ts.map +1 -1
  39. package/dist/utils/natural-language.js +9 -8
  40. package/dist/utils/natural-language.js.map +1 -1
  41. package/dist/utils/natural-language.test.js +22 -0
  42. package/dist/utils/natural-language.test.js.map +1 -1
  43. package/dist/utils/plan-parser.d.ts.map +1 -1
  44. package/dist/utils/plan-parser.js +2 -8
  45. package/dist/utils/plan-parser.js.map +1 -1
  46. package/dist/utils/projection.d.ts.map +1 -1
  47. package/dist/utils/projection.js +43 -1
  48. package/dist/utils/projection.js.map +1 -1
  49. package/dist/utils/projection.test.js +57 -7
  50. package/dist/utils/projection.test.js.map +1 -1
  51. package/dist/utils/terminal-ui.test.js +13 -22
  52. package/dist/utils/terminal-ui.test.js.map +1 -1
  53. package/package.json +1 -1
  54. package/src/schemas/response-format.ts +15 -2
  55. package/src/schemas/task.ts +3 -0
  56. package/src/utils/clustering.test.ts +285 -0
  57. package/src/utils/clustering.ts +336 -0
  58. package/src/utils/env.ts +41 -0
  59. package/src/utils/hierarchy.ts +9 -5
  60. package/src/utils/index.ts +17 -0
  61. package/src/utils/intent-extractor.test.ts +84 -0
  62. package/src/utils/intent-extractor.ts +156 -0
  63. package/src/utils/natural-language.test.ts +27 -0
  64. package/src/utils/natural-language.ts +10 -9
  65. package/src/utils/plan-parser.ts +4 -16
  66. package/src/utils/projection.test.ts +61 -7
  67. package/src/utils/projection.ts +44 -1
  68. package/src/utils/terminal-ui.test.ts +13 -22
@@ -0,0 +1,336 @@
1
+ /**
2
+ * Semantic clustering utilities for task similarity
3
+ *
4
+ * Uses lightweight text similarity without external APIs.
5
+ * Designed for session resumption to suggest related tasks.
6
+ */
7
+
8
+ import type { Task } from "../schemas/task.js";
9
+
10
+ /**
11
+ * Stop words to ignore in similarity calculation
12
+ * Common words that don't contribute to semantic meaning
13
+ */
14
+ const STOP_WORDS = new Set([
15
+ // English
16
+ "a",
17
+ "am",
18
+ "an",
19
+ "the",
20
+ "is",
21
+ "are",
22
+ "was",
23
+ "were",
24
+ "be",
25
+ "been",
26
+ "being",
27
+ "have",
28
+ "has",
29
+ "had",
30
+ "do",
31
+ "does",
32
+ "did",
33
+ "will",
34
+ "would",
35
+ "could",
36
+ "should",
37
+ "may",
38
+ "might",
39
+ "must",
40
+ "can",
41
+ "to",
42
+ "of",
43
+ "in",
44
+ "for",
45
+ "on",
46
+ "with",
47
+ "at",
48
+ "by",
49
+ "from",
50
+ "as",
51
+ "into",
52
+ "through",
53
+ "during",
54
+ "before",
55
+ "after",
56
+ "above",
57
+ "below",
58
+ "and",
59
+ "or",
60
+ "but",
61
+ "if",
62
+ "then",
63
+ "else",
64
+ "when",
65
+ "where",
66
+ "why",
67
+ "how",
68
+ "all",
69
+ "each",
70
+ "every",
71
+ "both",
72
+ "few",
73
+ "more",
74
+ "most",
75
+ "other",
76
+ "some",
77
+ "such",
78
+ "no",
79
+ "nor",
80
+ "not",
81
+ "only",
82
+ "own",
83
+ "same",
84
+ "so",
85
+ "than",
86
+ "too",
87
+ "very",
88
+ "just",
89
+ "also",
90
+ "this",
91
+ "that",
92
+ "these",
93
+ "those",
94
+ "it",
95
+ "its",
96
+ // Korean particles (common suffixes)
97
+ "을",
98
+ "를",
99
+ "이",
100
+ "가",
101
+ "은",
102
+ "는",
103
+ "에",
104
+ "에서",
105
+ "으로",
106
+ "로",
107
+ "와",
108
+ "과",
109
+ "의",
110
+ "도",
111
+ "만",
112
+ "까지",
113
+ "부터",
114
+ "하다",
115
+ "하기",
116
+ "해서",
117
+ "하고",
118
+ ]);
119
+
120
+ /**
121
+ * Tokenize text into meaningful words
122
+ * - Converts to lowercase
123
+ * - Splits on whitespace and punctuation
124
+ * - Removes stop words
125
+ * - Removes short tokens (< 2 chars)
126
+ */
127
+ export function tokenize(text: string): string[] {
128
+ if (!text) return [];
129
+
130
+ return text
131
+ .toLowerCase()
132
+ .split(/[\s\-_.,;:!?()\[\]{}'"\/\\]+/)
133
+ .filter((token) => token.length >= 2 && !STOP_WORDS.has(token));
134
+ }
135
+
136
+ /**
137
+ * Calculate Jaccard similarity between two token sets
138
+ * Returns value between 0 (no overlap) and 1 (identical)
139
+ */
140
+ export function jaccardSimilarity(tokensA: string[], tokensB: string[]): number {
141
+ if (tokensA.length === 0 && tokensB.length === 0) return 0;
142
+ if (tokensA.length === 0 || tokensB.length === 0) return 0;
143
+
144
+ const setA = new Set(tokensA);
145
+ const setB = new Set(tokensB);
146
+
147
+ let intersection = 0;
148
+ for (const token of setA) {
149
+ if (setB.has(token)) {
150
+ intersection++;
151
+ }
152
+ }
153
+
154
+ const union = setA.size + setB.size - intersection;
155
+ return union === 0 ? 0 : intersection / union;
156
+ }
157
+
158
+ /**
159
+ * Calculate weighted similarity between two tasks
160
+ *
161
+ * Weights:
162
+ * - Intent: 0.4 (highest priority - captures purpose)
163
+ * - Title: 0.3 (strong signal)
164
+ * - Description: 0.2 (additional context)
165
+ * - Tags: 0.1 (explicit categorization)
166
+ */
167
+ export function calculateTaskSimilarity(task1: Task, task2: Task): number {
168
+ // Extract text fields
169
+ const intent1 = task1.workContext?.intent || "";
170
+ const intent2 = task2.workContext?.intent || "";
171
+
172
+ const title1 = task1.title || "";
173
+ const title2 = task2.title || "";
174
+
175
+ const desc1 = task1.description || "";
176
+ const desc2 = task2.description || "";
177
+
178
+ const tags1 = task1.tags?.join(" ") || "";
179
+ const tags2 = task2.tags?.join(" ") || "";
180
+
181
+ // Tokenize
182
+ const intentTokens1 = tokenize(intent1);
183
+ const intentTokens2 = tokenize(intent2);
184
+
185
+ const titleTokens1 = tokenize(title1);
186
+ const titleTokens2 = tokenize(title2);
187
+
188
+ const descTokens1 = tokenize(desc1);
189
+ const descTokens2 = tokenize(desc2);
190
+
191
+ const tagTokens1 = tokenize(tags1);
192
+ const tagTokens2 = tokenize(tags2);
193
+
194
+ // Calculate component similarities
195
+ const intentSim = jaccardSimilarity(intentTokens1, intentTokens2);
196
+ const titleSim = jaccardSimilarity(titleTokens1, titleTokens2);
197
+ const descSim = jaccardSimilarity(descTokens1, descTokens2);
198
+ const tagSim = jaccardSimilarity(tagTokens1, tagTokens2);
199
+
200
+ // Weighted combination
201
+ // Adjust weights based on available content
202
+ let totalWeight = 0;
203
+ let weightedSum = 0;
204
+
205
+ if (intentTokens1.length > 0 || intentTokens2.length > 0) {
206
+ weightedSum += intentSim * 0.4;
207
+ totalWeight += 0.4;
208
+ }
209
+
210
+ if (titleTokens1.length > 0 || titleTokens2.length > 0) {
211
+ weightedSum += titleSim * 0.3;
212
+ totalWeight += 0.3;
213
+ }
214
+
215
+ if (descTokens1.length > 0 || descTokens2.length > 0) {
216
+ weightedSum += descSim * 0.2;
217
+ totalWeight += 0.2;
218
+ }
219
+
220
+ if (tagTokens1.length > 0 || tagTokens2.length > 0) {
221
+ weightedSum += tagSim * 0.1;
222
+ totalWeight += 0.1;
223
+ }
224
+
225
+ return totalWeight === 0 ? 0 : weightedSum / totalWeight;
226
+ }
227
+
228
+ /**
229
+ * Find tasks related to a given task
230
+ *
231
+ * @param task - The reference task
232
+ * @param allTasks - All tasks to search from
233
+ * @param options - Configuration options
234
+ * @returns Array of related tasks sorted by similarity (highest first)
235
+ */
236
+ export function findRelatedTasks(
237
+ task: Task,
238
+ allTasks: Task[],
239
+ options: {
240
+ limit?: number;
241
+ minSimilarity?: number;
242
+ excludeCompleted?: boolean;
243
+ excludeSameParent?: boolean;
244
+ } = {}
245
+ ): Array<{ task: Task; similarity: number }> {
246
+ const {
247
+ limit = 5,
248
+ minSimilarity = 0.1,
249
+ excludeCompleted = true,
250
+ excludeSameParent = true,
251
+ } = options;
252
+
253
+ const candidates = allTasks.filter((t) => {
254
+ // Exclude self
255
+ if (t.id === task.id) return false;
256
+
257
+ // Exclude completed if requested
258
+ if (excludeCompleted && t.status === "completed") return false;
259
+
260
+ // Exclude same parent (siblings are already contextually related)
261
+ if (excludeSameParent && task.parentId && t.parentId === task.parentId) return false;
262
+
263
+ // Exclude direct parent/child relationships
264
+ if (t.parentId === task.id || task.parentId === t.id) return false;
265
+
266
+ return true;
267
+ });
268
+
269
+ // Calculate similarities
270
+ const withSimilarity = candidates
271
+ .map((t) => ({
272
+ task: t,
273
+ similarity: calculateTaskSimilarity(task, t),
274
+ }))
275
+ .filter((item) => item.similarity >= minSimilarity)
276
+ .sort((a, b) => b.similarity - a.similarity)
277
+ .slice(0, limit);
278
+
279
+ return withSimilarity;
280
+ }
281
+
282
+ /**
283
+ * Cluster tasks by similarity
284
+ * Returns groups of related tasks
285
+ */
286
+ export function clusterTasks(
287
+ tasks: Task[],
288
+ options: {
289
+ minClusterSize?: number;
290
+ similarityThreshold?: number;
291
+ } = {}
292
+ ): Array<{ representative: Task; members: Task[]; avgSimilarity: number }> {
293
+ const { minClusterSize = 2, similarityThreshold = 0.2 } = options;
294
+
295
+ // Simple greedy clustering
296
+ const assigned = new Set<string>();
297
+ const clusters: Array<{ representative: Task; members: Task[]; avgSimilarity: number }> = [];
298
+
299
+ // Sort by priority to use higher priority tasks as cluster representatives
300
+ const sortedTasks = [...tasks].sort((a, b) => {
301
+ const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
302
+ return (priorityOrder[a.priority] ?? 2) - (priorityOrder[b.priority] ?? 2);
303
+ });
304
+
305
+ for (const task of sortedTasks) {
306
+ if (assigned.has(task.id)) continue;
307
+
308
+ // Find related tasks that haven't been assigned
309
+ const related = findRelatedTasks(task, tasks, {
310
+ minSimilarity: similarityThreshold,
311
+ excludeCompleted: false,
312
+ excludeSameParent: false,
313
+ limit: 10,
314
+ }).filter((r) => !assigned.has(r.task.id));
315
+
316
+ if (related.length >= minClusterSize - 1) {
317
+ // Create cluster
318
+ const members = [task, ...related.map((r) => r.task)];
319
+ const avgSimilarity =
320
+ related.length > 0 ? related.reduce((sum, r) => sum + r.similarity, 0) / related.length : 0;
321
+
322
+ clusters.push({
323
+ representative: task,
324
+ members,
325
+ avgSimilarity,
326
+ });
327
+
328
+ // Mark as assigned
329
+ for (const member of members) {
330
+ assigned.add(member.id);
331
+ }
332
+ }
333
+ }
334
+
335
+ return clusters;
336
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Environment detection utilities
3
+ */
4
+
5
+ /**
6
+ * Check if running in test environment.
7
+ * Detects: NODE_ENV=test, BUN_TEST (set by bun test), VITEST, JEST_WORKER_ID
8
+ */
9
+ export function isTestEnv(): boolean {
10
+ return (
11
+ process.env.NODE_ENV === "test" ||
12
+ process.env["BUN_TEST"] !== undefined ||
13
+ process.env["VITEST"] !== undefined ||
14
+ process.env["JEST_WORKER_ID"] !== undefined
15
+ );
16
+ }
17
+
18
+ /**
19
+ * Check if debug mode is enabled via environment variable
20
+ */
21
+ export function isDebugEnabled(): boolean {
22
+ const debugEnv = process.env["TASK_MCP_DEBUG"];
23
+ return debugEnv === "true" || debugEnv === "1";
24
+ }
25
+
26
+ /**
27
+ * Check if verbose logging should be suppressed.
28
+ *
29
+ * Logs are suppressed if:
30
+ * - In test environment AND
31
+ * - TASK_MCP_DEBUG is not set AND
32
+ * - TASK_MCP_TEST_VERBOSE is not set
33
+ *
34
+ * Set TASK_MCP_TEST_VERBOSE=true to enable logs in specific tests.
35
+ */
36
+ export function shouldSuppressLogs(): boolean {
37
+ if (!isTestEnv()) return false;
38
+ if (isDebugEnabled()) return false;
39
+ if (process.env["TASK_MCP_TEST_VERBOSE"] === "true") return false;
40
+ return true;
41
+ }
@@ -1,4 +1,5 @@
1
1
  import type { Task } from "../schemas/task.js";
2
+ import { shouldSuppressLogs } from "./env.js";
2
3
 
3
4
  /**
4
5
  * Maximum allowed hierarchy depth (0-indexed)
@@ -34,9 +35,11 @@ export function getTaskLevel(tasks: Task[], taskId: string): number {
34
35
  while (currentTask.parentId) {
35
36
  const parent = taskMap.get(currentTask.parentId);
36
37
  if (!parent) {
37
- console.warn(
38
- `[task-mcp] Orphaned parent reference: task "${currentTask.id}" references non-existent parent "${currentTask.parentId}"`
39
- );
38
+ if (!shouldSuppressLogs()) {
39
+ console.warn(
40
+ `[task-mcp] Detached parent reference: task "${currentTask.id}" references non-existent parent "${currentTask.parentId}"`
41
+ );
42
+ }
40
43
  break;
41
44
  }
42
45
  level++;
@@ -240,8 +243,9 @@ export function buildTaskTree(
240
243
  return [buildNode(rootTask, 0, new Set())];
241
244
  }
242
245
 
243
- // Return all root tasks (no parent)
244
- const rootTasks = tasks.filter((t) => !t.parentId);
246
+ // Return all root tasks (no parent OR parent not in filtered tasks)
247
+ // This ensures orphaned subtasks (whose parent was filtered out) are still visible
248
+ const rootTasks = tasks.filter((t) => !t.parentId || !taskMap.has(t.parentId));
245
249
  return rootTasks.map((task) => buildNode(task, 0, new Set()));
246
250
  }
247
251
 
@@ -42,6 +42,8 @@ export {
42
42
  MAX_HIERARCHY_DEPTH,
43
43
  getTaskLevel,
44
44
  validateHierarchyDepth,
45
+ validateHierarchy,
46
+ type HierarchyValidationResult,
45
47
  getAncestorIds,
46
48
  getDescendantIds,
47
49
  getChildTasks,
@@ -171,3 +173,18 @@ export {
171
173
  type ParsePlanOptions,
172
174
  type ParsePlanResult,
173
175
  } from "./plan-parser.js";
176
+
177
+ // Intent extractor (auto-generate intent from title/description)
178
+ export { extractIntent, generateFallbackIntent, getIntent } from "./intent-extractor.js";
179
+
180
+ // Semantic clustering (task similarity for session resumption)
181
+ export {
182
+ tokenize,
183
+ jaccardSimilarity,
184
+ calculateTaskSimilarity,
185
+ findRelatedTasks,
186
+ clusterTasks,
187
+ } from "./clustering.js";
188
+
189
+ // Environment detection
190
+ export { isTestEnv, isDebugEnabled, shouldSuppressLogs } from "./env.js";
@@ -0,0 +1,84 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { extractIntent, generateFallbackIntent, getIntent } from "./intent-extractor.js";
3
+
4
+ describe("extractIntent", () => {
5
+ test("extracts intent from fix patterns", () => {
6
+ expect(extractIntent("Fix login bug")).toBe("Fix issue or bug");
7
+ expect(extractIntent("Debug authentication issue")).toBe("Fix issue or bug");
8
+ expect(extractIntent("로그인 버그 수정")).toBe("Fix issue or bug");
9
+ });
10
+
11
+ test("extracts intent from implement patterns", () => {
12
+ expect(extractIntent("Implement user authentication")).toBe("Implement new functionality");
13
+ expect(extractIntent("Create new dashboard")).toBe("Implement new functionality");
14
+ expect(extractIntent("사용자 인증 구현")).toBe("Implement new functionality");
15
+ });
16
+
17
+ test("extracts intent from refactor patterns", () => {
18
+ expect(extractIntent("Refactor database layer")).toBe("Improve code quality");
19
+ expect(extractIntent("API 리팩토링")).toBe("Improve code quality");
20
+ });
21
+
22
+ test("extracts intent from add patterns", () => {
23
+ expect(extractIntent("Add dark mode support")).toBe("Extend with new feature");
24
+ expect(extractIntent("다크 모드 추가")).toBe("Extend with new feature");
25
+ });
26
+
27
+ test("extracts intent from update patterns", () => {
28
+ expect(extractIntent("Update dependencies")).toBe("Modify existing behavior");
29
+ expect(extractIntent("의존성 업데이트")).toBe("Modify existing behavior");
30
+ });
31
+
32
+ test("extracts intent from remove patterns", () => {
33
+ expect(extractIntent("Remove deprecated code")).toBe("Remove or clean up");
34
+ expect(extractIntent("레거시 코드 삭제")).toBe("Remove or clean up");
35
+ });
36
+
37
+ test("extracts intent from test patterns", () => {
38
+ expect(extractIntent("Test payment flow")).toBe("Verify functionality");
39
+ expect(extractIntent("결제 플로우 테스트")).toBe("Verify functionality");
40
+ });
41
+
42
+ test("extracts intent from description when title has no match", () => {
43
+ expect(extractIntent("API endpoints", "Fix the bug in user endpoint")).toBe("Fix issue or bug");
44
+ });
45
+
46
+ test("returns undefined when no pattern matches", () => {
47
+ expect(extractIntent("Random task title")).toBeUndefined();
48
+ expect(extractIntent("Something else")).toBeUndefined();
49
+ });
50
+ });
51
+
52
+ describe("generateFallbackIntent", () => {
53
+ test("generates intent from verb-like first word", () => {
54
+ expect(generateFallbackIntent("Get user data")).toBe("Get operation");
55
+ expect(generateFallbackIntent("Run tests")).toBe("Run operation");
56
+ });
57
+
58
+ test("generates fallback for non-verb titles", () => {
59
+ expect(generateFallbackIntent("Dashboard redesign")).toBe("Complete: Dashboard redesign");
60
+ // Test truncation for titles > 50 chars
61
+ const longTitle =
62
+ "This is a very long task title that definitely exceeds fifty characters limit";
63
+ expect(generateFallbackIntent(longTitle)).toContain("...");
64
+ });
65
+ });
66
+
67
+ describe("getIntent", () => {
68
+ test("returns extracted intent when pattern matches", () => {
69
+ expect(getIntent("Fix bug")).toBe("Fix issue or bug");
70
+ expect(getIntent("Implement feature")).toBe("Implement new functionality");
71
+ });
72
+
73
+ test("returns fallback when autoGenerate is true", () => {
74
+ expect(getIntent("Random task")).toBe("Complete: Random task");
75
+ });
76
+
77
+ test("returns undefined when autoGenerate is false and no match", () => {
78
+ expect(getIntent("Random task", undefined, false)).toBeUndefined();
79
+ });
80
+
81
+ test("uses description for extraction", () => {
82
+ expect(getIntent("Task", "Fix the authentication bug")).toBe("Fix issue or bug");
83
+ });
84
+ });
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Intent Extractor
3
+ *
4
+ * Extracts intent from task title/description using pattern matching.
5
+ * Provides fallback intent generation when workContext.intent is not provided.
6
+ */
7
+
8
+ interface IntentPattern {
9
+ patterns: RegExp[];
10
+ intent: string;
11
+ }
12
+
13
+ const INTENT_PATTERNS: IntentPattern[] = [
14
+ // Fix/Bug patterns
15
+ {
16
+ patterns: [/^fix\b/i, /^debug\b/i, /^resolve\b/i, /버그/, /수정/, /고치/],
17
+ intent: "Fix issue or bug",
18
+ },
19
+ // Implementation patterns
20
+ {
21
+ patterns: [/^implement\b/i, /^create\b/i, /^build\b/i, /구현/, /만들/],
22
+ intent: "Implement new functionality",
23
+ },
24
+ // Refactoring patterns
25
+ {
26
+ patterns: [/^refactor\b/i, /^restructure\b/i, /^reorganize\b/i, /리팩/, /개선/],
27
+ intent: "Improve code quality",
28
+ },
29
+ // Add/Extend patterns
30
+ {
31
+ patterns: [/^add\b/i, /^extend\b/i, /^include\b/i, /추가/, /확장/],
32
+ intent: "Extend with new feature",
33
+ },
34
+ // Update/Modify patterns
35
+ {
36
+ patterns: [/^update\b/i, /^modify\b/i, /^change\b/i, /업데이트/, /변경/],
37
+ intent: "Modify existing behavior",
38
+ },
39
+ // Remove/Delete patterns
40
+ {
41
+ patterns: [/^remove\b/i, /^delete\b/i, /^clean\b/i, /삭제/, /제거/],
42
+ intent: "Remove or clean up",
43
+ },
44
+ // Test patterns
45
+ {
46
+ patterns: [/^test\b/i, /^verify\b/i, /^validate\b/i, /테스트/, /검증/],
47
+ intent: "Verify functionality",
48
+ },
49
+ // Documentation patterns
50
+ {
51
+ patterns: [/^document\b/i, /^write doc/i, /문서/, /docs/i],
52
+ intent: "Document or explain",
53
+ },
54
+ // Research/Analysis patterns
55
+ {
56
+ patterns: [/^research\b/i, /^analyze\b/i, /^investigate\b/i, /조사/, /분석/],
57
+ intent: "Research or analyze",
58
+ },
59
+ // Migration patterns
60
+ {
61
+ patterns: [/^migrate\b/i, /^upgrade\b/i, /^convert\b/i, /마이그레이션/, /전환/],
62
+ intent: "Migrate or upgrade",
63
+ },
64
+ // Setup/Config patterns
65
+ {
66
+ patterns: [/^setup\b/i, /^configure\b/i, /^initialize\b/i, /설정/, /초기화/],
67
+ intent: "Setup or configure",
68
+ },
69
+ // Review patterns
70
+ {
71
+ patterns: [/^review\b/i, /^check\b/i, /^audit\b/i, /리뷰/, /검토/],
72
+ intent: "Review or audit",
73
+ },
74
+ ];
75
+
76
+ /**
77
+ * Extract intent from task title and description
78
+ *
79
+ * @param title - Task title
80
+ * @param description - Optional task description
81
+ * @returns Extracted intent or undefined if no pattern matches
82
+ */
83
+ export function extractIntent(title: string, description?: string): string | undefined {
84
+ const text = title.toLowerCase();
85
+
86
+ // Try to match patterns against title
87
+ for (const { patterns, intent } of INTENT_PATTERNS) {
88
+ for (const pattern of patterns) {
89
+ if (pattern.test(text)) {
90
+ return intent;
91
+ }
92
+ }
93
+ }
94
+
95
+ // If no match found in title, try description
96
+ if (description) {
97
+ const descText = description.toLowerCase();
98
+ for (const { patterns, intent } of INTENT_PATTERNS) {
99
+ for (const pattern of patterns) {
100
+ if (pattern.test(descText)) {
101
+ return intent;
102
+ }
103
+ }
104
+ }
105
+ }
106
+
107
+ return undefined;
108
+ }
109
+
110
+ /**
111
+ * Generate a fallback intent based on title structure
112
+ *
113
+ * Used when no pattern matches but we still want some intent
114
+ */
115
+ export function generateFallbackIntent(title: string): string {
116
+ // Extract first verb-like word
117
+ const words = title.split(/\s+/);
118
+ const firstWord = words[0]?.toLowerCase() || "";
119
+
120
+ // If it looks like a verb (common task action words)
121
+ const verbPatterns =
122
+ /^(get|set|make|do|run|use|find|show|hide|load|save|send|fetch|call|start|stop|enable|disable)/i;
123
+ if (verbPatterns.test(firstWord)) {
124
+ return `${firstWord.charAt(0).toUpperCase() + firstWord.slice(1)} operation`;
125
+ }
126
+
127
+ // Default: use truncated title as intent
128
+ const truncatedTitle = title.length > 50 ? title.slice(0, 47) + "..." : title;
129
+ return `Complete: ${truncatedTitle}`;
130
+ }
131
+
132
+ /**
133
+ * Get intent for a task, extracting from content or generating fallback
134
+ *
135
+ * @param title - Task title
136
+ * @param description - Optional task description
137
+ * @param autoGenerate - Whether to generate fallback if no pattern matches (default: true)
138
+ * @returns Intent string
139
+ */
140
+ export function getIntent(
141
+ title: string,
142
+ description?: string,
143
+ autoGenerate: boolean = true
144
+ ): string | undefined {
145
+ const extracted = extractIntent(title, description);
146
+
147
+ if (extracted) {
148
+ return extracted;
149
+ }
150
+
151
+ if (autoGenerate) {
152
+ return generateFallbackIntent(title);
153
+ }
154
+
155
+ return undefined;
156
+ }