# 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)`). - Язык субагентов (решение владельца 24.10.2026): брифы и отчёты implementer'ов — на английском; маркеры, статусы, термины обвязки и слэш-команды не переводятся; финальные отчёты контроллера владельцу — по-русски. - Синхронизация апстрима — только по тегу; 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 [--out ] [--ref ]`; exit 0 — печать `synced N files…`, exit 1 — FAIL с именем упавшего transform/якоря; `--out` по умолчанию `/.sync-out`; staging-каталог `.tmp-`; `--out` подменяется только после всех ассертов; в `--out/.sync-meta.json` пишется `{"ref": "", "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-референсов — как у оригинала (7 файлов: copilot-tools.md в v7.0.1 нет; T1.3); - `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.md`). - Produces: CLI `node dsh/sync/verify-skills.mjs `; exit 0 — `ok: ` на каждый скилл; exit 1 — первая ошибка с `/SKILL.md: `; проверки: YAML frontmatter с `name`+`description`, без остаточного `superpowers:`-префикса в теле, все относительные ссылки вида `..//…`/`references/…` из тела резолвятся в ``. - [ ] **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 --out "$DSH_HOME/superpowers/skills.synced" --ref "$REF"` → `node dsh/sync/verify-skills.mjs ` → копирование `/*` в `$DSH_HOME/skills` (как текущий цикл) → local skills (role, sdd) без изменений. `--check`: строка `ok: superpowers source (, git , 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://`): ```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` и не пересекается по файлам.