18 KiB
Raw Blame History

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