Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 7a878f4

Browse files
authored
Classifier: feature verb wins over debug keyword (part of getagentseal#196) (getagentseal#289)
Messages like "add error handling", "create an issue tracker", or "implement the 404 page" were landing in the Debugging bucket because the classifier checked DEBUG_KEYWORDS (which matches `error`, `issue`, `404`) before FEATURE_KEYWORDS in both `refineByKeywords` (tool-bearing turns) and `classifyConversation` (chat-only turns). The position of the matched word in the sentence is a much stronger intent signal than the order of the checks in code, so we now pick whichever pattern matches earliest. The new helper `firstMatchingCategory` runs each candidate regex once with `RegExp.exec` and keeps the match with the lowest `index`. Ties (rare in practice — same start position) break by the order the candidates were listed, which is `refactoring > feature > debugging` for coding turns. That ordering preserves existing behavior for plain bug reports (e.g. "login is broken, traceback below") while flipping mislabeled feature work to its correct category. 8 regression tests in `tests/classifier.test.ts` cover the mislabel cases from getagentseal#196 plus tie-break / chat-only cases. Full suite: 45 files / 609 tests, all green. Closes the activity-misattribution half of getagentseal#196. The Cursor provider attribution half (single 'cursor' project for all sessions) is addressed in a separate PR.
1 parent b72e51e commit 7a878f4

3 files changed

Lines changed: 99 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,17 @@
4040
period-level `activities[]` rollup so a consumer can sum across days and
4141
reconcile. Closes #279.
4242

43+
### Fixed (CLI)
44+
- **Activity classifier no longer mislabels feature work as debugging.**
45+
Messages like "add error handling", "create an issue tracker", or
46+
"implement the 404 page" used to land in the Debugging bucket because
47+
the classifier checked the debug-keyword regex (which matches `error`,
48+
`issue`, `404`) before the feature regex. Now the keyword that appears
49+
earliest in the user message wins, so "add" beats "error", "create"
50+
beats "issue", etc. A real bug report ("login is broken, traceback
51+
below") still classifies as debugging because the debug word leads.
52+
Fixes the activity-misattribution half of #196.
53+
4354
### Changed (CLI)
4455
- **`optimize` suggestions now declare their destination.** Every paste-style
4556
fix carries an explicit destination — `claude-md` (permanent project rule),

src/classifier.ts

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -93,12 +93,38 @@ function classifyByToolPattern(turn: ParsedTurn): TaskCategory | null {
9393
return null
9494
}
9595

96+
/// Picks the category whose keyword pattern matches earliest in the message.
97+
/// On a tie (same start index) the candidate listed first in `candidates` wins,
98+
/// so callers control tie-break priority by ordering. Returns null when no
99+
/// pattern matches. The first-match heuristic fixes the long-standing problem
100+
/// where "add error handling" was tagged Debugging because the DEBUG regex was
101+
/// checked before FEATURE; now FEATURE wins because "add" appears before
102+
/// "error". Issue #196.
103+
function firstMatchingCategory(
104+
text: string,
105+
candidates: ReadonlyArray<{ regex: RegExp; category: TaskCategory }>,
106+
): TaskCategory | null {
107+
let best: { index: number; order: number; category: TaskCategory } | null = null
108+
for (let i = 0; i < candidates.length; i++) {
109+
const c = candidates[i]!
110+
const m = c.regex.exec(text)
111+
if (!m) continue
112+
if (!best || m.index < best.index || (m.index === best.index && i < best.order)) {
113+
best = { index: m.index, order: i, category: c.category }
114+
}
115+
}
116+
return best?.category ?? null
117+
}
118+
96119
function refineByKeywords(category: TaskCategory, userMessage: string): TaskCategory {
97120
if (category === 'coding') {
98-
if (DEBUG_KEYWORDS.test(userMessage)) return 'debugging'
99-
if (REFACTOR_KEYWORDS.test(userMessage)) return 'refactoring'
100-
if (FEATURE_KEYWORDS.test(userMessage)) return 'feature'
101-
return 'coding'
121+
// Tie-break order (when two keywords match at the same index): refactoring
122+
// first because its words are the most specific, then feature, then debug.
123+
return firstMatchingCategory(userMessage, [
124+
{ regex: REFACTOR_KEYWORDS, category: 'refactoring' },
125+
{ regex: FEATURE_KEYWORDS, category: 'feature' },
126+
{ regex: DEBUG_KEYWORDS, category: 'debugging' },
127+
]) ?? 'coding'
102128
}
103129

104130
if (category === 'exploration') {
@@ -113,8 +139,14 @@ function refineByKeywords(category: TaskCategory, userMessage: string): TaskCate
113139
function classifyConversation(userMessage: string): TaskCategory {
114140
if (BRAINSTORM_KEYWORDS.test(userMessage)) return 'brainstorming'
115141
if (RESEARCH_KEYWORDS.test(userMessage)) return 'exploration'
116-
if (DEBUG_KEYWORDS.test(userMessage)) return 'debugging'
117-
if (FEATURE_KEYWORDS.test(userMessage)) return 'feature'
142+
// Same first-match-wins logic as refineByKeywords so a chat-only message
143+
// starting with a feature verb does not flip to debugging because of an
144+
// incidental "error" or "fix" word later in the same sentence.
145+
const debugOrFeature = firstMatchingCategory(userMessage, [
146+
{ regex: FEATURE_KEYWORDS, category: 'feature' },
147+
{ regex: DEBUG_KEYWORDS, category: 'debugging' },
148+
])
149+
if (debugOrFeature) return debugOrFeature
118150
if (FILE_PATTERNS.test(userMessage)) return 'coding'
119151
if (SCRIPT_PATTERNS.test(userMessage)) return 'coding'
120152
if (URL_PATTERN.test(userMessage)) return 'exploration'

tests/classifier.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,53 @@ describe('classifyTurn — Skill subCategory', () => {
101101
expect(c.subCategory).toBeUndefined()
102102
})
103103
})
104+
105+
// Regression coverage for issue #196: feature verbs that lead a message
106+
// were previously hijacked into 'debugging' just because the message contained
107+
// an incidental "error" / "fix" / "issue" word later in the same sentence.
108+
// Now whichever keyword pattern matches earliest wins.
109+
describe('classifyTurn — feature vs debugging precedence (#196)', () => {
110+
function codingTurn(userMessage: string): ParsedTurn {
111+
return makeTurn([makeCall({ tools: ['Edit'] })], userMessage)
112+
}
113+
114+
it('classifies "add error handling" as feature, not debugging', () => {
115+
const c = classifyTurn(codingTurn('add error handling to the auth module'))
116+
expect(c.category).toBe('feature')
117+
})
118+
119+
it('classifies "create an issue tracker" as feature, not debugging', () => {
120+
const c = classifyTurn(codingTurn('create an issue tracker page in the dashboard'))
121+
expect(c.category).toBe('feature')
122+
})
123+
124+
it('classifies "implement the 404 page" as feature, not debugging', () => {
125+
const c = classifyTurn(codingTurn('implement the 404 page with a friendly redirect'))
126+
expect(c.category).toBe('feature')
127+
})
128+
129+
it('still classifies "fix the layout for the new feature" as debugging', () => {
130+
const c = classifyTurn(codingTurn('fix the layout for the new feature'))
131+
expect(c.category).toBe('debugging')
132+
})
133+
134+
it('still classifies a plain bug report as debugging', () => {
135+
const c = classifyTurn(codingTurn('login is broken, traceback below'))
136+
expect(c.category).toBe('debugging')
137+
})
138+
139+
it('classifies "refactor the error handling" as refactoring', () => {
140+
const c = classifyTurn(codingTurn('refactor the error handling so it is cleaner'))
141+
expect(c.category).toBe('refactoring')
142+
})
143+
144+
it('chat-only message starting with "add" stays feature even with "fix" later', () => {
145+
const c = classifyTurn(makeTurn([], 'add a setting page; we will fix the styles after'))
146+
expect(c.category).toBe('feature')
147+
})
148+
149+
it('chat-only message starting with "fix" stays debugging even with "add" later', () => {
150+
const c = classifyTurn(makeTurn([], 'fix the bug introduced when we added the new flag'))
151+
expect(c.category).toBe('debugging')
152+
})
153+
})

0 commit comments

Comments
 (0)