324 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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