diff --git a/docs/superpowers/plans/2026-10-24-dsh-install-source.md b/docs/superpowers/plans/2026-10-24-dsh-install-source.md new file mode 100644 index 0000000..b6af05b --- /dev/null +++ b/docs/superpowers/plans/2026-10-24-dsh-install-source.md @@ -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.`; решения в этом плане уже с маркерами `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 ` (и `--superpowers-from=`), env `DSH_SUPERPOWERS_SOURCE`; + - bash-функции `normalize_superpowers_dir ` (печатает `skills`-каталог или rc 1) и `superpowers_source` (печатает `\t`, rc 1 если источника нет); + - вывод `--skills`: `copied N superpowers skills into (source: )`; при legacy — строка `WARN: legacy superpowers source ; prefer --superpowers-from or ~/.dsh/superpowers`; + - вывод `--check`: `ok: superpowers source ()` / `WARN: legacy superpowers source (); 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; источник: /src/superpowers/skills/using-superpowers/SKILL.md +// + brainstorming/SKILL.md; запуск: --skills --superpowers-from /src/superpowers +// assert: status 0; stdout /source: <...>/src\/superpowers\/skills/; +// /skills/using-superpowers/SKILL.md и role/sdd на месте; +// existsSync(/.claude) === false + +test('--superpowers-from accepts the skills directory itself', ...) +// тот же fixture, флаг указывает на /src/superpowers/skills; status 0 + +test('--superpowers-from wins over DSH_SUPERPOWERS_SOURCE', ...) +// два разных fixture-источника (A: body "a", B: body "b"); env=B, флаг=A; +// в /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 с /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/; +// /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 с /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`; печатать `\t`; явный невалидный источник — `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: )` в итоговой строке. +- `check`: `ok: dsh skills` оставить; добавить ветку источника (`ok`/`WARN legacy`/`WARN none` по kind); legacy-источник не делает `check` падающим. +- Dispatch: `install_skills || status=1; install_local_skills` — локальные скиллы ставятся всегда. +- `usage`: строка `--superpowers-from ` с описанием трёх источников. + +- [ ] **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 `), затем `--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` — истина для `/.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`) не затронут. diff --git a/openspec/changes/dsh-install-source/.openspec.yaml b/openspec/changes/dsh-install-source/.openspec.yaml new file mode 100644 index 0000000..1f5d280 --- /dev/null +++ b/openspec/changes/dsh-install-source/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-24 diff --git a/openspec/changes/dsh-install-source/design.md b/openspec/changes/dsh-install-source/design.md new file mode 100644 index 0000000..7c579a0 --- /dev/null +++ b/openspec/changes/dsh-install-source/design.md @@ -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 отмечено). diff --git a/openspec/changes/dsh-install-source/proposal.md b/openspec/changes/dsh-install-source/proposal.md new file mode 100644 index 0000000..8229787 --- /dev/null +++ b/openspec/changes/dsh-install-source/proposal.md @@ -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/.md` (унаследованы из `global/CLAUDE.md`, общего с CC): + adapter и skill `role` их перекрывают, но текст путает агента и человека. + +## What Changes + +- **Источник superpowers задаётся при установке.** `dsh/install.sh --skills` + разрешает источник в порядке: `--superpowers-from ` → `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 ()` для + явного/пиннинга, `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. diff --git a/openspec/changes/dsh-install-source/specs/dsh-install/spec.md b/openspec/changes/dsh-install-source/specs/dsh-install/spec.md new file mode 100644 index 0000000..3ebc627 --- /dev/null +++ b/openspec/changes/dsh-install-source/specs/dsh-install/spec.md @@ -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 ` → `$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=` и флаг `--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 ()` + +### 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: })` вызывается после установки +- **THEN** выбранный путь — `$HOME/.dsh/skills/using-superpowers/SKILL.md`, + текст содержит маркер ``, предупреждений о + legacy нет