| |
| |
| |
| |
| |
| |
|
|
| import type { ParsedTask } from '@automaker/types'; |
|
|
| |
| |
| |
| |
| function parseTaskLine(line: string, currentPhase?: string): ParsedTask | null { |
| |
| const taskMatch = line.match(/- \[ \] (T\d{3}):\s*([^|]+)(?:\|\s*File:\s*(.+))?$/); |
| if (!taskMatch) { |
| |
| const simpleMatch = line.match(/- \[ \] (T\d{3}):\s*(.+)$/); |
| if (simpleMatch) { |
| return { |
| id: simpleMatch[1], |
| description: simpleMatch[2].trim(), |
| phase: currentPhase, |
| status: 'pending', |
| }; |
| } |
| return null; |
| } |
|
|
| return { |
| id: taskMatch[1], |
| description: taskMatch[2].trim(), |
| filePath: taskMatch[3]?.trim(), |
| phase: currentPhase, |
| status: 'pending', |
| }; |
| } |
|
|
| |
| |
| |
| |
| |
| export function parseTasksFromSpec(specContent: string): ParsedTask[] { |
| const tasks: ParsedTask[] = []; |
|
|
| |
| const tasksBlockMatch = specContent.match(/```tasks\s*([\s\S]*?)```/); |
| if (!tasksBlockMatch) { |
| |
| const taskLines = specContent.match(/- \[ \] T\d{3}:.*$/gm); |
| if (!taskLines) { |
| return tasks; |
| } |
| |
| let currentPhase: string | undefined; |
| for (const line of taskLines) { |
| const parsed = parseTaskLine(line, currentPhase); |
| if (parsed) { |
| tasks.push(parsed); |
| } |
| } |
| return tasks; |
| } |
|
|
| const tasksContent = tasksBlockMatch[1]; |
| const lines = tasksContent.split('\n'); |
|
|
| let currentPhase: string | undefined; |
|
|
| for (const line of lines) { |
| const trimmedLine = line.trim(); |
|
|
| |
| const phaseMatch = trimmedLine.match(/^##\s*(.+)$/); |
| if (phaseMatch) { |
| currentPhase = phaseMatch[1].trim(); |
| continue; |
| } |
|
|
| |
| if (trimmedLine.startsWith('- [ ]')) { |
| const parsed = parseTaskLine(trimmedLine, currentPhase); |
| if (parsed) { |
| tasks.push(parsed); |
| } |
| } |
| } |
|
|
| return tasks; |
| } |
|
|
| |
| |
| |
| |
| export function detectTaskStartMarker(text: string): string | null { |
| const match = text.match(/\[TASK_START\]\s*(T\d{3})/); |
| return match ? match[1] : null; |
| } |
|
|
| |
| |
| |
| |
| export function detectTaskCompleteMarker(text: string): { id: string; summary?: string } | null { |
| |
| |
| |
| |
| |
| |
| |
| |
| const match = text.match(/\[TASK_COMPLETE\]\s*(T\d{3})(?::\s*(.+?))?(?=\n|$)/i); |
| if (!match) return null; |
|
|
| |
| let summary = match[2]?.trim(); |
| if (summary) { |
| |
| summary = summary.replace(/\s*\[TASK_[A-Z_]+\].*$/i, '').trim(); |
| } |
|
|
| return { |
| id: match[1], |
| summary: summary || undefined, |
| }; |
| } |
|
|
| |
| |
| |
| |
| export function detectPhaseCompleteMarker(text: string): number | null { |
| const match = text.match(/\[PHASE_COMPLETE\]\s*Phase\s*(\d+)/i); |
| return match ? parseInt(match[1], 10) : null; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function detectSpecFallback(text: string): boolean { |
| |
| const hasTasksBlock = /```tasks[\s\S]*```/.test(text); |
| const hasTaskLines = /- \[ \] T\d{3}:/.test(text); |
|
|
| |
| const hasAcceptanceCriteria = /acceptance criteria/i.test(text); |
| const hasTechnicalContext = /technical context/i.test(text); |
| const hasProblemStatement = /problem statement/i.test(text); |
| const hasUserStory = /user story/i.test(text); |
| |
| const hasGoal = /\*\*Goal\*\*:/i.test(text); |
| const hasSolution = /\*\*Solution\*\*:/i.test(text); |
| const hasImplementation = /implementation\s*(plan|steps|approach)/i.test(text); |
| const hasOverview = /##\s*(overview|summary)/i.test(text); |
|
|
| |
| const hasTaskStructure = hasTasksBlock || hasTaskLines; |
| const hasSpecContent = |
| hasAcceptanceCriteria || |
| hasTechnicalContext || |
| hasProblemStatement || |
| hasUserStory || |
| hasGoal || |
| hasSolution || |
| hasImplementation || |
| hasOverview; |
|
|
| return hasTaskStructure && hasSpecContent; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function extractSummary(text: string): string | null { |
| |
| const truncate = (content: string, maxLength: number): string => { |
| const firstPara = content.split(/\n\n/)[0]; |
| return firstPara.length > maxLength ? `${firstPara.substring(0, maxLength)}...` : firstPara; |
| }; |
|
|
| |
| const getLastMatch = (matches: IterableIterator<RegExpMatchArray>): RegExpMatchArray | null => { |
| const arr = [...matches]; |
| return arr.length > 0 ? arr[arr.length - 1] : null; |
| }; |
|
|
| |
| const summaryMatches = text.matchAll(/<summary>([\s\S]*?)<\/summary>/g); |
| const summaryMatch = getLastMatch(summaryMatches); |
| if (summaryMatch) { |
| return summaryMatch[1].trim(); |
| } |
|
|
| |
| |
| |
| const sectionMatches = text.matchAll(/##\s*Summary\s*\n+([\s\S]*?)(?=\n## [^#]|$)/gi); |
| const sectionMatch = getLastMatch(sectionMatches); |
| if (sectionMatch) { |
| const content = sectionMatch[1].trim(); |
| |
| return content.length > 500 ? `${content.substring(0, 500)}...` : content; |
| } |
|
|
| |
| const goalMatches = text.matchAll(/\*\*Goal\*\*:\s*(.+?)(?:\n|$)/gi); |
| const goalMatch = getLastMatch(goalMatches); |
| if (goalMatch) { |
| return goalMatch[1].trim(); |
| } |
|
|
| |
| const problemMatches = text.matchAll( |
| /\*\*Problem(?:\s*Statement)?\*\*:\s*([\s\S]*?)(?=\n\d+\.|\n\*\*|$)/gi |
| ); |
| const problemMatch = getLastMatch(problemMatches); |
| if (problemMatch) { |
| return truncate(problemMatch[1].trim(), 500); |
| } |
|
|
| |
| const solutionMatches = text.matchAll(/\*\*Solution\*\*:\s*([\s\S]*?)(?=\n\d+\.|\n\*\*|$)/gi); |
| const solutionMatch = getLastMatch(solutionMatches); |
| if (solutionMatch) { |
| return truncate(solutionMatch[1].trim(), 300); |
| } |
|
|
| return null; |
| } |
|
|