/** * Trigger detection for goal-driven-loop. * * Detects goal-setting phrases in user input and validates them * against anti-trigger rules. */ export interface TriggerMatch { goalText: string; rawCriteria?: string; } /** * Trigger phrases — order matters (longer patterns first to avoid * "当前目标是" matching "是" alone). */ const TRIGGER_PATTERNS: RegExp[] = [ // Chinese /设定当前目标为\s*(.+)/, /把当前目标设为\s*(.+)/, /当前目标是\s*(.+)/, /我的目标是\s*(.+)/, // English /set\s+(?:the\s+)?current\s+goal\s+to\s+(.+)/i, /current\s+goal\s+is\s+(.+)/i, /my\s+goal\s+is\s+(.+)/i, /the\s+goal\s+is\s+(.+)/i, // Uncertainty fallback: commitment language + deliverable /(?:goal|目标)\s*(?:is|为|是)\s+(?:(?:to|去)\s+)?(.+)/i, ]; const TRIVIAL_KEYWORDS = [ "hotfix", "tweak", "fix a bug", "fix bug", "small change", "quick fix", "修个bug", "小改", ]; const ARCHITECTURAL_KEYWORDS = [ "architecture", "architectural", "module", "system", "refactor", "integration", "重构", "模块", "系统", "集成", ]; const REPORTED_SPEECH_MARKERS = [ "he said", "she said", "they said", "he says", "she says", "they say", "他说", "她说", "他们说", ]; /** * Detect if the input is a goal-setting trigger and extract the goal text. * Returns null if no trigger or if anti-trigger rules match. */ export function detectTrigger(input: string): TriggerMatch | null { // Anti-trigger: code block — remove code blocks from input before checking // for trigger phrases, so triggers outside code blocks still fire. const inputWithoutCode = input.replace(/```[\s\S]*?```/g, ""); // Anti-trigger: markdown quote block — only block if trigger phrase appears // exclusively inside quote lines (no trigger in non-quote content) const nonQuoteLines = inputWithoutCode .split("\n") .filter((line) => !/^\s*>/.test(line)); const nonQuoteContent = nonQuoteLines.join("\n"); const hasTriggerOutsideQuote = TRIGGER_PATTERNS.some((p) => p.test(nonQuoteContent) ); if (!hasTriggerOutsideQuote) { // All trigger phrases are inside quotes or absent — block return null; } // Use the code-stripped, non-quote content for further checks const effectiveInput = nonQuoteContent; // Anti-trigger: reported speech — check per-sentence const lowerInput = effectiveInput.toLowerCase(); const hasReportedSpeech = REPORTED_SPEECH_MARKERS.some((marker) => lowerInput.includes(marker) ); if (hasReportedSpeech) { const sentences = effectiveInput.split(/[.!?。!?]/); for (const sentence of sentences) { const lowerSentence = sentence.toLowerCase(); const hasMarker = REPORTED_SPEECH_MARKERS.some((m) => lowerSentence.includes(m) ); const hasTrigger = TRIGGER_PATTERNS.some((p) => p.test(sentence)); if (hasMarker && hasTrigger) { return null; } } } // Anti-trigger: trivial task keywords const hasTrivialKeyword = TRIVIAL_KEYWORDS.some((kw) => lowerInput.includes(kw.toLowerCase()) ); if (hasTrivialKeyword) { return null; } // Try to match trigger pattern for (const pattern of TRIGGER_PATTERNS) { const match = effectiveInput.match(pattern); if (match) { const fullText = match[1].trim(); // Validate: must be non-empty and not a short fragment if (fullText.length < 3) { return null; } // Anti-trigger: short description without architectural keywords if (fullText.length <= 10) { const hasArchitectural = ARCHITECTURAL_KEYWORDS.some((kw) => fullText.toLowerCase().includes(kw.toLowerCase()) ); if (!hasArchitectural) { return null; } } // Try to extract acceptance criteria const criteriaMatch = fullText.match( /(?:验收条件[是为::]\s*|acceptance\s*(?:criteria)?\s*[::]\s*)(.+)$/i ); if (criteriaMatch) { const goalText = fullText.substring(0, criteriaMatch.index).trim(); return { goalText: cleanGoalText(goalText), rawCriteria: criteriaMatch[1].trim(), }; } return { goalText: cleanGoalText(fullText), }; } } return null; } function cleanGoalText(text: string): string { return text.replace(/[.,;:。;,]+$/, "").trim(); } /** * Determine if a goal is coarse-grained (should delegate to comet). */ export function isCoarseGrained(goalText: string): boolean { const lower = goalText.toLowerCase(); const multiModuleSignals = [ "across", "throughout", "整个", "全", "all modules", "multiple", "multiple files", "多模块", ]; for (const signal of multiModuleSignals) { if (lower.includes(signal)) { return true; } } const fileCountMatch = goalText.match(/(\d+)\s*(?:个)?\s*(?:file|files|文件)/i); if (fileCountMatch) { const count = parseInt(fileCountMatch[1], 10); if (count > 5) { return true; } } const architecturalSignals = [ "redesign", "rewrite", "migrate", "重构", "迁移", "重写", "重新设计", ]; for (const signal of architecturalSignals) { if (lower.includes(signal.toLowerCase())) { return true; } } return false; }