@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
@@ -255,6 +255,33 @@ describe("parseTaskInput", () => {
255
255
  const result = parseTaskInput("간단한 작업 !medium #task");
256
256
  expect(result.workContext).toBeUndefined();
257
257
  });
258
+
259
+ test("parses done: before :: to prevent intent capturing done:", () => {
260
+ // Regression test: done: should be extracted before intent (::)
261
+ const result = parseTaskInput(
262
+ "API 리팩토링 :: 성능 개선 done:테스트통과,코드리뷰 @thorough #backend !high"
263
+ );
264
+ expect(result.title).toBe("API 리팩토링");
265
+ expect(result.workContext?.intent).toBe("성능 개선");
266
+ expect(result.workContext?.acceptanceCriteria).toEqual(["테스트통과", "코드리뷰"]);
267
+ expect(result.workContext?.qualityLevel).toBe("thorough");
268
+ expect(result.priority).toBe("high");
269
+ expect(result.tags).toEqual(["backend"]);
270
+ });
271
+
272
+ test("parses done: without intent", () => {
273
+ const result = parseTaskInput("테스트 작성 done:유닛테스트,통합테스트 !high");
274
+ expect(result.title).toBe("테스트 작성");
275
+ expect(result.workContext?.acceptanceCriteria).toEqual(["유닛테스트", "통합테스트"]);
276
+ expect(result.workContext?.intent).toBeUndefined();
277
+ });
278
+
279
+ test("parses intent without done:", () => {
280
+ const result = parseTaskInput("리팩토링 :: 가독성 향상 !medium");
281
+ expect(result.title).toBe("리팩토링");
282
+ expect(result.workContext?.intent).toBe("가독성 향상");
283
+ expect(result.workContext?.acceptanceCriteria).toBeUndefined();
284
+ });
258
285
  });
259
286
  });
260
287
 
@@ -316,7 +316,16 @@ export function parseTaskInput(input: string): TaskCreateInput {
316
316
  const result: TaskCreateInput = { title: "" };
317
317
  const workContext: Partial<WorkContext> = {};
318
318
 
319
- // Extract intent first (:: separator)
319
+ // Extract acceptanceCriteria FIRST (done:xxx,yyy or done:xxx)
320
+ // Must be before intent extraction to prevent done: from being captured as intent
321
+ // Format: "done:criterion1,criterion2" or "done:single criterion"
322
+ const doneMatch = remaining.match(/done:([^\s@#!~^:]+(?:,[^\s@#!~^:]+)*)/i);
323
+ if (doneMatch) {
324
+ workContext.acceptanceCriteria = doneMatch[1]!.split(",").map((c) => c.trim());
325
+ remaining = remaining.replace(doneMatch[0], " ").trim();
326
+ }
327
+
328
+ // Extract intent (:: separator)
320
329
  // Format: "Task title :: intent description"
321
330
  const intentMatch = remaining.match(/\s*::\s*(.+?)(?=\s+[@#!~^]|$)/);
322
331
  if (intentMatch) {
@@ -324,14 +333,6 @@ export function parseTaskInput(input: string): TaskCreateInput {
324
333
  remaining = remaining.replace(intentMatch[0], " ").trim();
325
334
  }
326
335
 
327
- // Extract acceptanceCriteria (done:xxx,yyy or done:xxx)
328
- // Format: "done:criterion1,criterion2" or "done:single criterion"
329
- const doneMatch = remaining.match(/done:([^\s@#!~^]+(?:,[^\s@#!~^]+)*)/i);
330
- if (doneMatch) {
331
- workContext.acceptanceCriteria = doneMatch[1]!.split(",").map((c) => c.trim());
332
- remaining = remaining.replace(doneMatch[0], " ").trim();
333
- }
334
-
335
336
  // Extract qualityLevel (@quick, @thorough, etc.) - before contexts
336
337
  // These are reserved keywords that become qualityLevel, not contexts
337
338
  const qualityLevelKeywords = Object.keys(QUALITY_LEVEL_KEYWORDS);
@@ -93,10 +93,7 @@ function preprocessInput(input: string): string {
93
93
  * `);
94
94
  * ```
95
95
  */
96
- export function parsePlan(
97
- input: string,
98
- options: ParsePlanOptions = {}
99
- ): ParsePlanResult {
96
+ export function parsePlan(input: string, options: ParsePlanOptions = {}): ParsePlanResult {
100
97
  if (input.length > MAX_PLAN_LENGTH) {
101
98
  return {
102
99
  tasks: [],
@@ -121,7 +118,6 @@ export function parsePlan(
121
118
  let taskCounter = 0;
122
119
  let currentPhase: { tempId: string; title: string } | null = null;
123
120
  let currentSection: { tempId: string; level: number } | null = null;
124
- let currentParent: { tempId: string; level: number; indent: number } | null = null;
125
121
  const parentStack: Array<{ tempId: string; level: number; indent: number }> = [];
126
122
 
127
123
  for (const line of lines) {
@@ -160,7 +156,6 @@ export function parsePlan(
160
156
 
161
157
  // Reset section and parent stack
162
158
  currentSection = null;
163
- currentParent = null;
164
159
  parentStack.length = 0;
165
160
  continue;
166
161
  }
@@ -192,7 +187,6 @@ export function parsePlan(
192
187
  });
193
188
 
194
189
  currentSection = { tempId, level: 1 };
195
- currentParent = null;
196
190
  parentStack.length = 0;
197
191
  continue;
198
192
  }
@@ -230,7 +224,6 @@ export function parsePlan(
230
224
 
231
225
  // Extract metadata from content
232
226
  let title = content;
233
- let description: string | undefined;
234
227
  let priority: Priority | undefined;
235
228
  let tags: string[] | undefined;
236
229
  let dependsOn: string[] | undefined;
@@ -252,7 +245,7 @@ export function parsePlan(
252
245
  // Extract description from remaining parentheses (not dependency-related)
253
246
  const descExtracted = extractDescription(title);
254
247
  title = descExtracted.title;
255
- description = descExtracted.description;
248
+ const description = descExtracted.description;
256
249
 
257
250
  const task: ParsedPlanTask = {
258
251
  title,
@@ -269,7 +262,6 @@ export function parsePlan(
269
262
 
270
263
  // Push to parent stack for potential children
271
264
  parentStack.push({ tempId, level, indent });
272
- currentParent = { tempId, level, indent };
273
265
  continue;
274
266
  }
275
267
 
@@ -303,7 +295,6 @@ export function parsePlan(
303
295
  }
304
296
 
305
297
  let title = content;
306
- let description: string | undefined;
307
298
  let priority: Priority | undefined;
308
299
  let tags: string[] | undefined;
309
300
  let dependsOn: string[] | undefined;
@@ -323,7 +314,7 @@ export function parsePlan(
323
314
  // Extract description from remaining parentheses
324
315
  const descExtracted = extractDescription(title);
325
316
  title = descExtracted.title;
326
- description = descExtracted.description;
317
+ const description = descExtracted.description;
327
318
 
328
319
  tasks.push({
329
320
  title,
@@ -347,10 +338,7 @@ export function parsePlan(
347
338
  /**
348
339
  * Infer dependencies from text patterns like "depends on X", "after X"
349
340
  */
350
- function inferDependenciesFromText(
351
- content: string,
352
- existingTasks: ParsedPlanTask[]
353
- ): string[] {
341
+ function inferDependenciesFromText(content: string, existingTasks: ParsedPlanTask[]): string[] {
354
342
  const deps: string[] = [];
355
343
  const lower = content.toLowerCase();
356
344
 
@@ -45,7 +45,7 @@ const createInboxItem = (overrides: Partial<InboxItem> = {}): InboxItem => ({
45
45
  });
46
46
 
47
47
  describe("formatTask", () => {
48
- test("concise format returns only 4 essential fields", () => {
48
+ test("concise format returns essential fields plus description for context", () => {
49
49
  const task = createTask();
50
50
  const result = formatTask(task, "concise");
51
51
 
@@ -54,11 +54,12 @@ describe("formatTask", () => {
54
54
  title: "Test Task",
55
55
  status: "pending",
56
56
  priority: "medium",
57
+ description: "This is a test task with a long description",
57
58
  });
58
- expect(Object.keys(result)).toHaveLength(4);
59
+ expect(Object.keys(result)).toHaveLength(5);
59
60
  });
60
61
 
61
- test("standard format returns 8 common fields", () => {
62
+ test("standard format returns common fields including description", () => {
62
63
  const task = createTask();
63
64
  const result = formatTask(task, "standard");
64
65
 
@@ -67,6 +68,7 @@ describe("formatTask", () => {
67
68
  title: "Test Task",
68
69
  status: "pending",
69
70
  priority: "medium",
71
+ description: "This is a test task with a long description",
70
72
  dueDate: "2025-01-15",
71
73
  tags: ["test", "unit"],
72
74
  contexts: ["work"],
@@ -94,6 +96,57 @@ describe("formatTask", () => {
94
96
  expect(result).not.toHaveProperty("dueDate");
95
97
  expect(result).not.toHaveProperty("tags");
96
98
  });
99
+
100
+ test("concise format includes workContext fields (intent, methodology, quality, patterns, criteria)", () => {
101
+ const task = createTask({
102
+ workContext: {
103
+ intent: "Migrate API layer to service pattern",
104
+ methodology: "TDD + iterative refactoring",
105
+ qualityLevel: "thorough",
106
+ patterns: {
107
+ parallelExecution: true,
108
+ testFirst: true,
109
+ },
110
+ acceptanceCriteria: ["All tests pass", "No type errors"],
111
+ },
112
+ });
113
+ const result = formatTask(task, "concise");
114
+
115
+ expect(result).toHaveProperty("intent", "Migrate API layer to service pattern");
116
+ expect(result).toHaveProperty("methodology", "TDD + iterative refactoring");
117
+ expect(result).toHaveProperty("quality", "thorough");
118
+ expect(result).toHaveProperty("patterns", { parallelExecution: true, testFirst: true });
119
+ expect(result).toHaveProperty("criteria", ["All tests pass", "No type errors"]);
120
+ });
121
+
122
+ test("standard format includes full workContext with patterns and criteria", () => {
123
+ const task = createTask({
124
+ workContext: {
125
+ intent: "Refactor module",
126
+ methodology: "Analyze → Extract → Test",
127
+ qualityLevel: "standard",
128
+ patterns: {
129
+ parallelExecution: true,
130
+ testFirst: true,
131
+ reviewRequired: false,
132
+ },
133
+ acceptanceCriteria: ["Tests pass", "No regressions"],
134
+ },
135
+ });
136
+ const result = formatTask(task, "standard");
137
+
138
+ expect(result).toHaveProperty("workContext");
139
+ const wc = (result as { workContext?: unknown }).workContext as Record<string, unknown>;
140
+ expect(wc["intent"]).toBe("Refactor module");
141
+ expect(wc["methodology"]).toBe("Analyze → Extract → Test");
142
+ expect(wc["qualityLevel"]).toBe("standard");
143
+ expect(wc["patterns"]).toEqual({
144
+ parallelExecution: true,
145
+ testFirst: true,
146
+ reviewRequired: false,
147
+ });
148
+ expect(wc["acceptanceCriteria"]).toEqual(["Tests pass", "No regressions"]);
149
+ });
97
150
  });
98
151
 
99
152
  describe("formatTasks", () => {
@@ -107,6 +160,7 @@ describe("formatTasks", () => {
107
160
  title: "Test Task",
108
161
  status: "pending",
109
162
  priority: "medium",
163
+ description: "This is a test task with a long description",
110
164
  });
111
165
  });
112
166
 
@@ -404,8 +458,8 @@ describe("token efficiency", () => {
404
458
  const conciseSize = JSON.stringify(concise).length;
405
459
  const fullSize = JSON.stringify(full).length;
406
460
 
407
- // Concise should be at least 50% smaller
408
- expect(conciseSize).toBeLessThan(fullSize * 0.5);
461
+ // Concise should be at least 40% smaller (now includes description for session context)
462
+ expect(conciseSize).toBeLessThan(fullSize * 0.6);
409
463
  });
410
464
 
411
465
  test("concise list is significantly smaller than full list", () => {
@@ -424,7 +478,7 @@ describe("token efficiency", () => {
424
478
  const conciseSize = JSON.stringify(concise).length;
425
479
  const fullSize = JSON.stringify(full).length;
426
480
 
427
- // For list of 20 tasks, concise should be 70%+ smaller
428
- expect(conciseSize).toBeLessThan(fullSize * 0.4);
481
+ // For list of 20 tasks, concise should be 40%+ smaller (now includes description)
482
+ expect(conciseSize).toBeLessThan(fullSize * 0.6);
429
483
  });
430
484
  });
@@ -36,13 +36,43 @@ export function formatTask(task: Task, format: ResponseFormat): TaskSummary | Ta
36
36
  status: task.status,
37
37
  priority: task.priority,
38
38
  };
39
- // Include intent + qualityLevel for quick context understanding
39
+ // Include description for session context (truncated for token efficiency)
40
+ // Skip if intent exists and provides enough context
41
+ if (task.description) {
42
+ const hasIntent = !!task.workContext?.intent;
43
+ // With intent: shorter description (100 chars), without: fuller context (200 chars)
44
+ const maxLen = hasIntent ? 100 : 200;
45
+ summary.description = truncate(task.description, maxLen);
46
+ }
47
+ // Include workContext fields for session resumption
40
48
  if (task.workContext?.intent) {
41
49
  summary.intent = task.workContext.intent;
42
50
  }
51
+ if (task.workContext?.methodology) {
52
+ summary.methodology = task.workContext.methodology;
53
+ }
43
54
  if (task.workContext?.qualityLevel) {
44
55
  summary.quality = task.workContext.qualityLevel;
45
56
  }
57
+ // Include patterns for execution guidance
58
+ if (task.workContext?.patterns) {
59
+ const p = task.workContext.patterns;
60
+ const patterns: {
61
+ parallelExecution?: boolean;
62
+ testFirst?: boolean;
63
+ reviewRequired?: boolean;
64
+ } = {};
65
+ if (p.parallelExecution !== undefined) patterns.parallelExecution = p.parallelExecution;
66
+ if (p.testFirst !== undefined) patterns.testFirst = p.testFirst;
67
+ if (p.reviewRequired !== undefined) patterns.reviewRequired = p.reviewRequired;
68
+ if (Object.keys(patterns).length > 0) {
69
+ summary.patterns = patterns;
70
+ }
71
+ }
72
+ // Include acceptance criteria for completion validation
73
+ if (task.workContext?.acceptanceCriteria?.length) {
74
+ summary.criteria = task.workContext.acceptanceCriteria;
75
+ }
46
76
  return summary;
47
77
  }
48
78
 
@@ -53,6 +83,7 @@ export function formatTask(task: Task, format: ResponseFormat): TaskSummary | Ta
53
83
  title: task.title,
54
84
  status: task.status,
55
85
  priority: task.priority,
86
+ ...(task.description !== undefined && { description: task.description }),
56
87
  ...(task.dueDate !== undefined && { dueDate: task.dueDate }),
57
88
  ...(task.tags !== undefined && { tags: task.tags }),
58
89
  ...(task.contexts !== undefined && { contexts: task.contexts }),
@@ -65,6 +96,18 @@ export function formatTask(task: Task, format: ResponseFormat): TaskSummary | Ta
65
96
  ...(wc.intent !== undefined && { intent: wc.intent }),
66
97
  ...(wc.methodology !== undefined && { methodology: wc.methodology }),
67
98
  ...(wc.qualityLevel !== undefined && { qualityLevel: wc.qualityLevel }),
99
+ ...(wc.patterns !== undefined && {
100
+ patterns: {
101
+ ...(wc.patterns.parallelExecution !== undefined && {
102
+ parallelExecution: wc.patterns.parallelExecution,
103
+ }),
104
+ ...(wc.patterns.testFirst !== undefined && { testFirst: wc.patterns.testFirst }),
105
+ ...(wc.patterns.reviewRequired !== undefined && {
106
+ reviewRequired: wc.patterns.reviewRequired,
107
+ }),
108
+ },
109
+ }),
110
+ ...(wc.acceptanceCriteria?.length && { acceptanceCriteria: wc.acceptanceCriteria }),
68
111
  };
69
112
  }
70
113
  return result;
@@ -917,16 +917,13 @@ describe("formatPriorityBadge", () => {
917
917
 
918
918
  describe("renderSection", () => {
919
919
  test("renders header and items", () => {
920
- const items = [
921
- { primary: "Task 1" },
922
- { primary: "Task 2" },
923
- ];
920
+ const items = [{ primary: "Task 1" }, { primary: "Task 2" }];
924
921
  const lines = renderSection("Test Section", items);
925
922
 
926
923
  expect(lines.length).toBeGreaterThan(0);
927
924
  expect(stripAnsi(lines[0] ?? "")).toContain("Test Section");
928
- expect(lines.some(l => l.includes("Task 1"))).toBe(true);
929
- expect(lines.some(l => l.includes("Task 2"))).toBe(true);
925
+ expect(lines.some((l) => l.includes("Task 1"))).toBe(true);
926
+ expect(lines.some((l) => l.includes("Task 2"))).toBe(true);
930
927
  });
931
928
 
932
929
  test("renders with icon", () => {
@@ -936,25 +933,21 @@ describe("renderSection", () => {
936
933
 
937
934
  test("renders empty state message", () => {
938
935
  const lines = renderSection("Empty", [], { emptyMessage: "No items" });
939
- expect(lines.some(l => stripAnsi(l).includes("No items"))).toBe(true);
936
+ expect(lines.some((l) => stripAnsi(l).includes("No items"))).toBe(true);
940
937
  });
941
938
 
942
939
  test("renders item details", () => {
943
- const items = [
944
- { primary: "Task", details: ["Detail 1", "Detail 2"] },
945
- ];
940
+ const items = [{ primary: "Task", details: ["Detail 1", "Detail 2"] }];
946
941
  const lines = renderSection("With Details", items);
947
- expect(lines.some(l => l.includes("Detail 1"))).toBe(true);
948
- expect(lines.some(l => l.includes("Detail 2"))).toBe(true);
942
+ expect(lines.some((l) => l.includes("Detail 1"))).toBe(true);
943
+ expect(lines.some((l) => l.includes("Detail 2"))).toBe(true);
949
944
  });
950
945
 
951
946
  test("renders prefix and suffix", () => {
952
- const items = [
953
- { primary: "Task", prefix: "[x]", suffix: "P1" },
954
- ];
947
+ const items = [{ primary: "Task", prefix: "[x]", suffix: "P1" }];
955
948
  const lines = renderSection("Test", items);
956
- expect(lines.some(l => l.includes("[x]"))).toBe(true);
957
- expect(lines.some(l => l.includes("P1"))).toBe(true);
949
+ expect(lines.some((l) => l.includes("[x]"))).toBe(true);
950
+ expect(lines.some((l) => l.includes("P1"))).toBe(true);
958
951
  });
959
952
  });
960
953
 
@@ -966,8 +959,8 @@ describe("renderSections", () => {
966
959
  ];
967
960
  const lines = renderSections(sections);
968
961
 
969
- expect(lines.some(l => stripAnsi(l).includes("Section 1"))).toBe(true);
970
- expect(lines.some(l => stripAnsi(l).includes("Section 2"))).toBe(true);
962
+ expect(lines.some((l) => stripAnsi(l).includes("Section 1"))).toBe(true);
963
+ expect(lines.some((l) => stripAnsi(l).includes("Section 2"))).toBe(true);
971
964
  expect(lines.includes("")).toBe(true); // spacing between sections
972
965
  });
973
966
  });
@@ -1013,9 +1006,7 @@ describe("renderStats", () => {
1013
1006
  });
1014
1007
 
1015
1008
  test("applies color to values", () => {
1016
- const stats = [
1017
- { label: "Blocked", value: 3, color: "red" as const },
1018
- ];
1009
+ const stats = [{ label: "Blocked", value: 3, color: "red" as const }];
1019
1010
  const result = renderStats(stats);
1020
1011
  expect(result).toContain("\x1b[31m"); // red
1021
1012
  });