@task-mcp/shared 1.0.28 → 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.
- package/dist/algorithms/index.d.ts +1 -1
- package/dist/algorithms/index.d.ts.map +1 -1
- package/dist/algorithms/index.js +1 -1
- package/dist/algorithms/index.js.map +1 -1
- package/dist/algorithms/topological-sort.d.ts +21 -1
- package/dist/algorithms/topological-sort.d.ts.map +1 -1
- package/dist/algorithms/topological-sort.js +12 -1
- package/dist/algorithms/topological-sort.js.map +1 -1
- package/dist/schemas/inbox.d.ts +2 -2
- package/dist/schemas/index.d.ts +1 -0
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +2 -0
- package/dist/schemas/index.js.map +1 -1
- package/dist/schemas/response-format.d.ts +11 -0
- package/dist/schemas/response-format.d.ts.map +1 -1
- package/dist/schemas/response-format.js.map +1 -1
- package/dist/schemas/session.d.ts +521 -0
- package/dist/schemas/session.d.ts.map +1 -0
- package/dist/schemas/session.js +79 -0
- package/dist/schemas/session.js.map +1 -0
- package/dist/schemas/state.d.ts +2 -2
- package/dist/schemas/task.d.ts +9 -0
- package/dist/schemas/task.d.ts.map +1 -1
- package/dist/schemas/task.js +23 -6
- package/dist/schemas/task.js.map +1 -1
- package/dist/schemas/view.d.ts +18 -18
- package/dist/utils/clustering.d.ts +60 -0
- package/dist/utils/clustering.d.ts.map +1 -0
- package/dist/utils/clustering.js +283 -0
- package/dist/utils/clustering.js.map +1 -0
- package/dist/utils/clustering.test.d.ts +2 -0
- package/dist/utils/clustering.test.d.ts.map +1 -0
- package/dist/utils/clustering.test.js +237 -0
- package/dist/utils/clustering.test.js.map +1 -0
- package/dist/utils/env.d.ts +24 -0
- package/dist/utils/env.d.ts.map +1 -0
- package/dist/utils/env.js +40 -0
- package/dist/utils/env.js.map +1 -0
- package/dist/utils/hierarchy.d.ts.map +1 -1
- package/dist/utils/hierarchy.js +13 -6
- package/dist/utils/hierarchy.js.map +1 -1
- package/dist/utils/index.d.ts +6 -2
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +24 -2
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/intent-extractor.d.ts +30 -0
- package/dist/utils/intent-extractor.d.ts.map +1 -0
- package/dist/utils/intent-extractor.js +135 -0
- package/dist/utils/intent-extractor.js.map +1 -0
- package/dist/utils/intent-extractor.test.d.ts +2 -0
- package/dist/utils/intent-extractor.test.d.ts.map +1 -0
- package/dist/utils/intent-extractor.test.js +69 -0
- package/dist/utils/intent-extractor.test.js.map +1 -0
- package/dist/utils/natural-language.d.ts.map +1 -1
- package/dist/utils/natural-language.js +9 -8
- package/dist/utils/natural-language.js.map +1 -1
- package/dist/utils/natural-language.test.js +22 -0
- package/dist/utils/natural-language.test.js.map +1 -1
- package/dist/utils/plan-parser.d.ts +57 -0
- package/dist/utils/plan-parser.d.ts.map +1 -0
- package/dist/utils/plan-parser.js +371 -0
- package/dist/utils/plan-parser.js.map +1 -0
- package/dist/utils/projection.d.ts.map +1 -1
- package/dist/utils/projection.js +43 -1
- package/dist/utils/projection.js.map +1 -1
- package/dist/utils/projection.test.js +57 -7
- package/dist/utils/projection.test.js.map +1 -1
- package/dist/utils/terminal-ui.d.ts +129 -0
- package/dist/utils/terminal-ui.d.ts.map +1 -1
- package/dist/utils/terminal-ui.js +191 -0
- package/dist/utils/terminal-ui.js.map +1 -1
- package/dist/utils/terminal-ui.test.js +227 -0
- package/dist/utils/terminal-ui.test.js.map +1 -1
- package/package.json +2 -2
- package/src/algorithms/index.ts +3 -0
- package/src/algorithms/topological-sort.ts +31 -1
- package/src/schemas/index.ts +11 -0
- package/src/schemas/response-format.ts +15 -2
- package/src/schemas/session.ts +100 -0
- package/src/schemas/task.ts +33 -16
- package/src/utils/clustering.test.ts +285 -0
- package/src/utils/clustering.ts +336 -0
- package/src/utils/env.ts +41 -0
- package/src/utils/hierarchy.ts +17 -8
- package/src/utils/index.ts +48 -0
- package/src/utils/intent-extractor.test.ts +84 -0
- package/src/utils/intent-extractor.ts +156 -0
- package/src/utils/natural-language.test.ts +27 -0
- package/src/utils/natural-language.ts +10 -9
- package/src/utils/plan-parser.ts +466 -0
- package/src/utils/projection.test.ts +61 -7
- package/src/utils/projection.ts +44 -1
- package/src/utils/terminal-ui.test.ts +277 -0
- package/src/utils/terminal-ui.ts +315 -0
package/src/utils/env.ts
ADDED
|
@@ -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
|
+
}
|
package/src/utils/hierarchy.ts
CHANGED
|
@@ -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
|
-
|
|
38
|
-
|
|
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++;
|
|
@@ -217,13 +220,18 @@ export function buildTaskTree(
|
|
|
217
220
|
return { task, children: [] };
|
|
218
221
|
}
|
|
219
222
|
|
|
220
|
-
|
|
221
|
-
|
|
223
|
+
// Mark as visited (O(1) instead of copying the Set)
|
|
224
|
+
visited.add(task.id);
|
|
222
225
|
|
|
223
226
|
const children = childrenMap.get(task.id) || [];
|
|
227
|
+
const childNodes = children.map((child) => buildNode(child, depth + 1, visited));
|
|
228
|
+
|
|
229
|
+
// Restore visited state after processing children (backtracking)
|
|
230
|
+
visited.delete(task.id);
|
|
231
|
+
|
|
224
232
|
return {
|
|
225
233
|
task,
|
|
226
|
-
children:
|
|
234
|
+
children: childNodes,
|
|
227
235
|
};
|
|
228
236
|
}
|
|
229
237
|
|
|
@@ -235,8 +243,9 @@ export function buildTaskTree(
|
|
|
235
243
|
return [buildNode(rootTask, 0, new Set())];
|
|
236
244
|
}
|
|
237
245
|
|
|
238
|
-
// Return all root tasks (no parent)
|
|
239
|
-
|
|
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));
|
|
240
249
|
return rootTasks.map((task) => buildNode(task, 0, new Set()));
|
|
241
250
|
}
|
|
242
251
|
|
package/src/utils/index.ts
CHANGED
|
@@ -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,
|
|
@@ -127,6 +129,29 @@ export {
|
|
|
127
129
|
formatDependencies,
|
|
128
130
|
// Banner
|
|
129
131
|
banner,
|
|
132
|
+
// Status symbols (checkbox style)
|
|
133
|
+
STATUS_SYMBOLS,
|
|
134
|
+
formatStatusSymbol,
|
|
135
|
+
// Priority badge (P0-P3 style)
|
|
136
|
+
PRIORITY_LEVELS,
|
|
137
|
+
formatPriorityBadge,
|
|
138
|
+
// Section renderer (GitHub CLI style)
|
|
139
|
+
renderSection,
|
|
140
|
+
renderSections,
|
|
141
|
+
type SectionItem,
|
|
142
|
+
type SectionOptions,
|
|
143
|
+
// Alert box
|
|
144
|
+
renderAlert,
|
|
145
|
+
type AlertSeverity,
|
|
146
|
+
type AlertOptions,
|
|
147
|
+
// Stats row
|
|
148
|
+
renderStats,
|
|
149
|
+
type StatItem,
|
|
150
|
+
// Due date formatter
|
|
151
|
+
formatDueDate,
|
|
152
|
+
// Task line formatter
|
|
153
|
+
formatTaskLine,
|
|
154
|
+
type TaskLineOptions,
|
|
130
155
|
} from "./terminal-ui.js";
|
|
131
156
|
|
|
132
157
|
// Workspace detection
|
|
@@ -140,3 +165,26 @@ export {
|
|
|
140
165
|
detectWorkspace,
|
|
141
166
|
detectWorkspaceSync,
|
|
142
167
|
} from "./workspace.js";
|
|
168
|
+
|
|
169
|
+
// Plan parser
|
|
170
|
+
export {
|
|
171
|
+
parsePlan,
|
|
172
|
+
type ParsedPlanTask,
|
|
173
|
+
type ParsePlanOptions,
|
|
174
|
+
type ParsePlanResult,
|
|
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
|
+
}
|
|
@@ -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
|
|
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);
|