From f1b9cce71f5045547134993df4086d897c7abf97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Sun, 21 Jun 2026 08:51:03 +0300 Subject: [PATCH] =?UTF-8?q?feat(wall):=20=D0=BE=D0=BA=D0=BD=D0=BE=20=D0=B4?= =?UTF-8?q?=D0=BE=D0=B7=D0=B0=D0=BF=D0=B8=D1=81=D0=B8=20=D0=B6=D1=83=D1=80?= =?UTF-8?q?=D0=BD=D0=B0=D0=BB=D0=B0=20=D0=BE=D1=85=D0=BE=D1=82=D0=BD=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=B2=20=D0=BA=D0=B0=D1=80=D0=B0=D0=BD=D1=82?= =?UTF-8?q?=D0=B8=D0=BD=20docs/observer/questions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Правка машинерии №3 скила surfacing-open-questions (спека v6, раздел stena3): isLedgerAppend пускает Write/Edit в карантин open-questions-*.md (префикс/.md/без вложенности), включена в разрешение разговорной фазы рядом с isAuthoringWrite. Co-Authored-By: Claude Opus 4.8 (1M context) --- tools/enforce-supreme-gate.mjs | 14 ++++++++++++-- tools/enforce-supreme-gate.test.mjs | 16 ++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/tools/enforce-supreme-gate.mjs b/tools/enforce-supreme-gate.mjs index b58cdd0..8673428 100644 --- a/tools/enforce-supreme-gate.mjs +++ b/tools/enforce-supreme-gate.mjs @@ -37,6 +37,16 @@ export function isAuthoringWrite(toolUse, { existsImpl = existsSync } = {}) { try { return !existsImpl(fp); } catch { return false; } } +// Окно журнала охотника (№3): дозапись append-only реестра в карантин docs/observer/questions/. +// Только префикс open-questions-, только .md, без вложенности. .md не исполняется; карантин без +// исходников/нормативки/настроек. Негативы (вне карантина/не .md/не префикс/вложенность) — блок. +const LEDGER_PATH_RE = /(^|[/\\])docs[/\\]observer[/\\]questions[/\\]open-questions-[^/\\]+\.md$/i; +export function isLedgerAppend(toolUse) { + if (!toolUse || (toolUse.name !== 'Write' && toolUse.name !== 'Edit')) return false; + const fp = String(toolUse.input?.file_path || ''); + return LEDGER_PATH_RE.test(fp); +} + // Узкий технический allowlist загрузки (НЕ «карта критического») — без него // нельзя создать первый план: writing-plans пишет план, AskUser/EnterPlanMode // открывают одобрение. Обоснование — D12/D13. @@ -409,14 +419,14 @@ export function decideMode({ toolUse, frozenPlan, frozenArtifact, stepPtr = 0, k return { decision: 'allow', mode: 'conversational', finishPlan: true, reason: 'владелец завершил план досрочно (plan-done) — печать снята, возврат в разговор' }; } if (!frozenPlan) { - if (isSeed(toolUse) || isObserveOnly(toolUse) || isQueryOnly(toolUse) || isAuthoringWrite(toolUse)) return { decision: 'allow', mode: 'conversational', reason: 'seed/observe/query/authoring (разговорный режим)' }; + if (isSeed(toolUse) || isObserveOnly(toolUse) || isQueryOnly(toolUse) || isAuthoringWrite(toolUse) || isLedgerAppend(toolUse)) return { decision: 'allow', mode: 'conversational', reason: 'seed/observe/query/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) || isQueryOnly(toolUse) || isAuthoringWrite(toolUse)) return { decision: 'allow', mode: 'conversational', reason: 'seed/observe/query/authoring (бэкстоп: артефакт не опечатан)' }; + if (isSeed(toolUse) || isObserveOnly(toolUse) || isQueryOnly(toolUse) || isAuthoringWrite(toolUse) || isLedgerAppend(toolUse)) return { decision: 'allow', mode: 'conversational', reason: 'seed/observe/query/authoring (бэкстоп: артефакт не опечатан)' }; return { decision: 'block', mode: 'conversational', reason: 'нет опечатанного артефакта разговорной фазы — вернись в разговор (бэкстоп C-10)' }; } // SE-2 (fail-closed whitelist): энфорсмент ТОЛЬКО при live-block на ОБЕИХ печатях. diff --git a/tools/enforce-supreme-gate.test.mjs b/tools/enforce-supreme-gate.test.mjs index fe14fea..59b764e 100644 --- a/tools/enforce-supreme-gate.test.mjs +++ b/tools/enforce-supreme-gate.test.mjs @@ -9,6 +9,22 @@ import { resolveStepPtr } from './enforce-supreme-gate.mjs'; import { resolveSessionId } from './enforce-supreme-gate.mjs'; import { signStepState, verifyStepState } from './enforce-supreme-gate.mjs'; import { stepStatePath } from './enforce-supreme-gate.mjs'; +import { isLedgerAppend } from './enforce-supreme-gate.mjs'; + +describe('isLedgerAppend (окно журнала №3)', () => { + const ok = (fp, name = 'Write') => isLedgerAppend({ name, input: { file_path: fp } }); + it('пускает Write/Edit в карантин open-questions-*.md', () => { + expect(ok('docs/observer/questions/open-questions-export-leads.md')).toBe(true); + expect(ok('docs/observer/questions/open-questions-export-leads.md', 'Edit')).toBe(true); + }); + it('НЕ пускает вне карантина / не .md / не тот префикс / вложенность', () => { + expect(ok('docs/observer/questions/secrets.md')).toBe(false); + expect(ok('tools/open-questions-x.md')).toBe(false); + expect(ok('docs/observer/questions/open-questions-x.txt')).toBe(false); + expect(ok('docs/observer/questions/sub/open-questions-x.md')).toBe(false); + expect(ok('docs/observer/questions/open-questions-x.md', 'Bash')).toBe(false); + }); +}); // N3-shared (2026-06-07 аудит M1-M4): путь файла указателя шага строится из sessionId // (resolveSessionId(event), недоверенный источник) — тот же guard формы, что action-journal.