# Design: enforce-upstream-loop — технические гейты цикла Issue: #59. Статус: **черновик до builder-check**; владелец 25.10.2026: «оба гейта сразу», согласования отменены — решения ниже фиксируются в «Decisions made without discussion» и действуют как решения владельца. ## Контекст Сессия ce9187ca: модель пропустила builder-check, рационализировала пропуск, план появился до утверждения дизайна. Цель — гейты, которые нельзя пропустить текстом. ## Артефакты и их писатели | Артефакт | Пишет | Когда | |---|---|---| | `openspec/changes//builder-check.md` | контроллер (отчёт свежего субагента, скопированный в change) | после builder-check, до передачи дизайна владельцу | | `openspec/changes//approval.md` | только команда `/approve` | владелец утверждает design/plan | | План `docs/superpowers/plans/*.md` | writing-plans-флоу | после design-утверждения (гейт A) | | Диспатч implementer'а | sdd-флоу | после plan-утверждения (гейт B) | Формат `approval.md` (yaml): ```yaml design: by: aep at: 2026-10-25T10:00:00+04:00 plan: by: aep at: 2026-10-25T11:30:00+04:00 ``` ## Механика гейтов (в бандле, DSH-only) `ctx.tools.guard` уже зовёт bash-хуки (`lib/guards.js`). Новая `lib/gates.js` добавляет **content-проверки** после хуков, в той же цепочке (порядок: guard-main/require-plan → approval-gates; оба — deny с причиной, монотонно): 1. **Гейт A (план до approve дизайна).** Для Edit/Write/MultiEdit с путём под `docs/superpowers/plans/`: из содержимого (`tool_input.content` или `new_string`) извлечь `**Spec:** <путь>`; если путь указывает в `openspec/changes//` и `approval.md` без секции `design:` — DENY «design not approved; run /approve design». Содержимое без Spec-ссылки на change — пропуск. 2. **Гейт B (sdd до approve плана).** Для `subagent`/`subagent_fork`: в промпте найти ссылку на план — `docs/superpowers/plans/<имя>.md` или `.superpowers/sdd/<план>/`; из плана взять `**Spec:**` → change; `approval.md` без секции `plan:` — DENY «plan not approved; run /approve plan». Нет ссылки на план — пропуск. 3. **`/approve`** (`command-approve.js`): `design` требует существование proposal.md, design.md, builder-check.md; `plan` требует секцию `design:` и существование ровно одного плана со Spec на этот change (несколько — отказ с перечислением). Дописывает/обновляет секцию, остальные секции не трогает. Владелец-агент различие: команда пишет `by: owner`; вызовы не от владельца — отказ (как отличить: DSH не даёт ролей; используем факт вызова из интерактивной сессии и AskUserQuestion-подтверждение: `/approve` спрашивает подтверждение перед записью, ответ «да» = владелец). ## 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-цикл не проходит лупа и не должен блокироваться | Никакого | ## Тесты (идеи для плана) - 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; тесты.