84231a1470
Доска «кто на посту» (STATUS.md §7) теперь показывает реальные недавние escape владельца и блоки машин М1–М6 вместо хардкода []/[]. - new tools/guard-block-log.mjs: logGuardBlock (best-effort, fail-quiet, Node fs append в guard-blocks-<sess>.jsonl) + loadRecentBlocks/ loadRecentEscapes (скан session-файлов runtime, окно 24ч + cap 10, ts→ISO). - проводка logGuardBlock в block-ветку main() 9 машинных хуков (floor / supreme-gate / judge-gate / snapshot / read-path-deny / mcp-classification / normative-content-rules / verify-gate / criterion-gate). Логгер вызывается ПОСЛЕ решения, не влияет на block; decide() pure не тронут. - status-md-generator CLI: recentEscapes/recentBlocks из читателей вместо []/[]. До флипа Фазы 8 доска показывает 0/0 (хуки не зарегистрированы — данных нет); реальная польза — пост-флип наблюдаемость. TDD: guard-block-log.test (6) + 9 структурных wiring-тестов + 1 board-тест. Гейт закрытия: sharp-edges (промежуточный по 9 хукам + читатели) + variant-analysis (все block-ветки покрыты, иных источников нет). Регрессия tools-only 3465 passed / 2 skipped / 0 failed (было 3449+2skip). 0 регрессий. Plan: docs/superpowers/plans/2026-06-10-guard-board-live-source.md
71 lines
3.8 KiB
JavaScript
71 lines
3.8 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* guard-block-log — журнал блоков обороны М1–М6 для доски «кто на посту» (D-3).
|
|
* Логгер пишут машинные хуки при РЕШЁННОМ блоке (best-effort, fail-quiet, Node fs —
|
|
* как logVerdictLine/logViolation). Читатели сканируют все session-файлы runtime для
|
|
* глобальной доски (board-генератор не имеет одного session_id). Достоверность журнала —
|
|
* при зарегистрированном поле-страже runtime (Фаза 8); до флипа данных нет (0/0).
|
|
*/
|
|
import fsDefault from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { homedir } from 'node:os';
|
|
import { canonicalAction } from './escape-grant.mjs';
|
|
|
|
function defaultRuntimeDir() { return join(homedir(), '.claude', 'runtime'); }
|
|
|
|
/** Чистая запись блока. */
|
|
export function buildGuardBlockEntry({ machine, action, reason, now }) {
|
|
return { ts: now, machine: String(machine ?? ''), action: String(action ?? ''), reason: String(reason ?? '') };
|
|
}
|
|
|
|
/** Best-effort: записать блок машины. action — из canonicalAction(event); sess — из event.session_id.
|
|
* НИКОГДА не бросает (вызывается в block-ветке хука; сбой логгирования не влияет на блок). */
|
|
export function logGuardBlock(event, machine, reason, { fsImpl = fsDefault, runtimeDir = defaultRuntimeDir(), now = Date.now() } = {}) {
|
|
try {
|
|
const action = canonicalAction(event && event.tool_name, (event && event.tool_input) || {});
|
|
const sess = (event && event.session_id) || 'unknown';
|
|
const entry = buildGuardBlockEntry({ machine, action, reason, now });
|
|
fsImpl.mkdirSync(runtimeDir, { recursive: true });
|
|
fsImpl.appendFileSync(join(runtimeDir, `guard-blocks-${sess}.jsonl`), JSON.stringify(entry) + '\n');
|
|
} catch { /* fail-quiet */ }
|
|
}
|
|
|
|
function scanSessionFiles(fsImpl, runtimeDir, prefix) {
|
|
let names = [];
|
|
try { names = fsImpl.readdirSync(runtimeDir).filter((f) => f.startsWith(prefix) && f.endsWith('.jsonl')); }
|
|
catch { return []; }
|
|
const out = [];
|
|
for (const name of names) {
|
|
let raw; try { raw = fsImpl.readFileSync(join(runtimeDir, name), 'utf8'); } catch { continue; }
|
|
for (const line of String(raw).split(/\r?\n/)) {
|
|
const t = line.trim(); if (!t) continue;
|
|
let r; try { r = JSON.parse(t); } catch { continue; }
|
|
out.push(r);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function windowSortCap(recs, { now, windowMs, limit }) {
|
|
return recs
|
|
.filter((r) => r && typeof r.ts === 'number' && now - r.ts >= 0 && now - r.ts <= windowMs)
|
|
.sort((a, b) => b.ts - a.ts)
|
|
.slice(0, limit)
|
|
.map((r) => ({ ...r, ts: new Date(r.ts).toISOString() }));
|
|
}
|
|
|
|
/** Недавние блоки машин для доски. */
|
|
export function loadRecentBlocks({ fsImpl = fsDefault, runtimeDir = defaultRuntimeDir(), now = Date.now(), windowMs = 86400000, limit = 10 } = {}) {
|
|
const recs = scanSessionFiles(fsImpl, runtimeDir, 'guard-blocks-')
|
|
.map((r) => ({ ts: r.ts, machine: r.machine, action: r.action, reason: r.reason }));
|
|
return windowSortCap(recs, { now, windowMs, limit });
|
|
}
|
|
|
|
/** Недавние escape владельца (floor_escape) для доски. */
|
|
export function loadRecentEscapes({ fsImpl = fsDefault, runtimeDir = defaultRuntimeDir(), now = Date.now(), windowMs = 86400000, limit = 10 } = {}) {
|
|
const recs = scanSessionFiles(fsImpl, runtimeDir, 'askuser-decisions-')
|
|
.filter((r) => r && r.type === 'floor_escape' && typeof r.action === 'string')
|
|
.map((r) => ({ ts: typeof r.ts === 'number' ? r.ts : 0, machine: 'escape', action: r.action, reason: 'escape владельца' }));
|
|
return windowSortCap(recs, { now, windowMs, limit });
|
|
}
|