12 KiB
Design: enforce-upstream-loop — технические гейты цикла
Issue: #59. Статус: черновик до builder-check; владелец 25.10.2026: «оба гейта сразу», согласования отменены — решения ниже фиксируются в «Decisions made without discussion» и действуют как решения владельца.
Контекст
Сессия ce9187ca: модель пропустила builder-check, рационализировала пропуск, план появился до утверждения дизайна. Цель — гейты, которые нельзя пропустить текстом.
Артефакты и их писатели
| Артефакт | Пишет | Когда |
|---|---|---|
openspec/changes/<id>/builder-check.md |
контроллер (отчёт свежего субагента, скопированный в change) | после builder-check, до передачи дизайна владельцу |
openspec/changes/<id>/approval.md |
только команда /approve |
владелец утверждает design/plan |
План docs/superpowers/plans/*.md |
writing-plans-флоу | после design-утверждения (гейт A) |
| Диспатч implementer'а | sdd-флоу | после plan-утверждения (гейт B) |
Формат approval.md (yaml):
design:
by: aep
at: 2026-10-25T10:00:00+04:00
plan:
by: aep
at: 2026-10-25T11:30:00+04:00
sha256: 3f2c9a… (hex, 64 знака)
Механика гейтов (в бандле, DSH-only)
ctx.tools.guard уже зовёт bash-хуки (lib/guards.js). Новая
lib/gates.js добавляет content-проверки после хуков, в той же
цепочке (порядок: guard-main/require-plan → approval-gates; оба — deny с
причиной, монотонно):
- Гейт A (план до approve дизайна). Для Edit/Write/MultiEdit с
путём под
docs/superpowers/plans/: из содержимого (tool_input.contentилиnew_string) извлечь**Spec:** <путь>; если путь указывает вopenspec/changes/<id>/иapproval.mdбез секцииdesign:— DENY «design not approved; run /approve design». Содержимое без**Spec:**или со Spec-ссылкой внеopenspec/changes/(legacydocs/superpowers/specs/) — DENY «plan must carry Spec: pointing to openspec/changes/» (D-9); правка без содержимого (bash-цель,str_replace_editorcreate) и фрагментeditбез**Spec:**тоже DENY — гейт проверяет только то содержимое, которое видит. - Гейт B (sdd до approve плана). Два входа (D-7): (a) запись в
.superpowers/sdd/<план>/**(ledger, брифы — старт sdd в Режиме 1) и (b) диспатчsubagent/subagent_fork/workflow, в промпте или скрипте которого есть ссылка на план —docs/superpowers/plans/<имя>.mdили.superpowers/sdd/<план>/. Из плана взять**Spec:**→ change;approval.mdбез секцииplan:или с чужим sha256 — DENY «plan not approved; run /approve plan» / «plan changed after approval; re-approve». Проверяются все план-ссылки, а не первая; ссылка без читаемого плана и план без Spec — fail closed. /approve(command-approve.js):designтребует существование proposal.md, design.md, builder-check.md;planтребует секциюdesign:с непустымиby/atи существование ровно одного плана со Spec на этот change (несколько — отказ с перечислением). Дописывает/обновляет секцию, остальные секции не трогает. Владелец-агент различие: команда пишетby: owner; вызовы не от владельца — отказ (как отличить: DSH не даёт ролей; используем факт вызова из интерактивной сессии и AskUserQuestion-подтверждение:/approveспрашивает подтверждение перед записью, ответ «да» = владелец).
Исторические планы: четыре файла написаны до лупа (T1.3, T4.3) —
2026-10-11-roles-and-memory несёт legacy-**Spec:** в
docs/superpowers/specs/, а 2026-10-24-dsh-decouple,
2026-10-24-dsh-port-spike и 2026-10-24-dsh-role-loader — без **Spec:**.
По D-9 запись в любой из них — DENY: они заморожены, новая работа идёт
отдельным планом. Это ожидаемое поведение, а не регресс.
Decisions made without discussion
| # | Решение | Обоснование | Цена ошибки |
|---|---|---|---|
| D-1 | /approve — единственный писатель approval.md |
Маркер, который не может подделать модель (нет прямых записей в approval.md — гейт A защищает и его? нет: approval.md не в plans/; добавлю guard: любые Edit/Write в **/approval.md из сессии — DENY, кроме команды) |
Обход остаётся через API-инструменты — принимаем как остаточный риск, фиксируем в спеке |
| D-2 | Гейты в бандле (DSH-only), CC-хуки не трогаем | CC-поток не должен меняться; один enforcement-слой на рантайм | Два мира правил (CC/DSH) — уже так, осознанно |
| D-3 | Презентация дизайна владельцу не гейтится напрямую; закрыта цепочкой артефактов | Гейтить сообщения модели технически нельзя; цепочка: builder-check → approve → план → sdd делает пропуск видимым на первом же BLOCKED | Модель может «показать» дизайн без builder-check.md — тогда /approve откажет, и это видно владельцу |
| D-4 | /approve требует подтверждения (вопрос владельцу) перед записью |
Отделить владельца от модели в DSH (ролей нет) | Лишний клик на каждое утверждение — приемлемо |
| D-5 | builder-check.md пишет контроллер после завершения субагента | Единственная точка, где отчёт субагента гарантированно сохранён в change | Файл можно подделать вручную — принимаем: гейт против случайного пропуска, не против злого умысла |
| D-6 | Гейты не срабатывают для bounded-задач (нет change/плана) | Bounded-цикл не проходит лупа и не должен блокироваться | Никакого |
| D-7 | Гейт B = два механизма: (a) write в .superpowers/sdd/<plan>/** (ledger — старт sdd в Mode 1) → план → Spec → change → plan-утверждение; (b) диспатчи subagent/subagent_fork/workflow с план-ссылкой в промпте/скрипте → та же проверка |
Mode 1 передаёт брифы файлами, промпт короткий без пути плана; ledger пишет контроллер — write-гейт реально ловит старт sdd | Ложный блок при смене имён каталогов — митигируется нормализацией путей |
| D-8 | Bash-обход закрыт: для Bash-вызовов gates.js прогоняет python3 hooks/harness_target.py targets '<cmd>' <cwd> и применяет гейты A/C к целям под docs/superpowers/plans/ и **/approval.md |
Тот же парсер, что в хуках (heredoc-aware), без дублирования логики | Стоимость — subprocess на каждый Bash-вызов с подозрительным текстом; фильтруем до прогона по грубой подстроке |
| D-9 | План без **Spec:** в openspec/changes/ — DENY по умолчанию (включая legacy docs/superpowers/specs/) |
Реальные планы всегда несут Spec; .bounded-поток планов не пишет — проверено по репозиторию | Ложный блок легитимного Spec-less плана — такого флоу нет |
| D-10 | plan: в approval.md несёт sha256 файла плана; гейт B сверяет хэш — план изменён после утверждения → DENY «plan changed after approval; re-approve» |
Утверждение привязывается к содержимому исполняемого объекта | Правки плана после approve требуют re-approve — осознанно |
| D-11 | design: — только непустые by и at (без хэша): design.md дополняется решениями по ходу |
Контент дизайна не является исполняемым объектом; пустая секция design: утверждением не считается, by/at должны быть непустыми (T1.8) |
Правка дизайна после approve не отзывается — принято |
| D-12 | Подтверждение владельца в /approve — через ctx.userQuestions.ask (блокирующий вопрос); сервис добавляется в inject; отсутствует — /approve отказывает с сообщением |
Проверенный примитив рантайма (DELEGATED_CALLER не пройдёт) вместо эвристики «интерактивная сессия» | Нет userQuestions в профиле — /approve недоступен, гейты всё равно работают на блокировку |
| D-13 | Бутстрап: гейты активны только в сессиях с новым бандлом (после установки/рестарта); сама фича #59 исполняется на старом рантайме — ретро-утверждений не требует; для будущих change контроллер кладёт builder-check.md штатно | Развёртывание разводит enforcement и исполняемую фичу во времени | Окно до переустановки бандла — гейтов нет; фиксируем шаг переустановки в чеклисте закрытия |
Тесты (идеи для плана)
- parse approval.md / отказы / до-записи существующих секций;
- гейт A: write плана со Spec на неутверждённый change → DENY; без Spec → OK; утверждённый → OK;
- гейт B: subagent с промптом-ссылкой на план: без plan-секции → DENY, с ней → OK; промпт без ссылки → OK;
/approve designбез builder-check.md → отказ; полный флоу design→plan → OK;- guard на записи в
**/approval.mdвне команды → DENY.
На потом (в план)
- wiring
lib/gates.jsвctx.tools.guardцепочку + конфигapprovalGates: true; command-approve.js+ регистрация; sdd-skill текст; roles-заметка;- спека-дельта capability
approval-gates; README/PORT; тесты.