claude-harness/docs/superpowers/plans/2026-10-24-dsh-install-source.md

294 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`) не затронут.