12 KiB
Raw Blame History

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 с причиной, монотонно):

  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 design». Содержимое без **Spec:** или со Spec-ссылкой вне openspec/changes/ (legacy docs/superpowers/specs/) — DENY «plan must carry Spec: pointing to openspec/changes/» (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 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; тесты.