docs(openspec): change dsh-install-source и план (#49)

This commit is contained in:
Andrey Epifancev 2026-10-24 21:09:11 +04:00
parent de58eff8c0
commit 15cc2e38d7
5 changed files with 658 additions and 0 deletions

View File

@ -0,0 +1,293 @@
# DSH install without ~/.claude — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** `dsh/install.sh --global --skills --roles` ставит рабочий DSH на машине без `~/.claude`, а `~/.claude` остаётся только явно предупреждающим legacy-fallback'ом.
**Architecture:** Источник superpowers разрешается в `install.sh` по приоритету флаг → env → пиннинг `~/.dsh/superpowers` → legacy-кэш CC; отсутствие источника — `FAIL` (если копий нет) или `WARN` (идемпотентный повтор). `install_global` переписывает пути ролей в установленной копии `AGENTS.md`. Bundle сохраняет fallback-порядок, но один раз за процесс сообщает, что взял legacy-путь.
**Tech Stack:** bash (`set -euo pipefail`), `python3` stdlib, Node.js ESM (node:test), Cordis-бандл DSH.
**Spec:** `openspec/changes/dsh-install-source/specs/dsh-install/spec.md` (дизайн — `openspec/changes/dsh-install-source/design.md`).
## Global Constraints
- `global/CLAUDE.md` MUST NOT меняться: его читает Claude Code.
- `install.sh` MUST NOT выполнять `git clone`, `curl` и любые сетевые операции.
- Источник superpowers различается по файлу `using-superpowers/SKILL.md`; принимаются и корень с `skills/`, и сам `skills/`.
- Порядок источников: `--superpowers-from` → `$DSH_SUPERPOWERS_SOURCE` → `$DSH_HOME/superpowers/skills` → кэш CC (`~/.claude/plugins/cache/claude-plugins-official/superpowers/*/skills`, новая версия) → `~/.claude/skills`.
- Legacy-пути остаются рабочими: удаление fallback'ов — не эта задача.
- Тесты DSH идут без DSH runtime: `bash dsh/tests/run.sh`; все тесты на temp `HOME`.
- Маркировка находок: `T<n>.<k>`; решения в этом плане уже с маркерами `D-1…D-6` в design.md.
## Review Focus
- `--skills` без источника при уже установленных `~/.dsh/skills`: копии не должны исчезнуть, код 0, `WARN`.
- Явный источник без `using-superpowers/SKILL.md`: `FAIL`, ни одного скопированного файла из legacy — иначе опечатка маскируется.
- `DSH_SUPERPOWERS_SOURCE` пустой или невалидный: пустой игнорируется, невалидный — `FAIL` (не откат на legacy).
- Путь источника с пробелами: `--superpowers-from "$dir with space"` должен работать (кавычки на всех уровнях).
- Runtime: выбран legacy-путь → одно предупреждение за процесс; выбран `~/.dsh` → предупреждения нет.
- `--global` повторно: бэкап создаётся, в `AGENTS.md` нет `~/.claude/roles`, источник в репозитории не тронут.
---
### Task 1: install.sh — источник superpowers, FAIL/WARN, `--check`
**Files:**
- Modify: `dsh/install.sh` (`superpowers_skills` → новый резолвер; `check`; `install_skills`; разбор аргументов; `usage`)
- Modify: `dsh/tests/install.test.mjs`
- Modify: `dsh/README.md` (quick start: получение источника, флаг, пиннинг)
**Interfaces:**
- Produces:
- флаг `--superpowers-from <dir>` (и `--superpowers-from=<dir>`), env `DSH_SUPERPOWERS_SOURCE`;
- bash-функции `normalize_superpowers_dir <dir>` (печатает `skills`-каталог или rc 1) и `superpowers_source` (печатает `<path>\t<explicit|pinned|legacy>`, rc 1 если источника нет);
- вывод `--skills`: `copied N superpowers skills into <dest> (source: <path>)`; при legacy — строка `WARN: legacy superpowers source <path>; prefer --superpowers-from or ~/.dsh/superpowers`;
- вывод `--check`: `ok: superpowers source (<path>)` / `WARN: legacy superpowers source (<path>); run --skills from a DSH-owned source` / `WARN: no superpowers source; run: git clone --depth 1 https://github.com/obra/superpowers ~/.dsh/superpowers`.
- Consumes: `$DSH_HOME` (по умолчанию `$HOME/.dsh`).
- [ ] **Step 1: Написать падающие тесты в `dsh/tests/install.test.mjs`**
Тесты (каждый — отдельный `test(...)`, temp `HOME`, `spawnSync('bash', [script, ...])`):
```js
test('--skills takes superpowers from --superpowers-from in a clean HOME', ...)
// HOME без ~/.claude; источник: <home>/src/superpowers/skills/using-superpowers/SKILL.md
// + brainstorming/SKILL.md; запуск: --skills --superpowers-from <home>/src/superpowers
// assert: status 0; stdout /source: <...>/src\/superpowers\/skills/;
// <dshHome>/skills/using-superpowers/SKILL.md и role/sdd на месте;
// existsSync(<home>/.claude) === false
test('--superpowers-from accepts the skills directory itself', ...)
// тот же fixture, флаг указывает на <home>/src/superpowers/skills; status 0
test('--superpowers-from wins over DSH_SUPERPOWERS_SOURCE', ...)
// два разных fixture-источника (A: body "a", B: body "b"); env=B, флаг=A;
// в <dshHome>/skills/using-superpowers/SKILL.md лежит "a"
test('--skills fails when no source and nothing installed', ...)
// пустой HOME; --skills; assert status 1; stderr /FAIL: no superpowers source/;
// stdout /git clone --depth 1 https:\/\/github.com\/obra\/superpowers/;
// role и sdd всё равно на месте
test('--skills keeps installed copies when the source is gone', ...)
// HOME с <dshHome>/skills/using-superpowers/SKILL.md (заранее), без источника;
// --skills; status 0; stdout /WARN: no superpowers source/; файл не удалён
test('explicit invalid source fails without touching legacy', ...)
// HOME с legacy-кэшем (как в первом тесте файла) и с --superpowers-from <пустой dir>;
// status 1; stderr /FAIL: .*has no using-superpowers\/SKILL\.md/;
// <dshHome>/skills/brainstorming не создан
test('legacy plugin cache still works and warns', ...)
// HOME с ~/.claude/plugins/cache/.../skills; --skills; status 0;
// stdout /WARN: legacy superpowers source/ и /copied 2 superpowers skills/
test('--check reports the pinned source', ...)
// HOME с <dshHome>/superpowers/skills/using-superpowers/SKILL.md;
// --check; stdout /ok: superpowers source \(.*superpowers\/skills\)/
```
- [ ] **Step 2: Прогнать и увидеть падение**
Run: `node --test dsh/tests/install.test.mjs`
Expected: новые тесты падают (`status 0 !== 1`, нет `source:` в выводе); остальные зелёные.
- [ ] **Step 3: Реализовать в `dsh/install.sh`**
- Разбор аргументов: `--superpowers-from` (следующий аргумент) и `--superpowers-from=*` (через `"${arg#*=}"`); ошибка `unknown option` для голого `--superpowers-from`.
- `normalize_superpowers_dir dir` → `"$dir/skills"` если есть `"$dir/skills/using-superpowers/SKILL.md"`, иначе `"$dir"` если есть `"$dir/using-superpowers/SKILL.md"`, иначе rc 1.
- `superpowers_source` — порядок из Global Constraints; для legacy-кэша сохранить текущий `ls -d … | sort -V | tail -1`; печатать `<path>\t<kind>`; явный невалидный источник — `FAIL` в stderr и rc 2, чтобы `install_skills` не печатал «нет источника».
- `install_skills`: при rc 2 — `return 1`; при rc 1 и наличии `"$DSH_HOME/skills/using-superpowers/SKILL.md"` — `WARN` + `return 0`; иначе `FAIL` с командой клона + `return 1`. Копирование — как сейчас (`rm -rf` + `cp -R`), плюс `(source: <path>)` в итоговой строке.
- `check`: `ok: dsh skills` оставить; добавить ветку источника (`ok`/`WARN legacy`/`WARN none` по kind); legacy-источник не делает `check` падающим.
- Dispatch: `install_skills || status=1; install_local_skills` — локальные скиллы ставятся всегда.
- `usage`: строка `--superpowers-from <dir>` с описанием трёх источников.
- [ ] **Step 4: Прогнать тесты**
Run: `node --test dsh/tests/install.test.mjs`
Expected: PASS всех тестов файла.
- [ ] **Step 5: Обновить quick start в `dsh/README.md`**
Один блок: клон `git clone --depth 1 https://github.com/obra/superpowers ~/.dsh/superpowers` (или `--superpowers-from <dir>`), затем `--all`; в Known limits — «источник superpowers задаётся установщиком; `~/.claude` — legacy».
- [ ] **Step 6: Commit**
```bash
git add dsh/install.sh dsh/tests/install.test.mjs dsh/README.md
git commit -m "feat(dsh): install superpowers from an explicit source (#49)"
```
---
### Task 2: `--global` — пути ролей и формат инструкций
**Files:**
- Modify: `dsh/install.sh` (`install_global`)
- Modify: `dsh/tests/install.test.mjs`
**Interfaces:**
- Produces: `AGENTS.md`, где `~/.claude/roles` заменён на `~/.dsh/roles`, шапка содержит строки:
- `DSH adapter: role files are ~/.dsh/roles (installed by --roles); legacy ~/.claude/roles is a migration fallback.`
- `DSH adapter: superpowers skills are ~/.dsh/skills (installed by --skills from --superpowers-from, DSH_SUPERPOWERS_SOURCE, or ~/.dsh/superpowers); the ~/.claude cache is a legacy fallback.`
- `DSH adapter: a project CLAUDE.md/AGENTS.md is the instruction-file format of this harness; it does not imply a Claude Code runtime.`
- Consumes: `$ROOT/global/CLAUDE.md` (только чтение).
- [ ] **Step 1: Падающий тест**
```js
test('--global installs DSH role paths and explains the instruction format', ...)
// temp HOME; source global/CLAUDE.md до запуска (readFileSync);
// spawn --global; assert AGENTS.md: /~\/\.dsh\/roles\/dev\.md/, !/~\/\.claude\/roles/,
// /instruction-file format of this harness/;
// assert global/CLAUDE.md побайтно равен снимку; assert создан бэкап при повторном --global
```
- [ ] **Step 2: Прогнать, увидеть падение**
Run: `node --test dsh/tests/install.test.mjs`
Expected: FAIL — в `AGENTS.md` пока `~/.claude/roles`.
- [ ] **Step 3: Реализовать**
В `install_global` тело `global/CLAUDE.md` подать через `python3 - "$ROOT/global/CLAUDE.md"` с `text.replace("~/.claude/roles", "~/.dsh/roles")` и `sys.stdout.write`; три adapter-строки выше заменить текущие про роли и bootstrap; добавить строку про формат инструкций.
- [ ] **Step 4: Прогнать тесты**
Run: `node --test dsh/tests/install.test.mjs`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add dsh/install.sh dsh/tests/install.test.mjs
git commit -m "feat(dsh): DSH-owned role paths in installed instructions (#49)"
```
---
### Task 3: Runtime — legacy-предупреждение один раз
**Files:**
- Create: `dsh/bundle/lib/legacy.js`
- Modify: `dsh/bundle/lib/bootstrap.js` (`prepareBootstrap`)
- Modify: `dsh/bundle/index.js` (`apply` после `resolveRolesDir`)
- Modify: `dsh/tests/bootstrap.test.mjs`, `dsh/tests/role.test.mjs`
- Modify: `dsh/README.md` (Known limits), `dsh/PORT.md` (§3, §5, §7, §8)
**Interfaces:**
- Produces:
- `isLegacyClaudePath(path, home = homedir()) -> boolean` — истина для `<home>/.claude/...`;
- `warnLegacyOnce(logger, kind, path) -> void` — пишет `[dsh-harness] %s uses the legacy Claude Code source %s; run dsh/install.sh --%s` не более одного раза на `kind`+`path` за процесс;
- `prepareBootstrap` вызывает `warnLegacyOnce(logger, 'bootstrap', resolved)`, когда путь legacy;
- `apply` вызывает `warnLegacyOnce(ctx.logger, 'roles', rolesDir)` после `resolveRolesDir`.
- Consumes: `resolveRolesDir`, `resolveBootstrapSkill` — без изменения сигнатур.
- [ ] **Step 1: Падающие тесты**
```js
// dsh/tests/bootstrap.test.mjs
test('prepareBootstrap warns once when the legacy ~/.claude skill is used', ...)
// temp home: только ~/.claude/skills/using-superpowers/SKILL.md;
// logger = { warnings: [], warn(msg, ...args){ this.warnings.push([msg, ...args]) } };
// prepareBootstrap({ home, logger, skillPath: '' }) дважды;
// assert warnings.length === 1; assert /legacy Claude Code source/
test('prepareBootstrap is silent for the DSH-owned copy', ...)
// temp home: только ~/.dsh/skills/using-superpowers/SKILL.md; warnings.length === 0
// dsh/tests/role.test.mjs
test('isLegacyClaudePath detects ~/.claude paths only', ...)
// true для join(home,'.claude','roles','dev.md'); false для join(home,'.dsh','roles','dev.md')
// и для join(home,'.claude-other','x')
```
- [ ] **Step 2: Прогнать, увидеть падение**
Run: `node --test dsh/tests/bootstrap.test.mjs dsh/tests/role.test.mjs`
Expected: FAIL — модуля `legacy.js` нет, предупреждений ноль.
- [ ] **Step 3: Реализовать**
`lib/legacy.js`: `import { homedir } from 'node:os'; import { sep } from 'node:path';`
`isLegacyClaudePath` сравнивает через `startsWith(join(home, '.claude') + sep)`; `warnLegacyOnce` держит `const warned = new Set()` и ключ `` `${kind}|${path}` ``.
`prepareBootstrap`: после `resolveBootstrapSkill`, если `isLegacyClaudePath(resolved, home)` — вызвать предупреждение с `kind: 'bootstrap'` (суффикс `--skills`).
`index.js`: импорт `warnLegacyOnce`; в `apply` после `const rolesDir = ...` — `if (isLegacyClaudePath(rolesDir)) warnLegacyOnce(ctx.logger, 'roles', rolesDir)`.
- [ ] **Step 4: Прогнать тесты**
Run: `bash dsh/tests/run.sh`
Expected: PASS всего набора.
- [ ] **Step 5: Документация**
`dsh/README.md`: Known limits — источник superpowers (`--superpowers-from`/env/`~/.dsh/superpowers`), `~/.claude` — legacy с предупреждением; config-комментарий `rolesDir`/`bootstrapSkillPath`. `dsh/PORT.md`: §3 «install-time зависимость» убрать из deferred; §5 — источник и `--superpowers-from`; §7 — риск `~/.claude` переписать как legacy-fallback; §8 — фаза #49 закрыта.
- [ ] **Step 6: Commit**
```bash
git add dsh/bundle/lib/legacy.js dsh/bundle/lib/bootstrap.js dsh/bundle/index.js \
dsh/tests/bootstrap.test.mjs dsh/tests/role.test.mjs dsh/README.md dsh/PORT.md
git commit -m "feat(dsh): warn about legacy ~/.claude sources at runtime (#49)"
```
---
### Task 4: Приёмка, спека, PR
**Files:**
- Modify (по результатам): `openspec/changes/dsh-install-source/design.md` (раздел приёмки), `dsh/PORT.md` (verified live)
**Interfaces:**
- Consumes: всё предыдущее.
- [ ] **Step 1: Полный прогон**
Run:
```bash
bash dsh/tests/run.sh
bash tests/run.sh
openspec validate dsh-install-source --strict
```
Expected: все три — код 0, `dsh/tests` зелёный, `Change 'dsh-install-source' is valid`.
- [ ] **Step 2: Smoke в чистом HOME (без `~/.claude`)**
```bash
TMP=$(mktemp -d); HOME="$TMP" DSH_HOME="$TMP/.dsh" bash dsh/install.sh \
--global --skills --roles --superpowers-from <путь-к-superpowers>
# ожидание: код 0; файлы AGENTS.md, skills/{using-superpowers,role,sdd}/SKILL.md, roles/dev.md
# отсутствие $TMP/.claude; --check печатает ok: superpowers source и ok: roles
```
Bootstrap в этом HOME проверить Node-ом:
`node -e "import('./dsh/bundle/lib/bootstrap.js').then(m=>console.log(m.prepareBootstrap({home: process.env.HOME}).marker))"` — маркер на месте, предупреждений нет.
- [ ] **Step 3: Зафиксировать приёмку**
В `design.md` добавить `### Приёмка: прогон <дата>` с командами и результатами; в `dsh/PORT.md` — что проверено, что осталось владельцу (fresh-session smoke без `~/.claude`).
- [ ] **Step 4: Commit, push, PR**
```bash
git add -A
git commit -m "docs(dsh): acceptance for install without ~/.claude (#49)"
git push -u origin feat/49-dsh-install-source
tea pulls create --head feat/49-dsh-install-source --base main \
--title "DSH: установка без зависимости от ~/.claude (#49)" \
--description "<описание + Rulings + маркеры + what was tested>"
```
- [ ] **Step 5: Итоговый отчёт**
Таблица находок с маркерами и статусами, что проверено, что осталось человеку (merge, fresh-session smoke, `--skills --roles` на реальной машине).
## Acceptance
- Спека `dsh-install` покрыта тестами: источник (5 сценариев), отсутствие источника (2), legacy (2), инструкции (2), чистый HOME (2).
- `bash dsh/tests/run.sh`, `bash tests/run.sh`, `openspec validate --strict` — зелёные.
- Temp `HOME` без `~/.claude` проходит `--global --skills --roles`, bootstrap даёт маркер, `--check` зелёный.
- `global/CLAUDE.md` не изменён; CC-контур (`bash tests/run.sh`) не затронут.

View File

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-24

View File

@ -0,0 +1,148 @@
# Design: установка DSH без зависимости от ~/.claude
Issue: #49. Спека: `openspec/changes/dsh-install-source/specs/dsh-install/spec.md`.
Исходные факты: `dsh/PORT.md` §3/§5/§7, issue #37/#38 (runtime-отвязка уже
сделана), ответ владельца 24.10.2026 («нормально так» — без вендоринга).
## 1. Что уже сделано и чего не хватает
| Слой | Источник в runtime | Зависимость от `~/.claude` |
|---|---|---|
| bootstrap `using-superpowers` | `~/.dsh/skills` → `~/.agents/skills` → кэш CC → `~/.claude/skills` | только fallback |
| роли `/role`, skill `role` | `~/.dsh/roles` → `~/.claude/roles` → repo `roles/` | только fallback |
| установка `--skills` | кэш плагина CC | **жёсткая** |
| установка `--roles` | repo `roles/` | нет |
| установленные инструкции | `global/CLAUDE.md` + adapter-шапка | **текст путей ролей** |
То есть #37 снял зависимость в том, что исполняется, и оставил её в том, что
устанавливается. Первая же команда `--skills` на чистой машине не находит
источник, печатает `WARN` и оставляет bootstrap без скилла — при этом установка
формально успешна.
## 2. Решения
### D-1 (решение владельца). Без вендоринга superpowers
Копию апстрима в репозиторий не кладём. Обоснование: superpowers живёт своей
версией (в кэше уже 6.4.1 и 7.0.0), репозиторий обвязки не должен превращаться
в зеркало; обновление вендорной копии — ручная синхронизация, которая стареет
молча. Цена: install-time источник нужно получить один раз (клон), это лишний
шаг в quick start.
### D-2. Три уровня источника, один формат
Порядок: `--superpowers-from` → `DSH_SUPERPOWERS_SOURCE` →
`$DSH_HOME/superpowers/skills` → legacy-кэш CC → legacy `~/.claude/skills`.
- Флаг и переменная принимают **оба** каталога: корень superpowers (с `skills/`)
и сам `skills/`. Клон `git clone … ~/.dsh/superpowers` даёт первый вид, кэш
CC — второй; пользователь не должен помнить, какой именно.
- `$DSH_HOME/superpowers` — рекомендуемое место пиннинга: один клон на машину,
дальше `--skills` воспроизводим без флага и без `~/.claude`.
- Явно заданный невалидный источник — `FAIL`, а не fallback: иначе опечатка в
пути тихо даёт установку из legacy-кэша, и на чистой машине это снова
«установка прошла, bootstrap пуст».
### D-3. `FAIL` только когда ставить нечего
`--skills` без источника:
- `$DSH_HOME/skills/using-superpowers/SKILL.md` есть → `WARN` + код 0
(идемпотентный повтор, старые копии не трогаем);
- нет → `FAIL` + код 1 + готовая команда клона.
Локальные скиллы (`role`, `sdd`) ставятся в обоих случаях: они из репозитория и
от superpowers не зависят. Из-за этого `--skills` может вернуть 1, уже записав
часть файлов — это сказано в `FAIL`-сообщении.
### D-4. Legacy-пути остаются, но говорят о себе
`~/.claude` поддерживается как fallback: машины, где `--skills --roles` ещё не
перезапускали, не должны сломаться на этом change. Предупреждения:
- `--skills` из legacy-источника → `WARN: legacy superpowers source`;
- `--check` → `WARN` для legacy и для «источника нет»;
- bootstrap/`resolveRolesDir` при выборе `~/.claude` → одно предупреждение за
процесс (module-level `Set`), не на каждый `agent/pre-step`.
Удаление fallback'ов — отдельная задача: сначала все машины должны переехать на
`~/.dsh` (и это видно по `WARN`), потом код можно чистить. Цена: путь
`~/.claude` остаётся в bundle, и code review видит его ещё один цикл.
### D-5. Инструкции переписываются при установке, не в источнике
`global/CLAUDE.md` — общий с Claude Code, его правит только CC-контур. Поэтому
`install_global` пишет `AGENTS.md` в два шага: adapter-шапка (как сейчас, плюс
явная строка про формат инструкций) и тело `global/CLAUDE.md`, прогнанное через
замену `~/.claude/roles` → `~/.dsh/roles`.
Замена точечная и проверяемая тестом: после `--global` в `AGENTS.md` есть
`~/.dsh/roles/dev.md` и нет `~/.claude/roles`; сам `global/CLAUDE.md` не меняется
(тест сравнивает содержимое до/после). Цена: расхождение между источником и
установленной копией теперь intentional, и его надо помнить при правках CC-текста.
### D-6. `--check` не падает без источника
`--check` — диагностика, а не установка: отсутствие superpowers-источника
остаётся `WARN`, код возврата меняют только жёсткие `FAIL` (python3, git, node,
хуки, бандл). Так же уже сделано для openspec (#42, R-4).
## 3. Формулировки, которые попадут в установленный AGENTS.md
- `DSH adapter: role files are ~/.dsh/roles (installed by --roles); legacy ~/.claude/roles is a migration fallback.`
- `DSH adapter: superpowers skills are ~/.dsh/skills (installed by --skills from --superpowers-from, DSH_SUPERPOWERS_SOURCE, or ~/.dsh/superpowers); the ~/.claude cache is a legacy fallback.`
- `DSH adapter: a project CLAUDE.md/AGENTS.md is the instruction-file format of this harness; it does not imply a Claude Code runtime.`
- Тело `global/CLAUDE.md` — с заменой `~/.claude/roles` → `~/.dsh/roles`; строка
про `role-hint` остаётся, adapter-шапка говорит, что хука в DSH нет и роль
выводится из первого сообщения.
## 4. Решения, принятые без обсуждения
- **2026-10-24. Клон для пиннинга — `git clone --depth 1 https://github.com/obra/superpowers ~/.dsh/superpowers`.**
Это апстрим superpowers (public repo). `install.sh` его не запускает: сеть в
установке запрещена (D-1/спека). Цена ошибки: неверный URL — баг
документации, ловится первой же установкой на чистой машине; лечится одной
строкой в README/PORT.
- **2026-10-24. `--superpowers-from` принимает и корень, и `skills/`.**
Обоснование: два реальных источника дают разные формы (клон и кэш CC).
Цена: две ветки разбора и два теста; ошибка разбора видна сразу по `FAIL`.
- **2026-10-24. Предупреждение о legacy в runtime — один раз за процесс.**
`agent/pre-step` вызывается на каждом шаге; предупреждение на каждый шаг
утопило бы лог. Цена: после перезапуска процесса предупреждение повторится —
это ожидаемо; диагностика — `--check`.
- **2026-10-24. `--check` проверяет у пиннинга только наличие `using-superpowers`.**
Полная инвентаризация набора — работа `--skills` (он копирует всё, что есть).
Цена: неполный клон пройдёт `--check` и даст частичный набор скиллов; признак
— отсутствие ожидаемого имени в каталоге скиллов сессии.
- **2026-10-24. `tasks.md` OpenSpec не заполняется.**
Задачи живут в плане superpowers, как требует `CLAUDE.md` репозитория. Цена:
`openspec status` показывает незаполненный `tasks.md`; `archive` это не
блокирует (уже так в `dsh-openspec`).
## 5. Риски
- **`set -euo pipefail` против «частичного успеха».** `install_skills` при
`FAIL` возвращает 1; в `--skills` он вызывается через `|| status=1`, иначе
`install_local_skills` не выполнится. Тест на «первую установку без
источника» проверяет оба файла и код возврата.
- **Legacy-fallback маскирует неправильную настройку.** Лечится `WARN` в
установке, `--check` и runtime-предупреждением; полное удаление — D-4.
- **Замена `~/.claude/roles` → `~/.dsh/roles` в тексте.** Заменяем только этот
префикс, а не `~/.claude` целиком: в тексте есть упоминания хуков/CC, которые
DSH-версии не касаются. Тест проверяет и наличие нового пути, и отсутствие
старого.
- **Проверка «чистый HOME» — юнит-уровень.** Реальный fresh-session smoke в DSH
делает владелец: сессия должна подняться без `~/.claude` и показать
superpowers-скиллы. В PR это отмечено как единственный ручной шаг приёмки.
- **Апстрим superpowers может переехать.** Явный источник проверяется по
`using-superpowers/SKILL.md`; если апстрим переименует каталог, `FAIL` скажет,
что искать.
## 6. Приёмка
- `openspec validate dsh-install-source --strict` — change валиден.
- `bash dsh/tests/run.sh` — зелёный, включая новые тесты источника, `AGENTS.md`
и legacy-предупреждений.
- `bash tests/run.sh` — регрессий в CC-контуре нет.
- Temp `HOME` без `~/.claude`: `--global --skills --roles` с
`--superpowers-from`; `--check`; `prepareBootstrap({home})` — маркер на месте.
- Живой fresh-session smoke без `~/.claude` — за владельцем (в PR отмечено).

View File

@ -0,0 +1,68 @@
# Proposal: установка DSH без зависимости от ~/.claude
Issue: #49 (роадмап порта — #48). Источники:
- `dsh/PORT.md`, разделы 3 (deferred), 5 (install), 7 (risks: «`~/.claude`
dependence»);
- issue #37/#38: runtime-отвязка skills и roles от `~/.claude` — сделана;
- ответ владельца 24.10.2026 на вопрос о источнике superpowers: «нормально так»
(явный путь/`DSH_SUPERPOWERS_SOURCE`, без вендоринга в репозиторий).
## Why
После #37/#38 DSH не зависит от `~/.claude` в runtime: bootstrap читает
`~/.dsh/skills`, роли — `~/.dsh/roles`, `--skills --roles` копируют файлы.
Осталась зависимость **на этапе установки**: `dsh/install.sh --skills` берёт
superpowers только из кэша плагина Claude Code
(`~/.claude/plugins/cache/claude-plugins-official/superpowers/*/skills`).
Практические следствия:
- на машине без Claude Code свежая установка не может поставить superpowers:
`--skills` печатает `WARN` и молча ничего не копирует, bootstrap остаётся
пустым (`superpowers bootstrap skill not found; skipping injection`);
- после отката/удаления Claude Code установка воспроизводится только из уже
лежащих копий в `~/.dsh/skills` — то есть «повторяемая установка» существует
лишь пока никто не удалил `~/.dsh`;
- acceptance порта (#48, DoD) требует пройти полный цикл в чистом проекте, где
CC-харнес не участвует, — сейчас первый шаг цикла упирается в `~/.claude`;
- в установленных инструкциях `~/.dsh/AGENTS.md` остались пути
`~/.claude/roles/<r>.md` (унаследованы из `global/CLAUDE.md`, общего с CC):
adapter и skill `role` их перекрывают, но текст путает агента и человека.
## What Changes
- **Источник superpowers задаётся при установке.** `dsh/install.sh --skills`
разрешает источник в порядке: `--superpowers-from <dir>` → `DSH_SUPERPOWERS_SOURCE`
→ пиннинг `$DSH_HOME/superpowers/skills` → (legacy) кэш Claude Code →
(legacy) `~/.claude/skills`. Явно заданный, но невалидный источник — `FAIL`,
а не тихий откат на legacy.
- **Нет источника — нет тихого WARN.** Если `~/.dsh/skills` уже содержит
`using-superpowers`, повторный `--skills` предупреждает и сохраняет копии
(идемпотентность). Если копий нет — `FAIL` с готовой командой
`git clone … ~/.dsh/superpowers`, код возврата 1. Локальные скиллы обвязки
(`role`, `sdd`) ставятся в обоих случаях.
- **`--check` показывает источник.** `ok: superpowers source (<path>)` для
явного/пиннинга, `WARN: legacy superpowers source` для `~/.claude`,
`WARN` с подсказкой, когда источника нет.
- **Legacy-пути остаются fallback'ом с предупреждением.** Runtime-порядок
`~/.dsh` → `~/.claude` не меняется; bundle один раз за процесс пишет в лог,
что bootstrap/роли взяты из legacy-источника. Удаление fallback'ов — не эта
задача (см. design, D-2).
- **Установленные инструкции DSH не ссылаются на `~/.claude`.** `install_global`
заменяет в копии `~/.claude/roles` → `~/.dsh/roles` и явно говорит, что
проектный `CLAUDE.md`/`AGENTS.md` — формат инструкций DSH, а не runtime
Claude Code. `global/CLAUDE.md` в репозитории не меняется: его читает и CC.
- **Тесты без DSH runtime.** Чистый temp `HOME` без `~/.claude`: установка с
`--superpowers-from`, bootstrap с этим HOME, `FAIL`/`WARN` при отсутствии
источника, legacy-предупреждения, содержимое `AGENTS.md`.
- **Документация.** `dsh/README.md` (quick start с получением источника,
known limits), `dsh/PORT.md` (§3, §5, §7, §8).
## Non-goals
- Вендоринг superpowers в репозиторий обвязки: апстрим живёт своей версией,
копия в репозитории стареет молча (design, D-1).
- `git clone` внутри `install.sh`: установка не должна требовать сети.
- Удаление legacy-fallback'ов из `bootstrap.js`/`index.js`: обратная
совместимость машин, где `--skills --roles` ещё не перезапускали (D-2).
- Проектный слой ролей, briefing, harness-check, harness-doctor — свои фазы #48.

View File

@ -0,0 +1,147 @@
# Spec Delta: dsh-install
## Purpose
Слой установки DSH-обвязки: откуда берутся superpowers-скиллы и роли, что
делает `dsh/install.sh --skills/--roles`, какие пути попадают в установленные
инструкции. В Claude Code источником служит кэш плагина в `~/.claude`; в DSH
источник задаётся установщиком явно (аргумент, переменная или пиннинг в
`~/.dsh`), а `~/.claude` остаётся только legacy-fallback'ом с предупреждением.
Контракт нужен, чтобы свежая установка на машине без Claude Code проходила
полный цикл #48 и чтобы путь `~/.claude` не возвращался в runtime незаметно.
## ADDED Requirements
### Requirement: Источник superpowers задаётся при установке
`dsh/install.sh --skills` SHALL разрешать источник superpowers-скиллов в
порядке приоритета и MUST NOT исполнять `git clone` и любые сетевые операции.
- Порядок: `--superpowers-from <dir>` → `$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 запрещён.
- Копирование 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=<dir>` и флаг `--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: Пиннинг в ~/.dsh
- **WHEN** существует `$DSH_HOME/superpowers/skills/using-superpowers/SKILL.md`
и флаг/переменная не заданы
- **THEN** источником служит этот каталог, `--check` печатает
`ok: superpowers source (<path>)`
### Requirement: Отсутствие источника не бывает тихим
Если источник superpowers не найден, `--skills` MUST различать первичную и
повторную установку и MUST NOT завершаться успехом молча.
- Если `$DSH_HOME/skills/using-superpowers/SKILL.md` уже существует —
`WARN` «источник не найден, копии сохранены» и код 0 (идемпотентный повтор).
- Если копий нет — `FAIL` с готовой командой получения источника
(`git clone --depth 1 https://github.com/obra/superpowers ~/.dsh/superpowers`)
и код возврата 1.
- Локальные скиллы обвязки (`dsh/skills/*`: `role`, `sdd`) MUST ставиться в
обоих случаях: они не зависят от superpowers.
#### Scenario: Первая установка без источника
- **WHEN** `HOME` пуст, `$DSH_HOME/skills` не существует, источника нет
- **THEN** `--skills` печатает `FAIL`, код 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, установленные скиллы не удалены
### 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` с подсказкой.
- Runtime: bootstrap и резолвер ролей сохраняют порядок `~/.dsh` → `~/.claude`,
но при выборе legacy-пути один раз за процесс пишут в лог предупреждение.
- Удаление fallback'ов — отдельное изменение; в этом 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`, предупреждения нет
### Requirement: Установленные инструкции DSH не ссылаются на ~/.claude
`dsh/install.sh --global` MUST писать `$DSH_HOME/AGENTS.md` так, чтобы пути
ролей указывали на DSH-слой, а формат инструкций не выдавался за runtime
Claude Code.
- Строки `~/.claude/roles` в установленной копии MUST заменяться на
`~/.dsh/roles`; `global/CLAUDE.md` в репозитории MUST NOT меняться (его
читает Claude Code).
- Шапка установленного файла MUST говорить, что проектный `CLAUDE.md` /
`AGENTS.md` — это формат файлов инструкций DSH, а не запущенный Claude Code,
и что роли лежат в `~/.dsh/roles` (`--roles`), а superpowers-скиллы — в
`~/.dsh/skills` (`--skills`).
- Повторный `--global` обязан делать бэкап прежнего `AGENTS.md` (как сейчас).
#### Scenario: Пути ролей после установки
- **WHEN** выполнен `--global` во временный `DSH_HOME`
- **THEN** в `AGENTS.md` есть `~/.dsh/roles/dev.md` и нет `~/.claude/roles`
#### Scenario: global/CLAUDE.md не тронут
- **WHEN** выполнен `--global`
- **THEN** `global/CLAUDE.md` в репозитории побайтно не изменился
### Requirement: Чистый HOME без ~/.claude проходит установку
Установка, bootstrap и skill `role` MUST работать на машине, где Claude Code
никогда не устанавливался.
- `--global --skills --roles` с явным источником superpowers в пустом `HOME`
MUST создавать `$DSH_HOME/{AGENTS.md,skills/using-superpowers/SKILL.md,skills/role/SKILL.md,skills/sdd/SKILL.md,roles/dev.md}`.
- bootstrap MUST резолвить скилл из `$DSH_HOME/skills` при `home` этого HOME и
MUST NOT обращаться к `~/.claude`.
- Резолвер ролей MUST находить `$DSH_HOME/roles` при `home` этого HOME.
#### Scenario: Установка в чистый HOME
- **WHEN** `HOME` — пустой temp-каталог, `DSH_HOME=$HOME/.dsh`,
`--superpowers-from` указывает на каталог superpowers
- **THEN** `--global --skills --roles` завершается кодом 0, все перечисленные
файлы на месте, `--check` зелёный по skills и roles
#### Scenario: Bootstrap в чистом HOME
- **WHEN** `prepareBootstrap({ home: <temp HOME> })` вызывается после установки
- **THEN** выбранный путь — `$HOME/.dsh/skills/using-superpowers/SKILL.md`,
текст содержит маркер `<!-- DSH-HARNESS-BOOTSTRAP:... -->`, предупреждений о
legacy нет