chore(openspec): архив change enforce-upstream-loop (#59)
This commit is contained in:
parent
027dd09c8f
commit
27adb8d6d2
322
openspec/specs/approval-gates/spec.md
Normal file
322
openspec/specs/approval-gates/spec.md
Normal file
@ -0,0 +1,322 @@
|
||||
# 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): хэш фиксируется только для плана
|
||||
Loading…
x
Reference in New Issue
Block a user