docs: план dsh-port-v2 — 5 задач, вариант C (#69)

This commit is contained in:
Andrey Epifancev 2026-10-24 22:53:47 +04:00
parent ae2b55c497
commit cc0c22f6e0
2 changed files with 287 additions and 1 deletions

View File

@ -0,0 +1,286 @@
# DSH port v2 (вариант C) Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use the `sdd` skill (DSH) to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Перевести доставку superpowers-скиллов на вариант C: вендор sync-машинерии LayneChai, пин на тег v7.0.1, install.sh гоняет sync→verify→copy, бандл получает аддитивную самопроверку контракта.
**Architecture:** `dsh/sync/` — два вендорённых Node-скрипта (только stdlib): `sync-from-upstream.mjs` (ассертные transform-ы + staging-swap + override нашего `dsh-tools.md`) и `verify-skills.mjs` (frontmatter/префиксы/ссылки). `dsh/install.sh` оркестрирует: resolve источника (explicit/env → клон по пину → legacy сырой+WARN) → sync → verify → копирование в `$DSH_HOME/skills`. Бандл получает `lib/selfcheck.js` (чистые чекеры контракта ctx) и команду `dsh-harness-check`; существующее поведение бандла не меняется.
**Tech Stack:** bash (`set -euo pipefail`), Node builtins (без `@deepseek-ai/*`-импортов), git, python3 для существующих хуков. DSH pinned 0.2.1-rc.3.
**Spec:** `openspec/changes/dsh-port-v2/` — [proposal.md](../openspec/changes/dsh-port-v2/proposal.md) + [design.md](../openspec/changes/dsh-port-v2/design.md) (D-1…D-10; план спорит от них).
**Models (D-9):** Tasks 1, 2, 5 — `deepseek-official/deepseek-flash` (механика, готовый код); Tasks 3, 4 — `deepseek-official/deepseek-v4-pro` (интеграция); ревью каждой задачи и финальное ревью ветки — `deepseek-official/deepseek-v4-pro`.
## Global Constraints
- Runtime-зависимости sync и бандла — только Node builtins; запрещён импорт `@deepseek-ai/*` (ловит `dsh/tests/package.test.mjs`).
- `dsh/install.sh` идемпотентен; `--check` никогда не ходит в сеть; невалидный явный источник — FAIL (exit 1), не молчаливый фолбэк.
- Пин: `DSH_SUPERPOWERS_REF=v7.0.1`, репозиторий `DSH_SUPERPOWERS_REPO` (default `https://github.com/obra/superpowers`).
- Legacy-фолбэки `~/.claude/*` остаются (сырой копией + WARN) до #66 — в этой работе не удаляются.
- Тесты — `node --test` через `bash dsh/tests/run.sh`; весь набор зелёный, плюс `bash tests/run.sh` (CC-сторона) не ломается.
- Маркировка: находки с маркером и статусом; коммиты со ссылкой на issue (`(#69)`).
- Синхронизация апстрима — только по тегу; HEAD не используется нигде.
## Review Focus
| # | Вход/условие | Ожидаемое поведение |
|---|---|---|
| RF1 | Sync против checkout не v7.0.1 (якорь изменился/исчез) | FAIL loudly (exit 1), staging не подменяет назначение |
| RF2 | `--skills` без сети и без клона/источника | FAIL с готовой подсказкой, не «пустой успех» |
| RF3 | Повторный `--skills` (идемпотентность) | Повторный прогон не ломает установленное; нет мусора staging |
| RF4 | Бандл активируется без `ctx.tools.guard` (сломан контракт DSH) | Самопроверка помечает guards not-ok, остальное живо; бандл не падает целиком |
| RF5 | Недоклонированный каталог на месте клона | FAIL без оставленного полусостояния |
---
### Task 1: Вендор sync-from-upstream
**Files:**
- Create: `dsh/sync/sync-from-upstream.mjs`
- Create: `dsh/tests/fixtures/superpowers-min/` (минимальное дерево: `using-superpowers/SKILL.md` с якорями transform-ов, `brainstorming/SKILL.md` с `superpowers:`-префиксами и висячими harness-референсами, `references/claude-code-tools.md`, `.claude-plugin/plugin.json` с `"version": "7.0.1"`)
- Create: `dsh/tests/sync.test.mjs`
**Interfaces:**
- Consumes: `dsh/bundle/dsh-tools.md` (единственный источник маппинга).
- Produces: CLI `node dsh/sync/sync-from-upstream.mjs <upstream-checkout> [--out <dir>] [--ref <tag>]`; exit 0 — печать `synced N files…`, exit 1 — FAIL с именем упавшего transform/якоря; `--out` по умолчанию `<checkout>/.sync-out`; staging-каталог `<out>.tmp-<pid>`; `--out` подменяется только после всех ассертов; в `--out/.sync-meta.json` пишется `{"ref": "<tag|unknown>", "version": "<plugin.json version>"}`.
- [ ] **Step 1: Напиши падающие тесты** в `dsh/tests/sync.test.mjs` (helpers: `tempDir`, `write`, `spawnSync` из `./helpers.mjs`; фикстура — копия `dsh/tests/fixtures/superpowers-min/`):
```js
test('sync happy path: transforms, namespace strip, override, meta', ...)
// assert: exit 0; out/using-superpowers/SKILL.md содержит 'references/dsh-tools.md'
// и НЕ содержит 'superpowers:'; out/using-superpowers/references/dsh-tools.md
// == dsh/bundle/dsh-tools.md; out/.sync-meta.json.ref === 'v7.0.1'
// (запуск с --ref v7.0.1); references/claude-code-tools.md отсутствует (SKIP).
test('sync fails loud on broken anchor', ...)
// испортить якорь в копии фикстуры (replace уникального фрагмента);
// assert: exit 1, stderr содержит имя transform-а; существующий --out не тронут.
test('sync fails on changed skill set', ...)
// удалить один skill-каталог из копии; assert: exit 1, stderr содержит added/removed.
test('sync fails when no skills/ dir', ...) // assert: exit 1.
```
- [ ] **Step 2: Прогони — ожидай FAIL** (скрипта нет)
Run: `node --test dsh/tests/sync.test.mjs`
Expected: FAIL (module not found / exit не 1).
- [ ] **Step 3: Реализуй `dsh/sync/sync-from-upstream.mjs`** — перенос из LayneChai/superpowers-dsh `scripts/sync-from-upstream.mjs` с адаптациями:
- без `package.json`-привязки: `upstreamRoot = resolve(argv[2])`, `--out` из argv;
- `SKIP`-список чужих harness-референсов — как у оригинала (8 файлов);
- `TRANSFORMS` — как у оригинала, кроме: их `using-superpowers`-rewrite сохраняется как есть (указывает на `references/dsh-tools.md`);
- override: после копирования дерева — скопировать `dsh/bundle/dsh-tools.md` → `using-superpowers/references/dsh-tools.md` (путь до бандла — `resolve(import.meta.url, '../../bundle/dsh-tools.md')`);
- namespace-strip `superpowers:` → `''` и сверка набора скиллов (15 имён) — как у оригинала;
- staging-swap и «unfired transforms» — как у оригинала; `--ref`/`plugin.json`-версия — в `.sync-meta.json`.
- Только Node builtins.
- [ ] **Step 4: Прогони — ожидай PASS**
Run: `node --test dsh/tests/sync.test.mjs`
Expected: все 4 теста PASS.
- [ ] **Step 5: Весь набор + коммит**
Run: `bash dsh/tests/run.sh`
Expected: весь набор зелёный.
```bash
git add dsh/sync/sync-from-upstream.mjs dsh/tests/sync.test.mjs dsh/tests/fixtures/superpowers-min
git commit -m "feat(dsh): вендор sync-from-upstream — ассертные transform-ы, staging-swap, override dsh-tools.md (#69)"
```
### Task 2: Вендор verify-skills
**Files:**
- Create: `dsh/sync/verify-skills.mjs`
- Create: `dsh/tests/verify.test.mjs`
**Interfaces:**
- Consumes: synced-каталог из Task 1 (формат `<skill>/SKILL.md`).
- Produces: CLI `node dsh/sync/verify-skills.mjs <skills-dir>`; exit 0 — `ok: <name>` на каждый скилл; exit 1 — первая ошибка с `<skill>/SKILL.md: <reason>`; проверки: YAML frontmatter с `name`+`description`, без остаточного `superpowers:`-префикса в теле, все относительные ссылки вида `../<skill>/…`/`references/…` из тела резолвятся в `<skills-dir>`.
- [ ] **Step 1: Напиши падающие тесты** в `dsh/tests/verify.test.mjs` (фикстура — результат sync из Task 1, плюс испорченные копии):
```js
test('verify ok on synced tree', ...) // exit 0, stdout содержит 'ok: using-superpowers'.
test('verify fails on missing description', ...) // exit 1, stderr содержит путь и reason.
test('verify fails on leftover superpowers: prefix', ...)
test('verify fails on dangling relative reference', ...) // ссылка ../brainstorming/x.md без файла.
```
- [ ] **Step 2: Прогони — ожидай FAIL**
Run: `node --test dsh/tests/verify.test.mjs`
Expected: FAIL.
- [ ] **Step 3: Реализуй `dsh/sync/verify-skills.mjs`** — перенос идеи LayneChai `verify-skills.mjs`, без provider-части: только проверки выше, чистый Node. Парсер frontmatter — как в их `parseFrontmatter` (скалярные поля).
- [ ] **Step 4: Прогони — ожидай PASS**
Run: `node --test dsh/tests/verify.test.mjs`
Expected: все тесты PASS.
- [ ] **Step 5: Весь набор + коммит**
Run: `bash dsh/tests/run.sh`
Expected: зелёный.
```bash
git add dsh/sync/verify-skills.mjs dsh/tests/verify.test.mjs
git commit -m "feat(dsh): вендор verify-skills — frontmatter, префиксы, висячие ссылки (#69)"
```
### Task 3: install.sh — пин, fetch, sync-оркестрация
**Files:**
- Modify: `dsh/install.sh`
- Modify: `dsh/tests/install.test.mjs`
- Modify: `dsh/README.md`, `dsh/PORT.md` (разделы установки/источника)
**Interfaces:**
- Consumes: `dsh/sync/sync-from-upstream.mjs` (Task 1), `dsh/sync/verify-skills.mjs` (Task 2); env `DSH_SUPERPOWERS_REF` (default `v7.0.1`), `DSH_SUPERPOWERS_REPO` (default `https://github.com/obra/superpowers`), `DSH_SUPERPOWERS_SOURCE`, `--superpowers-from`.
- Produces: поток `--skills`: resolve (explicit/env → клон `$DSH_HOME/superpowers` на пине → legacy сырой+WARN) → `node dsh/sync/sync-from-upstream.mjs <source> --out "$DSH_HOME/superpowers/skills.synced" --ref "$REF"` → `node dsh/sync/verify-skills.mjs <out>` → копирование `<out>/*` в `$DSH_HOME/skills` (как текущий цикл) → local skills (role, sdd) без изменений. `--check`: строка `ok: superpowers source (<dir>, git <sha>, ref <ref>)`, сети нет.
- [ ] **Step 1: Напиши падающие тесты** в `dsh/tests/install.test.mjs` (helpers уже есть; локальный git-репозиторий: `git init` + коммит фикстуры `dsh/tests/fixtures/superpowers-min` + `git tag v7.0.1`; env `DSH_SUPERPOWERS_REPO=file://<repo>`):
```js
test('--skills clones pinned ref, syncs, verifies and installs adapted skills', ...)
// fresh HOME: exit 0; $DSH_HOME/skills/using-superpowers/SKILL.md содержит 'references/dsh-tools.md'
// и НЕ содержит 'superpowers:'; references/dsh-tools.md установлен; stdout содержит 'synced' и 'copied N superpowers skills'.
test('--skills offline without source fails with hint', ...)
// fresh HOME, DSH_SUPERPOWERS_REPO=file:///nonexistent, без explicit: exit 1,
// stderr содержит 'git clone' hint; $DSH_HOME/skills не создан с пустыми скиллами.
test('--skills removes failed empty clone dir (RF5)', ...)
// fresh HOME, DSH_SUPERPOWERS_REPO=file:///nonexistent, предварительно mkdir $DSH_HOME/superpowers (пустой):
// exit 1; existsSync($DSH_HOME/superpowers) === false (полусостояния не остаётся).
test('--skills re-run updates from moved tag', ...)
// второй прогон после 'git commit --amend' + 'git tag -f v7.0.1': установленный текст обновился (маркер из amend).
test('--skills leaves no staging garbage', ...)
// после прогона: в $DSH_HOME/superpowers нет *.tmp-*; existsSync skills.synced — допустимо, но .sync-meta.json.ref==='v7.0.1'.
test('--check is offline and reports ref', ...)
// env DSH_SUPERPOWERS_REPO=file:///nonexistent: exit 0; stdout содержит 'ref v7.0.1' или 'no superpowers source'.
test('.claude/code-paths содержит dsh/', ...)
// readFileSync('../../.claude/code-paths') содержит строку 'dsh/' (D-5).
```
- [ ] **Step 2: Прогони — ожидай FAIL**
Run: `node --test dsh/tests/install.test.mjs`
Expected: новые тесты FAIL (clone-логики нет), старые — PASS.
- [ ] **Step 3: Реализуй в `dsh/install.sh`** — только `superpowers_source()`/`check()`/`install_skills()`:
- новая ветка resolve: explicit (`--superpowers-from`/`DSH_SUPERPOWERS_SOURCE`) → **sync от этого каталога без сети**; иначе `ensure_pinned_clone`: если `$DSH_HOME/superpowers` уже клон на `$REF` — использовать (WARN при отсутствии сети); иначе `git clone --depth 1 --branch "$REF" "$REPO" "$DSH_HOME/superpowers"`; при неудаче и существующем каталоге без `.git` — FAIL с подсказкой, каталог не трогать; при неудаче и пустом месте — удалить недоклон и FAIL (RF5);
- `install_skills()`: sync → verify → копирование из `--out`; legacy-источник — прежний сырой путь с WARN (без sync);
- `--check`: офлайн, печатает источник/ref/sha из `.sync-meta.json` или git;
- `usage()` — новые переменные.
- Bash с python3-вставками в прежнем стиле.
- [ ] **Step 4: Прогони — ожидай PASS**
Run: `node --test dsh/tests/install.test.mjs`
Expected: все тесты (старые и новые) PASS.
- [ ] **Step 5: Доки + весь набор + коммит** — обнови `dsh/README.md` (Quick start: `--skills` сам тянет пин, переменные, офлайн-кейсы) и `dsh/PORT.md` (§3 убрать «Vendored superpowers»-абзац как устаревший, §6 команды установки).
Run: `bash dsh/tests/run.sh && bash tests/run.sh`
Expected: оба набора зелёные.
```bash
git add dsh/install.sh dsh/tests/install.test.mjs dsh/README.md dsh/PORT.md
git commit -m "feat(dsh): --skills гоняет sync/verify по пину v7.0.1, офлайн-кейсы (#69)"
```
### Task 4: Самопроверка бандла + команда dsh-harness-check
**Files:**
- Create: `dsh/bundle/lib/selfcheck.js`
- Modify: `dsh/bundle/index.js` (аддитивно)
- Create: `dsh/bundle/command-check.js`
- Create: `dsh/tests/selfcheck.test.mjs`
- Modify: `dsh/README.md` (Known limits → самопроверка), `dsh/PORT.md` (§7 риск → чем закрыт)
**Interfaces:**
- Consumes: `ctx`-форма из `dsh/bundle/index.js` (`tools.guard`, `on`, `inject`, `commands`, `workingDirectory.get`).
- Produces: `collectSelfCheck(ctxLike) -> [{ feature: 'guards'|'bootstrap'|'commands'|'workingDirectory', ok: boolean, detail: string }]` — чистая функция, без вызовов ctx; `registerCheckCommand({ commands, collect, logger })` — регистрирует команду `dsh-harness-check`, которая собирает чек и возвращает текст-отчёт; в `index.js`: конфиг-ключи `selfCheck: true` (default) и `checkCommand: true` (default); после wiring — прогон `collectSelfCheck`, при not-ok критической службы (guards) — `ctx.logger.error` + пропуск только этой фичи (существующие фичи при их службах на месте работают как раньше).
- [ ] **Step 1: Напиши падающие тесты** в `dsh/tests/selfcheck.test.mjs`:
```js
test('collectSelfCheck ok on full ctx-like', ...) // все features ok:true.
test('collectSelfCheck flags missing tools.guard', ...) // guards ok:false, detail содержит 'tools.guard'; остальные ok:true.
test('collectSelfCheck flags missing on()', ...) // bootstrap ok:false.
test('registerCheckCommand builds report text', ...)
// fake commands.register(name, handler) — вызвать handler с fake collect, вернуть текст:
// содержит 'guards: not-ok' и имя фичи; ok-ветка содержит 'ok'.
```
- [ ] **Step 2: Прогони — ожидай FAIL**
Run: `node --test dsh/tests/selfcheck.test.mjs`
Expected: FAIL.
- [ ] **Step 3: Реализуй** `lib/selfcheck.js` и `command-check.js` по интерфейсам; в `index.js` добавь в `DEFAULTS` ключи и вызовы после существующих блоков (порядок: guards → bootstrap → roleCommand → selfcheck/command), чтобы при отсутствии `ctx.tools.guard` guard-блок пропускался с `ctx.logger.error('[dsh-harness] self-check: guards not-ok: …')`, а bootstrap/command оставались.
- [ ] **Step 4: Прогони — ожидай PASS**
Run: `node --test dsh/tests/selfcheck.test.mjs`
Expected: PASS.
- [ ] **Step 5: Весь набор + доки + коммит** — `dsh/README.md` (Known limits: самопроверка при активации + команда `dsh-harness-check`), `dsh/PORT.md` §7 (риск контракта → закрыт самопроверкой).
Run: `bash dsh/tests/run.sh`
Expected: зелёный (package.test.mjs без `@deepseek-ai/*` тоже).
```bash
git add dsh/bundle/lib/selfcheck.js dsh/bundle/command-check.js dsh/bundle/index.js dsh/tests/selfcheck.test.mjs dsh/README.md dsh/PORT.md
git commit -m "feat(dsh): самопроверка контракта бандла при активации + команда dsh-harness-check (#69)"
```
### Task 5: Спека-дельта dsh-install и проверка code-paths
**Files:**
- Modify: `openspec/changes/dsh-port-v2/specs/dsh-install/spec.md` (delta)
- Modify: `dsh/PORT.md` (§1 таблица, §3: строка «Vendored superpowers» → «Pinned sync (v7.0.1)»)
- Modify: `openspec/changes/dsh-port-v2/design.md` (D-5 — статус «выполнено в этой ветке» после Task 3; D-6 — ссылки на selfcheck)
**Interfaces:**
- Consumes: поведение из Tasks 3–4.
- Produces: spec-delta: REQUIREMENT `sync-по-пину` (ADDED: `--skills` без явного источника клонирует `DSH_SUPERPOWERS_REPO` на `DSH_SUPERPOWERS_REF`, sync+verify обязательны, FAIL на несовпадении якорей; MODIFIED: сценарии «нет сети» — FAIL с подсказкой вместо тихого legacy; legacy — только WARN-путь), `selfcheck` (ADDED: активационный чек + команда).
- [ ] **Step 1: Напиши падающую проверку** — в `dsh/tests/install.test.mjs` уже есть тест `.claude/code-paths содержит dsh/` (Task 3); добавь в `dsh/tests/sync.test.mjs`:
```js
test('sync meta records ref and version', ...) // .sync-meta.json: ref==='v7.0.1', version==='7.0.1'.
```
Run: `node --test dsh/tests/sync.test.mjs`
Expected: FAIL, если Task 1 не писал `version` (иначе шаг помечается как уже зелёный и пропускается).
- [ ] **Step 2: Напиши дельту** `openspec/changes/dsh-port-v2/specs/dsh-install/spec.md` — прочитай `openspec/specs/dsh-install/spec.md` и оформи ADDED/MODIFIED по формату; BREAKING отметить в `## MODIFIED Requirements` (сеть для `--skills`).
Run: `openspec validate dsh-port-v2 --strict`
Expected: `Change 'dsh-port-v2' is valid`.
- [ ] **Step 3: Обнови** `dsh/PORT.md` §1/§3, `dsh/README.md` (раздел «Обновление»: каденс D-7 — issue на каждый релиз obra/superpowers, команда sync→verify→--skills, проверка `--check`) и `design.md` (D-5/D-6 статусы).
- [ ] **Step 4: Оба набора + коммит**
Run: `bash dsh/tests/run.sh && bash tests/run.sh && openspec validate dsh-port-v2 --strict`
Expected: всё зелёное.
```bash
git add openspec/changes/dsh-port-v2/specs/dsh-install/spec.md openspec/changes/dsh-port-v2/design.md dsh/PORT.md dsh/tests/sync.test.mjs
git commit -m "docs(openspec): дельта dsh-install под sync-по-пину и самопроверку (#69)"
```
## Предусловия окружения (проверяются контроллером один раз до Task 1)
- `python3`, `git`, `node` в PATH; `bash dsh/tests/run.sh` зелёный на входной точке ветки.
- Сеть не нужна для тестов: все прогоны используют `DSH_SUPERPOWERS_REPO=file://…` или temp-HOME; реальный `~/.dsh` не трогается (тесты — temp `DSH_HOME`).
- Ключи Gitea доступны только для `tea` (PR), не в переменных окружения сессий.
- Ветка: `feat/69-dsh-port-v2`, worktree `.claude/worktrees/dsh-port-v2`; `#63` идёт отдельной веткой `feat/63-guard-worktree-chain` и не пересекается по файлам.

View File

@ -44,7 +44,7 @@ RV-6 (legacy-фолбэки как техдолг), RV-11 (нет compat-чек
| D-2 | Пин на тег (v7.0.1) через `DSH_SUPERPOWERS_REF`; #67 — rejected | Воспроизводимость; unpinned HEAD = случайная семантика | Каждый релиз upstream — свой issue+PR; пропустили релиз = молча старый текст (митигируется `--check`) |
| D-3 | Результат sync — в `$DSH_HOME/skills`; bootstrap бандла не меняется | Один источник текста; install.sh остаётся инсталлятором, а не публикуемым пакетом | Нет |
| D-4 | Legacy-фолбэки `~/.claude/*` живут до #66, удаляются им | #49 уже ввёл их с WARN; рубить сейчас = сломать машины без переустановки | Техдолг висит дольше; контракт удаления — в #66 |
| D-5 | `dsh/sync/` и `dsh/bundle/` добавляются в `.claude/code-paths` | Сейчас require-plan защищает только `hooks/` и `skills/`; вендоримый код обязан быть под хук | Без этого правки sync-скриптов не гейтятся планом |
| D-5 | `dsh/` уже в `.claude/code-paths` (проверено 24.10.2026 — файл содержит `dsh/`); require-plan защищает весь `dsh/`, включая `dsh/sync/` | Вендоримый код обязан быть под хук | Без проверки правки sync-скриптов не гейтятся планом — в план добавлен тест-assert на строку `dsh/` в code-paths |
| D-6 | Compat-чек (решение владельца 24.10.2026): **самопроверка бандла внутри DSH** — при активации и по команде проверяет контракт, на который бандл опирается (`ctx.tools.guard`, `agent/pre-step`, `session.surface.nodes`, `ctx.commands`, `workingDirectory`); терминальный `--check` гоняет только verify synced-скиллов (frontmatter, префиксы, висячие ссылки) и окружение | SDK DSH лежит в app.asar, терминал до него не дотягивается — проверка контракта возможна только изнутри DSH; это закрывает RV-11 по-настоящему | Бандл тронут аддитивно (активационный assert + команда) — «не трогаем бандл» смягчено до «без изменения существующего поведения» |
| D-7 | Каденс: issue на каждый релиз obra/superpowers; compat — после апгрейда DSH | Ритм без напоминаний; влезает в обычный цикл фичи | Пропущенный релиз не виден между ретро — добавить строку в `--check`/doctor (#44) |
| D-8 | Болевые точки эксплуатации заранее не фиксируются: ответ владельца 24.10.2026 — «пока трудно сказать, идёт разработка только самого харнеса». План сосредоточен на машинерии порта; первые реальные затыки DSH-сессий копятся в #69 и двигают приоритеты | Не выдумывать боли за владельца, пока нет сигнала | Задачи плана упорядочены без пользовательского приоритета — по зависимостям и P-классам находок |