docs: спека approval-gates — точные формулировки гейтов A/B и frozen-планов (T4.1-T4.3, #59)

This commit is contained in:
Andrey Epifancev 2026-10-25 02:40:39 +04:00
parent 734c80899d
commit 7e3b86b59a
3 changed files with 68 additions and 28 deletions

View File

@ -209,10 +209,12 @@ repository layout:
enable them. They are active only in a session with the reinstalled bundle
(D-13): an old install keeps the text-only loop, and the `sdd` skill checks the
plan approval by hand.
- Plan files are always rewritten in full: an `edit` fragment cannot carry the
`**Spec:**` line, so gate A denies it — as it denies bash plan targets and
`str_replace_editor` creates, whose content it cannot see. The deny text then
names the missing `**Spec:**`, not the real reason.
- Gate A is content-based: it reads `**Spec:**` from `content` / `new_string`.
An `edit` fragment is checked against its own text, so a fragment carrying a
`**Spec:**` line for an approved change passes, while the usual Spec-less
fragment is denied (rewrite the plan file in full). Bash plan targets and
`str_replace_editor` creates are denied because the gate cannot see their
content; the deny text then names the missing `**Spec:**`, not the real reason.
- `terminal_open` / `terminal_send` write files outside both the gates and the
bash hooks — a residual risk not covered by the port.
- `bundle/hooks` is a symlink for local-directory installation. A published

View File

@ -46,7 +46,8 @@ plan:
`**Spec:**` или со Spec-ссылкой вне `openspec/changes/` (legacy
`docs/superpowers/specs/`) — DENY «plan must carry **Spec:** pointing to
openspec/changes/<id>» (D-9); правка без содержимого (bash-цель,
edit-фрагмент) тоже DENY — гейт не видит содержимого.
`str_replace_editor` create) и фрагмент `edit` без `**Spec:**` тоже DENY —
гейт проверяет только то содержимое, которое видит.
2. **Гейт B (sdd до approve плана).** Два входа (D-7): (a) запись в
`.superpowers/sdd/<план>/**` (ledger, брифы — старт sdd в Режиме 1) и
(b) диспатч `subagent`/`subagent_fork`/`workflow`, в промпте или
@ -66,10 +67,11 @@ plan:
AskUserQuestion-подтверждение: `/approve` спрашивает подтверждение
перед записью, ответ «да» = владелец).
Исторические планы: четыре файла — `2026-10-11-roles-and-memory`,
`2026-10-24-dsh-decouple`, `2026-10-24-dsh-port-spike`,
`2026-10-24-dsh-role-loader` — написаны до лупа и не несут `**Spec:**`
(T1.3). По D-9 любая их правка — DENY: они заморожены, новая работа идёт
Исторические планы: четыре файла написаны до лупа (T1.3, T4.3) —
`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:**`.
По D-9 запись в любой из них — DENY: они заморожены, новая работа идёт
отдельным планом. Это ожидаемое поведение, а не регресс.
## Decisions made without discussion

View File

@ -101,19 +101,22 @@ MUST отказать с перечислением отсутствующих
(`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.
(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 не
проверяется.
Исторические планы, написанные до лупа и не несущие `**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; это ожидаемое поведение, а не дефект.
Исторические планы заморожены — все написаны до лупа: четыре файла.
`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 на неутверждённый дизайн
@ -133,16 +136,30 @@ MUST получать отказ по D-9; это ожидаемое повед
на этот change
- **THEN** запись плана разрешена
#### Scenario: Правка плана фрагментом
#### Scenario: Правка плана фрагментом без Spec
- **WHEN** edit плана передаёт `new_string` без `**Spec:**` (фрагмент правки)
- **THEN** отказ: план пишется только full-file write
- **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 в
@ -154,12 +171,22 @@ MUST получать отказ по D-9; это ожидаемое повед
Для каждого входа гейт 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 различаться по причине:
- (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: вызов, в котором ссылка не распознана, не
@ -185,6 +212,13 @@ found: <plan-name>`; план без `**Spec:**` — отказ `plan not approv
- **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`
@ -245,9 +279,11 @@ Bounded-задача без change и плана MUST NOT гейтиться (D-
бандлом (D-13): до переустановки их нет, и sdd-скилл делает ручную проверку
маркера; после — её дублирует enforcement.
Правка плана фрагментом (`edit`) MUST всегда получать отказ, план пишется
full-file write (T3.5). Bash-цели и `str_replace_editor` create с невидимым
содержимым тоже отказывают; deny-текст про отсутствующий `**Spec:**` для них
Правка плана фрагментом (`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 оставаться вне гейтов: они пишут файлы в