feat(m7): re-plan на ходу (impl-карвут) + эскалация судьи (escape-honor + счётчик NO-GO) + docs-хвост

This commit is contained in:
Дмитрий
2026-06-12 15:48:17 +03:00
parent b8684db135
commit 09620f368e
6 changed files with 124 additions and 28 deletions
+5
View File
@@ -18,6 +18,7 @@
```
Не дублирует:
- `docs/Открытые_вопросы_v8_3.md` — там бизнес/продуктовые вопросы (`Биз-*`, `CTO-*`, `Ю-*`, `Диз-*`)
- `docs/superpowers/specs/` — там design specs
- `docs/superpowers/plans/` — там implementation plans
@@ -77,10 +78,12 @@
**Где:** [tools/enforce-router-gate.mjs](../tools/enforce-router-gate.mjs) (matcher Bash) + [tools/enforce-powershell-gate.mjs](../tools/enforce-powershell-gate.mjs) (matcher PowerShell).
**Симптом:** Невозможно выполнить ни одну git-команду из проектной сессии — даже read-only (`git rev-parse`, `git status`). Обе оболочки default-deny:
- Bash: `[router-gate] команда не в whitelist — default-deny (§5.1)`
- PowerShell: `[powershell-gate] cmdlet «git» не в whitelist — default-deny (§5.1.2)`
**Детали:**
- 30.05.2026 заказчик попросил закоммитить spec + plan фичи lead-region-resolution в ветку `feat/lead-region-resolution`. Попытка `git switch -c` + `git commit -- <explicit paths>` через Bash → блок router-gate. Тот же git через PowerShell-tool → блок powershell-gate (F1 из v3.8 — PowerShell-путь закрыт).
- Архитектурно git из проектной сессии под v4 **неработоспособен**: router-gate default-deny на Bash блокирует git-команду ДО того, как сработает side-channel `approve_git_operation` (§4.5 / §3.5 v3.5 design) — то есть механизм AskUserQuestion-одобрения git-операции недостижим, потому что сама команда не доходит до gate-логики одобрения.
- Файлы spec + plan **сохранены на диск** (untracked, не потеряны): `docs/superpowers/specs/2026-05-29-lead-region-resolution-design.md` + `docs/superpowers/plans/2026-05-29-lead-region-resolution.md`. Просто не закоммичены.
@@ -88,6 +91,7 @@
**Статус:** открыт. Связано с общей темой over-blocking защиты v4 (см. запись выше про memory).
**Решение:** TBD. Варианты:
- (1) Добавить git в whitelist router-gate / powershell-gate под AskUserQuestion-одобрение (восстановить путь S8 v3.5 — gate должен пропускать git-команду ДО проверки whitelist, если есть свежий `approve_git_operation`).
- (2) Закоммитить вручную заказчиком (вне Claude-сессии) или из сессии с ослабленной защитой.
- (3) Дождаться фикса v4 (зона параллельной router-gate v4 сессии).
@@ -101,6 +105,7 @@
**Где:** [tools/enforce-router-gate.mjs](../tools/enforce-router-gate.mjs) whitelist (§5.1).
**Симптом:** Невозможно реализовать ни одну кодовую фичу из проектной сессии — гейт режет инструменты разработки:
- `php artisan test` / `php artisan migrate``команда не в whitelist — default-deny` (whitelist разрешает только `list`/`route:list`/`migrate:status`).
- `./vendor/bin/pest` / `composer test` → не в whitelist → блок.
- `cd app && <что-угодно>` → цепочка → не в whitelist → блок. А т.к. artisan лежит в `app/`, до даже whitelisted-команд (`php artisan migrate:status`) не добраться без `cd app`.
+24 -23
View File
@@ -1,6 +1,6 @@
# Brain Status (auto-generated)
Last updated: 2026-06-12T07:52:35.138Z
Last updated: 2026-06-12T08:38:34.416Z
| Контролёр | Состояние | Детали |
|---|---|---|
@@ -8,7 +8,7 @@ Last updated: 2026-06-12T07:52:35.138Z
| 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 | ✅ | 1016 episode(s) this month · Stop-hook + post-commit OK |
| C5 Observer-coverage | ✅ | 1029 episode(s) this month · Stop-hook + post-commit OK |
| C6 Chain map sync | ✅ | [chain-map-checker] OK — 16 chains in sync |
## Кто на посту (оборона М1–М6)
@@ -39,21 +39,21 @@ Last updated: 2026-06-12T07:52:35.138Z
| Время | Действие | Причина |
|---|---|---|
| 2026-06-12T07:50:46.978Z | write:c:/users/administrator/.claude/projects/c---------------------crm-------------/08ffc075-92c9-49b2-9a80-33075dee4aa | path «C:/Users/Administrator/.claude/projects/c---------------------crm-------------/08ffc075-92c9-49b2-9a80-33075dee4aa |
| 2026-06-12T07:47:10.194Z | write:c:/users/administrator/.claude/projects/c---------------------crm-------------/08ffc075-92c9-49b2-9a80-33075dee4aa | path «C:/Users/Administrator/.claude/projects/c---------------------crm-------------/08ffc075-92c9-49b2-9a80-33075dee4aa |
| 2026-06-12T07:45:36.089Z | write:c:/users/administrator/.claude/projects/c---------------------crm-------------/08ffc075-92c9-49b2-9a80-33075dee4aa | path «C:/Users/Administrator/.claude/projects/c---------------------crm-------------/08ffc075-92c9-49b2-9a80-33075dee4aa |
| 2026-06-12T07:42:58.116Z | write:c:/users/administrator/.claude/projects/c---------------------crm-------------/08ffc075-92c9-49b2-9a80-33075dee4aa | path «C:/Users/Administrator/.claude/projects/c---------------------crm-------------/08ffc075-92c9-49b2-9a80-33075dee4aa |
| 2026-06-12T07:32:35.103Z | write:c:/users/administrator/.claude/projects/c---------------------crm-------------/08ffc075-92c9-49b2-9a80-33075dee4aa | path «C:/Users/Administrator/.claude/projects/c---------------------crm-------------/08ffc075-92c9-49b2-9a80-33075dee4aa |
| 2026-06-12T07:26:14.531Z | write:c:/users/administrator/.claude/projects/c---------------------crm-------------/08ffc075-92c9-49b2-9a80-33075dee4aa | path «C:/Users/Administrator/.claude/projects/c---------------------crm-------------/08ffc075-92c9-49b2-9a80-33075dee4aa |
| 2026-06-12T07:11:57.995Z | write:c:/users/administrator/.claude/projects/c---------------------crm-------------/8d15e1a4-4cf9-4bbd-8565-5e7aa8cc344 | path «C:/Users/Administrator/.claude/projects/c---------------------crm-------------/8d15e1a4-4cf9-4bbd-8565-5e7aa8cc344 |
| 2026-06-12T07:10:09.575Z | write:c:/users/administrator/.claude/projects/c---------------------crm-------------/memory/project_router_mentor_main_s | §6: правка ЗАКОНА (content edits a rules/discipline section) требует escape владельца — скил недостаточен |
| 2026-06-12T07:07:33.824Z | write:c:/users/administrator/.claude/projects/c---------------------crm-------------/8d15e1a4-4cf9-4bbd-8565-5e7aa8cc344 | path «C:/Users/Administrator/.claude/projects/c---------------------crm-------------/8d15e1a4-4cf9-4bbd-8565-5e7aa8cc344 |
| 2026-06-12T07:04:51.171Z | write:c:/users/administrator/.claude/projects/c---------------------crm-------------/8d15e1a4-4cf9-4bbd-8565-5e7aa8cc344 | path «C:/Users/Administrator/.claude/projects/c---------------------crm-------------/8d15e1a4-4cf9-4bbd-8565-5e7aa8cc344 |
| 2026-06-12T08:30:55.553Z | write:c:/users/administrator/.claude/projects/c---------------------crm-------------/f3950d20-72ed-4576-b13b-e863cc9afdd | path «C:/Users/Administrator/.claude/projects/c---------------------crm-------------/f3950d20-72ed-4576-b13b-e863cc9afdd |
| 2026-06-12T08:28:26.644Z | write:c:/users/administrator/.claude/projects/c---------------------crm-------------/f3950d20-72ed-4576-b13b-e863cc9afdd | path «C:/Users/Administrator/.claude/projects/c---------------------crm-------------/f3950d20-72ed-4576-b13b-e863cc9afdd |
| 2026-06-12T08:23:50.976Z | write:c:/users/administrator/.claude/projects/c---------------------crm-------------/f3950d20-72ed-4576-b13b-e863cc9afdd | path «C:/Users/Administrator/.claude/projects/c---------------------crm-------------/f3950d20-72ed-4576-b13b-e863cc9afdd |
| 2026-06-12T08:22:39.219Z | write:c:/users/administrator/.claude/projects/c---------------------crm-------------/2b6762d2-da4b-4f9e-b7d4-c7dd7896b10 | path «C:/Users/Administrator/.claude/projects/c---------------------crm-------------/2b6762d2-da4b-4f9e-b7d4-c7dd7896b10 |
| 2026-06-12T08:21:49.847Z | write:c:/users/administrator/.claude/projects/c---------------------crm-------------/f3950d20-72ed-4576-b13b-e863cc9afdd | path «C:/Users/Administrator/.claude/projects/c---------------------crm-------------/f3950d20-72ed-4576-b13b-e863cc9afdd |
| 2026-06-12T08:17:11.915Z | write:c:/users/administrator/.claude/projects/c---------------------crm-------------/f3950d20-72ed-4576-b13b-e863cc9afdd | path «C:/Users/Administrator/.claude/projects/c---------------------crm-------------/f3950d20-72ed-4576-b13b-e863cc9afdd |
| 2026-06-12T08:16:42.806Z | write:c:/users/administrator/.claude/projects/c---------------------crm-------------/f3950d20-72ed-4576-b13b-e863cc9afdd | path «C:/Users/Administrator/.claude/projects/c---------------------crm-------------/f3950d20-72ed-4576-b13b-e863cc9afdd |
| 2026-06-12T08:11:27.068Z | write:c:/users/administrator/.claude/projects/c---------------------crm-------------/2b6762d2-da4b-4f9e-b7d4-c7dd7896b10 | path «C:/Users/Administrator/.claude/projects/c---------------------crm-------------/2b6762d2-da4b-4f9e-b7d4-c7dd7896b10 |
| 2026-06-12T08:10:12.924Z | write:c:/users/administrator/.claude/projects/c---------------------crm-------------/08ffc075-92c9-49b2-9a80-33075dee4aa | path «C:/Users/Administrator/.claude/projects/c---------------------crm-------------/08ffc075-92c9-49b2-9a80-33075dee4aa |
| 2026-06-12T08:01:58.123Z | write:c:/users/administrator/.claude/projects/c---------------------crm-------------/08ffc075-92c9-49b2-9a80-33075dee4aa | path «C:/Users/Administrator/.claude/projects/c---------------------crm-------------/08ffc075-92c9-49b2-9a80-33075dee4aa |
## Метрики (информационные, не алерты)
- Observer evidence: 1016 episodes this month, 0 observer_error markers, 0 PII matches before filter
- Legacy v1 episodes (not in factor analysis): 1016
- Observer evidence: 1029 episodes this month, 0 observer_error markers, 5 PII matches before filter
- Legacy v1 episodes (not in factor analysis): 1029
- Last /brain-retro: 16 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).
@@ -63,15 +63,15 @@ Baseline дисциплины роутера (этап 2 router discipline overh
| Тип задачи | Эпизодов | % с триггер-матчем | % через скил |
|---|---|---|---|
| planning | 138 | 7.2% | 20.3% |
| analysis | 38 | 5.3% | 0.0% |
| planning | 140 | 7.1% | 20.7% |
| analysis | 39 | 5.1% | 0.0% |
| feature | 32 | 9.4% | 3.1% |
| bugfix | 30 | 13.3% | 16.7% |
| bugfix | 32 | 12.5% | 15.6% |
| cleanup | 1 | 0.0% | 0.0% |
Router step distribution: 1: 501, 2: 361, 3: 18, 5: 116
Router step distribution: 1: 508, 2: 366, 3: 18, 5: 117
Boundaries applied (ADR / границы): 8 of 996 эпизодов (0.8%).
Boundaries applied (ADR / границы): 8 of 1009 эпизодов (0.8%).
## Активные многоэтапные проекты
@@ -89,10 +89,10 @@ Boundaries applied (ADR / границы): 8 of 996 эпизодов (0.8%).
| Компонент | Токены (in/out) | USD |
|---|---|---|
| Classifier (Sonnet 4.6) | 59182/218321 | $3.45 |
| Classifier (Sonnet 4.6) | 61793/222668 | $3.53 |
| Self-assessment (Sonnet 4.6) | 0/0 | $0.00 |
| Reviewer (Opus 4.7 + fallback) | 0/0 | $0.00 |
| **Итого** | | **$3.45** |
| **Итого** | | **$3.53** |
## Аномалии классификатора
@@ -105,7 +105,7 @@ Episodes since last run: 542 / threshold: 10
## Reviewer: субагент vs fallback
0 эпизодов проверено из 1016.
0 эпизодов проверено из 1029.
## Reviewer findings
@@ -131,7 +131,8 @@ Episodes since last run: 542 / threshold: 10
| PID | Имя | CPU-время | Возраст |
|---|---|---|---|
| 3508 | MsMpEng | 1.65ч | 16667722.4ч |
| 3508 | MsMpEng | 1.74ч | 220164.6ч |
| 1248 | svchost | 1.00ч | 0.0ч |
⚠️ Проверь, не «осиротевшие» ли это процессы от завершённых Claude-сессий.
@@ -30,9 +30,9 @@
вместе с регистрацией М1-М6: хук `enforce-normative-content-rules` УЖЕ живой, а коммит
расширяет его матчер на `tools/enforce-*.mjs` (ad-hoc → ЗАКОН → нужен escape М6, которого
до Фазы 8 нет) — отдельный cherry-pick запрёт Claude от правок enforce-инструментов.
- [ ] Регистрация М1-М6 в `.claude/settings.json` (шаг владельца — Claude файл закрыт).
- [ ] Увольнение v4-зоопарка (§10): старые enforce-хуки заменяются машинами.
- [ ] План отката.
- [x] ~~Регистрация М1-М6 в `.claude/settings.json`~~ (выполнено владельцем 12.06).
- [x] ~~Увольнение v4-зоопарка (§10): старые enforce-хуки заменяются машинами.~~
- [x] ~~План отката.~~ Бэкап `.claude/settings.json.bak-phase8-2026-06-12-flip` снят.
Фаза 8 попутно закрывает три известных занозы:
+38
View File
@@ -231,6 +231,16 @@ export async function runJudgeTurn(event, { mode, logImpl = logVerdictLine, warn
try { warnImpl(event); } catch { /* best-effort */ }
}
if (mode === 'shadow') return { block: false }; // D28: логирует, не блокирует
// M7: escape-валве владельца (эскалация). Судья чтит escape-грант, как стена/пол: упёрся
// NO-GO → владелец кликает «разрешаю» → судья пропускает. «Не смог договориться → вышел на владельца».
try {
const eg = await import('./escape-grant.mjs');
const sess = event && event.session_id;
const action = eg.canonicalAction(event && event.tool_name, (event && event.tool_input) || {});
if (eg.escapeGrantOpen(action, eg.loadFloorEscapes(sess), eg.loadConsumed(sess))) {
return { block: false, message: 'judge: разрешено аварийным выходом владельца (escape)' };
}
} catch { /* escape недоступен → обычное решение судьи */ }
const d = decide({ mode, verdict, floorBlocked: false });
return { block: d.block, message: d.message };
}
@@ -300,6 +310,22 @@ function sealTurnProd(event, verdict, mode) {
});
}
const JUDGE_ESCALATE_AFTER = 3;
/**
* M7 эскалация (round-control C-12): счётчик ПОДРЯД идущих NO-GO судьи в сессии.
* blocked=true → +1; blocked=false (allow) → сброс 0. Возвращает новый счёт.
* fsImpl/dir инъектируемы для тестов. Best-effort — ошибка I/O не ломает судью.
*/
export function bumpJudgeNoGo({ sessionId, blocked, fsImpl = fsDefault, dir = runtimeDir() } = {}) {
const safe = String(sessionId || 'unknown').replace(/[^a-zA-Z0-9_-]/g, '_');
const path = join(dir, `judge-nogo-${safe}.json`);
let count = 0;
try { count = (JSON.parse(fsImpl.readFileSync(path, 'utf8')).count) || 0; } catch { count = 0; }
const next = blocked ? count + 1 : 0;
try { fsImpl.mkdirSync(dir, { recursive: true }); fsImpl.writeFileSync(path, JSON.stringify({ count: next })); } catch { /* best-effort */ }
return next;
}
async function main() {
let event, mode;
try {
@@ -310,6 +336,18 @@ async function main() {
try {
result = await runJudgeTurn(event, { mode, onWiredSeal: sealTurnProd }); // inert/shadow/live-block внутри
} catch { exitDecision({ block: mode === 'live-block' }); return; } // fail-CLOSE только в live-block
// M7 эскалация (round-control C-12): подряд идущие NO-GO судьи. allow → сброс. После 3-го подряд —
// сообщение «ЭСКАЛАЦИЯ ВЛАДЕЛЬЦУ» (судья сам выходит на владельца; продавить — escape, который судья
// уже чтит). Счётчик best-effort, на сам факт block не влияет.
try {
const judgedPlan = extractGate1Product(event).shouldJudge || extractGate2Product(event).shouldJudge;
if (judgedPlan) {
const n = bumpJudgeNoGo({ sessionId: (event && event.session_id) || 'unknown', blocked: !!result.block });
if (result.block && n >= JUDGE_ESCALATE_AFTER) {
result = { ...result, message: `[judge-gate] ЭСКАЛАЦИЯ ВЛАДЕЛЬЦУ: судья отказал ${n} раз(а) подряд по плану. Реши — дай escape на это действие, чтобы продавить, либо подтверди отказ. (round-control C-12)` };
}
}
} catch { /* счётчик best-effort */ }
if (result.block) { logGuardBlock(event, 'М4 Судья', result.message); exitDecision({ block: true, message: result.message || '[judge-gate] block' }); }
else exitDecision({ block: false });
}
+20 -2
View File
@@ -18,6 +18,19 @@ import { assertSafeSessionId } from './action-journal.mjs';
import { classifyDestructive } from './classify-destructive.mjs';
import { canonicalAction, escapeGrantOpen, escapeAllowsEvent, loadFloorEscapes, loadConsumed } from './escape-grant.mjs';
import { logGuardBlock } from './guard-block-log.mjs';
import { existsSync } from 'node:fs';
// M7 Ф8 (bootstrap): авторская запись НОВОГО артефакта/плана (Write нового .md в
// docs/superpowers/{specs,plans}/) — это КАНАЛ печати, не реализация. В разговорном режиме
// пропускаем: судья оценит содержимое и опечатает ТОЛЬКО на GO. Перезапись существующего
// файла НЕ разрешаем (existsSync). До печати запись власти не даёт — стена закрыта для правок.
const AUTHORING_PATH_RE = /(^|[/\\])docs[/\\]superpowers[/\\](?:specs|plans)[/\\][^/\\]+\.md$/i;
export function isAuthoringWrite(toolUse, { existsImpl = existsSync } = {}) {
if (!toolUse || toolUse.name !== 'Write') return false;
const fp = String(toolUse.input?.file_path || '');
if (!AUTHORING_PATH_RE.test(fp)) return false;
try { return !existsImpl(fp); } catch { return false; }
}
// Узкий технический allowlist загрузки (НЕ «карта критического») — без него
// нельзя создать первый план: writing-plans пишет план, AskUser/EnterPlanMode
@@ -178,6 +191,11 @@ function extOf(p) {
*/
export function decide({ toolUse, frozenPlan, frozenArtifact = null, stepPtr = 0, key, verifyImpl = verifyFrozenPlan, verifyArtifactImpl = verifyFrozenArtifact, normalize }) {
if (isSeed(toolUse)) return { decision: 'allow', reason: 'seed (bootstrap D12/D13)' };
// M7 Ф8 (re-plan): авторская запись НОВОГО плана/спеки разрешена и В РЕЖИМЕ РЕАЛИЗАЦИИ —
// иначе «по ходу работы возникло препятствие, нужно переписать план» не сработает (стена
// держит старый план, новый не записать). Безопасно: новый план так же судят наставник +
// судья + freeze-gate; запись черновика власти не даёт до печати (как в разговорном режиме).
if (isAuthoringWrite(toolUse)) return { decision: 'allow', reason: 'authoring write (re-plan, build-loop) — impl-режим' };
if (frozenPlan && (toolUse?.name === 'Read' || toolUse?.name === 'Grep')) {
const p = actionOf(toolUse).object;
const ev = decideReadEvent({
@@ -254,14 +272,14 @@ export function decideMode({ toolUse, frozenPlan, frozenArtifact, stepPtr = 0, k
return { decision: 'allow', mode: 'escape', reason: 'разрешено аварийным выходом владельца (floor_escape) — указатель не двигается' };
}
if (!frozenPlan) {
if (isSeed(toolUse) || isObserveOnly(toolUse)) return { decision: 'allow', mode: 'conversational', reason: 'seed/observe (разговорный режим)' };
if (isSeed(toolUse) || isObserveOnly(toolUse) || isAuthoringWrite(toolUse)) return { decision: 'allow', mode: 'conversational', reason: 'seed/observe/authoring (разговорный режим)' };
return { decision: 'block', mode: 'conversational', reason: 'разговорный режим: только думать/спрашивать (реализация — после печати артефакта и плана)' };
}
if (!frozenArtifact || !verifyArtifactImpl(frozenArtifact, key)) {
// F-B (аудит 2026-06-07): observe-only (Read/Grep/Glob/readonly-Bash/TodoWrite) пускаем
// и в этом деградированном состоянии — инвариант finding 9 «смотрящие не душатся» +
// согласованность с decide() (там observe-only allow безусловно). Бэкстоп держит только мутаторы.
if (isSeed(toolUse) || isObserveOnly(toolUse)) return { decision: 'allow', mode: 'conversational', reason: 'seed/observe (бэкстоп: артефакт не опечатан)' };
if (isSeed(toolUse) || isObserveOnly(toolUse) || isAuthoringWrite(toolUse)) return { decision: 'allow', mode: 'conversational', reason: 'seed/observe/authoring (бэкстоп: артефакт не опечатан)' };
return { decision: 'block', mode: 'conversational', reason: 'нет опечатанного артефакта разговорной фазы — вернись в разговор (бэкстоп C-10)' };
}
// SE-2 (fail-closed whitelist): энфорсмент ТОЛЬКО при live-block на ОБЕИХ печатях.
+34
View File
@@ -0,0 +1,34 @@
import { describe, it, expect } from 'vitest';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { bumpJudgeNoGo } from './enforce-judge-gate.mjs';
describe('M7 эскалация судьи: счётчик подряд идущих NO-GO (round-control C-12)', () => {
it('blocked подряд → счёт растёт; allow → сброс в 0', () => {
const dir = mkdtempSync(join(tmpdir(), 'judge-nogo-'));
try {
expect(bumpJudgeNoGo({ sessionId: 's1', blocked: true, dir })).toBe(1);
expect(bumpJudgeNoGo({ sessionId: 's1', blocked: true, dir })).toBe(2);
expect(bumpJudgeNoGo({ sessionId: 's1', blocked: true, dir })).toBe(3); // ← порог эскалации
expect(bumpJudgeNoGo({ sessionId: 's1', blocked: false, dir })).toBe(0); // allow → сброс
expect(bumpJudgeNoGo({ sessionId: 's1', blocked: true, dir })).toBe(1); // снова с 1
} finally { rmSync(dir, { recursive: true, force: true }); }
});
it('разные сессии — независимые счётчики', () => {
const dir = mkdtempSync(join(tmpdir(), 'judge-nogo-'));
try {
expect(bumpJudgeNoGo({ sessionId: 'a', blocked: true, dir })).toBe(1);
expect(bumpJudgeNoGo({ sessionId: 'b', blocked: true, dir })).toBe(1);
expect(bumpJudgeNoGo({ sessionId: 'a', blocked: true, dir })).toBe(2);
} finally { rmSync(dir, { recursive: true, force: true }); }
});
it('битый/опасный sessionId не ломает (санитизация пути) и стартует с 1', () => {
const dir = mkdtempSync(join(tmpdir(), 'judge-nogo-'));
try {
expect(bumpJudgeNoGo({ sessionId: '../../evil', blocked: true, dir })).toBe(1);
} finally { rmSync(dir, { recursive: true, force: true }); }
});
});