20 KiB
approval-gates Specification
Purpose
Технические гейты цикла разработки в DSH-бандле: builder-check → утверждение
дизайна → план → утверждение плана → sdd. Инструкции superpowers сами по себе не
являются механизмом (сессия ce9187ca 24.10.2026 пропустила builder-check, а план
появился до утверждения дизайна), поэтому бандл проверяет артефакты утверждения на
каждом переходе и отказывает с BLOCKED [approval-gates]: <причина>. Гейты живут
только в DSH-бандле (D-2); Claude Code-хуки и поток не меняются.
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-цель, str_replace_editor create) MUST
получать тот же отказ: гейт не видит содержимого и не может подтвердить Spec —
fail closed. Фрагмент edit проверяется по своему new_string (content-based):
фрагмент, который сам несёт **Spec:** на утверждённый change, проходит; фрагмент
без Spec, с legacy-ссылкой или с неутверждённым change — DENY.
Если change определён, а его design-секция не является утверждением, гейт MUST
отказать design not approved; run /approve <id> design. Утверждённый дизайн
разрешает запись плана; запись вне docs/superpowers/plans/ гейтом A не
проверяется.
Исторические планы заморожены — все написаны до лупа: четыре файла.
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:** (T1.3, T4.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: Правка плана фрагментом без Spec
- WHEN edit плана передаёт
new_stringбез**Spec:**(обычный фрагмент правки) - THEN отказ
plan must carry **Spec:** …: гейт видит только фрагмент, план целиком пишется full-file write
Scenario: Фрагмент edit со Spec на утверждённый change
- WHEN edit плана передаёт
new_string, в котором есть**Spec:**на change с непустой design-секцией - THEN запись разрешена: гейт проверяет содержимое, а не инструмент
Scenario: Правка исторического плана
- WHEN write в
docs/superpowers/plans/2026-10-24-dsh-decouple.md - THEN отказ
plan must carry **Spec:** …по D-9
Scenario: Правка плана с legacy-Spec
- WHEN write в
docs/superpowers/plans/2026-10-11-roles-and-memory.md, чей**Spec:**указывает вdocs/superpowers/specs/ - 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 текущего
содержимого плана. Отказы MUST различаться по причине:
- (a) файл плана по ссылке отсутствует или не читается —
plan not approved; run /approve <id> plan(fail closed; id остаётся литеральным<id>, если план не прочитан); - (b) план читается, но без
**Spec:**или с legacy-ссылкой внеopenspec/changes/— тот жеplan not approved; run /approve <id> plan; - (c) план и change определены, но plan-секции нет или в ней нет sha256 — тот же
plan not approved; run /approve <id> plan, уже с id change; - (d) plan-секция есть, но sha256 не совпадает с текущим содержимым плана —
plan changed after approval; re-approve.
Для входа через .superpowers/sdd/<plan-name>/ без файла
docs/superpowers/plans/<plan-name>.md отказ — plan not found: <plan-name>.
Проверяться 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
subagentполучает промпт со ссылкой наdocs/superpowers/plans/missing.md, которого нет - THEN отказ
plan not approved; run /approve <id> plan(fail closed), а неplan changed after approval
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) обычно получает отказ: гейт проверяет только
new_string, а обычный фрагмент не несёт **Spec:**, поэтому план целиком пишется
full-file write; фрагмент со своей строкой **Spec:** на утверждённый change
проходит (T3.5, T4.1). 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): хэш фиксируется только для плана