diff --git a/.changeset/fix-routing-strip-example-quotes.md b/.changeset/fix-routing-strip-example-quotes.md new file mode 100644 index 000000000..365753800 --- /dev/null +++ b/.changeset/fix-routing-strip-example-quotes.md @@ -0,0 +1,9 @@ +--- +"@bradygaster/squad-sdk": patch +--- + +Fix: strip surrounding quotes from routing.md example phrases in `parseRoutingMarkdown` + +`parseRoutingMarkdown` split the Examples cell on commas but never stripped the surrounding quotes, so a quoted example like `"unit tests"` was stored verbatim (quotes included). When `compileRoutingRules` / `matchRoute` later tokenized it, the quote characters glued onto the boundary words (`"unit`, `tests"`), producing regex patterns that could never match — so routing tables that used quoted examples silently routed every message to the fallback agent. + +Leading/trailing quotes (`"`, `'`, `` ` ``) are now stripped from each example, so quoted and unquoted examples behave identically. Adds regression tests covering both the parse step and end-to-end `matchRoute`. diff --git a/packages/squad-sdk/src/config/routing.ts b/packages/squad-sdk/src/config/routing.ts index 4c47eb08f..ba6db4793 100644 --- a/packages/squad-sdk/src/config/routing.ts +++ b/packages/squad-sdk/src/config/routing.ts @@ -162,7 +162,16 @@ export function parseRoutingMarkdown(content: string): RoutingConfig { if (cells.length >= 2) { const workType = cells[0]; const routeTo = cells[1]!; - const examples = cells.length >= 3 ? cells[2]!.split(',').map(ex => ex.trim()) : undefined; + // Strip surrounding quotes (", ', `) so quoted and unquoted examples + // tokenize identically. Without this, a quoted example like "unit tests" + // keeps its quotes and compiles to patterns that never match, which made + // quoted routing examples silently route everything to fallback. + const examples = cells.length >= 3 + ? cells[2]! + .split(',') + .map(ex => ex.trim().replace(/^["'`]+|["'`]+$/g, '').trim()) + .filter(ex => ex.length > 0) + : undefined; // Parse agent names (may be comma-separated or single) const agents = routeTo.split(',').map(a => a.trim()).filter(a => a.length > 0); diff --git a/test/routing.test.ts b/test/routing.test.ts index a90089e79..b3dc5842b 100644 --- a/test/routing.test.ts +++ b/test/routing.test.ts @@ -36,6 +36,20 @@ describe('parseRoutingMarkdown', () => { expect(config.rules[1].agents).toEqual(['Developer']); }); + it('strips surrounding quotes from examples', () => { + const markdown = ` +## Routing Table + +| Work Type | Route To | Examples | +|-----------|----------|----------| +| testing | Tester | "unit tests", "jest coverage" | +`; + + const config = parseRoutingMarkdown(markdown); + + expect(config.rules[0].examples).toEqual(['unit tests', 'jest coverage']); + }); + it('handles multiple agents per rule', () => { const markdown = ` ## Routing Table @@ -200,6 +214,20 @@ describe('matchRoute', () => { expect(match.rule?.workType).toBe('bug-fix'); }); + it('matches quoted examples after quote-stripping (regression)', () => { + const md = ` +## Routing Table + +| Work Type | Route To | Examples | +|-----------|----------|----------| +| testing | Tester | "unit tests", "jest coverage" | +`; + const quotedRouter = compileRoutingRules(parseRoutingMarkdown(md)); + const match = matchRoute('please write unit tests now', quotedRouter); + + expect(match.agents).toEqual(['Tester']); + }); + it('returns fallback for no match', () => { const match = matchRoute('what is the weather today?', router);