From 4eda494624775ba9ba8af04b726ba9ededeae78b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=AE=97=E5=B9=B3?= Date: Wed, 10 Jun 2026 17:34:04 +0800 Subject: [PATCH] feat(ext): add trigger-detector for goal-driven-loop --- .../goal-driven-loop/trigger-detector.ts | 218 ++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 .pi/extensions/goal-driven-loop/trigger-detector.ts diff --git a/.pi/extensions/goal-driven-loop/trigger-detector.ts b/.pi/extensions/goal-driven-loop/trigger-detector.ts new file mode 100644 index 0000000..65d85e6 --- /dev/null +++ b/.pi/extensions/goal-driven-loop/trigger-detector.ts @@ -0,0 +1,218 @@ +/** + * 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, +]; + +const TRIVIAL_KEYWORDS = [ + "hotfix", + "tweak", + "fix a 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 + if (/```[\s\S]*?```/.test(input) && containsTriggerInCodeBlock(input)) { + return null; + } + + // Anti-trigger: markdown quote block + const lines = input.split("\n"); + for (const line of lines) { + if (/^\s*>/.test(line)) { + const triggerInQuote = TRIGGER_PATTERNS.some((p) => p.test(line)); + if (triggerInQuote) { + return null; + } + } + } + + // Anti-trigger: reported speech + const lowerInput = input.toLowerCase(); + const hasReportedSpeech = REPORTED_SPEECH_MARKERS.some((marker) => + lowerInput.includes(marker) + ); + if (hasReportedSpeech) { + const sentences = input.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 = input.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 containsTriggerInCodeBlock(input: string): boolean { + const codeBlocks = input.match(/```[\s\S]*?```/g) || []; + for (const block of codeBlocks) { + for (const pattern of TRIGGER_PATTERNS) { + if (pattern.test(block)) { + return true; + } + } + } + return false; +} + +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; +}