docs(dsh): sdd-скилл, roles-заметка, спека approval-gates, доки (#59)
This commit is contained in:
parent
653d1df30f
commit
734c80899d
15
dsh/PORT.md
15
dsh/PORT.md
@ -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
|
||||
|
||||
@ -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`).
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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`: контекст,
|
||||
|
||||
@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
@ -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')
|
||||
|
||||
|
||||
@ -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 и исполняемую фичу во времени | Окно до переустановки бандла — гейтов нет; фиксируем шаг переустановки в чеклисте закрытия |
|
||||
|
||||
|
||||
@ -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): хэш фиксируется только для плана
|
||||
Loading…
x
Reference in New Issue
Block a user