20 KiB
Raw Blame History

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): хэш фиксируется только для плана