diff --git a/dsh/README.md b/dsh/README.md index 6d97d92..84ea039 100644 --- a/dsh/README.md +++ b/dsh/README.md @@ -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 diff --git a/openspec/changes/enforce-upstream-loop/design.md b/openspec/changes/enforce-upstream-loop/design.md index ba345ba..5ed170d 100644 --- a/openspec/changes/enforce-upstream-loop/design.md +++ b/openspec/changes/enforce-upstream-loop/design.md @@ -46,7 +46,8 @@ plan: `**Spec:**` или со Spec-ссылкой вне `openspec/changes/` (legacy `docs/superpowers/specs/`) — DENY «plan must carry **Spec:** pointing to openspec/changes/» (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 diff --git a/openspec/changes/enforce-upstream-loop/specs/approval-gates/spec.md b/openspec/changes/enforce-upstream-loop/specs/approval-gates/spec.md index e9de04a..9bf45b0 100644 --- a/openspec/changes/enforce-upstream-loop/specs/approval-gates/spec.md +++ b/openspec/changes/enforce-upstream-loop/specs/approval-gates/spec.md @@ -101,19 +101,22 @@ MUST отказать с перечислением отсутствующих (`content` или `new_string`). Ссылка MUST указывать в `openspec/changes/`; план без `**Spec:**` и план с legacy-ссылкой в `docs/superpowers/specs/**` MUST получать отказ `plan must carry **Spec:** pointing to openspec/changes/` -(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 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/.md`, взять из него `**Spec:**` → change и потребовать plan-секцию `approval.md` с sha256, равным sha256 текущего -содержимого плана. Нет секции или sha256 — отказ `plan not approved; run -/approve plan`; несовпадение хэша или нечитаемый план — отказ `plan changed -after approval; re-approve`; нет файла плана для sdd-каталога — отказ `plan not -found: `; план без `**Spec:**` — отказ `plan not approved` (id -неизвестен). Проверяться MUST все план-ссылки, а не первая; возвращается первый -отказ, чтобы утверждённый план не маскировал неутверждённый. +содержимого плана. Отказы 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: вызов, в котором ссылка не распознана, не @@ -185,6 +212,13 @@ found: `; план без `**Spec:**` — отказ `plan not approv - **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` @@ -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 оставаться вне гейтов: они пишут файлы в