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
62 lines
3.6 KiB
JavaScript
62 lines
3.6 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* enforce-snapshot (М6, Блок 2) — PreToolUse после enforce-floor. Перед разрушительным
|
||
* (snapshotNeeded) делает git-точку возврата (git stash create + update-ref
|
||
* refs/floor-snapshots/<id>), пишет restore-points.jsonl. Чистое дерево → ref=HEAD (успех);
|
||
* реальная ошибка git → fail-CLOSE (block). Пишет в runtime как процесс-хук (легитимно).
|
||
*/
|
||
import { appendFileSync, mkdirSync } from 'node:fs';
|
||
import { join, dirname } from 'node:path';
|
||
import { homedir } from 'node:os';
|
||
import { execFileSync } from 'node:child_process';
|
||
import { readStdin, parseEventJson, exitDecision } from './enforce-hook-helpers.mjs';
|
||
import { snapshotNeeded, resolveGitState } from './snapshot-decide.mjs';
|
||
import { canonicalAction } from './escape-grant.mjs';
|
||
import { logGuardBlock } from './guard-block-log.mjs';
|
||
|
||
function defaultGit() {
|
||
try {
|
||
const stashOut = execFileSync('git', ['stash', 'create'], { encoding: 'utf-8' });
|
||
const headOut = execFileSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf-8' });
|
||
return { stashOut, headOut, error: false };
|
||
} catch { return { stashOut: '', headOut: '', error: true }; }
|
||
}
|
||
function defaultWrite(rec) {
|
||
const path = join(homedir(), '.claude', 'runtime', 'restore-points.jsonl');
|
||
try { mkdirSync(dirname(path), { recursive: true }); } catch {}
|
||
try { appendFileSync(path, JSON.stringify(rec) + '\n'); } catch {}
|
||
}
|
||
|
||
// FIX-4: уникальный id снимка по умолчанию — ts + pid + монотонный счётчик, чтобы два
|
||
// разрушительных в одну миллисекунду не записали один и тот же refs/floor-snapshots/<id>
|
||
// (update-ref второго клобберил бы точку возврата первого).
|
||
let __snapSeq = 0;
|
||
function defaultSnapId(now) { __snapSeq += 1; return `${now}-${process.pid}-${__snapSeq}`; }
|
||
|
||
/** Чистое решение (инъекция git/write/id/now для тестов). */
|
||
export function snapshotDecision(event, { gitImpl = defaultGit, writeImpl = defaultWrite, idImpl, now = Date.now() } = {}) {
|
||
if (!snapshotNeeded(event.tool_name, event.tool_input || {})) return { block: false };
|
||
const st = resolveGitState(gitImpl());
|
||
if (!st.ok) return { block: true, message: '[snapshot] не смог сделать точку возврата (ошибка git) — действие остановлено' };
|
||
const id = (idImpl ? idImpl() : defaultSnapId(now)) + '';
|
||
if (gitImpl === defaultGit) {
|
||
try { execFileSync('git', ['update-ref', `refs/floor-snapshots/${id}`, st.ref]); }
|
||
catch { return { block: true, message: '[snapshot] update-ref не удался — fail-close' }; }
|
||
}
|
||
writeImpl({ ts: now, action: canonicalAction(event.tool_name, event.tool_input || {}), head: st.ref,
|
||
snapshot_ref: `refs/floor-snapshots/${id}`, restore_command: `git checkout refs/floor-snapshots/${id} -- .` });
|
||
return { block: false };
|
||
}
|
||
|
||
async function main() {
|
||
try {
|
||
const ev = parseEventJson(await readStdin());
|
||
const r = snapshotDecision(ev);
|
||
if (r.block) logGuardBlock(ev, 'М6 Снимок', r.message);
|
||
exitDecision({ block: r.block, message: r.block ? r.message : undefined });
|
||
} catch { exitDecision({ block: false }); } // снимок — страховка; своя инфра-ошибка не должна клинить (но git-ошибка выше = block)
|
||
}
|
||
import { fileURLToPath } from 'node:url';
|
||
const isCli = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
|
||
if (isCli) main();
|