diff --git a/docs/observer/STATUS.md b/docs/observer/STATUS.md index c1c31cfb..8bcd257d 100644 --- a/docs/observer/STATUS.md +++ b/docs/observer/STATUS.md @@ -1,6 +1,6 @@ # Brain Status (auto-generated) -Last updated: 2026-06-10T02:04:54.234Z +Last updated: 2026-06-10T02:13:22.381Z | Контролёр | Состояние | Детали | |---|---|---| @@ -8,7 +8,7 @@ Last updated: 2026-06-10T02:04:54.234Z | C2 Cross-ref consistency | ✅ | [cross-ref-checker] OK — 0 drift in 4 files | | C3 Observer-of-observer | ✅ | [observer-of-observer] OK — last read 2 week(s) ago | | C4 Сигнальный статус | ✅ | This file (self-reference) | -| C5 Observer-coverage | ✅ | 801 episode(s) this month · Stop-hook + post-commit OK | +| C5 Observer-coverage | ✅ | 803 episode(s) this month · Stop-hook + post-commit OK | | C6 Chain map sync | ✅ | [chain-map-checker] OK — 16 chains in sync | ## Кто на посту (оборона М1–М6) @@ -33,20 +33,21 @@ Last updated: 2026-06-10T02:04:54.234Z | enforce-verify-gate.mjs | `enforce-verify-gate.mjs` | 🔴 | | enforce-criterion-gate.mjs | `enforce-criterion-gate.mjs` | 🔴 | -Недавние escape владельца: 0 · Недавние блоки: 3 +Недавние escape владельца: 0 · Недавние блоки: 4 **Недавние блоки (детали):** | Время | Действие | Причина | |---|---|---| +| 2026-06-10T02:05:25.416Z | write:c:/users/administrator/.claude/projects/c---------------------crm-------------/3faf96c1-77d9-4993-bdd6-6c9d4859be4 | path «C:/Users/Administrator/.claude/projects/c---------------------crm-------------/3faf96c1-77d9-4993-bdd6-6c9d4859be4 | | 2026-06-10T02:04:19.082Z | write:c:/users/administrator/.claude/projects/c---------------------crm-------------/3faf96c1-77d9-4993-bdd6-6c9d4859be4 | path «C:/Users/Administrator/.claude/projects/c---------------------crm-------------/3faf96c1-77d9-4993-bdd6-6c9d4859be4 | | 2026-06-10T02:03:29.396Z | write:c:/users/administrator/.claude/projects/c---------------------crm-------------/3faf96c1-77d9-4993-bdd6-6c9d4859be4 | path «C:/Users/Administrator/.claude/projects/c---------------------crm-------------/3faf96c1-77d9-4993-bdd6-6c9d4859be4 | | 2026-06-10T01:29:36.756Z | write:c:/users/administrator/.claude/projects/c---------------------crm-------------/3faf96c1-77d9-4993-bdd6-6c9d4859be4 | path «C:/Users/Administrator/.claude/projects/c---------------------crm-------------/3faf96c1-77d9-4993-bdd6-6c9d4859be4 | ## Метрики (информационные, не алерты) -- Observer evidence: 801 episodes this month, 0 observer_error markers, 0 PII matches before filter -- Legacy v1 episodes (not in factor analysis): 801 +- Observer evidence: 803 episodes this month, 0 observer_error markers, 0 PII matches before filter +- Legacy v1 episodes (not in factor analysis): 803 - Last /brain-retro: 14 day(s) ago - Использование узлов: см. `/brain-retro` (раз в спринт). missed_activations: 0. **Неиспользованные узлы — не алерт, если профильной задачи не было** (Pravila §16.4 v1.36; capability-readiness; см. memory `feedback_brain_unused_tools_not_problem` — outside-repo memory store). @@ -56,14 +57,14 @@ Baseline дисциплины роутера (этап 2 router discipline overh | Тип задачи | Эпизодов | % с триггер-матчем | % через скил | |---|---|---|---| -| planning | 111 | 9.0% | 19.8% | +| planning | 112 | 8.9% | 19.6% | | analysis | 35 | 5.7% | 0.0% | | feature | 27 | 11.1% | 3.7% | | bugfix | 27 | 14.8% | 18.5% | -Router step distribution: 1: 384, 2: 295, 3: 18, 5: 88 +Router step distribution: 1: 385, 2: 296, 3: 18, 5: 88 -Boundaries applied (ADR / границы): 8 of 785 эпизодов (1.0%). +Boundaries applied (ADR / границы): 8 of 787 эпизодов (1.0%). ## Активные многоэтапные проекты @@ -97,7 +98,7 @@ Episodes since last run: 542 / threshold: 10 ## Reviewer: субагент vs fallback -0 эпизодов проверено из 801. +0 эпизодов проверено из 803. ## Reviewer findings @@ -123,9 +124,9 @@ Episodes since last run: 542 / threshold: 10 | PID | Имя | CPU-время | Возраст | |---|---|---|---| -| 3916 | MsMpEng | 3.61ч | 98.6ч | -| 1208 | svchost | 1.45ч | 1327490.1ч | -| 4 | System | 1.18ч | 0.0ч | +| 3916 | MsMpEng | 3.66ч | NaNч | +| 1208 | svchost | 1.45ч | NaNч | +| 4 | System | 1.19ч | 16667668.7ч | ⚠️ Проверь, не «осиротевшие» ли это процессы от завершённых Claude-сессий. diff --git a/tools/enforce-askuser-answer-parser-floor-escape.test.mjs b/tools/enforce-askuser-answer-parser-floor-escape.test.mjs new file mode 100644 index 00000000..7baed1f7 --- /dev/null +++ b/tools/enforce-askuser-answer-parser-floor-escape.test.mjs @@ -0,0 +1,38 @@ +// tools/enforce-askuser-answer-parser-floor-escape.test.mjs +import { describe, it, expect } from 'vitest'; +import { join } from 'node:path'; +import { processEvent } from './enforce-askuser-answer-parser.mjs'; +import { verifyFloorEscapeRecord } from './askuser-answer-parser.mjs'; + +function memFs() { + const s = new Map(); const norm = (p) => String(p).replace(/\\/g, '/'); + return { s, + appendFileSync: (p, d) => { const n = norm(p); s.set(n, (s.get(n) || '') + d); }, + mkdirSync: () => {} }; +} +const DIR = '/rt'; const KEY = 'test-receipt-key'; +const ev = (action) => ({ + session_id: 's1', + tool_input: { questions: [{ question: 'Q?' }] }, + tool_response: { answers: { 'Q?': `да, разрешаю. FLOOR-ESCAPE: ${action}` } }, +}); +function readLines(fs) { + const raw = fs.s.get(join(DIR, 'askuser-decisions-s1.jsonl').replace(/\\/g, '/')) || ''; + return raw.trim().split('\n').filter(Boolean).map((l) => JSON.parse(l)); +} + +describe('processEvent — key-gated подпись floor_escape', () => { + it('ключ есть → floor_escape несёт валидную подпись', () => { + const fs = memFs(); + processEvent(ev('bash:git push --force'), { runtimeDir: DIR, nowMs: 5, keyImpl: () => KEY, fsImpl: fs }); + const esc = readLines(fs).find((r) => r.type === 'floor_escape'); + expect(esc).toBeTruthy(); + expect(verifyFloorEscapeRecord(esc, KEY)).toBe(true); + }); + it('ключ null → floor_escape без подписи (как сегодня)', () => { + const fs = memFs(); + processEvent(ev('bash:git push --force'), { runtimeDir: DIR, nowMs: 5, keyImpl: () => null, fsImpl: fs }); + const esc = readLines(fs).find((r) => r.type === 'floor_escape'); + expect(esc.sig).toBeUndefined(); + }); +}); diff --git a/tools/enforce-askuser-answer-parser.mjs b/tools/enforce-askuser-answer-parser.mjs index f28da4cb..d056fdf3 100644 --- a/tools/enforce-askuser-answer-parser.mjs +++ b/tools/enforce-askuser-answer-parser.mjs @@ -16,7 +16,8 @@ import { appendFileSync, mkdirSync } from 'node:fs'; import { homedir } from 'node:os'; import { join, dirname } from 'node:path'; -import { toApprovalRecord, toFloorEscapeRecord } from './askuser-answer-parser.mjs'; +import { toApprovalRecord, toFloorEscapeRecord, signFloorEscapeRecord } from './askuser-answer-parser.mjs'; +import { resolveReceiptKey } from './receipt-key-config.mjs'; /** * Pure event processor for test-injection of runtimeDir + nowMs. @@ -26,7 +27,7 @@ import { toApprovalRecord, toFloorEscapeRecord } from './askuser-answer-parser.m * @param {string} [opts.runtimeDir] - override default ~/.claude/runtime * @param {number} [opts.nowMs] - override timestamp for test determinism */ -export function processEvent(event, { runtimeDir, nowMs } = {}) { +export function processEvent(event, { runtimeDir, nowMs, keyImpl = resolveReceiptKey, fsImpl = { appendFileSync, mkdirSync } } = {}) { try { const sessionId = event && event.session_id; const toolInput = event && event.tool_input; @@ -35,6 +36,9 @@ export function processEvent(event, { runtimeDir, nowMs } = {}) { const questions = toolInput.questions || []; const answers = toolResponse.answers || {}; + // M6 FIX-5: резолв ключа подписи один раз на событие (fail-safe — ошибка резолва + // → key=null → floor_escape пишется неподписанным, PostToolUse-наблюдаемость цела). + let key = null; try { key = keyImpl(); } catch { key = null; } const dir = runtimeDir || join(homedir(), '.claude', 'runtime'); const path = join(dir, `askuser-decisions-${sessionId}.jsonl`); @@ -45,14 +49,16 @@ export function processEvent(event, { runtimeDir, nowMs } = {}) { const ans = answers[q.question]; if (!ans) continue; const rec = toApprovalRecord(ans, { question: q.question, nowMs }); - const esc = toFloorEscapeRecord(ans, { nowMs }); + // M6 FIX-5: подписываем ТОЛЬКО floor_escape (§2.2 — approve_git_operation не трогаем). + let esc = toFloorEscapeRecord(ans, { nowMs }); + if (esc && key) esc = signFloorEscapeRecord(esc, key); for (const out of [rec, esc]) { if (!out) continue; if (!wroteAny) { - try { mkdirSync(dirname(path), { recursive: true }); } catch { /* ignore */ } + try { fsImpl.mkdirSync(dirname(path), { recursive: true }); } catch { /* ignore */ } wroteAny = true; } - try { appendFileSync(path, JSON.stringify(out) + '\n'); } catch { /* fail-open */ } + try { fsImpl.appendFileSync(path, JSON.stringify(out) + '\n'); } catch { /* fail-open */ } } } } catch {