docs(openspec): спека approval-gates — request_approval как легитимный писатель (T1.1, #77)

This commit is contained in:
Andrey Epifancev 2026-10-25 11:06:28 +04:00
parent 1723806e16
commit b81afaf59a

View File

@ -16,9 +16,11 @@
builder-check субагента, скопированный в change контроллером; он MUST быть записан builder-check субагента, скопированный в change контроллером; он MUST быть записан
после builder-check и до передачи дизайна владельцу. после builder-check и до передачи дизайна владельцу.
`openspec/changes/<id>/approval.md` SHALL быть маркером утверждения владельцем; `openspec/changes/<id>/approval.md` SHALL быть маркером утверждения владельцем.
его единственный писатель — команда `/approve` (D-1), инструменты модели писать Писать его MUST только код бандла — команда `/approve` или инструмент
его MUST NOT (гейт C ниже). `request_approval` (D-1), и оба MUST писать только после подтверждения владельца.
Прямые записи модели (`write`, `edit`, `multi_edit`, `str_replace_editor`, `bash`)
MUST NOT создавать или менять этот файл (гейт C ниже, без изменений).
Формат `approval.md` — yaml с секциями `design:` (поля `by`, `at`) и `plan:` Формат `approval.md` — yaml с секциями `design:` (поля `by`, `at`) и `plan:`
(поля `by`, `at`, `sha256`); `sha256` — hex-хэш sha256 содержимого файла плана на (поля `by`, `at`, `sha256`); `sha256` — hex-хэш sha256 содержимого файла плана на
@ -42,11 +44,14 @@ builder-check субагента, скопированный в change конт
### Requirement: Команда /approve ### Requirement: Команда /approve
`/approve <change-id> design|plan` SHALL быть единственным писателем `/approve <change-id> design|plan` SHALL быть ручным писателем `approval.md` —
`approval.md` и MUST регистрироваться при `enabled` и `approvalGates: true`, одним из двух писателей бандла, наравне с инструментом `request_approval`
независимо от флагов `roleCommand` и `checkCommand`: иначе гейты остаются без (следующее требование); оба пишут только после подтверждения владельца, прямые
команды-разблокировки (T3.2). `change-id` MUST быть одним безопасным сегментом записи модели — MUST NOT (гейт C). Команда MUST регистрироваться при `enabled` и
пути; небезопасный id или неизвестный вид — usage-отказ без записи. `approvalGates: true`, независимо от флагов `roleCommand` и `checkCommand`: иначе
гейты остаются без команды-разблокировки (T3.2). `change-id` MUST быть одним
безопасным сегментом пути; небезопасный id или неизвестный вид — usage-отказ без
записи.
Вид `design` SHALL требовать существования `proposal.md`, `design.md` и Вид `design` SHALL требовать существования `proposal.md`, `design.md` и
`builder-check.md` в `openspec/changes/<id>/`; при отсутствии файла команда `builder-check.md` в `openspec/changes/<id>/`; при отсутствии файла команда
@ -93,6 +98,71 @@ MUST отказать с перечислением отсутствующих
- **THEN** команда отказывает с `userQuestions service unavailable; /approve - **THEN** команда отказывает с `userQuestions service unavailable; /approve
requires it` и `approval.md` не пишется requires it` и `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 — план до утверждения дизайна ### Requirement: Гейт A — план до утверждения дизайна
Для write/edit/multi_edit/`str_replace_editor` с целевым путём под Для write/edit/multi_edit/`str_replace_editor` с целевым путём под
@ -231,9 +301,12 @@ fail closed. Фрагмент `edit` проверяется по своему `n
### Requirement: Гейт C и bash-обход ### Requirement: Гейт C и bash-обход
`approval.md` MUST писаться только командой `/approve`: любой write/edit с целью, `approval.md` MUST писаться только кодом бандла — командой `/approve` или
чей basename — `approval.md`, и любая bash-цель с таким именем MUST получать инструментом `request_approval`: любой write/edit с целью, чей basename —
отказ `approval.md is written only by /approve` (D-1). `approval.md`, и любая bash-цель с таким именем MUST получать отказ `approval.md
is written only by /approve` (D-1). Deny-текст не меняется: он адресован прямым
записям модели (`write`/`edit`/`bash`), а `request_approval` пишет маркер кодом
бандла и под этот запрет не попадает.
Bash-обход MUST быть закрыт (D-8): если команда содержит грубую подстроку Bash-обход MUST быть закрыт (D-8): если команда содержит грубую подстроку
`docs/superpowers/plans`, `approval.md` или `.superpowers/sdd`, её файловые цели `docs/superpowers/plans`, `approval.md` или `.superpowers/sdd`, её файловые цели