docs(dsh): sdd-скилл, roles-заметка, спека approval-gates, доки (#59)

This commit is contained in:
Andrey Epifancev 2026-10-25 02:30:36 +04:00
parent 653d1df30f
commit 734c80899d
8 changed files with 393 additions and 12 deletions

View File

@ -15,7 +15,7 @@ This is a port, not a copy. Three layers map differently:
| Instructions | `~/.claude/CLAUDE.md`, project `CLAUDE.md` | `$DSH_HOME/AGENTS.md`, project `CLAUDE.md`/`AGENTS.md`, nested | almost 1:1 |
| Skills | `~/.claude/skills`, `.claude/skills` | `~/.dsh/skills`, `.agents/skills`, `customSkillDirs` | 1:1 format |
| Roles | `roles/`, `/role` skill | same files + `/role` command + model-invocable `role` skill | close |
| Hard rules | `PreToolUse` shell hooks | `ctx.tools.guard` Cordis guard | logic reused, wrapper new |
| Hard rules | `PreToolUse` shell hooks | `ctx.tools.guard` Cordis guard + approval gates A/B/C | logic reused, wrapper new |
| Superpowers | plugin + `SessionStart` hook | pinned sync of tag `v7.0.1` (`DSH_SUPERPOWERS_REF`) into `~/.dsh/skills` + `agent/pre-step` bootstrap | content adapted by sync, bootstrap new |
| SDD | `Task` (synchronous) | `subagent` (asynchronous) / `workflow` | semantics differ |
| Gitea | CLI + skills | `bash` + skills | 1:1 |
@ -172,6 +172,7 @@ for the repository layout:
config:
enabled: true
guards: true
approvalGates: true # gates A/B/C; active only inside the guard chain
bootstrap: true
roleCommand: true
selfCheck: true # activation check of the DSH contract; not-ok is logged
@ -354,6 +355,18 @@ profile was removed; the real `desktop` profile was not touched.
## 7. Risks and open questions
- **Instructions are not enforcement — closed by gates A/B/C.** The upstream loop
(builder-check → `/approve <id> design` → plan → `/approve <id> plan` → sdd)
exists in superpowers as text, and the 24.10.2026 session skipped builder-check
while the plan was written before design approval. Change
`enforce-upstream-loop` adds `lib/gates.js` + `command-approve.js`: gate A
denies a plan for an unapproved design, gate B denies an sdd start (a dispatch
or a `.superpowers/sdd/**` write) for an unapproved or changed plan (sha256),
gate C keeps `approval.md` `/approve`-only, and bash write targets are parsed
by `harness_target.py` and gated too. Residual risk: only a session with the
reinstalled bundle is gated (D-13), and `terminal_*` tools stay outside the
chain (T3.6); the contract is pinned in
`openspec/changes/enforce-upstream-loop/specs/approval-gates/spec.md`.
- **DSH is 0.2.1-rc.3.** The plugin/event API is not frozen; pin the version and
re-run the tests after upgrades.
- **Contract self-check.** The plugin/event API drift is now caught from inside

View File

@ -9,6 +9,10 @@ separate Cordis bundle plus a thin install helper.
- `guard-main` / `require-plan` — the tested bash/Python hooks run through
`ctx.tools.guard`; the agent gets `BLOCKED` with the same messages as in
Claude Code;
- approval gates A/B/C — `lib/gates.js` + `command-approve.js`: a plan cannot be
written before its change design is approved, sdd cannot start before the plan
is approved (sha256-pinned), and `approval.md` is written only by
`/approve <change-id> design|plan`;
- superpowers bootstrap — `using-superpowers` is injected once per session from
`agent/pre-step` and re-injected after compaction;
- roles — the model loads them with the `role` skill (`~/.dsh/roles`, then
@ -145,7 +149,9 @@ dsh/
index.js ctx.tools.guard + agent/pre-step + /role
command-role.js /role command glue
command-check.js dsh-harness-check command glue
command-approve.js /approve command: the only writer of approval.md
lib/guards.js DSH tool call -> claude-harness hook JSON
lib/gates.js approval gates A/B/C and approval.md/plan io (pure)
lib/bootstrap.js superpowers skill resolution, marker, dedupe
lib/role.js role layers and text (pure)
lib/selfcheck.js bundle contract self-check (pure)
@ -178,6 +184,7 @@ repository layout:
- id: dsh-harness
config:
guards: true
approvalGates: true # gates A/B/C; effective only inside the guard chain
bootstrap: true
roleCommand: true
selfCheck: true # activation check of the DSH contract; not-ok is logged
@ -196,6 +203,18 @@ repository layout:
- The guard adapter uses the repository's bash/Python hooks; if `python3` is
missing, the hooks fail open by design.
- Approval gates (change `enforce-upstream-loop`) live inside the guard chain:
they need `enabled: true`, `guards: true`, `approvalGates: true` **and** a
passing `guards` self-check (`guardsOk`); `approvalGates: true` alone does not
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.
- `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
bundle needs a real copy of `hooks/` (already listed in `package.json`
`files`).

View File

@ -617,7 +617,11 @@ note = (
"Режим контроллера — по числу параллельных планов: один план — главная сессия, "
"несколько — фоновый контроллер (нужен `maxDepth: 2` из "
"`dsh/subagent-models.yml`). В Claude Code строка не действует: там исходный "
"`subagent-driven-development`."
"`subagent-driven-development`. "
"Бандл гейтит цикл: builder-check обязателен до передачи дизайна владельцу "
"(иначе `/approve design` откажет); план пишется только после `/approve design`; "
"sdd стартует только после `/approve plan` (гейты A/B/C, "
"`BLOCKED [approval-gates]`). Пропуск — только явное решение владельца."
)
anchor = "- Режим по умолчанию: subagent-driven-development."
lines = text.splitlines(keepends=True)

View File

@ -39,6 +39,12 @@ overlay ребёнок наследует модель сессии.
## Протокол диспатча и отчёта
**Проверка approval-маркера до первого диспатча.** Найди change по строке
`**Spec:**` плана и проверь `openspec/changes/<id>/approval.md`: секция `plan:`
есть — можно начинать; нет — не диспатчить, вернуть владельцу `NEEDS_CONTEXT`
с «run /approve <id> plan». Гейты активны только с переустановленным бандлом
(D-13): до него это ручная проверка, после — гейт B (`BLOCKED [approval-gates]`).
1. Первым делом — ledger (см. ниже) и скан плана на конфликты; рулинги до
Task 1.
2. Бриф implementer'а — **файлом** `<workspace>/briefs/task-<n>.md`: контекст,

View File

@ -315,3 +315,35 @@ test('approve resolves the session cwd per invocation (T3.1)', async () => {
cleanup(second.tmp)
}
})
// T3.9 pin: the process.cwd() fallback of currentDirectory is part of the
// T3.1 contract — a workingDirectory that throws must not break /approve.
// The test changes the process directory (each test file runs in its own
// process) so the write lands in an assertable place, then restores it.
test('approve falls back to process.cwd() when the session cwd lookup throws (T3.9)', async () => {
const { tmp, repo } = repoFixture()
const previous = process.cwd()
try {
process.chdir(repo)
const captured = {}
const commands = { register(definition) { Object.assign(captured, definition) } }
registerApproveCommand({
commands,
userQuestions: { async ask() { return approveAnswer('design') } },
workingDirectory: {
get() {
throw new Error('cannot get property "session" without inject')
},
},
logger: { warn() {} },
})
const result = await captured.handler({ agent: { session: 's1' }, rawInput: 'demo design' })
assert.equal(result.kind, 'success')
assert.equal(existsSync(approvalPath(repo)), true, 'approval.md is written into process.cwd()')
} finally {
process.chdir(previous)
cleanup(tmp)
}
})

View File

@ -50,7 +50,10 @@ test('install.sh copies skills and roles instead of linking into ~/.claude', ()
const sddSkill = join(dshHome, 'skills', 'sdd', 'SKILL.md')
assert.ok(existsSync(sddSkill), 'sdd skill was not installed')
assert.match(readFileSync(sddSkill, 'utf8'), /name: sdd/)
const sddText = readFileSync(sddSkill, 'utf8')
assert.match(sddText, /name: sdd/)
assert.match(sddText, /\/approve/, 'the sdd skill must check the plan approval marker before dispatch')
assert.match(sddText, /approval\.md/, 'the sdd skill must name the approval marker file')
for (const role of ['dev', 'admin', 'analyst', 'qa']) {
assert.ok(existsSync(join(dshHome, 'roles', `${role}.md`)), `missing ${role}.md`)
@ -60,6 +63,9 @@ test('install.sh copies skills and roles instead of linking into ~/.claude', ()
const devText = readFileSync(devRole, 'utf8')
assert.match(devText, /В DSH-сессии исполнение плана идёт по skill `sdd`/)
assert.match(devText, /`maxDepth: 2`/)
assert.match(devText, /Бандл гейтит цикл/)
assert.match(devText, /\/approve/, 'the DSH note must point at /approve')
assert.match(devText, /approval-gates/, 'the DSH note must name the approval-gates deny prefix')
assert.match(first.stdout, /role dsh notes/)
assert.equal(countNotes(devText), 1, 'note must be inserted once')

View File

@ -28,6 +28,7 @@ design:
plan:
by: aep
at: 2026-10-25T11:30:00+04:00
sha256: 3f2c9a… (hex, 64 знака)
```
## Механика гейтов (в бандле, DSH-only)
@ -42,22 +43,35 @@ plan:
или `new_string`) извлечь `**Spec:** <путь>`; если путь указывает в
`openspec/changes/<id>/` и `approval.md` без секции `design:` — DENY
«design not approved; run /approve <id> design». Содержимое без
Spec-ссылки на change — пропуск.
2. **Гейт B (sdd до approve плана).** Для `subagent`/`subagent_fork`:
в промпте найти ссылку на план — `docs/superpowers/plans/<имя>.md`
или `.superpowers/sdd/<план>/`; из плана взять `**Spec:**` →
change; `approval.md` без секции `plan:` — DENY «plan not approved;
run /approve <id> plan». Нет ссылки на план — пропуск.
`**Spec:**` или со Spec-ссылкой вне `openspec/changes/` (legacy
`docs/superpowers/specs/`) — DENY «plan must carry **Spec:** pointing to
openspec/changes/<id>» (D-9); правка без содержимого (bash-цель,
edit-фрагмент) тоже DENY — гейт не видит содержимого.
2. **Гейт B (sdd до approve плана).** Два входа (D-7): (a) запись в
`.superpowers/sdd/<план>/**` (ledger, брифы — старт sdd в Режиме 1) и
(b) диспатч `subagent`/`subagent_fork`/`workflow`, в промпте или
скрипте которого есть ссылка на план — `docs/superpowers/plans/<имя>.md`
или `.superpowers/sdd/<план>/`. Из плана взять `**Spec:**` → change;
`approval.md` без секции `plan:` или с чужим sha256 — DENY «plan not
approved; run /approve <id> plan» / «plan changed after approval;
re-approve». Проверяются все план-ссылки, а не первая; ссылка без
читаемого плана и план без Spec — fail closed.
3. **`/approve`** (`command-approve.js`): `design` требует существование
proposal.md, design.md, builder-check.md; `plan` требует секцию
`design:` и существование ровно одного плана со Spec на этот change
(несколько — отказ с перечислением). Дописывает/обновляет секцию,
`design:` с непустыми `by`/`at` и существование ровно одного плана со
Spec на этот change (несколько — отказ с перечислением). Дописывает/обновляет секцию,
остальные секции не трогает. Владелец-агент различие: команда пишет
`by: owner`; вызовы не от владельца — отказ (как отличить: DSH не
даёт ролей; используем факт вызова из интерактивной сессии и
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: они заморожены, новая работа идёт
отдельным планом. Это ожидаемое поведение, а не регресс.
## Decisions made without discussion
| # | Решение | Обоснование | Цена ошибки |
@ -72,7 +86,7 @@ plan:
| D-8 | Bash-обход закрыт: для Bash-вызовов gates.js прогоняет `python3 hooks/harness_target.py targets '<cmd>' <cwd>` и применяет гейты A/C к целям под `docs/superpowers/plans/` и `**/approval.md` | Тот же парсер, что в хуках (heredoc-aware), без дублирования логики | Стоимость — subprocess на каждый Bash-вызов с подозрительным текстом; фильтруем до прогона по грубой подстроке |
| D-9 | План без `**Spec:**` в `openspec/changes/` — DENY по умолчанию (включая legacy `docs/superpowers/specs/`) | Реальные планы всегда несут Spec; .bounded-поток планов не пишет — проверено по репозиторию | Ложный блок легитимного Spec-less плана — такого флоу нет |
| D-10 | `plan:` в approval.md несёт `sha256` файла плана; гейт B сверяет хэш — план изменён после утверждения → DENY «plan changed after approval; re-approve» | Утверждение привязывается к содержимому исполняемого объекта | Правки плана после approve требуют re-approve — осознанно |
| D-11 | `design:` — только существование секции (без хэша): design.md дополняется решениями по ходу | Контент дизайна не является исполняемым объектом | Правка дизайна после approve не отзывается — принято |
| D-11 | `design:` — только непустые `by` и `at` (без хэша): design.md дополняется решениями по ходу | Контент дизайна не является исполняемым объектом; пустая секция `design:` утверждением не считается, `by`/`at` должны быть непустыми (T1.8) | Правка дизайна после approve не отзывается — принято |
| D-12 | Подтверждение владельца в `/approve` — через `ctx.userQuestions.ask` (блокирующий вопрос); сервис добавляется в inject; отсутствует — `/approve` отказывает с сообщением | Проверенный примитив рантайма (DELEGATED_CALLER не пройдёт) вместо эвристики «интерактивная сессия» | Нет userQuestions в профиле — /approve недоступен, гейты всё равно работают на блокировку |
| D-13 | Бутстрап: гейты активны только в сессиях с новым бандлом (после установки/рестарта); сама фича #59 исполняется на старом рантайме — ретро-утверждений не требует; для будущих change контроллер кладёт builder-check.md штатно | Развёртывание разводит enforcement и исполняемую фичу во времени | Окно до переустановки бандла — гейтов нет; фиксируем шаг переустановки в чеклисте закрытия |

View File

@ -0,0 +1,287 @@
# Spec Delta: approval-gates
## Purpose
Технические гейты цикла разработки в DSH-бандле: builder-check → утверждение
дизайна → план → утверждение плана → sdd. Инструкции superpowers сами по себе не
являются механизмом (сессия ce9187ca 24.10.2026 пропустила builder-check, а план
появился до утверждения дизайна), поэтому бандл проверяет артефакты утверждения на
каждом переходе и отказывает с `BLOCKED [approval-gates]: <причина>`. Гейты живут
только в DSH-бандле (D-2); Claude Code-хуки и поток не меняются.
## ADDED 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-цель, edit-фрагмент, `str_replace_editor`
create) MUST получать тот же отказ: гейт не видит содержимого и не может
подтвердить Spec — fail closed.
Если 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; это ожидаемое поведение, а не дефект.
#### 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: Правка плана фрагментом
- **WHEN** edit плана передаёт `new_string` без `**Spec:**` (фрагмент правки)
- **THEN** отказ: план пишется только full-file write
#### Scenario: Правка исторического плана
- **WHEN** write в `docs/superpowers/plans/2026-10-24-dsh-decouple.md`
- **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 текущего
содержимого плана. Нет секции или 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 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** содержимое плана изменилось после `/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`) MUST всегда получать отказ, план пишется
full-file write (T3.5). 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): хэш фиксируется только для плана