From 27adb8d6d2e481befcc2cdd23719dc0e02b735c6 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 25 Oct 2026 02:49:58 +0400 Subject: [PATCH] =?UTF-8?q?chore(openspec):=20=D0=B0=D1=80=D1=85=D0=B8?= =?UTF-8?q?=D0=B2=20change=20enforce-upstream-loop=20(#59)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../.openspec.yaml | 0 .../builder-check.md | 0 .../design.md | 0 .../proposal.md | 0 .../specs/approval-gates/spec.md | 0 openspec/specs/approval-gates/spec.md | 322 ++++++++++++++++++ 6 files changed, 322 insertions(+) rename openspec/changes/{enforce-upstream-loop => archive/2026-10-25-enforce-upstream-loop}/.openspec.yaml (100%) rename openspec/changes/{enforce-upstream-loop => archive/2026-10-25-enforce-upstream-loop}/builder-check.md (100%) rename openspec/changes/{enforce-upstream-loop => archive/2026-10-25-enforce-upstream-loop}/design.md (100%) rename openspec/changes/{enforce-upstream-loop => archive/2026-10-25-enforce-upstream-loop}/proposal.md (100%) rename openspec/changes/{enforce-upstream-loop => archive/2026-10-25-enforce-upstream-loop}/specs/approval-gates/spec.md (100%) create mode 100644 openspec/specs/approval-gates/spec.md diff --git a/openspec/changes/enforce-upstream-loop/.openspec.yaml b/openspec/changes/archive/2026-10-25-enforce-upstream-loop/.openspec.yaml similarity index 100% rename from openspec/changes/enforce-upstream-loop/.openspec.yaml rename to openspec/changes/archive/2026-10-25-enforce-upstream-loop/.openspec.yaml diff --git a/openspec/changes/enforce-upstream-loop/builder-check.md b/openspec/changes/archive/2026-10-25-enforce-upstream-loop/builder-check.md similarity index 100% rename from openspec/changes/enforce-upstream-loop/builder-check.md rename to openspec/changes/archive/2026-10-25-enforce-upstream-loop/builder-check.md diff --git a/openspec/changes/enforce-upstream-loop/design.md b/openspec/changes/archive/2026-10-25-enforce-upstream-loop/design.md similarity index 100% rename from openspec/changes/enforce-upstream-loop/design.md rename to openspec/changes/archive/2026-10-25-enforce-upstream-loop/design.md diff --git a/openspec/changes/enforce-upstream-loop/proposal.md b/openspec/changes/archive/2026-10-25-enforce-upstream-loop/proposal.md similarity index 100% rename from openspec/changes/enforce-upstream-loop/proposal.md rename to openspec/changes/archive/2026-10-25-enforce-upstream-loop/proposal.md diff --git a/openspec/changes/enforce-upstream-loop/specs/approval-gates/spec.md b/openspec/changes/archive/2026-10-25-enforce-upstream-loop/specs/approval-gates/spec.md similarity index 100% rename from openspec/changes/enforce-upstream-loop/specs/approval-gates/spec.md rename to openspec/changes/archive/2026-10-25-enforce-upstream-loop/specs/approval-gates/spec.md diff --git a/openspec/specs/approval-gates/spec.md b/openspec/specs/approval-gates/spec.md new file mode 100644 index 0000000..9bca38d --- /dev/null +++ b/openspec/specs/approval-gates/spec.md @@ -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//builder-check.md` SHALL содержать отчёт свежего +builder-check субагента, скопированный в change контроллером; он MUST быть записан +после builder-check и до передачи дизайна владельцу. + +`openspec/changes//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 plan` записывает plan-секцию в существующий `approval.md` +- **THEN** design-секция остаётся дословно прежней + +### Requirement: Команда /approve + +`/approve 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//`; при отсутствии файла команда +MUST отказать с перечислением отсутствующих и MUST NOT спрашивать владельца. + +Вид `plan` SHALL требовать design-секцию с непустыми `by` и `at` (иначе отказ +`design not approved; run /approve design`) и ровно одного плана в +`docs/superpowers/plans/`, чей `**Spec:**` указывает на ``; ноль планов — +отказ, несколько — отказ с перечислением файлов. Утверждаемый план 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 design` и + не спрашивает владельца + +#### Scenario: /approve plan при двух планах на change + +- **WHEN** два файла в `docs/superpowers/plans/` несут `**Spec:**` на один change +- **THEN** команда отказывает, перечисляя оба файла, и `approval.md` не меняется + +#### Scenario: Утверждение плана фиксирует sha256 + +- **WHEN** владелец подтвердил `/approve 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/`; +план без `**Spec:**` и план с legacy-ссылкой в `docs/superpowers/specs/**` MUST +получать отказ `plan must carry **Spec:** pointing to openspec/changes/` +(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 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//`, а `approval.md` без design-секции +- **THEN** отказ `BLOCKED [approval-gates]: design not approved; run /approve + design` + +#### Scenario: План без Spec + +- **WHEN** write в `docs/superpowers/plans/x.md` без строки `**Spec:**` +- **THEN** отказ `plan must carry **Spec:** pointing to openspec/changes/` + +#### 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//**` (ledger, брифы) — старт sdd в Режиме 1; +(b) диспатч `subagent`, `subagent_fork` или `workflow`, в `prompt` или `script` +которого есть ссылка на план — `docs/superpowers/plans/<имя>.md` или +`.superpowers/sdd/<имя>/`. + +Для каждого входа гейт MUST определить файл +`docs/superpowers/plans/.md`, взять из него `**Spec:**` → change и +потребовать plan-секцию `approval.md` с sha256, равным sha256 текущего +содержимого плана. Отказы MUST различаться по причине: + +- (a) файл плана по ссылке отсутствует или не читается — `plan not approved; run + /approve plan` (fail closed; id остаётся литеральным ``, если план не + прочитан); +- (b) план читается, но без `**Spec:**` или с legacy-ссылкой вне + `openspec/changes/` — тот же `plan not approved; run /approve plan`; +- (c) план и change определены, но plan-секции нет или в ней нет sha256 — тот же + `plan not approved; run /approve plan`, уже с id change; +- (d) plan-секция есть, но sha256 не совпадает с текущим содержимым плана — + `plan changed after approval; re-approve`. + +Для входа через `.superpowers/sdd//` без файла +`docs/superpowers/plans/.md` отказ — `plan not found: `. +Проверяться MUST все план-ссылки, а не первая; возвращается первый отказ, чтобы +утверждённый план не маскировал неутверждённый. + +Вызов без план-ссылки MUST NOT гейтиться. Разбор промпта — эвристика по +подстрокам, поэтому разбор fail open: вызов, в котором ссылка не распознана, не +блокируется. Диспатч-гейт резолвит ссылки против cwd сессии и игнорирует +`arguments.cwd`: диспатч с cwd в другом worktree и ссылкой на план этого worktree +получает отказ (fail closed, over-blocking, T3.8) — план должен быть утверждён в +репозитории сессии. + +#### Scenario: Ledger до утверждения плана + +- **WHEN** write в `.superpowers/sdd//progress.md`, а plan-секции нет +- **THEN** отказ `BLOCKED [approval-gates]: plan not approved; run /approve + 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 plan` (fail closed), а не + `plan changed after approval` + +#### Scenario: План изменён после утверждения + +- **WHEN** содержимое плана изменилось после `/approve 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 /harness_target.py +targets '' `. К целям применяются гейт 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//approval.md` +- **THEN** отказ `approval.md is written only by /approve` + +#### Scenario: Запись в sdd-каталог через bash + +- **WHEN** bash-цель — файл в `.superpowers/sdd//`, а план не утверждён +- **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 design` +- **THEN** утверждение не отзывается (D-11): хэш фиксируется только для плана -- 2.47.2