| 1 | package control |
| 2 | |
| 3 | import ( |
| 4 | "slices" |
| 5 | "strings" |
| 6 | "unicode/utf8" |
| 7 | ) |
| 8 | |
| 9 | // heuristicInputIsTask reports whether a user input reads as an actionable |
| 10 | // task rather than conversational chat. The delivery evidence gate uses it to |
| 11 | // decide when a turn should be held to acceptance-criteria expectations |
| 12 | // (NeedsEvidence); greetings and acknowledgements must not arm the |
| 13 | // delivery gates. |
| 14 | func heuristicInputIsTask(input string) bool { |
| 15 | trimmed := strings.TrimSpace(input) |
| 16 | if trimmed == "" { |
| 17 | return false |
| 18 | } |
| 19 | |
| 20 | normalized := strings.ToLower(strings.Trim(trimmed, " \t\r\n.!?。!?,,;;::")) |
| 21 | |
| 22 | // Very short greeting/acknowledgement whitelist (1-3 words). |
| 23 | shortGreetings := []string{ |
| 24 | "hello", "hi", "hey", "你好", "您好", "nihao", |
| 25 | "thanks", "thank you", "谢谢", "谢了", |
| 26 | "ok", "okay", "好的", "嗯", "行", |
| 27 | "got it", "i see", "明白", "了解", "收到", "我知道了", "先不用", |
| 28 | } |
| 29 | |
| 30 | words := strings.Fields(normalized) |
| 31 | if len(words) <= 3 { |
| 32 | if slices.Contains(shortGreetings, normalized) { |
| 33 | return false |
| 34 | } |
| 35 | } |
| 36 | |
| 37 | // Polite acknowledgements can contain action words from the completed |
| 38 | // task ("thanks for fixing") but should stay conversational. |
| 39 | chatPhrases := []string{ |
| 40 | "thanks for", "thank you for", "i'll check later", "i will check later", |
| 41 | "i'll test it later", "i will test it later", "that test was helpful", "the test was helpful", |
| 42 | "谢谢你", "辛苦了", |
| 43 | } |
| 44 | for _, phrase := range chatPhrases { |
| 45 | before, after, ok := strings.Cut(normalized, phrase) |
| 46 | if !ok { |
| 47 | continue |
| 48 | } |
| 49 | // Acknowledgement wording only short-circuits a purely conversational |
| 50 | // turn. Preserve a real task before it or after an explicit transition, |
| 51 | // e.g. "thanks for fixing that; now update the tests". |
| 52 | if prefix := strings.TrimSpace(before); prefix != "" && heuristicInputHasStrongTaskSignal(prefix) { |
| 53 | return true |
| 54 | } |
| 55 | if deliveryTaskHasFollowUpAfterChat(after) { |
| 56 | return true |
| 57 | } |
| 58 | return false |
| 59 | } |
| 60 | // Ambiguous prose stays conversational. Length is not a task signal; |
| 61 | // mutations, files, commands, failures, and audit verbs are classified below. |
| 62 | return heuristicInputHasStrongTaskSignal(normalized) |
| 63 | } |
| 64 | |
| 65 | func heuristicInputHasStrongTaskSignal(input string) bool { |
| 66 | normalized := strings.ToLower(strings.TrimSpace(input)) |
| 67 | // File references and concrete commands are strong task signals. Shared |
| 68 | // parsing keeps email addresses and remote product names from accidentally |
| 69 | // arming the delivery gate while covering ordinary repository file types. |
| 70 | if deliveryTaskHasFileReference(normalized) || deliveryTaskHasCommand(normalized) { |
| 71 | return true |
| 72 | } |
| 73 | // Mutation intent has a richer, negation-aware vocabulary than this generic |
| 74 | // task heuristic. Reuse it so short requests such as "push the branch" do not |
| 75 | // bypass delivery gates merely because the two keyword lists drift apart. |
| 76 | if deliveryTaskHasMutationIntent(normalized) || NeedsPersistentAction(normalized) { |
| 77 | return true |
| 78 | } |
| 79 | |
| 80 | // Failure/help descriptions are actionable even when phrased without an |
| 81 | // imperative verb, e.g. "the auth isn't working". Shared fault signals keep |
| 82 | // task recognition and Goal budget classification from drifting apart. |
| 83 | if taskInputHasFaultSignal(normalized) { |
| 84 | return true |
| 85 | } |
| 86 | helpPhrases := []string{ |
| 87 | "can you help", "help with", "cannot", "can't", |
| 88 | "无法", "不能", |
| 89 | } |
| 90 | for _, phrase := range helpPhrases { |
| 91 | if strings.Contains(normalized, phrase) { |
| 92 | return true |
| 93 | } |
| 94 | } |
| 95 | |
| 96 | // Action keyword detection. |
| 97 | actionNeedles := []string{ |
| 98 | "fix", "debug", "repair", "resolve", "reproduce", |
| 99 | "create", "add", "write", "edit", "update", "change", "delete", "remove", "rename", |
| 100 | "review", "inspect", "analyze", "check", "audit", "verify", "test", "run", "build", "implement", "refactor", "modify", "patch", "replace", |
| 101 | "configure", "upgrade", "downgrade", "enable", "disable", "merge", "make changes", "make a change", "make the changes", |
| 102 | "make the requested changes", "make the necessary changes", "make these changes", "make those changes", "make code changes", |
| 103 | "continue work", "continue the", "continue this", |
| 104 | "修复", "调试", "解决", "复现", "创建", "新建", "添加", "编写", "编辑", "修改", "更新", |
| 105 | "删除", "移除", "重命名", "评审", "检查", "分析", "审计", "验证", "测试", "运行", "构建", "实现", "重构", "继续处理", |
| 106 | "调整", "替换", "移动", "升级", "降级", "启用", "禁用", "合并", "改动", "打补丁", |
| 107 | "看看", "看下", "帮我看", "帮我看下", "处理下", "处理一下", "排查", "定位", |
| 108 | } |
| 109 | |
| 110 | for _, needle := range actionNeedles { |
| 111 | if containsTaskNeedle(normalized, needle) { |
| 112 | return true |
| 113 | } |
| 114 | } |
| 115 | |
| 116 | return false |
| 117 | } |
| 118 | |
| 119 | func deliveryTaskHasFollowUpAfterChat(input string) bool { |
| 120 | for index, current := range input { |
| 121 | switch current { |
| 122 | case '.', ',', ';', '!', '?', '。', ',', ';', '!', '?': |
| 123 | candidate := strings.TrimSpace(input[index+utf8.RuneLen(current):]) |
| 124 | if candidate != "" && heuristicInputHasStrongTaskSignal(candidate) { |
| 125 | return true |
| 126 | } |
| 127 | } |
| 128 | } |
| 129 | for _, cue := range []string{ |
| 130 | " but ", " however ", " nevertheless ", " now ", " then ", " and ", " please ", " so ", " therefore ", |
| 131 | "但是", "但请", "不过", "现在", "然后", "所以", "请", "继续", "再", |
| 132 | } { |
| 133 | for rest := input; ; { |
| 134 | index := strings.Index(rest, cue) |
| 135 | if index < 0 { |
| 136 | break |
| 137 | } |
| 138 | candidate := strings.TrimSpace(rest[index+len(cue):]) |
| 139 | if candidate != "" && heuristicInputHasStrongTaskSignal(candidate) { |
| 140 | return true |
| 141 | } |
| 142 | rest = rest[index+len(cue):] |
| 143 | } |
| 144 | } |
| 145 | return false |
| 146 | } |
| 147 | |
| 148 | func containsTaskNeedle(input, needle string) bool { |
| 149 | if needle == "" { |
| 150 | return false |
| 151 | } |
| 152 | if goalBudgetContainsNonASCII(needle) || strings.Contains(needle, " ") { |
| 153 | return strings.Contains(input, needle) |
| 154 | } |
| 155 | return slices.Contains(strings.FieldsFunc(input, func(r rune) bool { |
| 156 | return !(r >= 'a' && r <= 'z') && !(r >= '0' && r <= '9') && r != '_' |
| 157 | }), needle) |
| 158 | } |
| 159 | |
| 160 | func goalBudgetContainsNonASCII(s string) bool { |
| 161 | for _, r := range s { |
| 162 | if r > 127 { |
| 163 | return true |
| 164 | } |
| 165 | } |
| 166 | return false |
| 167 | } |
| 168 |