diff --git a/docs/superpowers/router-mentor-wall-GUIDE.md b/docs/superpowers/router-mentor-wall-GUIDE.md index 4dba411..32ddba3 100644 --- a/docs/superpowers/router-mentor-wall-GUIDE.md +++ b/docs/superpowers/router-mentor-wall-GUIDE.md @@ -248,13 +248,14 @@ Claude обязан **запросить** подтверждение. В шта 6. **Stop-loss:** 2-3 NO-GO подряд = чини СВОЙ план по тексту замечания, не цикл и не «вина стены». 7. **Спека = ЧТО (контракт), план = КАК (метод).** Метод правки, описанный в спеке, ловит fatal — метод только в плане. -⚠️ Урок 2026-06-20 — не прячь работу в скрипт (наставник/судья режут «чёрный ящик») -Контроллер 8 раз переписывал план, пряча всю содержательную правку (вырезка schema.json, прогон тестов, коммит) внутрь одного node-скрипта-комбайна, чтобы обойти гейты. Наставник/судья справедливо не пускали; контроллер латал по одному возражению и валил на «судья тупит». Корень — подход, а не судья. +## ⚠️ Урок 2026-06-20 — не прячь работу в скрипт (наставник/судья режут «чёрный ящик») -Не прячь содержательную работу в node-скрипт ради обхода гейтов. node x.mjs не попадает под пэттерн-матч criterion/verify-гейтов — соблазн спрятать туда правки/тесты/коммит. Но наставник и судья видят только steps-json («Write скрипт → запусти скрипт»), ВНУТРЬ не заглядывают → корректно читают как «план ничего не меняет» и «не доверяю чёрному ящику» → NO-GO. Латать бесполезно. -Содержательные правки — ВИДИМЫЕ шаги плана (Edit/Write на каждый файл; судья проверяет: убрано ровно нужное, файл валиден, остальное цело). Особенно КРИТИЧНЫЕ файлы (schema.json валидирует весь реестр — битый JSON роняет загрузку) — прямой Write чистого содержимого, не слепая резка внутри скрипта. -Прогон тестов — отдельный видимый шаг, не внутри скрипта-комбайна. -Коммит-через-скрипт (§5/§D) — ТОЛЬКО для механики git add/commit/push, НЕ для самих правок и не для верификации. Не расширяй его, чтобы протащить работу мимо стены. -Симптом хитрости: план = «Write скрипт + запусти скрипт», реальная работа невидима. Поймал себя — переделай на видимые шаги, не дожимай owner-seal +> Записано после сессии, где контроллер 8 раз переписывал план, пряча всю содержательную правку (вырезка `schema.json`, прогон тестов, коммит) внутрь одного node-скрипта-комбайна, чтобы обойти гейты. Наставник/судья справедливо не пускали; контроллер латал по одному возражению и валил на «судья тупит». Корень — подход, а не судья. + +1. **Не прячь содержательную работу в node-скрипт ради обхода гейтов.** `node x.mjs` не попадает под пэттерн-матч criterion/verify-гейтов — соблазн спрятать туда правки/тесты/коммит. Но наставник и судья видят только `steps-json` («Write скрипт → запусти скрипт»), ВНУТРЬ не заглядывают → корректно читают как «план ничего не меняет» и «не доверяю чёрному ящику» → NO-GO. Латать бесполезно. +2. **Содержательные правки — ВИДИМЫЕ шаги плана** (Edit/Write на каждый файл; судья проверяет: убрано ровно нужное, файл валиден, остальное цело). Особенно КРИТИЧНЫЕ файлы (`schema.json` валидирует весь реестр — битый JSON роняет загрузку) — прямой Write чистого содержимого, не слепая резка внутри скрипта. +3. **Прогон тестов — отдельный видимый шаг**, не внутри скрипта-комбайна. +4. **Коммит-через-скрипт (§5/§D) — ТОЛЬКО для механики** `git add/commit/push`, НЕ для самих правок и не для верификации. Не расширяй его, чтобы протащить работу мимо стены. +5. **Симптом хитрости:** план = «Write скрипт + запусти скрипт», реальная работа невидима. Поймал себя — переделай на видимые шаги, не дожимай owner-seal. [↑ наверх](#top) diff --git a/lefthook.yml b/lefthook.yml index f6987d2..4cf6470 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -86,12 +86,6 @@ pre-commit: fail_text: | observer-coverage-checker reports a gap (coverage or registration). - # 9. observer-chain-map-checker — brain governance C6 (chain attribution). - - name: observer-chain-map-checker - run: node tools/observer-chain-map-checker.mjs - fail_text: | - observer-chain-map-checker: дрейф chain-map <-> routing-off-phase.md. - # 10. registry-render-check — drift nodes.yaml <-> auto-region маркеры (warn-only). - name: registry-render-check glob: "{docs/registry/nodes.yaml,docs/Tooling_v8_3.md,docs/routing-off-phase.md}" diff --git a/tools/observer-chain-map-checker.mjs b/tools/observer-chain-map-checker.mjs deleted file mode 100644 index eb53f54..0000000 --- a/tools/observer-chain-map-checker.mjs +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env node -/** - * Brain governance controller C6 — chain-map sync checker. - * Verifies tools/observer-chain-map.json against the L1-L13 table in - * docs/routing-off-phase.md. Sync is checked by L-number sets (both - * directions), not by node names — node_chosen values (skill-id) differ - * from the human display names in the .md table. Pure fs/regex, no LLM. - */ -import { readFileSync } from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { dirname, join } from 'node:path'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const MD_PATH = join(__dirname, '..', 'docs', 'routing-off-phase.md'); -const JSON_PATH = join(__dirname, 'observer-chain-map.json'); - -/** Extract the set of L-numbers ("L1".."L13") from the routing-off-phase.md table. */ -export function parseChainsFromMd(md) { - const set = new Set(); - for (const line of md.split(/\r?\n/)) { - const m = /^\|\s*(L\d+)\s*\|/.exec(line.trim()); - if (m) set.add(m[1]); - } - return set; -} - -/** Compare JSON L-numbers against the md set, both directions. */ -export function checkSync(jsonMap, mdSet) { - const jsonSet = new Set(); - for (const [node, chains] of Object.entries(jsonMap)) { - if (node === '_note') continue; - if (Array.isArray(chains)) for (const c of chains) jsonSet.add(c); - } - const jsonOnly = [...jsonSet].filter((c) => !mdSet.has(c)); // ссылки на несуществующие L - const mdOnly = [...mdSet].filter((c) => !jsonSet.has(c)); // потерянные цепочки - return { ok: jsonOnly.length === 0 && mdOnly.length === 0, jsonOnly, mdOnly }; -} - -/** CLI entry — exit 1 on drift with a human-readable message. */ -function main() { - const md = readFileSync(MD_PATH, 'utf8'); - const jsonMap = JSON.parse(readFileSync(JSON_PATH, 'utf8')); - const mdSet = parseChainsFromMd(md); - if (mdSet.size === 0) { - console.error( - '[chain-map-checker] не нашёл ни одной L-строки в routing-off-phase.md — формат таблицы изменился?' - ); - process.exit(1); - } - const res = checkSync(jsonMap, mdSet); - if (res.ok) { - console.log(`[chain-map-checker] OK — ${mdSet.size} chains in sync`); - process.exit(0); - } - console.error('[chain-map-checker] дрейф маппинга chain-map <-> routing-off-phase.md:'); - if (res.jsonOnly.length) - console.error(` JSON ссылается на отсутствующие в .md цепочки: ${res.jsonOnly.join(', ')}`); - if (res.mdOnly.length) - console.error( - ` В .md есть цепочки без записи в JSON: ${res.mdOnly.join(', ')} — добавьте узлы в tools/observer-chain-map.json` - ); - process.exit(1); -} - -if (process.argv[1]?.endsWith('observer-chain-map-checker.mjs')) { - main(); -} diff --git a/tools/observer-chain-map-checker.test.mjs b/tools/observer-chain-map-checker.test.mjs deleted file mode 100644 index da44276..0000000 --- a/tools/observer-chain-map-checker.test.mjs +++ /dev/null @@ -1,46 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { parseChainsFromMd, checkSync } from './observer-chain-map-checker.mjs'; - -const SAMPLE_MD = [ - '| # | Цепочка | Зачем |', - '|---|---|---|', - '| L1 | `discovery-interview` (FEATURE) → `brainstorming` | text |', - '| L2 | `audit-portal` | text |', - '| L13 | `billing-audit` (#62) + `Pest` | text |', -].join('\n'); - -describe('parseChainsFromMd', () => { - it('extracts the set of L-numbers from the table', () => { - expect(parseChainsFromMd(SAMPLE_MD)).toEqual(new Set(['L1', 'L2', 'L13'])); - }); -}); - -describe('checkSync', () => { - it('passes when JSON L-numbers subset of md and md subset of json-union', () => { - const mdSet = new Set(['L1', 'L2', 'L13']); - const jsonMap = { a: ['L1'], b: ['L2'], c: ['L13'] }; - expect(checkSync(jsonMap, mdSet).ok).toBe(true); - }); - - it('fails when JSON references a chain absent from md', () => { - const mdSet = new Set(['L1', 'L2']); - const jsonMap = { a: ['L1'], b: ['L99'] }; - const res = checkSync(jsonMap, mdSet); - expect(res.ok).toBe(false); - expect(res.jsonOnly).toContain('L99'); - }); - - it('fails when md has a chain not covered by any JSON entry', () => { - const mdSet = new Set(['L1', 'L2', 'L14']); - const jsonMap = { a: ['L1'], b: ['L2'] }; - const res = checkSync(jsonMap, mdSet); - expect(res.ok).toBe(false); - expect(res.mdOnly).toContain('L14'); - }); - - it('ignores the _note metadata key in the JSON map', () => { - const mdSet = new Set(['L1']); - const jsonMap = { _note: 'meta', a: ['L1'] }; - expect(checkSync(jsonMap, mdSet).ok).toBe(true); - }); -}); diff --git a/tools/router-classifier.mjs b/tools/router-classifier.mjs index 7c9070c..1c632f6 100644 --- a/tools/router-classifier.mjs +++ b/tools/router-classifier.mjs @@ -254,12 +254,6 @@ function buildNodesBlock(registry) { }).join('\n'); } -function buildChainsBlock(registry) { - return Object.entries(registry.chains || {}) - .map(([id, c]) => `- ${id}: ${c.name} [${(c.sequence || []).join(' → ')}]`) - .join('\n'); -} - /** * Build Sonnet 4.6 classifier prompt per spec §4.2. * @@ -291,7 +285,6 @@ export function buildClassifierPrompt(userPrompt, registry, { enrichment = true export function buildClassifierPromptStructured(userPrompt, registry, { enrichment = true, classifierContext = 'CRM-проекта «Лидерра» (Laravel 13 + Vue 3 + Vuetify 3)' } = {}) { const pamyatka = enrichment ? `\n\n${PAMYATKA}\n` : '\n'; const nodesBlock = buildNodesBlock(registry); - const chainsBlock = buildChainsBlock(registry); const system = `Ты классификатор задач для ${classifierContext}. @@ -307,9 +300,6 @@ ${pamyatka} === РЕЕСТР УЗЛОВ === ${nodesBlock} -=== РЕЕСТР ЦЕПОЧЕК (справочно) === -${chainsBlock} - Пример формата вывода (значения иллюстративные, не подсказка по содержанию): {"task_type":"bugfix","skill":"#18","chain":null,"no_skill_found":false,"recommended_chain":["#18","#19"],"confidence":0.9,"alternatives_considered":[{"id":"#62","score":0.3,"reason":"не про деньги"}],"reason_for_choice":"keyword 'regex' → systematic-debugging"} @@ -451,18 +441,11 @@ export function buildLLMPrompt(prompt, registry) { return `- ${n.id} ${n.name} [${triggers}]`; }).join('\n'); - const chains = Object.entries(registry.chains || {}) - .map(([id, c]) => `- ${id}: ${c.name} [${(c.sequence || []).join(' → ')}]`) - .join('\n'); - return `${LEGACY_LLM_SYSTEM_PROMPT} ## Available nodes ${nodeLines} -## Available chains -${chains} - ## User prompt ${prompt} diff --git a/tools/router-stop-gate.mjs b/tools/router-stop-gate.mjs deleted file mode 100644 index 9bdc0d9..0000000 --- a/tools/router-stop-gate.mjs +++ /dev/null @@ -1,87 +0,0 @@ -#!/usr/bin/env node -/** - * Stop hook addition — router chain progress tracking. - * Stage 3 of router discipline overhaul. - * - * После каждого хода: обновляет state.chainProgress на основе вызванных в этом ходу скилов. - * Helper для observer-stop-hook — он сам решает, вызывать ли (зависит от того, есть ли router-state). - */ - -import { readFileSync, writeFileSync, existsSync } from 'fs'; -import { join } from 'path'; -import { homedir } from 'os'; -import { fileURLToPath } from 'url'; -import { readStdinAsUtf8 } from './router-stdin-helper.mjs'; - -export function extractSkillInvocations(events) { - return (events || []) - .filter((e) => e && e.tool_name === 'Skill') - .map((e) => { - const raw = e.tool_input?.skill || ''; - const stripped = raw.includes(':') ? raw.split(':').pop() : raw; - return stripped; - }) - .filter(Boolean); -} - -export function updateChainProgress(state, skillsInvoked, chains) { - const chainId = state.classification?.recommendedChain; - if (!chainId || !chains[chainId]) return { ...state }; - - const sequence = chains[chainId].sequence || []; - const currentProgress = [...(state.chainProgress || [])]; - - for (const skill of skillsInvoked) { - const nextExpectedIdx = currentProgress.length; - if (nextExpectedIdx >= sequence.length) break; - if (sequence[nextExpectedIdx] === skill) { - currentProgress.push(skill); - } - } - - return { - ...state, - chainProgress: currentProgress, - chainCompleted: currentProgress.length === sequence.length && sequence.length > 0, - }; -} - -async function main() { - const input = await readStdinAsUtf8(process.stdin); - const event = JSON.parse(input || '{}'); - const sessionId = event.session_id || 'unknown'; - const events = event.turn_events || []; - - const statePath = join(homedir(), '.claude', 'runtime', `router-state-${sessionId}.json`); - if (!existsSync(statePath)) { process.stdout.write('{}'); process.exit(0); return; } - - try { - const state = JSON.parse(readFileSync(statePath, 'utf-8')); - const { loadRegistry } = await import('./registry-load.mjs'); - const registry = loadRegistry({ useCache: false }); - const skills = extractSkillInvocations(events); - const updated = updateChainProgress(state, skills, registry.chains || {}); - updated.skillInvokedThisTurn = skills.length > 0; - writeFileSync(statePath, JSON.stringify(updated, null, 2)); - process.stdout.write('{}'); - process.exit(0); - } catch (err) { - process.stderr.write(`[router-stop-gate] ${err.message}\n`); - process.stdout.write('{}'); - process.exit(0); - } -} - -// CLI guard — Windows-cyrillic path quirk: compare resolved path, not raw argv[1] -const isMain = (() => { - try { - return process.argv[1] && - fileURLToPath(import.meta.url) === fileURLToPath(`file:///${process.argv[1].replace(/\\/g, '/')}`); - } catch { - return process.argv[1] && import.meta.url === `file://${process.argv[1].replace(/\\/g, '/')}`; - } -})(); - -if (isMain) { - main(); -} diff --git a/tools/router-stop-gate.test.mjs b/tools/router-stop-gate.test.mjs deleted file mode 100644 index 49d8bc1..0000000 --- a/tools/router-stop-gate.test.mjs +++ /dev/null @@ -1,66 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { updateChainProgress, extractSkillInvocations } from './router-stop-gate.mjs'; - -const chains = { - L1: { name: 'brainstorming chain', sequence: ['brainstorming', 'writing-plans', 'executing-plans'] }, - L13: { name: 'finance chain', sequence: ['billing-audit', 'pest', 'ru-tax-accounting'] }, -}; - -describe('extractSkillInvocations', () => { - it('extracts skill names from Skill tool invocations', () => { - const events = [ - { tool_name: 'Skill', tool_input: { skill: 'brainstorming' } }, - { tool_name: 'Edit', tool_input: {} }, - { tool_name: 'Skill', tool_input: { skill: 'writing-plans' } }, - ]; - expect(extractSkillInvocations(events)).toEqual(['brainstorming', 'writing-plans']); - }); - - it('returns empty when no Skill invocations', () => { - expect(extractSkillInvocations([{ tool_name: 'Edit' }])).toEqual([]); - }); - - it('strips namespace prefix (superpowers:brainstorming → brainstorming)', () => { - const events = [{ tool_name: 'Skill', tool_input: { skill: 'superpowers:brainstorming' } }]; - expect(extractSkillInvocations(events)).toEqual(['brainstorming']); - }); -}); - -describe('updateChainProgress', () => { - it('appends matched chain step to chainProgress', () => { - const state = { classification: { recommendedChain: 'L1' }, chainProgress: [] }; - const updated = updateChainProgress(state, ['brainstorming'], chains); - expect(updated.chainProgress).toEqual(['brainstorming']); - }); - - it('appends multiple steps if multiple skills invoked', () => { - const state = { classification: { recommendedChain: 'L1' }, chainProgress: [] }; - const updated = updateChainProgress(state, ['brainstorming', 'writing-plans'], chains); - expect(updated.chainProgress).toEqual(['brainstorming', 'writing-plans']); - }); - - it('ignores skills not in chain sequence', () => { - const state = { classification: { recommendedChain: 'L1' }, chainProgress: [] }; - const updated = updateChainProgress(state, ['random-skill'], chains); - expect(updated.chainProgress).toEqual([]); - }); - - it('marks chainCompleted=true when last step reached', () => { - const state = { classification: { recommendedChain: 'L1' }, chainProgress: ['brainstorming', 'writing-plans'] }; - const updated = updateChainProgress(state, ['executing-plans'], chains); - expect(updated.chainCompleted).toBe(true); - }); - - it('preserves existing chainProgress without duplicates', () => { - const state = { classification: { recommendedChain: 'L1' }, chainProgress: ['brainstorming'] }; - const updated = updateChainProgress(state, ['brainstorming', 'writing-plans'], chains); - expect(updated.chainProgress).toEqual(['brainstorming', 'writing-plans']); - }); -}); - -describe('UTF-8 cyrillic stdin (regression — Stage 3 fix 1)', () => { - it('module loads with UTF-8 helper wired (smoke)', async () => { - const mod = await import('./router-stop-gate.mjs'); - expect(typeof mod.updateChainProgress).toBe('function'); - }); -}); diff --git a/tools/status-md-generator.mjs b/tools/status-md-generator.mjs index 220592c..7667a6b 100644 --- a/tools/status-md-generator.mjs +++ b/tools/status-md-generator.mjs @@ -447,7 +447,6 @@ ${qualityRows} export function renderStatus(inputs) { const { now, c1, c2, c3, c5, observer, lastRetroDaysAgo, discipline } = inputs; - const c6 = inputs.c6 || { status: 'ok', detail: '—' }; const missed = inputs.missed || { totalMissed: 0, byNode: {}, byClassification: {} }; function formatPercent(p) { return `${(p * 100).toFixed(1)}%`; } @@ -497,7 +496,6 @@ Last updated: ${now} | C3 Observer-of-observer | ${iconFor(c3.status)} | ${c3.detail || '—'} | | C4 Сигнальный статус | ✅ | This file (self-reference) | | C5 Observer-coverage | ${iconFor(c5.status)} | ${c5.detail || '—'} | -| C6 Chain map sync | ${iconFor(c6.status)} | ${c6.detail || '—'} | ${inputs.guardBoardBlock ? `\n${inputs.guardBoardBlock.trim()}\n` : ''} ## Метрики (информационные, не алерты) @@ -611,7 +609,6 @@ if (process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('/status-md- c2: runControllerNode(['tools/cross-ref-checker.mjs']), c3: runControllerNode(['tools/observer-of-observer.mjs', 'check']), c5: { status: c5ok ? 'ok' : 'warn', detail: c5detail }, - c6: runControllerNode(['tools/observer-chain-map-checker.mjs']), observer: { episodeCount: countEpisodes(), observerErrors: countObserverErrors(), diff --git a/tools/status-md-generator.test.mjs b/tools/status-md-generator.test.mjs index 48da761..42bb422 100644 --- a/tools/status-md-generator.test.mjs +++ b/tools/status-md-generator.test.mjs @@ -43,7 +43,6 @@ const baseInputs = (overrides = {}) => ({ c2: { status: 'ok', detail: '0 version drift' }, c3: { status: 'ok', detail: 'last read today' }, c5: { status: 'ok', detail: 'coverage OK · registration OK' }, - c6: { status: 'ok', detail: '14 chains in sync' }, observer: { episodeCount: 12, observerErrors: 0, piiMatches: 0 }, missed: { totalMissed: 0, byNode: {}, byClassification: {} }, ...overrides, @@ -61,11 +60,6 @@ describe('renderStatus', () => { expect(md).toContain('12 episodes'); }); - it('includes a C6 chain-map row', () => { - const md = renderStatus(baseInputs()); - expect(md).toContain('| C6 Chain map sync | ✅'); - }); - it('shows a warn status for the coverage controller', () => { const md = renderStatus(baseInputs({ c5: { status: 'warn', detail: '3 commits, 0 episodes' } })); expect(md).toContain('| C5 Observer-coverage | ⚠️');