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

18 KiB
Raw Permalink Blame History

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, ...])):

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
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: Падающий тест

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
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: Падающие тесты

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