fix: escape-окошки мимо anti-cosmetic-стража (escape-flow unblock)

Anti-cosmetic-детектор (>2 простых AskUser за сессию → hard-block, требует brainstorming) глушил
ЛЕГИТИМНЫЙ поток escape-окошек: владелец даёт разрешение FLOOR-ESCAPE через AskUser, и после >2
таких окошек стена их блокировала — нормативку под стеной нельзя было довести (баг найден живым
прогоном 18.06). Фикс по аналогии с git-approval exemption (Calibration 5): isEscapeAuthQuestion
(вопрос несёт метку FLOOR-ESCAPE) освобождается в decide() — не косметика, не считаем, не блокируем.
Не абьюзится: метка сигналит авторизацию владельца, а не подмену идеации. Свод 4383 зелёный.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-06-18 23:20:01 +03:00
parent bf3d557cce
commit f2365caf4b
2 changed files with 37 additions and 1 deletions
+13 -1
View File
@@ -50,6 +50,18 @@ export function isGitApprovalQuestion(questions) {
q.options.some((o) => o && typeof o.label === 'string' && GIT_CMD_RE.test(o.label)));
}
// Escape-авторизация владельца — санкционированный канал floor_escape (владелец вставляет строку
// FLOOR-ESCAPE в ответ окошка), НЕ замена структурной идеации. По аналогии с git-approval
// (Calibration 5): такие окошки не косметика → не считаем и не блокируем. Иначе anti-cosmetic
// страж глушил легитимный поток разрешений после >2 окошек (баг найден живым прогоном 18.06).
const FLOOR_ESCAPE_RE = /FLOOR-ESCAPE:/i;
/** True if this AskUser is an owner escape-authorization prompt (question text carries the FLOOR-ESCAPE marker). */
export function isEscapeAuthQuestion(questions) {
if (!Array.isArray(questions)) return false;
return questions.some((q) => q && typeof q.question === 'string' && FLOOR_ESCAPE_RE.test(q.question));
}
/**
* Pure cosmetic-AskUser decision (v4.1 §4.5).
* Caller passes PRIOR counts; decide computes prospective new counts.
@@ -62,7 +74,7 @@ export function decide({ questions, simpleCountSession = 0, simpleCountTurn = 0,
// git-approval channel, never cosmetic ideation. Allow, do not count, never
// block. (Cannot be abused to dodge ideation discipline: a git-command label
// makes the answer a real approve_git_operation, not a cosmetic clarification.)
if (isGitApprovalQuestion(questions)) {
if (isGitApprovalQuestion(questions) || isEscapeAuthQuestion(questions)) {
return { action: 'allow', block: false, reason: null, isSimpleAB: false, newSessionCount: simpleCountSession, newTurnCount: simpleCountTurn };
}
const simple = isSimpleAB(questions);
@@ -0,0 +1,24 @@
import { describe, it, expect } from 'vitest';
import { isEscapeAuthQuestion, decide } from './askuser-cosmetic-detector.mjs';
const escQ = [{ question: 'Разрешение: FLOOR-ESCAPE: write:c:/x.md — авторизуешь?', options: [{ label: 'Отмена' }, { label: 'Не получается' }] }];
const plainQ = [{ question: 'Какой вариант?', options: [{ label: 'A' }, { label: 'B' }] }];
describe('escape-окошки освобождены от cosmetic-счётчика (FLOOR-ESCAPE в вопросе)', () => {
it('isEscapeAuthQuestion: вопрос с FLOOR-ESCAPE → true', () => {
expect(isEscapeAuthQuestion(escQ)).toBe(true);
});
it('isEscapeAuthQuestion: обычный вопрос → false; не массив → false', () => {
expect(isEscapeAuthQuestion(plainQ)).toBe(false);
expect(isEscapeAuthQuestion(null)).toBe(false);
});
it('decide: escape-окошко при >2 простых за сессию → allow (не hard_block)', () => {
const r = decide({ questions: escQ, simpleCountSession: 3, brainstormingInvoked: false });
expect(r.action).toBe('allow');
expect(r.block).toBe(false);
});
it('decide: обычное простое окошко при >2 → hard_block (без регрессии)', () => {
const r = decide({ questions: plainQ, simpleCountSession: 3, brainstormingInvoked: false });
expect(r.action).toBe('hard_block');
});
});