18 KiB
Spec Delta: approval-gates
Purpose
Технические гейты цикла разработки в DSH-бандле: builder-check → утверждение
дизайна → план → утверждение плана → sdd. Инструкции superpowers сами по себе не
являются механизмом (сессия ce9187ca 24.10.2026 пропустила builder-check, а план
появился до утверждения дизайна), поэтому бандл проверяет артефакты утверждения на
каждом переходе и отказывает с BLOCKED [approval-gates]: <причина>. Гейты живут
только в DSH-бандле (D-2); Claude Code-хуки и поток не меняются.
ADDED Requirements
Requirement: Артефакты утверждения
openspec/changes/<id>/builder-check.md SHALL содержать отчёт свежего
builder-check субагента, скопированный в change контроллером; он MUST быть записан
после builder-check и до передачи дизайна владельцу.
openspec/changes/<id>/approval.md SHALL быть маркером утверждения владельцем;
его единственный писатель — команда /approve (D-1), инструменты модели писать
его MUST NOT (гейт C ниже).
Формат approval.md — yaml с секциями design: (поля by, at) и plan:
(поля by, at, sha256); sha256 — hex-хэш sha256 содержимого файла плана на
момент утверждения (D-10). Запись MUST опускать пустые поля, разбор MUST быть
терпимым: неизвестные секции и строки игнорируются, битый файл деградирует в
«секции нет», и гейт отказывает.
Секция design: SHALL считаться утверждением только при непустых by и at:
пустая секция — не утверждение (T1.8). Запись одной секции MUST NOT менять
содержимое другой.
Scenario: Пустая секция design не разблокирует план
- WHEN
approval.mdсодержит заголовокdesign:без полейbyиat - THEN гейт A отказывает с
design not approved, как будто секции нет
Scenario: Утверждение плана не теряет утверждение дизайна
- WHEN
/approve <id> planзаписывает plan-секцию в существующийapproval.md - THEN design-секция остаётся дословно прежней
Requirement: Команда /approve
/approve <change-id> design|plan SHALL быть единственным писателем
approval.md и MUST регистрироваться при enabled и approvalGates: true,
независимо от флагов roleCommand и checkCommand: иначе гейты остаются без
команды-разблокировки (T3.2). change-id MUST быть одним безопасным сегментом
пути; небезопасный id или неизвестный вид — usage-отказ без записи.
Вид design SHALL требовать существования proposal.md, design.md и
builder-check.md в openspec/changes/<id>/; при отсутствии файла команда
MUST отказать с перечислением отсутствующих и MUST NOT спрашивать владельца.
Вид plan SHALL требовать design-секцию с непустыми by и at (иначе отказ
design not approved; run /approve <id> design) и ровно одного плана в
docs/superpowers/plans/, чей **Spec:** указывает на <id>; ноль планов —
отказ, несколько — отказ с перечислением файлов. Утверждаемый план MUST получать
sha256 своего текущего содержимого.
Подтверждение владельца MUST идти через ctx.userQuestions.ask (D-12): вопрос с
вариантами Approve и Cancel; без сервиса команда MUST отказать, а не писать. При
подтверждении пишутся by: owner и at в ISO 8601 UTC; при отмене или
неподтверждении — отказ not confirmed, файл не меняется. Команда MUST NOT
создавать план и не трогать proposal.md, design.md, builder-check.md.
Scenario: /approve design без builder-check
- WHEN в change есть proposal и design, но нет
builder-check.md - THEN команда отказывает с перечислением
builder-check.md, владелец не спрашивается,approval.mdне создаётся
Scenario: /approve plan без утверждения дизайна
- WHEN
approval.mdне содержит непустой design-секции - THEN команда отказывает с
design not approved; run /approve <id> designи не спрашивает владельца
Scenario: /approve plan при двух планах на change
- WHEN два файла в
docs/superpowers/plans/несут**Spec:**на один change - THEN команда отказывает, перечисляя оба файла, и
approval.mdне меняется
Scenario: Утверждение плана фиксирует sha256
- WHEN владелец подтвердил
/approve <id> planпри единственном плане - THEN plan-секция содержит
by: owner,atв ISO 8601 и sha256 содержимого плана
Scenario: /approve без сервиса вопросов
- WHEN
ctx.userQuestionsотсутствует - THEN команда отказывает с
userQuestions service unavailable; /approve requires itиapproval.mdне пишется
Requirement: Гейт A — план до утверждения дизайна
Для write/edit/multi_edit/str_replace_editor с целевым путём под
docs/superpowers/plans/ гейт SHALL извлекать **Spec:** из содержимого
(content или new_string). Ссылка MUST указывать в openspec/changes/<id>;
план без **Spec:** и план с legacy-ссылкой в docs/superpowers/specs/** MUST
получать отказ plan must carry **Spec:** pointing to openspec/changes/<id>
(D-9). Правка без содержимого (bash-цель, edit-фрагмент, str_replace_editor
create) MUST получать тот же отказ: гейт не видит содержимого и не может
подтвердить Spec — fail closed.
Если change определён, а его design-секция не является утверждением, гейт MUST
отказать design not approved; run /approve <id> design. Утверждённый дизайн
разрешает запись плана; запись вне docs/superpowers/plans/ гейтом A не
проверяется.
Исторические планы, написанные до лупа и не несущие **Spec:**, заморожены —
четыре файла: 2026-10-11-roles-and-memory, 2026-10-24-dsh-decouple,
2026-10-24-dsh-port-spike, 2026-10-24-dsh-role-loader (T1.3). Любая их правка
MUST получать отказ по D-9; это ожидаемое поведение, а не дефект.
Scenario: План со Spec на неутверждённый дизайн
- WHEN write в
docs/superpowers/plans/x.mdс**Spec:** openspec/changes/<id>/, аapproval.mdбез design-секции - THEN отказ
BLOCKED [approval-gates]: design not approved; run /approve <id> design
Scenario: План без Spec
- WHEN write в
docs/superpowers/plans/x.mdбез строки**Spec:** - THEN отказ
plan must carry **Spec:** pointing to openspec/changes/<id>
Scenario: План после утверждения дизайна
- WHEN design-секция содержит непустые
byиat, а план несёт**Spec:**на этот change - THEN запись плана разрешена
Scenario: Правка плана фрагментом
- WHEN edit плана передаёт
new_stringбез**Spec:**(фрагмент правки) - THEN отказ: план пишется только full-file write
Scenario: Правка исторического плана
- WHEN write в
docs/superpowers/plans/2026-10-24-dsh-decouple.md - THEN отказ
plan must carry **Spec:** …по D-9
Requirement: Гейт B — sdd до утверждения плана
Гейт SHALL срабатывать на двух входах (D-7): (a) write в
.superpowers/sdd/<plan-name>/** (ledger, брифы) — старт sdd в Режиме 1;
(b) диспатч subagent, subagent_fork или workflow, в prompt или script
которого есть ссылка на план — docs/superpowers/plans/<имя>.md или
.superpowers/sdd/<имя>/.
Для каждого входа гейт MUST определить файл
docs/superpowers/plans/<plan-name>.md, взять из него **Spec:** → change и
потребовать plan-секцию approval.md с sha256, равным sha256 текущего
содержимого плана. Нет секции или sha256 — отказ plan not approved; run /approve <id> plan; несовпадение хэша или нечитаемый план — отказ plan changed after approval; re-approve; нет файла плана для sdd-каталога — отказ plan not found: <plan-name>; план без **Spec:** — отказ plan not approved (id
неизвестен). Проверяться MUST все план-ссылки, а не первая; возвращается первый
отказ, чтобы утверждённый план не маскировал неутверждённый.
Вызов без план-ссылки MUST NOT гейтиться. Разбор промпта — эвристика по
подстрокам, поэтому разбор fail open: вызов, в котором ссылка не распознана, не
блокируется. Диспатч-гейт резолвит ссылки против cwd сессии и игнорирует
arguments.cwd: диспатч с cwd в другом worktree и ссылкой на план этого worktree
получает отказ (fail closed, over-blocking, T3.8) — план должен быть утверждён в
репозитории сессии.
Scenario: Ledger до утверждения плана
- WHEN write в
.superpowers/sdd/<plan-name>/progress.md, а plan-секции нет - THEN отказ
BLOCKED [approval-gates]: plan not approved; run /approve <id> plan
Scenario: Диспатч с утверждённым планом
- WHEN
subagentполучает промпт со ссылкой на план, чей sha256 совпадает с plan-секцией - THEN диспатч разрешён
Scenario: Один из двух планов не утверждён
- WHEN промпт
subagentссылается на два плана, и второй не утверждён - THEN диспатч отклонён по второму плану, даже если первый утверждён
Scenario: План изменён после утверждения
- WHEN содержимое плана изменилось после
/approve <id> plan - THEN отказ
plan changed after approval; re-approve; снимает его только повторное утверждение
Scenario: Диспатч без ссылки на план
- WHEN в промпте
subagentнет ни пути плана, ни sdd-каталога - THEN гейт B не применяется
Requirement: Гейт C и bash-обход
approval.md MUST писаться только командой /approve: любой write/edit с целью,
чей basename — approval.md, и любая bash-цель с таким именем MUST получать
отказ approval.md is written only by /approve (D-1).
Bash-обход MUST быть закрыт (D-8): если команда содержит грубую подстроку
docs/superpowers/plans, approval.md или .superpowers/sdd, её файловые цели
разбираются тем же парсером, что у хуков, — python3 <hooksDir>/harness_target.py targets '<cmd>' <cwd>. К целям применяются гейт A (без содержимого), гейт C и
write-гейт B; возвращается первый отказ. Сбой запуска, разбора или таймаут
парсера MUST fail open, как в хуках: сбой разбора входа не блокирует.
Команда без плановых подстрок и целей MUST NOT проверяться гейтами.
Scenario: Heredoc в план без Spec
- WHEN bash-команда
cat > docs/superpowers/plans/x.md <<'EOF' … EOFбез**Spec:** - THEN отказ
plan must carry **Spec:** …(содержимое bash-цели гейту недоступно)
Scenario: Запись approval.md через bash
- WHEN bash-команда
echo x > openspec/changes/<id>/approval.md - THEN отказ
approval.md is written only by /approve
Scenario: Запись в sdd-каталог через bash
- WHEN bash-цель — файл в
.superpowers/sdd/<plan-name>/, а план не утверждён - THEN отказ
plan not approved
Scenario: Обычная bash-команда
- WHEN в команде нет плановых подстрок
- THEN гейты не запускаются
Requirement: Исключения и известные пределы
Bounded-задача без change и плана MUST NOT гейтиться (D-6): у неё нет план-ссылок,
записей в docs/superpowers/plans/ и sdd-каталога.
Гейты SHALL жить в guard-цепочке и быть активными только при enabled: true,
guards: true, успешном self-check guards (guardsOk) и approvalGates: true;
approvalGates: true при выключенных guards или проваленном self-check MUST NOT их
включать (T3.3). Гейты MUST быть активны только в сессии с переустановленным
бандлом (D-13): до переустановки их нет, и sdd-скилл делает ручную проверку
маркера; после — её дублирует enforcement.
Правка плана фрагментом (edit) MUST всегда получать отказ, план пишется
full-file write (T3.5). Bash-цели и str_replace_editor create с невидимым
содержимым тоже отказывают; deny-текст про отсутствующий **Spec:** для них
вводит в заблуждение — реальная причина в невидимом содержимом (T3.4).
terminal_open / terminal_send MUST оставаться вне гейтов: они пишут файлы в
обход и гейтов, и bash-хуков — остаточный риск (T3.6). Симлинк вне
docs/superpowers/plans/, указывающий на план внутри, лексически обходит гейт A —
класс остаточного риска D-1; гейт защищает от случайного пропуска, а не от злого
умысла (T1.5). Гейты не перехватывают сообщения модели: презентация дизайна
владельцу не гейтится, пропуск становится виден на цепочке builder-check →
/approve → план → sdd (D-3).
/approve SHALL считать подтверждением ответ
{answers: [{selected: [Approve]}]}, а также тестовые формы {approved},
{confirmed} и true; в рантайме подтверждение — блокирующий вопрос владельцу, а
не эвристика по вызывающему (D-12, T2.3). Тестовые формы MUST NOT использоваться
моделью для самоутверждения: команда вызывается только владельцем.
Scenario: Bounded-правка
- WHEN в репозитории нет change и плана, а правка не трогает
docs/superpowers/plans/,approval.mdи.superpowers/sdd/ - THEN гейты не применяются
Scenario: approvalGates без guards
- WHEN профиль задаёт
guards: falseиapprovalGates: true - THEN гейты не работают: они часть guard-цепочки
Scenario: terminal_send пишет план
- WHEN
terminal_sendвыполняет команду, пишущую вdocs/superpowers/plans/ - THEN гейты не срабатывают — документированный предел, не регресс
Scenario: Правка дизайна после утверждения
- WHEN
design.mdдополняется решением после/approve <id> design - THEN утверждение не отзывается (D-11): хэш фиксируется только для плана