feat(escape-sign): writer подписывает floor_escape при наличии ключа (M6 FIX-5 Task 2)
processEvent (PostToolUse AskUser) теперь подписывает floor_escape-пропуск
подписью FLOOR_ESCAPE, когда ключ доступен (resolveReceiptKey):
- +keyImpl=resolveReceiptKey, fsImpl={appendFileSync,mkdirSync} — инъекция для
hermetic-тестов; резолв ключа один раз на событие (fail-safe: ошибка → key=null
→ floor_escape пишется неподписанным, PostToolUse-наблюдаемость не ломается).
- esc подписывается только при наличии ключа; approve_git_operation (rec) НЕ
трогаем (§2.2). Нет ключа → esc без sig (как сегодня).
- Запись через fsImpl.* вместо прямых node:fs.
TDD: 2 новых теста (ключ → валидная подпись; ключ null → без sig). Регрессия
существующего enforce-askuser-answer-parser GREEN (approve_git_operation-путь цел).
Суммарно 10 GREEN по затронутым файлам.
План: docs/superpowers/plans/2026-06-10-floor-escape-signing.md (Task 2)
Прод-код инертен до провижининга ключа (Фаза 8).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+13
-12
@@ -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-сессий.
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user