27 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 быть маркером утверждения владельцем.
Писать его MUST только код бандла — команда /approve или инструмент
request_approval (D-1), и оба MUST писать только после подтверждения владельца.
Прямые записи модели (write, edit, multi_edit, str_replace_editor, bash)
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 —
одним из двух писателей бандла, наравне с инструментом request_approval
(следующее требование); оба пишут только после подтверждения владельца, прямые
записи модели — MUST NOT (гейт C). Команда 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, файл не меняется. Сбой самого диалога
(исключение из ctx.userQuestions.ask) MUST давать отказ с текстом ошибки —
userQuestions failed: <message>, — а не литерал not confirmed (T1.5 #77):
владелец должен видеть причину, по которой вопрос не дошёл, а отмена и сбой
различаются в логе. approval.md не меняется ни в одном из этих случаев. Команда
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не пишется
Scenario: Сбой диалога вопросов
- WHEN
ctx.userQuestions.askзавершается исключением - THEN команда отказывает с
userQuestions failed: <message>, а не с литераломnot confirmed, иapproval.mdне меняется
Scenario: Отмена в диалоге
- WHEN владелец ответил Cancel (или вопрос остался без ответа)
- THEN команда отказывает с
not confirmedиapproval.mdне меняется
Requirement: Инструмент request_approval
Инструмент модели request_approval SHALL быть вторым писателем approval.md:
модель инициирует утверждение, владелец только подтверждает в диалоге, а маркер
пишет код бандла (lib/approval.js) — с теми же проверками предпосылок и той же
записью, что у /approve. Инструмент MUST регистрироваться при enabled и
approvalGates: true, если в контексте доступен ctx.tools.register;
userQuestions MUST резолвиться лениво (не через inject), чтобы отсутствие
сервиса не парковало весь бандл.
Параметры инструмента — change_id (строка) и kind (design | plan).
Валидация MUST быть собственной, в инструменте: пустой или небезопасный
change_id и неизвестный kind — usage-отказ без записи и без вопроса
владельцу; проверки предпосылок и отказы те же, что у /approve (design —
proposal/design/builder-check, plan — утверждённый дизайн, ровно один план,
sha256), и любой отказ MUST NOT писать approval.md. Подтверждением SHALL
считаться только ответ answers[0].selected[0] === 'Approve'; Cancel и
отсутствие ответа — отказ not confirmed без записи; ошибка диалога — отказ
с текстом ошибки без записи. При отсутствии сервиса userQuestions
инструмент MUST отказать (userQuestions service unavailable), а не писать.
Scenario: Модель просит утвердить дизайн
- WHEN модель вызывает
request_approvalс{change_id: <id>, kind: design}, предпосылки выполнены, а владелец выбрал Approve - THEN в
approval.mdпишется design-секция (by: owner,atв ISO 8601 UTC), инструмент возвращает{approved: true, message}
Scenario: Модель просит утвердить план
- WHEN модель вызывает
request_approvalс{change_id: <id>, kind: plan}, дизайн утверждён, план единственный, владелец выбрал Approve - THEN plan-секция содержит
by: owner,atв ISO 8601 и sha256 текущего содержимого плана
Scenario: Владелец отменил утверждение
- WHEN в диалоге
request_approvalвладелец выбрал Cancel - THEN инструмент возвращает
{approved: false, message: 'not confirmed'}, аapproval.mdне меняется
Scenario: Некорректные аргументы request_approval
- WHEN модель передала неизвестный
kindили небезопасныйchange_id(например,../x) - THEN инструмент возвращает usage-отказ без записи, владелец не спрашивается
Scenario: Отсутствует builder-check при утверждении дизайна моделью
- WHEN
kind: design, аbuilder-check.mdв change нет - THEN инструмент отказывает с перечислением отсутствующего файла, владелец не
спрашивается,
approval.mdне создаётся
Scenario: request_approval plan без утверждённого дизайна
- WHEN
kind: plan, а design-секции нет - THEN инструмент отказывает
design not approved; run /approve <id> design, владелец не спрашивается,approval.mdне меняется
Scenario: request_approval без сервиса вопросов
- WHEN
userQuestionsотсутствует - THEN инструмент возвращает
{approved: false}сuserQuestions service unavailable, владелец не спрашивается,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 или
инструментом request_approval: любой write/edit с целью, чей basename —
approval.md, и любая bash-цель с таким именем MUST получать отказ approval.md is written only by /approve (D-1). Deny-текст не меняется: он адресован прямым
записям модели (write/edit/bash), а request_approval пишет маркер кодом
бандла и под этот запрет не попадает.
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): хэш фиксируется только для плана