feat(dsh): skill sdd — асинхронный протокол исполнения плана (#41)

dsh/skills/sdd/SKILL.md: два режима контроллера (главная сессия / фоновый
субагент), проверка глубины и маршрутов моделей, диспатч и отчёт файлами,
контракт DONE/DONE_WITH_CONCERNS/NEEDS_CONTEXT/BLOCKED, ожидание через
завершение хода и уведомление, ревью задачи и fix-loop 5 раундов, ledger
с ## Findings, таблица отличий от Claude-SDD.

Тесты: frontmatter, режимы, инструменты, маршруты, ledger, отсутствие
job_output как ожидания; install.test.mjs ждёт два локальных skill.

Co-Authored-By: DeepSeek Harness <noreply@deepseek.com>
This commit is contained in:
Andrey Epifancev 2026-10-24 17:40:24 +04:00
parent 2bee63d305
commit f714426b11
4 changed files with 226 additions and 8 deletions

View File

@ -14,11 +14,15 @@ separate Cordis bundle plus a thin install helper.
- roles — the model loads them with the `role` skill (`~/.dsh/roles`, then
`~/.claude/roles`, then repo `roles/`); `/role dev|admin|analyst|qa` remains
the user-facing command and injects the same text, without Claude's
auto-memory step.
auto-memory step;
- SDD — the `sdd` skill executes a plan through asynchronous `subagent`
dispatches (report files, task review, fix loop, ledger), with controller
modes for one plan and for several parallel plans; the optional overlay
enables model routes and depth 2.
**Not yet:** SDD (DSH subagents are asynchronous), `briefing`,
`harness-check`, `role-hint`, `harness-doctor`, auto-memory, caveman. See
[PORT.md](PORT.md) for the design, mapping table, and risks.
**Not yet:** `briefing`, `harness-check`, `role-hint`, `harness-doctor`,
auto-memory, caveman. See [PORT.md](PORT.md) for the design, mapping table, and
risks.
## Quick start
@ -91,6 +95,8 @@ dsh/
icon.svg
tests/
install.sh
skills/{role,sdd}/SKILL.md model-invocable skills installed into ~/.dsh
subagent-models.yml opt-in overlay: model routes + maxDepth 2
mcp.codegraph.yml optional CodeGraph MCP overlay
PORT.md design, mapping, risks, next phases
```
@ -131,6 +137,8 @@ repository layout:
`agent/pre-step` injection. If the model skips the skill, use the `/role`
command or ask it to load the role explicitly.
- Model names in roles and superpowers (`sonnet`, `haiku`, `opus`) are not DSH
models. Map them to live provider/model routes before porting SDD.
- SDD is intentionally out of scope for this spike; see
[PORT.md](PORT.md#3-what-is-deliberately-deferred).
models. The `sdd` skill uses two live routes from the opt-in
`dsh/subagent-models.yml` overlay; without it every dispatch runs on the
session model.
- SDD on DSH is asynchronous, not a copy of Claude's `Task`. See the `sdd`
skill and `openspec/changes/dsh-sdd/`.

142
dsh/skills/sdd/SKILL.md Normal file
View File

@ -0,0 +1,142 @@
---
name: sdd
description: Use when executing an implementation plan in DeepSeek Harness (DSH) with subagents — async dispatch, controller modes, ledger, task review, fix loop, final branch review. Not for Claude Code, where subagent-driven-development applies.
---
# SDD в DeepSeek Harness
Исполнение плана субагентами в DSH. Заменяет `subagent-driven-development`
только в DSH-сессиях: у Claude Code синхронный `Task`, у DSH — асинхронный
`subagent`. Это **не паритет** с Claude-SDD и не имитация: протокол построен
на том, что DSH умеет (уведомление о завершении, `send_message`, ledger), а не
на том, чего у него нет (возврат результата из вызова инструмента).
## Когда применять
- Есть план в `docs/superpowers/plans/`, задачи независимы, сессия — DSH.
- Не применять: Claude Code (там исходный `subagent-driven-development`);
плана нет (сначала `brainstorming` → `writing-plans`); правка одна
(дешевле `executing-plans`).
## Режимы контроллера
| Условие | Контроллер | Глубина | Когда |
|---|---|---|---|
| Один план в сессии | главная сессия | 1 | по умолчанию; рулинги по конфликтам плана делает тот, у кого спека |
| Несколько задач, каждая по SDD, параллельно | фоновый субагент на план | 2 | когда главная сессия нужна другой задаче |
**Правило выбора:** режим зависит от числа параллельных планов, а не от
привычки. Один план — Режим 1; несколько — Режим 2 на каждый план.
**Проверка до первого диспатча.** В Режиме 2 поднята ли глубина:
`bash dsh/install.sh --check` должен сказать `ok: subagent maxDepth=2 (...)`,
или первый диспатч контроллера падает с `exceeds maxDepth`. Если глубины нет —
не диспатчить контроллера, зафиксировать рулинг `R-n` об **откате** в Режим 1 и
продолжить в нём. Проверка выбора модели: в схеме `subagent` есть
`provider`/`model` (overlay `dsh/subagent-models.yml`); нет — все диспатчи идут
на модели сессии, и это наблюдение `O-n`, а не выполненное требование. Без
overlay ребёнок наследует модель сессии.
## Протокол диспатча и отчёта
1. Первым делом — ledger (см. ниже) и скан плана на конфликты; рулинги до
Task 1.
2. Бриф implementer'а — **файлом** `<workspace>/briefs/task-<n>.md`: контекст,
требования задачи, точные имена и сигнатуры, тест, команда проверки,
формат коммита. Промпт короткий: «прочитай бриф, выполни, напиши отчёт,
первая строка отчёта — статус».
3. Отчёт — **файлом** `reports/task-<n>.md`. Контракт первой строки:
`DONE`, `DONE_WITH_CONCERNS`, `NEEDS_CONTEXT`, `BLOCKED`. Дальше: коммиты,
одна строка сводки тестов, concerns, что осталось.
4. В контекст контроллера возвращается только: статус, коммиты, одна строка
тестов и concerns. Полный отчёт читается с диска при ревью, а не тащится
в контекст.
5. `NEEDS_CONTEXT` — ответить в тот же `send_message`; `BLOCKED` — рулинг
контроллера, а не остановка сессии.
## Ожидание без блокировки
`subagent` возвращает id и сразу завершается; результата в значении
инструмента нет. Поэтому:
- вызвал `subagent` — **закончи ход**. Следующий ход начнётся с уведомления о
завершении ребёнка;
- пока ребёнок работает и есть локальная работа (ревью-пакет, ledger,
следующий бриф) — делай её, а не жди;
- перед каждым следующим диспатчем — `list_agents`: неактивный ребёнок без
отчёта означает, что уведомление потерялось; истина — файл отчёта и git, а
не перезапуск задачи;
- ребёнок висит дольше разумного — `interrupt_agent`, затем решение по
состоянию git;
- `job_output` — не ожидание сабагента, а стрим фонового job'а: здесь
неприменим.
## Ревью задачи и fix-loop
Каждая задача проходит ревью **свежим** субагентом: соответствие спеке и
качество кода. Self-review implementer'а ревью не заменяет.
1. Пакет ревью — файлом: дифф задачи, отчёт implementer'а, выдержка из спеки.
2. Ревьюер возвращает вердикт и находки `T<n>.<k>` со статусом.
3. Fix-loop — не более 5 раундов: раунды 1–3 — `send_message` тому же
implementer'у, раунды 4–5 — свежий implementer на более дорогом маршруте.
4. Ре-ревью — только исправление (скоуп), не вся задача.
5. После 5-го раунда — адъюдикация каждой открытой находки: рулинг
`R-n (design #n)` и статус `fixed`/`deferred`/`rejected <причина>`.
Находка без статуса в отчёте контроллера — ошибка контроллера.
## Финальное ревью ветки
После всех задач — один ревьюер на весь дифф `main...HEAD`: находки `C-n`
(critical), `I-n` (important), `M-n` (minor). При находках — **один**
fix-диспатч и одно скоуп-ревью; остатки адъюдицируются и уходят в
`deferred`/follow-up.
## Ledger
`.superpowers/sdd/<plan>/progress.md` в рабочем каталоге плана; первая строка —
путь плана. Обязательные секции:
- таблица сканирования плана: конфликт, решение, рулинг `R-n (design #n)`;
- `Task <n>: pending | complete` с коммитом;
- `## Findings` — каждая находка: маркер, одна строка, статус
`fixed <commit>`, `deferred` или `rejected <причина>`;
- `## Rulings` — рулинги с ценой ошибки.
Ledger — единственная память между ходами: чат и контекст теряются, а
ledger лежит в git-ignored scratch (после `git clean -fdx` восстанавливается
по `git log`). Поэтому записи не сжимать.
## Маршруты моделей
| Роль в SDD | Маршрут | Почему |
|---|---|---|
| implementer по готовому коду из плана | `deepseek-official/deepseek-flash` | транскрипция и тесты |
| implementer по прозе, интеграция | `deepseek-official/deepseek-v4-pro` | решение в нескольких файлах |
| ревью задачи, ре-ревью | `deepseek-official/deepseek-v4-pro` | вердикт важнее цены |
| финальное ревью ветки | `deepseek-official/deepseek-v4-pro` | самый широкий дифф |
Маршрут указывается в каждом диспатче: `provider`, `model`,
`reasoning_effort`. Параметры действуют только при включённом overlay
(`dsh/subagent-models.yml`); без него всё идёт на модели сессии, и отчёт
фиксирует это наблюдением `O-n`.
## Отличие от Claude-SDD
| Свойство | Claude Code `subagent-driven-development` | DSH `sdd` |
|---|---|---|
| Вызов исполнителя | синхронный `Task`, результат в том же ходе | асинхронный `subagent`, id + уведомление позже |
| Ожидание | возврат значения инструмента | закончи ход, продолжи по уведомлению |
| Продолжение ребёнка | новый `Task` | `send_message` в того же ребёнка |
| Глубина по умолчанию | не ограничена | `maxDepth: 2` только с overlay |
| Выбор модели | тиры sonnet/haiku/opus | два маршрута, только с overlay |
| Изоляция контекста | наследование не по умолчанию | `subagent_fork` наследует ходы — для implementer'а не годится |
## Запреты
- Не ждать сабагента через `job_output`, поллинг или повторные `list_agents`.
- Не делать merge и force-push; финиш — `finishing-a-development-branch` и PR.
- Не выдавать прогон без overlay за работу на разных моделях.
- `workflow` — расширение для веера независимых задач, в этой версии не
используется: решения по конфликтам и адъюдикация требуют суждения в моменте.

View File

@ -32,7 +32,7 @@ test('install.sh copies skills and roles instead of linking into ~/.claude', ()
const first = run()
assert.equal(first.status, 0, first.stderr)
assert.match(first.stdout, /copied 2 superpowers skills/)
assert.match(first.stdout, /copied 1 local skills/)
assert.match(first.stdout, /copied 2 local skills/)
assert.match(first.stdout, /installed 4 roles/)
const skill = join(dshHome, 'skills', 'using-superpowers')
@ -44,6 +44,10 @@ test('install.sh copies skills and roles instead of linking into ~/.claude', ()
assert.ok(existsSync(roleSkill), 'role skill was not installed')
assert.match(readFileSync(roleSkill, 'utf8'), /name: role/)
const sddSkill = join(dshHome, 'skills', 'sdd', 'SKILL.md')
assert.ok(existsSync(sddSkill), 'sdd skill was not installed')
assert.match(readFileSync(sddSkill, 'utf8'), /name: sdd/)
for (const role of ['dev', 'admin', 'analyst', 'qa']) {
assert.ok(existsSync(join(dshHome, 'roles', `${role}.md`)), `missing ${role}.md`)
}

View File

@ -0,0 +1,64 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import test from 'node:test'
import { fileURLToPath } from 'node:url'
const here = dirname(fileURLToPath(import.meta.url))
const skillPath = join(here, '..', 'skills', 'sdd', 'SKILL.md')
const text = () => readFileSync(skillPath, 'utf8')
test('sdd skill is model-invocable', () => {
const body = text()
assert.match(body, /^---\nname: sdd\n/)
assert.match(body, /\ndescription: .+/)
assert.ok(Buffer.byteLength(body) < 12288, 'skill is over the 12 KiB budget')
})
test('sdd skill describes both controller modes and the depth check', () => {
const body = text()
assert.match(body, /Режим 1/)
assert.match(body, /Режим 2/)
assert.match(body, /maxDepth: 2/)
assert.match(body, /глубин/)
assert.match(body, /откат/i)
})
test('sdd skill names the asynchronous tools and never job_output', () => {
const body = text()
for (const tool of ['subagent', 'send_message', 'list_agents', 'interrupt_agent']) {
assert.ok(body.includes(tool), `missing ${tool}`)
}
for (const line of body.split('\n').filter((l) => l.includes('job_output'))) {
assert.match(line, /не /i, `job_output proposed as a wait mechanism: ${line}`)
}
assert.match(body, /закончи ход/)
assert.match(body, /уведомлен/)
})
test('sdd skill fixes the report contract and the model routes', () => {
const body = text()
for (const status of ['DONE_WITH_CONCERNS', 'NEEDS_CONTEXT', 'BLOCKED']) {
assert.ok(body.includes(status), `missing status ${status}`)
}
assert.match(body, /deepseek-official\/deepseek-flash/)
assert.match(body, /deepseek-official\/deepseek-v4-pro/)
assert.match(body, /модель сессии/)
})
test('sdd skill requires the ledger with findings and statuses', () => {
const body = text()
assert.match(body, /## Findings/)
assert.match(body, /fixed <commit>/)
assert.match(body, /T<n>\.<k>/)
assert.match(body, /deferred/)
assert.match(body, /rejected <причина>/)
})
test('sdd skill states the difference from Claude SDD without claiming parity', () => {
const body = text()
assert.match(body, /синхронн/i)
assert.match(body, /асинхронн/i)
assert.match(body, /subagent-driven-development/)
assert.match(body, /не паритет/i)
})