92 lines
11 KiB
Markdown
92 lines
11 KiB
Markdown
# 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):
|
||
|
||
```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/<id>/` и `approval.md` без секции `design:` — DENY
|
||
«design not approved; run /approve <id> 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 <id> 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-цикл не проходит лупа и не должен блокироваться | Никакого |
|
||
| 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:` — только существование секции (без хэша): design.md дополняется решениями по ходу | Контент дизайна не является исполняемым объектом | Правка дизайна после 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; тесты.
|