From e103e577c69f3e020b9df4311956c9c508255c87 Mon Sep 17 00:00:00 2001 From: garrytan Date: Tue, 6 Oct 2026 18:25:25 +0000 Subject: [PATCH] fix(router): route only to loaded skills, and say when not to invoke (#3018) The root router listed every skill and ended 'When in doubt, invoke the skill'. Current Claude models follow that literally: they tried to invoke skills a user had turned off in Claude Code's skillOverrides (which gstack's own disabled_skills render cannot see), and borderline requests tipped into full workflows. The router now routes only to skills in the session's available-skills list and names when to answer directly; the CLAUDE.md routing block skill-start offers says the same. Co-authored-by: Rekin226 --- SKILL.md | 12 +++++++----- SKILL.md.tmpl | 12 +++++++----- bin/gstack-skill-start | 2 +- scripts/gen-skill-docs.ts | 4 ++-- test/gen-skill-docs.test.ts | 13 +++++++++++-- 5 files changed, 28 insertions(+), 15 deletions(-) diff --git a/SKILL.md b/SKILL.md index 635cf5ef9..36313bbaf 100644 --- a/SKILL.md +++ b/SKILL.md @@ -227,11 +227,13 @@ Generic “second opinion”, “outside review”, or “cross-model review” - User asks to tune question sensitivity, "stop asking me that" → invoke `/plan-tune` - User asks for code quality dashboard, "health check" → invoke `/health` -**When in doubt, invoke the skill.** A false positive (invoking a skill that wasn't -needed) is cheaper than a false negative (answering ad-hoc when a structured workflow -exists). The skill provides multi-step workflows, checklists, and quality gates that -always produce better results than an ad-hoc answer. If no skill matches, answer -directly as usual. +**Route only to skills in your available-skills list.** A rule above whose skill is +not listed there is turned off on this install; skip it, never try to invoke it. + +**When to invoke, and when not to.** Invoke when the request matches a skill's purpose: +the skill's workflow, checklists and gates beat an ad-hoc answer. Answer directly when +no skill matches, for a quick factual question or a small edit the user scoped +themselves, and when the user asks for a direct answer instead of a workflow. If the user opts out of suggestions, run `gstack-config set proactive false`. If they opt back in, run `gstack-config set proactive true`. diff --git a/SKILL.md.tmpl b/SKILL.md.tmpl index e0c33a2da..4e6012ecf 100644 --- a/SKILL.md.tmpl +++ b/SKILL.md.tmpl @@ -98,11 +98,13 @@ quality gates that produce better results than answering inline. - User asks to tune question sensitivity, "stop asking me that" → invoke `/plan-tune` - User asks for code quality dashboard, "health check" → invoke `/health` -**When in doubt, invoke the skill.** A false positive (invoking a skill that wasn't -needed) is cheaper than a false negative (answering ad-hoc when a structured workflow -exists). The skill provides multi-step workflows, checklists, and quality gates that -always produce better results than an ad-hoc answer. If no skill matches, answer -directly as usual. +**Route only to skills in your available-skills list.** A rule above whose skill is +not listed there is turned off on this install; skip it, never try to invoke it. + +**When to invoke, and when not to.** Invoke when the request matches a skill's purpose: +the skill's workflow, checklists and gates beat an ad-hoc answer. Answer directly when +no skill matches, for a quick factual question or a small edit the user scoped +themselves, and when the user asks for a direct answer instead of a workflow. If the user opts out of suggestions, run `gstack-config set proactive false`. If they opt back in, run `gstack-config set proactive true`. diff --git a/bin/gstack-skill-start b/bin/gstack-skill-start index 64f5605b3..5869d3641 100755 --- a/bin/gstack-skill-start +++ b/bin/gstack-skill-start @@ -495,7 +495,7 @@ If A: append this section to the end of CLAUDE.md, then commit it (`git add CLAU ## Skill routing -When the user's request matches an available skill, invoke it via the Skill tool. When in doubt, invoke the skill. +When the user's request matches an available skill, invoke it via the Skill tool. Route only to skills in the session's available-skills list; answer directly for quick questions or small scoped edits. Key routing rules: - Product ideas/brainstorming → invoke /office-hours diff --git a/scripts/gen-skill-docs.ts b/scripts/gen-skill-docs.ts index 68fe320a7..ed1d93ae7 100644 --- a/scripts/gen-skill-docs.ts +++ b/scripts/gen-skill-docs.ts @@ -169,8 +169,8 @@ function rewriteInstallRoot(content: string, hostConfig: HostConfig, installRoot } /** - * C8 (#3018): the router names every skill and ends "When in doubt, invoke the - * skill", so it routed to skills the user disabled. A per-install render that + * C8 (#3018): the router names every skill, so it routed to skills the user + * disabled (it also said "When in doubt, invoke the skill" until v1.91.31). A per-install render that * knows its disabled skills drops their routing rules (keeping the other side * of an "A or B" rule) and says which skills are off. The router itself and * the way back (gstack-upgrade) can never be disabled. diff --git a/test/gen-skill-docs.test.ts b/test/gen-skill-docs.test.ts index bceb515dd..3ead7c008 100644 --- a/test/gen-skill-docs.test.ts +++ b/test/gen-skill-docs.test.ts @@ -2116,10 +2116,19 @@ describe('preamble routing injection (bin/gstack-skill-start emission layer)', ( expect(routingBlock).not.toContain('invoke checkpoint'); }); - test('routing section uses soft "when in doubt" policy, not hard "ALWAYS invoke"', () => { - expect(routingBlock).toContain('When in doubt, invoke the skill'); + test('routing section routes only to available skills, without "when in doubt" over-triggering (#3018)', () => { + expect(routingBlock).toContain("Route only to skills in the session's available-skills list"); + expect(routingBlock).not.toContain('When in doubt'); expect(routingBlock).not.toContain('Do NOT answer directly'); }); + + test('root router skips routes to skills that are not loaded and says when not to invoke (#3018)', () => { + const router = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8'); + expect(router).toContain('Route only to skills in your available-skills list'); + expect(router).toContain('skip it, never try to invoke it'); + expect(router).toContain('Answer directly when\nno skill matches'); + expect(router).not.toContain('When in doubt, invoke the skill'); + }); }); // --- {{DESIGN_OUTSIDE_VOICES}} resolver tests ---