From bdd573a129fc48ef31b7ce798ced2ffe13a2ee71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Fri, 29 May 2026 21:26:10 +0300 Subject: [PATCH] =?UTF-8?q?fix(router-gate):=20stream=20A=20decide=20?= =?UTF-8?q?=E2=80=94=20unicode=20boundary=20on=20cyrillic=20direct-invocat?= =?UTF-8?q?ion,=20polite=20skill=5Fcall=20forms,=20+tests,=20knownInRegist?= =?UTF-8?q?ry=20contract=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- tools/router-gate-decide.mjs | 20 ++++++++++++++++++-- tools/router-gate-decide.test.mjs | 20 ++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/tools/router-gate-decide.mjs b/tools/router-gate-decide.mjs index 80f0d2cb3..fde6bfb34 100644 --- a/tools/router-gate-decide.mjs +++ b/tools/router-gate-decide.mjs @@ -3,6 +3,11 @@ * Core decision module — router-gate v4 spec §4 (4 поведения) + §10.1. * Pure: вся behavior-логика allow/block/unlock. Safe-baseline metering, skill-scope, * Bash-content — отдельные слои в хуке (Stream B/G), поверх decide(). + * + * NOTE on knownInRegistry: directInvocation.knownInRegistry===false is NOT handled + * here. Caller (Stream G hook) MUST check knownInRegistry and emit AskUser (with + * fuzzy-match suggestions) before calling decide(). decide() unlocks direct + * invocations regardless of registry membership. */ export const SAFE_BASELINE_TOOLS = ['Read', 'Grep', 'Glob', 'LS', 'TodoWrite', 'AskUserQuestion']; @@ -34,9 +39,12 @@ export function nodeMatches(recommendation, toolUse, resolveAlias = (x) => x) { export const DIRECT_INVOCATION_PATTERNS = [ { re: /^\/([a-z0-9_-]+)(\s|$)/iu, type: 'slash', group: 1 }, - { re: /(?:вызови|примени)\s+Skill\(([^)]+)\)/iu, type: 'skill_call', group: 1 }, + // Fix 1: left boundary (? { const r = detectDirectInvocation(''); expect(r.matched).toBe(false); }); + // Fix 3: boundary tests for Fix 1+2 + it('делай subagent-driven-development → literal_name (boundary at string start)', () => { + const r = detectDirectInvocation('делай subagent-driven-development'); + expect(r.matched).toBe(true); + expect(r.type).toBe('literal_name'); + expect(r.name).toBe('subagent-driven-development'); + }); + it('вообщеиспользуй brain-retro → does NOT match literal_name (fused word rejected)', () => { + const r = detectDirectInvocation('вообщеиспользуй brain-retro'); + // The literal_name pattern must NOT fire because "используй" is fused with "вообще" + expect(r.matched).toBe(false); + }); + it('примените Skill(brain-retro) → skill_call (polite form Fix 2)', () => { + const r = detectDirectInvocation('примените Skill(brain-retro)'); + expect(r.matched).toBe(true); + expect(r.type).toBe('skill_call'); + expect(r.name).toBe('brain-retro'); + }); }); // ─── crossCheckSelfSuggested ───────────────────────────────────────────── @@ -335,6 +353,8 @@ describe('decide — Поведение 3 chain', () => { }); expect(r.decision).toBe('block'); expect(r.behavior_branch).toBe('3_chain'); + // Fix 4: chain-block reason must mention AskUserQuestion + expect(r.reason).toContain('AskUserQuestion'); }); it('Read with active chain → allow', () => { const r = decide({