@ginkoai/cli 2.0.5 → 2.1.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 (91) hide show
  1. package/dist/commands/epic/index.d.ts +29 -0
  2. package/dist/commands/epic/index.d.ts.map +1 -0
  3. package/dist/commands/epic/index.js +94 -0
  4. package/dist/commands/epic/index.js.map +1 -0
  5. package/dist/commands/epic/status.d.ts +40 -0
  6. package/dist/commands/epic/status.d.ts.map +1 -0
  7. package/dist/commands/epic/status.js +244 -0
  8. package/dist/commands/epic/status.js.map +1 -0
  9. package/dist/commands/graph/api-client.d.ts +209 -0
  10. package/dist/commands/graph/api-client.d.ts.map +1 -1
  11. package/dist/commands/graph/api-client.js +125 -0
  12. package/dist/commands/graph/api-client.js.map +1 -1
  13. package/dist/commands/graph/load.d.ts.map +1 -1
  14. package/dist/commands/graph/load.js +40 -2
  15. package/dist/commands/graph/load.js.map +1 -1
  16. package/dist/commands/migrate/index.d.ts +27 -0
  17. package/dist/commands/migrate/index.d.ts.map +1 -0
  18. package/dist/commands/migrate/index.js +76 -0
  19. package/dist/commands/migrate/index.js.map +1 -0
  20. package/dist/commands/migrate/status-migration.d.ts +58 -0
  21. package/dist/commands/migrate/status-migration.d.ts.map +1 -0
  22. package/dist/commands/migrate/status-migration.js +323 -0
  23. package/dist/commands/migrate/status-migration.js.map +1 -0
  24. package/dist/commands/sprint/index.d.ts.map +1 -1
  25. package/dist/commands/sprint/index.js +4 -0
  26. package/dist/commands/sprint/index.js.map +1 -1
  27. package/dist/commands/sprint/status.d.ts +42 -0
  28. package/dist/commands/sprint/status.d.ts.map +1 -0
  29. package/dist/commands/sprint/status.js +278 -0
  30. package/dist/commands/sprint/status.js.map +1 -0
  31. package/dist/commands/start/start-reflection.d.ts +39 -0
  32. package/dist/commands/start/start-reflection.d.ts.map +1 -1
  33. package/dist/commands/start/start-reflection.js +311 -91
  34. package/dist/commands/start/start-reflection.js.map +1 -1
  35. package/dist/commands/sync/sprint-syncer.d.ts +19 -12
  36. package/dist/commands/sync/sprint-syncer.d.ts.map +1 -1
  37. package/dist/commands/sync/sprint-syncer.js +58 -140
  38. package/dist/commands/sync/sprint-syncer.js.map +1 -1
  39. package/dist/commands/sync/sync-command.d.ts.map +1 -1
  40. package/dist/commands/sync/sync-command.js +6 -18
  41. package/dist/commands/sync/sync-command.js.map +1 -1
  42. package/dist/commands/task/index.d.ts +25 -0
  43. package/dist/commands/task/index.d.ts.map +1 -0
  44. package/dist/commands/task/index.js +100 -0
  45. package/dist/commands/task/index.js.map +1 -0
  46. package/dist/commands/task/status.d.ts +43 -0
  47. package/dist/commands/task/status.d.ts.map +1 -0
  48. package/dist/commands/task/status.js +301 -0
  49. package/dist/commands/task/status.js.map +1 -0
  50. package/dist/index.js +12 -29
  51. package/dist/index.js.map +1 -1
  52. package/dist/lib/context-loader-events.d.ts +1 -0
  53. package/dist/lib/context-loader-events.d.ts.map +1 -1
  54. package/dist/lib/context-loader-events.js +28 -41
  55. package/dist/lib/context-loader-events.js.map +1 -1
  56. package/dist/lib/output-formatter.d.ts +12 -4
  57. package/dist/lib/output-formatter.d.ts.map +1 -1
  58. package/dist/lib/output-formatter.js +186 -14
  59. package/dist/lib/output-formatter.js.map +1 -1
  60. package/dist/lib/pending-updates.d.ts +148 -0
  61. package/dist/lib/pending-updates.d.ts.map +1 -0
  62. package/dist/lib/pending-updates.js +301 -0
  63. package/dist/lib/pending-updates.js.map +1 -0
  64. package/dist/lib/sprint-loader.d.ts +86 -14
  65. package/dist/lib/sprint-loader.d.ts.map +1 -1
  66. package/dist/lib/sprint-loader.js +293 -98
  67. package/dist/lib/sprint-loader.js.map +1 -1
  68. package/dist/lib/state-cache.d.ts +142 -0
  69. package/dist/lib/state-cache.d.ts.map +1 -0
  70. package/dist/lib/state-cache.js +259 -0
  71. package/dist/lib/state-cache.js.map +1 -0
  72. package/dist/lib/task-graph-sync.d.ts +105 -0
  73. package/dist/lib/task-graph-sync.d.ts.map +1 -0
  74. package/dist/lib/task-graph-sync.js +178 -0
  75. package/dist/lib/task-graph-sync.js.map +1 -0
  76. package/dist/lib/task-parser.d.ts +107 -0
  77. package/dist/lib/task-parser.d.ts.map +1 -0
  78. package/dist/lib/task-parser.js +384 -0
  79. package/dist/lib/task-parser.js.map +1 -0
  80. package/dist/templates/ai-instructions-template.d.ts.map +1 -1
  81. package/dist/templates/ai-instructions-template.js +7 -5
  82. package/dist/templates/ai-instructions-template.js.map +1 -1
  83. package/dist/templates/epic-template.md +0 -2
  84. package/dist/types/config.d.ts.map +1 -1
  85. package/dist/types/config.js +7 -5
  86. package/dist/types/config.js.map +1 -1
  87. package/dist/utils/synthesis.d.ts +4 -0
  88. package/dist/utils/synthesis.d.ts.map +1 -1
  89. package/dist/utils/synthesis.js +12 -18
  90. package/dist/utils/synthesis.js.map +1 -1
  91. package/package.json +1 -1
@@ -0,0 +1,178 @@
1
+ /**
2
+ * @fileType: utility
3
+ * @status: current
4
+ * @updated: 2026-01-19
5
+ * @tags: [task-sync, graph, neo4j, epic-015, sprint-0a]
6
+ * @related: [task-parser.ts, ../commands/graph/api-client.ts]
7
+ * @priority: high
8
+ * @complexity: medium
9
+ * @dependencies: [api-client]
10
+ */
11
+ /**
12
+ * Task Graph Sync (EPIC-015 Sprint 0a Tasks 2-3)
13
+ *
14
+ * Syncs parsed tasks to Neo4j graph via the dashboard API.
15
+ * Creates Task nodes and BELONGS_TO relationships.
16
+ *
17
+ * Key principle (ADR-060): Content from Git, State from Graph.
18
+ * - On CREATE: Uses initial_status from markdown
19
+ * - On UPDATE: Preserves existing status (graph-authoritative)
20
+ */
21
+ import { GraphApiClient } from '../commands/graph/api-client.js';
22
+ import { loadGraphConfig, isGraphInitialized } from '../commands/graph/config.js';
23
+ /**
24
+ * Sync tasks to graph via API
25
+ *
26
+ * @param tasks - Array of parsed tasks
27
+ * @param graphId - Graph namespace ID
28
+ * @param client - GraphApiClient instance
29
+ * @param options - Sync options
30
+ * @returns TaskSyncResponse
31
+ */
32
+ export async function syncTasksToGraph(tasks, graphId, client, options = {}) {
33
+ const { createRelationships = true, batchSize = 50, onProgress } = options;
34
+ if (tasks.length === 0) {
35
+ return {
36
+ success: true,
37
+ created: 0,
38
+ updated: 0,
39
+ relationships: 0,
40
+ tasks: [],
41
+ };
42
+ }
43
+ // Process in batches
44
+ const batches = [];
45
+ for (let i = 0; i < tasks.length; i += batchSize) {
46
+ batches.push(tasks.slice(i, i + batchSize));
47
+ }
48
+ let totalCreated = 0;
49
+ let totalUpdated = 0;
50
+ let totalRelationships = 0;
51
+ const allTaskIds = [];
52
+ for (let i = 0; i < batches.length; i++) {
53
+ const batch = batches[i];
54
+ const response = await client.syncTasks(graphId, batch, createRelationships);
55
+ totalCreated += response.created;
56
+ totalUpdated += response.updated;
57
+ totalRelationships += response.relationships;
58
+ allTaskIds.push(...response.tasks);
59
+ if (onProgress) {
60
+ const synced = Math.min((i + 1) * batchSize, tasks.length);
61
+ onProgress(synced, tasks.length);
62
+ }
63
+ }
64
+ return {
65
+ success: true,
66
+ created: totalCreated,
67
+ updated: totalUpdated,
68
+ relationships: totalRelationships,
69
+ tasks: allTaskIds,
70
+ };
71
+ }
72
+ /**
73
+ * Sync a single sprint's tasks to graph
74
+ *
75
+ * @param sprintResult - Parsed sprint with tasks
76
+ * @param client - GraphApiClient instance (optional, will create if not provided)
77
+ * @param options - Sync options
78
+ * @returns TaskSyncResponse
79
+ */
80
+ export async function syncSprintTasksToGraph(sprintResult, client, options = {}) {
81
+ // Check if graph is initialized
82
+ if (!await isGraphInitialized()) {
83
+ throw new Error('Graph not initialized. Run "ginko graph init" first.');
84
+ }
85
+ // Load config to get graphId
86
+ const config = await loadGraphConfig();
87
+ if (!config) {
88
+ throw new Error('Failed to load graph configuration');
89
+ }
90
+ // Create client if not provided
91
+ const apiClient = client || new GraphApiClient(config.apiEndpoint);
92
+ return syncTasksToGraph(sprintResult.tasks, config.graphId, apiClient, options);
93
+ }
94
+ /**
95
+ * Sync multiple sprints' tasks to graph
96
+ *
97
+ * @param sprintResults - Array of parsed sprints with tasks
98
+ * @param options - Sync options
99
+ * @returns BatchSyncResult
100
+ */
101
+ export async function syncAllSprintTasksToGraph(sprintResults, options = {}) {
102
+ // Check if graph is initialized
103
+ if (!await isGraphInitialized()) {
104
+ return {
105
+ success: false,
106
+ totalTasks: 0,
107
+ created: 0,
108
+ updated: 0,
109
+ relationships: 0,
110
+ errors: ['Graph not initialized. Run "ginko graph init" first.'],
111
+ };
112
+ }
113
+ // Load config
114
+ const config = await loadGraphConfig();
115
+ if (!config) {
116
+ return {
117
+ success: false,
118
+ totalTasks: 0,
119
+ created: 0,
120
+ updated: 0,
121
+ relationships: 0,
122
+ errors: ['Failed to load graph configuration'],
123
+ };
124
+ }
125
+ // Collect all tasks
126
+ const allTasks = [];
127
+ for (const result of sprintResults) {
128
+ allTasks.push(...result.tasks);
129
+ }
130
+ if (allTasks.length === 0) {
131
+ return {
132
+ success: true,
133
+ totalTasks: 0,
134
+ created: 0,
135
+ updated: 0,
136
+ relationships: 0,
137
+ errors: [],
138
+ };
139
+ }
140
+ // Create client
141
+ const client = new GraphApiClient(config.apiEndpoint);
142
+ const errors = [];
143
+ try {
144
+ const response = await syncTasksToGraph(allTasks, config.graphId, client, options);
145
+ return {
146
+ success: true,
147
+ totalTasks: allTasks.length,
148
+ created: response.created,
149
+ updated: response.updated,
150
+ relationships: response.relationships,
151
+ errors,
152
+ };
153
+ }
154
+ catch (error) {
155
+ errors.push(error instanceof Error ? error.message : 'Unknown error during sync');
156
+ return {
157
+ success: false,
158
+ totalTasks: allTasks.length,
159
+ created: 0,
160
+ updated: 0,
161
+ relationships: 0,
162
+ errors,
163
+ };
164
+ }
165
+ }
166
+ /**
167
+ * Get task sync status from graph
168
+ *
169
+ * @param graphId - Graph namespace ID
170
+ * @param sprintId - Optional sprint ID filter
171
+ * @param epicId - Optional epic ID filter
172
+ * @param client - GraphApiClient instance
173
+ * @returns Array of task status objects
174
+ */
175
+ export async function getTasksFromGraph(graphId, client, filters) {
176
+ return client.getTasks(graphId, filters);
177
+ }
178
+ //# sourceMappingURL=task-graph-sync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task-graph-sync.js","sourceRoot":"","sources":["../../src/lib/task-graph-sync.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,107 @@
1
+ /**
2
+ * @fileType: utility
3
+ * @status: current
4
+ * @updated: 2026-01-19
5
+ * @tags: [task-parser, sprint, epic-015, sprint-0a]
6
+ * @related: [sprint-parser.ts, task-graph-sync.ts]
7
+ * @priority: high
8
+ * @complexity: medium
9
+ * @dependencies: [fs-extra]
10
+ */
11
+ /**
12
+ * Task status values (aligned with Status API)
13
+ */
14
+ export type TaskStatus = 'not_started' | 'in_progress' | 'blocked' | 'complete' | 'paused';
15
+ /**
16
+ * Parsed task from sprint markdown
17
+ */
18
+ export interface ParsedTask {
19
+ /** Task ID (e.g., e015_s00a_t01, TASK-1, adhoc_260119_s01_t01) */
20
+ id: string;
21
+ /** Derived sprint ID (e.g., e015_s00a, adhoc_260119_s01) */
22
+ sprint_id: string;
23
+ /** Derived epic ID (e.g., e015, adhoc_260119) */
24
+ epic_id: string;
25
+ /** Task title */
26
+ title: string;
27
+ /** Estimated effort (e.g., "3h", "4-6h") */
28
+ estimate: string | null;
29
+ /** Priority level */
30
+ priority: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW';
31
+ /** Assignee email or null */
32
+ assignee: string | null;
33
+ /** Initial status from checkbox (used only on CREATE) */
34
+ initial_status: TaskStatus;
35
+ /** Task goal/description */
36
+ goal: string | null;
37
+ /** Acceptance criteria list */
38
+ acceptance_criteria: string[];
39
+ /** Referenced files */
40
+ files: string[];
41
+ /** Related ADR references */
42
+ related_adrs: string[];
43
+ }
44
+ /**
45
+ * Sprint metadata extracted alongside tasks
46
+ */
47
+ export interface ParsedSprint {
48
+ /** Sprint ID (derived from filename or content) */
49
+ id: string;
50
+ /** Sprint name */
51
+ name: string;
52
+ /** Epic ID */
53
+ epic_id: string;
54
+ /** Sprint file path */
55
+ file_path: string;
56
+ }
57
+ /**
58
+ * Result of parsing a sprint file
59
+ */
60
+ export interface SprintParseResult {
61
+ sprint: ParsedSprint;
62
+ tasks: ParsedTask[];
63
+ }
64
+ /**
65
+ * Parse task hierarchy from task ID
66
+ *
67
+ * @param taskId - Task ID in various formats
68
+ * @returns Object with sprint_id and epic_id, or null if invalid
69
+ */
70
+ export declare function parseTaskHierarchy(taskId: string): {
71
+ sprint_id: string;
72
+ epic_id: string;
73
+ } | null;
74
+ /**
75
+ * Parse a single task block
76
+ *
77
+ * @param blockText - Raw markdown text for one task
78
+ * @param sprintContext - Sprint context for legacy TASK-N format
79
+ * @returns ParsedTask or null if invalid
80
+ */
81
+ export declare function parseTaskBlock(blockText: string, sprintContext?: {
82
+ sprint_id: string;
83
+ epic_id: string;
84
+ }): ParsedTask | null;
85
+ /**
86
+ * Parse sprint markdown file to extract all tasks
87
+ *
88
+ * @param content - Raw sprint markdown content
89
+ * @param filePath - Path to sprint file (for metadata extraction)
90
+ * @returns SprintParseResult with sprint metadata and parsed tasks
91
+ */
92
+ export declare function parseSprintTasks(content: string, filePath: string): SprintParseResult;
93
+ /**
94
+ * Parse sprint file from filesystem
95
+ *
96
+ * @param filePath - Absolute path to sprint markdown file
97
+ * @returns SprintParseResult or null if file not found
98
+ */
99
+ export declare function parseSprintFile(filePath: string): Promise<SprintParseResult | null>;
100
+ /**
101
+ * Parse all sprint files in a directory
102
+ *
103
+ * @param sprintsDir - Path to sprints directory
104
+ * @returns Array of SprintParseResult
105
+ */
106
+ export declare function parseAllSprints(sprintsDir: string): Promise<SprintParseResult[]>;
107
+ //# sourceMappingURL=task-parser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task-parser.d.ts","sourceRoot":"","sources":["../../src/lib/task-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAmBH;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,aAAa,GAAG,aAAa,GAAG,SAAS,GAAG,UAAU,GAAG,QAAQ,CAAC;AAE3F;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,kEAAkE;IAClE,EAAE,EAAE,MAAM,CAAC;IACX,4DAA4D;IAC5D,SAAS,EAAE,MAAM,CAAC;IAClB,iDAAiD;IACjD,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,4CAA4C;IAC5C,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,qBAAqB;IACrB,QAAQ,EAAE,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;IACjD,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,yDAAyD;IACzD,cAAc,EAAE,UAAU,CAAC;IAC3B,4BAA4B;IAC5B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,+BAA+B;IAC/B,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,uBAAuB;IACvB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,6BAA6B;IAC7B,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,mDAAmD;IACnD,EAAE,EAAE,MAAM,CAAC;IACX,kBAAkB;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,cAAc;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,uBAAuB;IACvB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,YAAY,CAAC;IACrB,KAAK,EAAE,UAAU,EAAE,CAAC;CACrB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA8BhG;AA4GD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,aAAa,CAAC,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACrD,UAAU,GAAG,IAAI,CAgGnB;AAgFD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,iBAAiB,CAwBrF;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAazF;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAyBtF"}
@@ -0,0 +1,384 @@
1
+ /**
2
+ * @fileType: utility
3
+ * @status: current
4
+ * @updated: 2026-01-19
5
+ * @tags: [task-parser, sprint, epic-015, sprint-0a]
6
+ * @related: [sprint-parser.ts, task-graph-sync.ts]
7
+ * @priority: high
8
+ * @complexity: medium
9
+ * @dependencies: [fs-extra]
10
+ */
11
+ /**
12
+ * Task Parser for Sprint Markdown (EPIC-015 Sprint 0a Task 1)
13
+ *
14
+ * Parses task definitions from sprint markdown files into structured data
15
+ * for syncing to Neo4j graph. Handles multiple task ID formats:
16
+ * - Standard: e{NNN}_s{NN}_t{NN} (e.g., e015_s00a_t01)
17
+ * - Legacy: TASK-N (e.g., TASK-1)
18
+ * - Ad-hoc: adhoc_{YYMMDD}_s{NN}_t{NN} (e.g., adhoc_260119_s01_t01)
19
+ *
20
+ * Key principle (ADR-060): Content from Git, State from Graph.
21
+ * Parser extracts CONTENT fields only (title, goal, priority, estimate).
22
+ * Status in markdown is only used for initial creation, not updates.
23
+ */
24
+ import fs from 'fs-extra';
25
+ import path from 'path';
26
+ /**
27
+ * Parse task hierarchy from task ID
28
+ *
29
+ * @param taskId - Task ID in various formats
30
+ * @returns Object with sprint_id and epic_id, or null if invalid
31
+ */
32
+ export function parseTaskHierarchy(taskId) {
33
+ // Standard format: e015_s00a_t01 or e015_s00_t01
34
+ const standardMatch = taskId.match(/^(e\d{3})_(s\d{2}[a-z]?)_(t\d{2})$/i);
35
+ if (standardMatch) {
36
+ const epicId = standardMatch[1].toLowerCase();
37
+ const sprintSuffix = standardMatch[2].toLowerCase();
38
+ return {
39
+ sprint_id: `${epicId}_${sprintSuffix}`,
40
+ epic_id: epicId,
41
+ };
42
+ }
43
+ // Ad-hoc format: adhoc_260119_s01_t01
44
+ const adhocMatch = taskId.match(/^(adhoc_\d{6})_(s\d{2})_(t\d{2})$/i);
45
+ if (adhocMatch) {
46
+ const adhocId = adhocMatch[1].toLowerCase();
47
+ const sprintSuffix = adhocMatch[2].toLowerCase();
48
+ return {
49
+ sprint_id: `${adhocId}_${sprintSuffix}`,
50
+ epic_id: adhocId,
51
+ };
52
+ }
53
+ // Legacy TASK-N format - derive from context (requires sprint info)
54
+ // For legacy format, we cannot derive hierarchy without sprint context
55
+ if (taskId.match(/^TASK-\d+$/i)) {
56
+ return null; // Caller must provide sprint context
57
+ }
58
+ return null;
59
+ }
60
+ /**
61
+ * Map checkbox character to task status
62
+ *
63
+ * @param checkbox - Single character from checkbox ([x], [@], [ ], [Z])
64
+ * @returns TaskStatus value
65
+ */
66
+ function mapCheckboxToStatus(checkbox) {
67
+ if (!checkbox)
68
+ return 'not_started';
69
+ const char = checkbox.trim().toLowerCase();
70
+ switch (char) {
71
+ case 'x':
72
+ return 'complete';
73
+ case '@':
74
+ return 'in_progress';
75
+ case 'z':
76
+ return 'paused';
77
+ case ' ':
78
+ default:
79
+ return 'not_started';
80
+ }
81
+ }
82
+ /**
83
+ * Normalize priority value
84
+ */
85
+ function normalizePriority(priority) {
86
+ if (!priority)
87
+ return 'MEDIUM';
88
+ const upper = priority.trim().toUpperCase();
89
+ if (['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'].includes(upper)) {
90
+ return upper;
91
+ }
92
+ return 'MEDIUM';
93
+ }
94
+ /**
95
+ * Extract acceptance criteria from task block
96
+ */
97
+ function extractAcceptanceCriteria(blockText) {
98
+ const criteria = [];
99
+ // Find acceptance criteria section
100
+ const sectionMatch = blockText.match(/\*\*Acceptance Criteria:\*\*\s*([\s\S]*?)(?=\n\*\*|\n###|\n---|\n##|$)/i);
101
+ if (!sectionMatch)
102
+ return criteria;
103
+ // Extract checkbox items: - [ ] or - [x]
104
+ const checkboxMatches = sectionMatch[1].matchAll(/^-\s+\[.\]\s+(.+?)$/gm);
105
+ for (const match of checkboxMatches) {
106
+ criteria.push(match[1].trim());
107
+ }
108
+ // Also extract plain bullets if no checkboxes found
109
+ if (criteria.length === 0) {
110
+ const bulletMatches = sectionMatch[1].matchAll(/^-\s+(.+?)$/gm);
111
+ for (const match of bulletMatches) {
112
+ criteria.push(match[1].trim());
113
+ }
114
+ }
115
+ return criteria;
116
+ }
117
+ /**
118
+ * Extract file paths from task block
119
+ */
120
+ function extractFiles(blockText) {
121
+ const files = new Set();
122
+ // Pattern: **Files:** section with bullet list
123
+ const filesSection = blockText.match(/\*\*Files(?:\sto\s(?:Create|Modify))?:\*\*\s*([\s\S]*?)(?=\n\*\*|\n###|\n---|\n##|$)/i);
124
+ if (filesSection) {
125
+ // Match: - Create: `path/to/file.ts` or - Modify: `path/to/file.ts` or just `path`
126
+ const pathMatches = filesSection[1].matchAll(/`([^`]+\.[a-z]+)`/gi);
127
+ for (const match of pathMatches) {
128
+ files.add(match[1]);
129
+ }
130
+ }
131
+ // Pattern: inline code paths that look like file paths
132
+ const inlineMatches = blockText.matchAll(/`((?:packages|dashboard|src|docs)\/[^`]+\.[a-z]+)`/gi);
133
+ for (const match of inlineMatches) {
134
+ files.add(match[1]);
135
+ }
136
+ return Array.from(files).sort();
137
+ }
138
+ /**
139
+ * Extract related ADR references
140
+ */
141
+ function extractRelatedADRs(blockText) {
142
+ const adrs = new Set();
143
+ // Match ADR-XXX patterns
144
+ const adrMatches = blockText.matchAll(/ADR-(\d+)/gi);
145
+ for (const match of adrMatches) {
146
+ adrs.add(`ADR-${match[1]}`);
147
+ }
148
+ return Array.from(adrs).sort();
149
+ }
150
+ /**
151
+ * Parse a single task block
152
+ *
153
+ * @param blockText - Raw markdown text for one task
154
+ * @param sprintContext - Sprint context for legacy TASK-N format
155
+ * @returns ParsedTask or null if invalid
156
+ */
157
+ export function parseTaskBlock(blockText, sprintContext) {
158
+ // Extract task header - multiple formats supported
159
+ // Standard: ### e015_s00a_t01: Title (3h)
160
+ // Legacy: ### TASK-1: Title (4-6h)
161
+ // Ad-hoc: ### adhoc_260119_s01_t01 - Title
162
+ // Without time: ### e015_s00a_t01: Title
163
+ const headerPatterns = [
164
+ // Standard with colon and optional time
165
+ /^###\s+([a-z0-9_]+):\s+(.+?)\s*(?:\(([0-9]+(?:-[0-9]+)?h?)\))?$/im,
166
+ // With dash separator (ad-hoc style)
167
+ /^###\s+([a-z0-9_]+)\s+-\s+(.+?)$/im,
168
+ // Legacy TASK-N format
169
+ /^###\s+(TASK-\d+):\s+(.+?)\s*(?:\(([0-9]+(?:-[0-9]+)?h?)\))?$/im,
170
+ ];
171
+ let taskId = null;
172
+ let title = null;
173
+ let estimate = null;
174
+ for (const pattern of headerPatterns) {
175
+ const match = blockText.match(pattern);
176
+ if (match) {
177
+ taskId = match[1];
178
+ title = match[2].trim();
179
+ estimate = match[3] || null;
180
+ break;
181
+ }
182
+ }
183
+ if (!taskId || !title) {
184
+ return null;
185
+ }
186
+ // Parse hierarchy
187
+ let hierarchy = parseTaskHierarchy(taskId);
188
+ // For legacy TASK-N format, use sprint context
189
+ if (!hierarchy && sprintContext) {
190
+ hierarchy = {
191
+ sprint_id: sprintContext.sprint_id,
192
+ epic_id: sprintContext.epic_id,
193
+ };
194
+ }
195
+ if (!hierarchy) {
196
+ // Cannot determine hierarchy, skip task
197
+ console.warn(`Cannot determine hierarchy for task: ${taskId}`);
198
+ return null;
199
+ }
200
+ // Extract status from checkbox: **Status:** [x]
201
+ const statusMatch = blockText.match(/\*\*Status:\*\*\s+\[(.)\]/i);
202
+ const initialStatus = mapCheckboxToStatus(statusMatch?.[1]);
203
+ // Extract priority
204
+ const priorityMatch = blockText.match(/\*\*Priority:\*\*\s+([A-Z_]+)/i);
205
+ const priority = normalizePriority(priorityMatch?.[1]);
206
+ // Extract assignee (accepts both Assignee and Owner)
207
+ const assigneeMatch = blockText.match(/\*\*(?:Assignee|Owner):\*\*\s+([^\n]+)/i);
208
+ let assignee = null;
209
+ if (assigneeMatch) {
210
+ const value = assigneeMatch[1].trim();
211
+ // Filter out "TBD", "None", empty values
212
+ if (value && !['tbd', 'none', 'n/a', '-'].includes(value.toLowerCase())) {
213
+ assignee = value;
214
+ }
215
+ }
216
+ // Extract goal
217
+ const goalMatch = blockText.match(/\*\*Goal:\*\*\s+([^\n]+)/i);
218
+ const goal = goalMatch ? goalMatch[1].trim() : null;
219
+ // Extract acceptance criteria
220
+ const acceptanceCriteria = extractAcceptanceCriteria(blockText);
221
+ // Extract files
222
+ const files = extractFiles(blockText);
223
+ // Extract related ADRs
224
+ const relatedADRs = extractRelatedADRs(blockText);
225
+ return {
226
+ id: taskId.toLowerCase(),
227
+ sprint_id: hierarchy.sprint_id,
228
+ epic_id: hierarchy.epic_id,
229
+ title,
230
+ estimate,
231
+ priority,
232
+ assignee,
233
+ initial_status: initialStatus,
234
+ goal,
235
+ acceptance_criteria: acceptanceCriteria,
236
+ files,
237
+ related_adrs: relatedADRs,
238
+ };
239
+ }
240
+ /**
241
+ * Extract sprint metadata from sprint file
242
+ *
243
+ * @param content - Sprint file content
244
+ * @param filePath - Path to sprint file
245
+ * @returns ParsedSprint metadata
246
+ */
247
+ function extractSprintMetadata(content, filePath) {
248
+ const filename = path.basename(filePath, '.md');
249
+ // Try to extract sprint ID from filename
250
+ // Pattern: SPRINT-2026-01-e015-s00a-... → e015_s00a
251
+ const filenameMatch = filename.match(/SPRINT-\d{4}-\d{2}-(e\d{3})-(s\d{2}[a-z]?)-/i);
252
+ if (filenameMatch) {
253
+ const epicId = filenameMatch[1].toLowerCase();
254
+ const sprintSuffix = filenameMatch[2].toLowerCase();
255
+ const sprintId = `${epicId}_${sprintSuffix}`;
256
+ // Extract sprint name from title
257
+ const titleMatch = content.match(/^#\s+(?:SPRINT:\s+)?(.+?)(?:\s+\(|$)/m);
258
+ const name = titleMatch ? titleMatch[1].trim() : filename;
259
+ return {
260
+ id: sprintId,
261
+ name,
262
+ epic_id: epicId,
263
+ file_path: filePath,
264
+ };
265
+ }
266
+ // Ad-hoc pattern: SPRINT-adhoc_260119-...
267
+ const adhocMatch = filename.match(/SPRINT-(adhoc_\d{6})-/i);
268
+ if (adhocMatch) {
269
+ const adhocId = adhocMatch[1].toLowerCase();
270
+ const sprintId = `${adhocId}_s01`; // Default to s01 for adhoc
271
+ const titleMatch = content.match(/^#\s+(.+?)(?:\s+\(|$)/m);
272
+ const name = titleMatch ? titleMatch[1].trim() : filename;
273
+ return {
274
+ id: sprintId,
275
+ name,
276
+ epic_id: adhocId,
277
+ file_path: filePath,
278
+ };
279
+ }
280
+ // Legacy pattern from content: # SPRINT: Name (EPIC-XXX Sprint N)
281
+ const legacyMatch = content.match(/^#\s+SPRINT:\s+(.+?)\s+\(EPIC-(\d+)\s+Sprint\s+(\d+)\)/m);
282
+ if (legacyMatch) {
283
+ const name = legacyMatch[1].trim();
284
+ const epicNum = legacyMatch[2];
285
+ const sprintNum = legacyMatch[3];
286
+ const epicId = `e${epicNum.padStart(3, '0')}`;
287
+ const sprintId = `${epicId}_s${sprintNum.padStart(2, '0')}`;
288
+ return {
289
+ id: sprintId,
290
+ name,
291
+ epic_id: epicId,
292
+ file_path: filePath,
293
+ };
294
+ }
295
+ // Fallback: generate from filename
296
+ const fallbackId = filename
297
+ .toLowerCase()
298
+ .replace(/[^a-z0-9]+/g, '_')
299
+ .replace(/^sprint_/, '');
300
+ return {
301
+ id: fallbackId,
302
+ name: filename,
303
+ epic_id: 'unknown',
304
+ file_path: filePath,
305
+ };
306
+ }
307
+ /**
308
+ * Parse sprint markdown file to extract all tasks
309
+ *
310
+ * @param content - Raw sprint markdown content
311
+ * @param filePath - Path to sprint file (for metadata extraction)
312
+ * @returns SprintParseResult with sprint metadata and parsed tasks
313
+ */
314
+ export function parseSprintTasks(content, filePath) {
315
+ // Extract sprint metadata
316
+ const sprint = extractSprintMetadata(content, filePath);
317
+ // Split content by task headers (### followed by task ID pattern)
318
+ // Match: ### e015_s00a_t01:, ### TASK-1:, ### adhoc_..._t01
319
+ const taskSections = content.split(/(?=^###\s+(?:e\d{3}_s\d{2}[a-z]?_t\d{2}|TASK-\d+|adhoc_\d{6}_s\d{2}_t\d{2})[\s:-])/im);
320
+ const tasks = [];
321
+ const sprintContext = {
322
+ sprint_id: sprint.id,
323
+ epic_id: sprint.epic_id,
324
+ };
325
+ for (const section of taskSections) {
326
+ if (!section.trim().startsWith('###'))
327
+ continue;
328
+ const task = parseTaskBlock(section, sprintContext);
329
+ if (task) {
330
+ tasks.push(task);
331
+ }
332
+ }
333
+ return { sprint, tasks };
334
+ }
335
+ /**
336
+ * Parse sprint file from filesystem
337
+ *
338
+ * @param filePath - Absolute path to sprint markdown file
339
+ * @returns SprintParseResult or null if file not found
340
+ */
341
+ export async function parseSprintFile(filePath) {
342
+ try {
343
+ if (!await fs.pathExists(filePath)) {
344
+ console.warn(`Sprint file not found: ${filePath}`);
345
+ return null;
346
+ }
347
+ const content = await fs.readFile(filePath, 'utf-8');
348
+ return parseSprintTasks(content, filePath);
349
+ }
350
+ catch (error) {
351
+ console.error(`Failed to parse sprint file ${filePath}:`, error);
352
+ return null;
353
+ }
354
+ }
355
+ /**
356
+ * Parse all sprint files in a directory
357
+ *
358
+ * @param sprintsDir - Path to sprints directory
359
+ * @returns Array of SprintParseResult
360
+ */
361
+ export async function parseAllSprints(sprintsDir) {
362
+ const results = [];
363
+ try {
364
+ if (!await fs.pathExists(sprintsDir)) {
365
+ console.warn(`Sprints directory not found: ${sprintsDir}`);
366
+ return results;
367
+ }
368
+ const files = await fs.readdir(sprintsDir);
369
+ const sprintFiles = files.filter(f => f.startsWith('SPRINT-') && f.endsWith('.md'));
370
+ for (const file of sprintFiles) {
371
+ const filePath = path.join(sprintsDir, file);
372
+ const result = await parseSprintFile(filePath);
373
+ if (result) {
374
+ results.push(result);
375
+ }
376
+ }
377
+ return results;
378
+ }
379
+ catch (error) {
380
+ console.error(`Failed to parse sprints directory ${sprintsDir}:`, error);
381
+ return results;
382
+ }
383
+ }
384
+ //# sourceMappingURL=task-parser.js.map