# Spec Delta: dsh-install Дельта change `dsh-port-v2` (#69). Доставка superpowers переходит с «сырой копии клона» на sync по пину `v7.0.1`, бандл получает самопроверку контракта. **BREAKING:** `--skills` без явного источника теперь требует сеть (клон пина); офлайн-прогон без клона — `FAIL`, а не тихий legacy и не «WARN + код 0». Отмена #67 (авто-pull unpinned HEAD) — решение design.md D-2. ## ADDED Requirements ### Requirement: Sync по пину `dsh/install.sh --skills` без явного источника MUST получить superpowers из клона `$DSH_HOME/superpowers` на пине `$DSH_SUPERPOWERS_REF` (default `v7.0.1`) из `$DSH_SUPERPOWERS_REPO` (default `https://github.com/obra/superpowers`) и MUST прогнать sync (`dsh/sync/sync-from-upstream.mjs`) и verify (`dsh/sync/verify-skills.mjs`) до копирования в `$DSH_HOME/skills`. - Копирование MUST брать только synced-дерево; сырой текст апстрима при пине MUST NOT попадать в `$DSH_HOME/skills`. Адаптации: `superpowers:`-префиксы сняты, ссылки ведут на `using-superpowers/references/dsh-tools.md` из `dsh/bundle/`, чужие harness-референсы не копируются. - Sync MUST завершаться `FAIL` (код 1) на несовпадении якоря transform-а (ноль или больше одного совпадения), на изменении набора скиллов и на отсутствии `skills/` в checkout. При любом падении назначение (`--out`) MUST остаться прежним, а staging-каталог MUST быть удалён. - Verify MUST проверять YAML frontmatter (`name` и `description` непусты, без дублей ключей), отсутствие остаточных `superpowers:`-префиксов в телах `SKILL.md` и резолв всех относительных ссылок внутри synced-дерева. Дерево, прошедшее verify с нулём скиллов (`verified 0 skills`), MUST трактоваться как `FAIL`: пустое дерево не устанавливается. - В `/.sync-meta.json` MUST записываться `ref` (значение `--ref`) и `version` (версия из `.claude-plugin/plugin.json` апстрима); `--check` MUST печатать ref и version из этого файла, не обращаясь в сеть. - Повторный `--skills` MUST быть идемпотентным: каталоги `*.tmp-*` не остаются, установленные копии заменяются целиком (`rm -rf` + `cp -R`), а не дополняются. #### Scenario: Свежий HOME, установка по пину - **WHEN** `HOME` пуст, флаг и `DSH_SUPERPOWERS_SOURCE` не заданы, а `DSH_SUPERPOWERS_REPO` указывает на репозиторий с тегом `v7.0.1` - **THEN** `--skills` клонирует пин, печатает `synced N files` и `verified 15 skills`, копирует адаптированные скиллы; в `using-superpowers/SKILL.md` есть `references/dsh-tools.md` и нет `superpowers:`, а `references/dsh-tools.md` совпадает с `dsh/bundle/dsh-tools.md` #### Scenario: Сломанный якорь апстрима - **WHEN** checkout на пине не содержит якоря одного из transform-ов - **THEN** sync печатает `FAIL` с именем transform-а и завершается кодом 1, прежнее `--out` и `$DSH_HOME/skills` не изменены #### Scenario: Изменился набор скиллов - **WHEN** в checkout на пине появился или исчез каталог скилла относительно ожидаемого списка - **THEN** sync печатает `added upstream:` / `removed upstream:` и завершается кодом 1; установка ничего не копирует #### Scenario: Verify нашёл висячую ссылку - **WHEN** synced-дерево содержит ссылку на несуществующий файл (markdown или inline-код) - **THEN** verify печатает `/SKILL.md: dangling relative reference: …` и завершается кодом 1; в `$DSH_HOME/skills` ничего не копируется #### Scenario: Нулевое дерево - **WHEN** verify завершился кодом 0, но сообщил `verified 0 skills` - **THEN** установка завершается `FAIL`, ни один скилл не скопирован #### Scenario: Повторный прогон - **WHEN** `--skills` запущен второй раз на том же пине - **THEN** установленные скиллы заменены, в `$DSH_HOME/superpowers` нет каталогов `*.tmp-*`, а `.sync-meta.json.ref` равен пину ### Requirement: Самопроверка контракта бандла Бандл `dsh-harness` MUST при активации проверять контракт DSH, на который опирается, и MUST предоставлять команду `dsh-harness-check` для повторного прогона той же проверки. - Проверка MUST покрывать `ctx.tools.guard`, `ctx.on` для `agent/pre-step`, `ctx.inject` (регистрация команд) и `workingDirectory.get`. Каждая отсутствующая служба MUST логироваться строкой `[dsh-harness] self-check: not-ok: `. - Отсутствие службы MUST отключать только свою фичу: без `ctx.tools.guard` guard-обвязка пропускается, bootstrap и `/role` продолжают работать при своих службах. Активация бандла MUST NOT падать целиком из-за несовпадения контракта. - Команда `dsh-harness-check` MUST возвращать текстовый отчёт с одной строкой `ok`/`not-ok` на каждую проверяемую фичу. - Ключи конфига `selfCheck` и `checkCommand` (default `true`) MUST позволять выключить активационную проверку и команду соответственно. - Терминальный `--check` MUST оставаться офлайн-проверкой файлов и окружения: SDK внутри `app.asar` ему недоступен, контракт проверяется только изнутри DSH. #### Scenario: Сломанный контракт при активации - **WHEN** бандл активируется в `ctx` без `tools.guard` - **THEN** в лог попадает `self-check: guards not-ok: tools.guard`, guard-wiring пропускается, bootstrap и команды продолжают работать, бандл не падает #### Scenario: Проверка по требованию - **WHEN** пользователь вызывает `/dsh-harness-check` - **THEN** отчёт перечисляет каждую фичу со статусом `ok` или `not-ok` ## MODIFIED Requirements ### Requirement: Источник superpowers задаётся при установке `dsh/install.sh --skills` SHALL разрешать источник superpowers-скиллов в порядке приоритета. Сеть нужна только ветке пина; явный источник синхронизируется локально. - Порядок: `--superpowers-from ` → `$DSH_SUPERPOWERS_SOURCE` → клон `$DSH_HOME/superpowers` на `$DSH_SUPERPOWERS_REF` → legacy-кэш Claude Code → legacy `~/.claude/skills`. - Явный источник (`--superpowers-from`, `$DSH_SUPERPOWERS_SOURCE`) MUST NOT требовать сети и MUST NOT проверять пин. Допускаются две формы: корень checkout (есть `skills/using-superpowers/SKILL.md`) или каталог `skills` такого корня. Синк явного источника MUST идти в staging вне каталога клона (`$DSH_HOME/.skills.synced`), потому что `$DSH_HOME/superpowers` зарезервирован под клон пина. - Каталог скиллов, который не является ни checkout-корнем, ни его `skills/`, MUST быть `FAIL` (код 1) и в `--skills`, и в `--check`: вердикты двух команд MUST совпадать. Тихий откат на legacy запрещён. - Клон-пин: отсутствующий или пустой каталог MUST клонироваться `git clone --depth 1 --branch `; неудачный клон MUST удалять недоклонированный каталог (полусостояние не остаётся). - Существующий клон MUST обновляться fetch'ем от `$DSH_SUPERPOWERS_REPO` (не от origin клона) с force-рефспеком тега (`+refs/tags/:refs/tags/`) и checkout `FETCH_HEAD`: смена репозитория и переехавший тег MUST подхватываться следующим прогоном. Офлайн клон MAY переиспользоваться, только если HEAD уже ровно на пине, и тогда MUST печататься `WARN`; иначе — `FAIL` с командой клона. - Непустой каталог на месте клона без рабочего `.git` MUST быть `FAIL` (код 1) без изменения каталога. - Копирование MUST быть заменой каталога назначения (`rm -rf` + `cp -R`), а не симлинком: `~/.dsh/skills` не должен зависеть от каталога-источника. #### Scenario: Явный источник без ~/.claude - **WHEN** `HOME` — пустой temp-каталог без `~/.claude`, а `--superpowers-from` указывает на каталог superpowers - **THEN** `--skills` синхронизирует и копирует скиллы в `$DSH_HOME/skills`, печатает источник и завершается кодом 0, не обращаясь в сеть #### Scenario: Переменная окружения вместо флага - **WHEN** задан `DSH_SUPERPOWERS_SOURCE=` и флаг `--superpowers-from` не передан - **THEN** источник берётся из переменной; поведение совпадает с флагом #### Scenario: Флаг важнее переменной - **WHEN** заданы и `--superpowers-from`, и `DSH_SUPERPOWERS_SOURCE` - **THEN** используется каталог из флага #### Scenario: Невалидный явный источник - **WHEN** `--superpowers-from` указывает на каталог без `using-superpowers/SKILL.md` - **THEN** `--skills` печатает `FAIL` с ожидаемой структурой и завершается кодом 1, ничего не копируя из legacy-источников #### Scenario: Голый каталог скиллов - **WHEN** явный источник — каталог `my-skills` с `using-superpowers/SKILL.md`, но без checkout-корня - **THEN** и `--skills`, и `--check` печатают `FAIL: explicit superpowers source must be a checkout root or its skills/ directory` и завершаются кодом 1 #### Scenario: Пиннинг в ~/.dsh - **WHEN** существует клон `$DSH_HOME/superpowers` с `skills/using-superpowers/SKILL.md` и флаг/переменная не заданы - **THEN** источником служит этот клон, а `--check` офлайн печатает `ok: superpowers source ()` и ref из `.sync-meta.json` #### Scenario: Обновление клона от DSH_SUPERPOWERS_REPO - **WHEN** клон уже есть, а `DSH_SUPERPOWERS_REPO` сменился или тег переехал - **THEN** прогон фетчит от `DSH_SUPERPOWERS_REPO` с force-рефспеком, делает checkout `FETCH_HEAD` и устанавливает текст новой версии тега #### Scenario: Офлайн-переиспользование клона - **WHEN** клон существует, HEAD ровно на пине, сеть недоступна - **THEN** `--skills` печатает `WARN: offline; reusing the existing clone` и завершается кодом 0 #### Scenario: Каталог без .git на месте клона - **WHEN** `$DSH_HOME/superpowers` непустой и не является git-клоном - **THEN** `--skills` завершается `FAIL` (код 1), каталог не изменяется ### Requirement: Отсутствие источника не бывает тихим Если источник superpowers недоступен, `--skills` MUST завершаться `FAIL`, кроме случая, когда существует legacy-источник. - Офлайн, без явного источника, без клона на пине и без legacy MUST печататься `FAIL: no superpowers source` с готовой командой клона (`git clone --depth 1 --branch /superpowers`) и код возврата 1. Тихий успех запрещён даже при уже установленных копиях. - Уже установленные копии в `$DSH_HOME/skills` MUST NOT удаляться или портиться при таком `FAIL`. - Если legacy-источник существует — скиллы копируются сырым текстом с `WARN: legacy superpowers source` и кодом 0 (путь живёт до #66). - Локальные скиллы обвязки (`dsh/skills/*`: `role`, `sdd`) MUST ставиться в любом исходе: они не зависят от superpowers. #### Scenario: Первая установка без источника - **WHEN** `HOME` пуст, `$DSH_HOME/skills` не существует, клон получить не удалось, legacy нет - **THEN** `--skills` печатает `FAIL: no superpowers source` и команду клона, код 1, но `$DSH_HOME/skills/role/SKILL.md` и `$DSH_HOME/skills/sdd/SKILL.md` на месте #### Scenario: Повторная установка без источника - **WHEN** `$DSH_HOME/skills/using-superpowers/SKILL.md` уже есть, а клон получить не удалось - **THEN** `--skills` печатает `FAIL: no superpowers source`, код 1, установленный файл не изменён (раньше здесь был `WARN` и код 0) ### Requirement: ~/.claude — только legacy-fallback с предупреждением superpowers-скиллы и роли MUST иметь рабочий путь `~/.dsh`; пути `~/.claude` SHALL оставаться fallback'ом для машин, где `--skills --roles` ещё не перезапускали, и MUST сопровождаться предупреждением. - Установка: legacy-кэш Claude Code и `~/.claude/skills` MAY быть источником только после того, как клон пина получить не удалось; копирование в этом случае — сырое (без sync и verify), с `WARN: legacy superpowers source` и подсказкой перейти на пин/явный источник. - `--check`: legacy-источник — `WARN`, явный/пин — `ok`, отсутствие — `WARN` с подсказкой клона, невалидный явный источник — `FAIL` и код 1 (пользователь задал его сам, подсказка «нет источника» здесь неверна). - Runtime: bootstrap и резолвер ролей сохраняют порядок `~/.dsh` → `~/.claude`, но при выборе legacy-пути один раз за процесс пишут в лог предупреждение. - Удаление fallback'ов — отдельное изменение (#66); в этом change они не удаляются. #### Scenario: Legacy-кэш как источник установки - **WHEN** в `HOME` есть только кэш Claude Code с superpowers, `$DSH_HOME` пуст - **THEN** `--skills` копирует скиллы и печатает `WARN: legacy superpowers source` #### Scenario: Runtime-предупреждение - **WHEN** bootstrap резолвит `using-superpowers` из `~/.claude/...` - **THEN** в лог один раз за процесс попадает предупреждение с предложением выполнить `dsh/install.sh --skills` #### Scenario: ~/.dsh приоритетнее - **WHEN** существуют и `~/.dsh/skills/using-superpowers/SKILL.md`, и legacy-копия - **THEN** используется `~/.dsh`, предупреждения нет