From 8900e38502a1d755190ee8da656a559ee6f43d42 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 25 Oct 2026 01:13:10 +0400 Subject: [PATCH] =?UTF-8?q?chore(openspec):=20=D0=B0=D1=80=D1=85=D0=B8?= =?UTF-8?q?=D0=B2=20change=20dsh-port-v2=20(#69)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2026-10-25-dsh-port-v2}/.openspec.yaml | 0 .../2026-10-25-dsh-port-v2}/design.md | 0 .../2026-10-25-dsh-port-v2}/proposal.md | 0 .../specs/dsh-install/spec.md | 0 openspec/specs/dsh-install/spec.md | 208 +++++++++++++++--- 5 files changed, 173 insertions(+), 35 deletions(-) rename openspec/changes/{dsh-port-v2 => archive/2026-10-25-dsh-port-v2}/.openspec.yaml (100%) rename openspec/changes/{dsh-port-v2 => archive/2026-10-25-dsh-port-v2}/design.md (100%) rename openspec/changes/{dsh-port-v2 => archive/2026-10-25-dsh-port-v2}/proposal.md (100%) rename openspec/changes/{dsh-port-v2 => archive/2026-10-25-dsh-port-v2}/specs/dsh-install/spec.md (100%) diff --git a/openspec/changes/dsh-port-v2/.openspec.yaml b/openspec/changes/archive/2026-10-25-dsh-port-v2/.openspec.yaml similarity index 100% rename from openspec/changes/dsh-port-v2/.openspec.yaml rename to openspec/changes/archive/2026-10-25-dsh-port-v2/.openspec.yaml diff --git a/openspec/changes/dsh-port-v2/design.md b/openspec/changes/archive/2026-10-25-dsh-port-v2/design.md similarity index 100% rename from openspec/changes/dsh-port-v2/design.md rename to openspec/changes/archive/2026-10-25-dsh-port-v2/design.md diff --git a/openspec/changes/dsh-port-v2/proposal.md b/openspec/changes/archive/2026-10-25-dsh-port-v2/proposal.md similarity index 100% rename from openspec/changes/dsh-port-v2/proposal.md rename to openspec/changes/archive/2026-10-25-dsh-port-v2/proposal.md diff --git a/openspec/changes/dsh-port-v2/specs/dsh-install/spec.md b/openspec/changes/archive/2026-10-25-dsh-port-v2/specs/dsh-install/spec.md similarity index 100% rename from openspec/changes/dsh-port-v2/specs/dsh-install/spec.md rename to openspec/changes/archive/2026-10-25-dsh-port-v2/specs/dsh-install/spec.md diff --git a/openspec/specs/dsh-install/spec.md b/openspec/specs/dsh-install/spec.md index d27e6b5..52982de 100644 --- a/openspec/specs/dsh-install/spec.md +++ b/openspec/specs/dsh-install/spec.md @@ -13,25 +13,40 @@ ### Requirement: Источник superpowers задаётся при установке `dsh/install.sh --skills` SHALL разрешать источник superpowers-скиллов в -порядке приоритета и MUST NOT исполнять `git clone` и любые сетевые операции. +порядке приоритета. Сеть нужна только ветке пина; явный источник +синхронизируется локально. - Порядок: `--superpowers-from ` → `$DSH_SUPERPOWERS_SOURCE` → - `$DSH_HOME/superpowers/skills` (пиннинг; допускается и корень - `$DSH_HOME/superpowers` с подкаталогом `skills/`) → legacy-кэш Claude Code → - legacy `~/.claude/skills`. -- Для явного источника (`--superpowers-from`, `$DSH_SUPERPOWERS_SOURCE`) - допускаются обе формы: корень superpowers (есть `skills/using-superpowers/SKILL.md`) - или сам каталог `skills`. -- Явно заданный, но невалидный источник MUST приводить к `FAIL` и коду - возврата 1: тихий откат на legacy запрещён. + клон `$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 +- **THEN** `--skills` синхронизирует и копирует скиллы в `$DSH_HOME/skills`, + печатает источник и завершается кодом 0, не обращаясь в сеть #### Scenario: Переменная окружения вместо флага - **WHEN** задан `DSH_SUPERPOWERS_SOURCE=` и флаг `--superpowers-from` не передан @@ -47,51 +62,81 @@ - **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 ()` +- **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 различать первичную и -повторную установку и MUST NOT завершаться успехом молча. +Если источник superpowers недоступен, `--skills` MUST завершаться `FAIL`, кроме +случая, когда существует legacy-источник. -- Если `$DSH_HOME/skills/using-superpowers/SKILL.md` уже существует — - `WARN` «источник не найден, копии сохранены» и код 0 (идемпотентный повтор). -- Если копий нет — `FAIL` с готовой командой получения источника - (`git clone --depth 1 https://github.com/obra/superpowers ~/.dsh/superpowers`) - и код возврата 1. +- Офлайн, без явного источника, без клона на пине и без 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. + любом исходе: они не зависят от superpowers. #### Scenario: Первая установка без источника -- **WHEN** `HOME` пуст, `$DSH_HOME/skills` не существует, источника нет -- **THEN** `--skills` печатает `FAIL`, код 1, но `$DSH_HOME/skills/role/SKILL.md` - и `$DSH_HOME/skills/sdd/SKILL.md` на месте +- **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` печатает `WARN`, код 0, установленные скиллы не удалены +- **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` допустимы как - источник, но `--skills` печатает `WARN: legacy superpowers source` с - подсказкой перейти на пиннинг/флаг. -- `--check`: legacy-источник — `WARN`, явный/пиннинг — `ok`, отсутствие — - `WARN` с подсказкой, невалидный явный источник — `FAIL` и код 1 +- Установка: 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'ов — отдельное изменение; в этом change они не удаляются. +- Удаление fallback'ов — отдельное изменение (#66); в этом change они не + удаляются. #### Scenario: Legacy-кэш как источник установки - **WHEN** в `HOME` есть только кэш Claude Code с superpowers, `$DSH_HOME` пуст -- **THEN** `--skills` копирует скиллы и печатает `WARN: legacy superpowers source` +- **THEN** `--skills` копирует скиллы и печатает `WARN: legacy superpowers + source` #### Scenario: Runtime-предупреждение - **WHEN** bootstrap резолвит `using-superpowers` из `~/.claude/...` @@ -147,3 +192,96 @@ Claude Code. - **THEN** выбранный путь — `$HOME/.dsh/skills/using-superpowers/SKILL.md`, текст содержит маркер ``, предупреждений о legacy нет + +### 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` -- 2.47.2