import { STAGE_SEQUENCE, STRATEGIC_KEYWORDS, TECHNICAL_KEYWORDS } from './constants.js'; import type { BriefClass, ClassSource } from './types.js'; const VALID_CLASSES: ReadonlySet = new Set([ 'strategic', 'technical', 'hotfix', ]); /** * Auto-classify a brief based on keyword analysis. * Returns 'strategic' if strategic keywords dominate, * 'technical' if any technical keywords are found, * otherwise defaults to 'strategic' (full pipeline). */ export function classifyBrief(text: string): BriefClass { const lower = text.toLowerCase(); let strategicHits = 0; let technicalHits = 0; for (const kw of STRATEGIC_KEYWORDS) { if (lower.includes(kw)) strategicHits++; } for (const kw of TECHNICAL_KEYWORDS) { if (lower.includes(kw)) technicalHits++; } if (strategicHits > technicalHits) return 'strategic'; if (technicalHits > 0) return 'technical'; return 'strategic'; } /** * Parse YAML frontmatter from a brief. * Supports simple `key: value` pairs via regex (no YAML dependency). */ export function parseBriefFrontmatter(text: string): Record { const match = text.match(/^---\s*\n([\s\S]*?)\n---\s*\n/); if (!match?.[1]) return {}; const result: Record = {}; for (const line of match[1].split('\n')) { const km = line.trim().match(/^(\w[\w-]*)\s*:\s*(.+)$/); if (km?.[1] && km[2]) { result[km[1]] = km[2].trim().replace(/^["']|["']$/g, ''); } } return result; } /** * Determine brief class from all sources with priority: * CLI flag > frontmatter > auto-classify. */ export function determineBriefClass( text: string, cliClass?: string, ): { briefClass: BriefClass; classSource: ClassSource } { if (cliClass && VALID_CLASSES.has(cliClass)) { return { briefClass: cliClass as BriefClass, classSource: 'cli' }; } const fm = parseBriefFrontmatter(text); if (fm['class'] && VALID_CLASSES.has(fm['class'])) { return { briefClass: fm['class'] as BriefClass, classSource: 'frontmatter' }; } return { briefClass: classifyBrief(text), classSource: 'auto' }; } /** * Build the stage list based on brief classification. * - strategic: full pipeline (all stages) * - technical: skip board (01-board) * - hotfix: skip board + brief analyzer * * forceBoard re-adds the board stage regardless of class. */ export function stagesForClass(briefClass: BriefClass, forceBoard = false): string[] { const stages = ['00-intake', '00b-discovery']; if (briefClass === 'strategic' || forceBoard) { stages.push('01-board'); } if (briefClass === 'strategic' || briefClass === 'technical' || forceBoard) { stages.push('01b-brief-analyzer'); } stages.push( '02-planning-1', '03-planning-2', '04-planning-3', '05-coding', '06-review', '07-remediate', '08-test', '09-deploy', ); // Maintain canonical order return stages.filter((s) => STAGE_SEQUENCE.includes(s)); }