From 5222e109e0d18c6314af39cd661d33cc975e8ea3 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: Sat, 23 May 2026 13:49:42 +0300 Subject: [PATCH] =?UTF-8?q?fix(observer):=20hook-resolver=20=E2=80=94=20sp?= =?UTF-8?q?lit=20combined=20matchers=20(Edit|Write)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Final-review followup. .claude/settings.json uses regex-style combined matchers like "Edit|Write"; transcript writes per-tool PreToolUse:Edit. Split on | when building map so per-tool counts resolve. Also sync spec doc loadHookMap -> buildHookMap (impl name). Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/observer/STATUS.md | 2 +- ...bserver-parser-skill-hook-expand-design.md | 4 +-- tools/observer-hook-resolver.mjs | 15 ++++++++--- tools/observer-hook-resolver.test.mjs | 27 +++++++++++++++++++ 4 files changed, 41 insertions(+), 7 deletions(-) diff --git a/docs/observer/STATUS.md b/docs/observer/STATUS.md index e05ab728..0a8f77ec 100644 --- a/docs/observer/STATUS.md +++ b/docs/observer/STATUS.md @@ -1,6 +1,6 @@ # Brain Status (auto-generated) -Last updated: 2026-05-23T10:39:02.076Z +Last updated: 2026-05-23T10:45:47.776Z | Контролёр | Состояние | Детали | |---|---|---| diff --git a/docs/superpowers/specs/2026-05-23-observer-parser-skill-hook-expand-design.md b/docs/superpowers/specs/2026-05-23-observer-parser-skill-hook-expand-design.md index e67db3e2..649946f3 100644 --- a/docs/superpowers/specs/2026-05-23-observer-parser-skill-hook-expand-design.md +++ b/docs/superpowers/specs/2026-05-23-observer-parser-skill-hook-expand-design.md @@ -23,7 +23,7 @@ **`tools/observer-hook-resolver.mjs`** (новый, ~80 LoC, pure) ```js -export function loadHookMap({ projectSettings, userSettings } = {}) +export function buildHookMap({ projectSettings, userSettings } = {}) // Map // matcher: "PreToolUse:Bash" | "UserPromptSubmit" | "SessionStart:startup" | ... // value: ["tools/observer-stop-hook.mjs", "inline:claude-md-guard-7f3a", ...] @@ -117,7 +117,7 @@ Stop-hook → parser.parseTranscript(transcriptText) ├─ collectToolUse → skills[], counts, errors ├─ extractProcessEvents │ ├─ hookCounts (matcher) ← из attachment.hookName - │ ├─ resolveScriptCounts(hookCounts, hookResolver.loadHookMap()) + │ ├─ resolveScriptCounts(hookCounts, hookResolver.buildHookMap()) │ └─ event hook_fired = { counts, scripts, errors } └─ primary_rationale └─ recommended_node = skills.length === 0 diff --git a/tools/observer-hook-resolver.mjs b/tools/observer-hook-resolver.mjs index 83499456..3b7d8d84 100644 --- a/tools/observer-hook-resolver.mjs +++ b/tools/observer-hook-resolver.mjs @@ -57,14 +57,21 @@ export function buildHookMap(projectSettings = {}, userSettings = {}) { if (!Array.isArray(entries)) continue; for (const entry of entries) { if (!entry || typeof entry !== 'object') continue; - const matcher = entry.matcher ? `${event}:${entry.matcher}` : event; const scripts = Array.isArray(entry.hooks) ? entry.hooks : []; - const existing = map.get(matcher) || []; + const scriptNames = []; for (const h of scripts) { if (!h || h.type !== 'command') continue; - existing.push(extractScriptName(h.command)); + scriptNames.push(extractScriptName(h.command)); + } + if (scriptNames.length === 0) continue; + const matcherKeys = entry.matcher + ? String(entry.matcher).split('|').map((t) => `${event}:${t.trim()}`).filter(Boolean) + : [event]; + for (const matcher of matcherKeys) { + const existing = map.get(matcher) || []; + existing.push(...scriptNames); + map.set(matcher, existing); } - map.set(matcher, existing); } } } diff --git a/tools/observer-hook-resolver.test.mjs b/tools/observer-hook-resolver.test.mjs index f9cdb93f..2af90806 100644 --- a/tools/observer-hook-resolver.test.mjs +++ b/tools/observer-hook-resolver.test.mjs @@ -97,6 +97,33 @@ describe('buildHookMap', () => { const map = buildHookMap(project, user); expect(map.get('PreToolUse:Bash')).toEqual(['tools/a.mjs', 'tools/b.mjs']); }); + + it('splits combined matcher "Edit|Write" into two map entries', () => { + const settings = { + hooks: { + PreToolUse: [ + { matcher: 'Edit|Write', hooks: [{ type: 'command', command: 'node tools/guard.mjs' }] }, + ], + }, + }; + const map = buildHookMap(settings); + expect(map.get('PreToolUse:Edit')).toEqual(['tools/guard.mjs']); + expect(map.get('PreToolUse:Write')).toEqual(['tools/guard.mjs']); + expect(map.get('PreToolUse:Edit|Write')).toBeUndefined(); + }); + + it('trims whitespace around matchers split on |', () => { + const settings = { + hooks: { + PreToolUse: [ + { matcher: 'Edit | Write', hooks: [{ type: 'command', command: 'node tools/g.mjs' }] }, + ], + }, + }; + const map = buildHookMap(settings); + expect(map.get('PreToolUse:Edit')).toEqual(['tools/g.mjs']); + expect(map.get('PreToolUse:Write')).toEqual(['tools/g.mjs']); + }); }); describe('resolveScriptCounts', () => {