feat(wall): окно дозаписи журнала охотника в карантин docs/observer/questions

Правка машинерии №3 скила surfacing-open-questions (спека v6, раздел stena3):
isLedgerAppend пускает Write/Edit в карантин open-questions-*.md (префикс/.md/без
вложенности), включена в разрешение разговорной фазы рядом с isAuthoringWrite.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-06-21 08:51:03 +03:00
parent 81da2e2c45
commit f1b9cce71f
2 changed files with 28 additions and 2 deletions
+12 -2
View File
@@ -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 на ОБЕИХ печатях.
+16
View File
@@ -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.