108 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
sha256: 3f2c9a… (hex, 64 знака)
```
## Механика гейтов (в бандле, 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:**` или со Spec-ссылкой вне `openspec/changes/` (legacy
`docs/superpowers/specs/`) — DENY «plan must carry **Spec:** pointing to
openspec/changes/<id>» (D-9); правка без содержимого (bash-цель,
`str_replace_editor` create) и фрагмент `edit` без `**Spec:**` тоже DENY —
гейт проверяет только то содержимое, которое видит.
2. **Гейт 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 <id> plan» / «plan changed after approval;
re-approve». Проверяются все план-ссылки, а не первая; ссылка без
читаемого плана и план без Spec — fail closed.
3. **`/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; тесты.