claude-harness/docs/superpowers/plans/2026-10-24-dsh-port-v2.md

288 lines
22 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 port v2 (вариант C) Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use the `sdd` skill (DSH) to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Перевести доставку superpowers-скиллов на вариант C: вендор sync-машинерии LayneChai, пин на тег v7.0.1, install.sh гоняет sync→verify→copy, бандл получает аддитивную самопроверку контракта.
**Architecture:** `dsh/sync/` — два вендорённых Node-скрипта (только stdlib): `sync-from-upstream.mjs` (ассертные transform-ы + staging-swap + override нашего `dsh-tools.md`) и `verify-skills.mjs` (frontmatter/префиксы/ссылки). `dsh/install.sh` оркестрирует: resolve источника (explicit/env → клон по пину → legacy сырой+WARN) → sync → verify → копирование в `$DSH_HOME/skills`. Бандл получает `lib/selfcheck.js` (чистые чекеры контракта ctx) и команду `dsh-harness-check`; существующее поведение бандла не меняется.
**Tech Stack:** bash (`set -euo pipefail`), Node builtins (без `@deepseek-ai/*`-импортов), git, python3 для существующих хуков. DSH pinned 0.2.1-rc.3.
**Spec:** `openspec/changes/dsh-port-v2/` — [proposal.md](../openspec/changes/dsh-port-v2/proposal.md) + [design.md](../openspec/changes/dsh-port-v2/design.md) (D-1…D-10; план спорит от них).
**Models (D-9):** Tasks 1, 2, 5 — `deepseek-official/deepseek-flash` (механика, готовый код); Tasks 3, 4 — `deepseek-official/deepseek-v4-pro` (интеграция); ревью каждой задачи и финальное ревью ветки — `deepseek-official/deepseek-v4-pro`.
## Global Constraints
- Runtime-зависимости sync и бандла — только Node builtins; запрещён импорт `@deepseek-ai/*` (ловит `dsh/tests/package.test.mjs`).
- `dsh/install.sh` идемпотентен; `--check` никогда не ходит в сеть; невалидный явный источник — FAIL (exit 1), не молчаливый фолбэк.
- Пин: `DSH_SUPERPOWERS_REF=v7.0.1`, репозиторий `DSH_SUPERPOWERS_REPO` (default `https://github.com/obra/superpowers`).
- Legacy-фолбэки `~/.claude/*` остаются (сырой копией + WARN) до #66 — в этой работе не удаляются.
- Тесты — `node --test` через `bash dsh/tests/run.sh`; весь набор зелёный, плюс `bash tests/run.sh` (CC-сторона) не ломается.
- Маркировка: находки с маркером и статусом; коммиты со ссылкой на issue (`(#69)`).
- Язык субагентов (решение владельца 24.10.2026): брифы и отчёты implementer'ов — на английском; маркеры, статусы, термины обвязки и слэш-команды не переводятся; финальные отчёты контроллера владельцу — по-русски.
- Синхронизация апстрима — только по тегу; HEAD не используется нигде.
## Review Focus
| # | Вход/условие | Ожидаемое поведение |
|---|---|---|
| RF1 | Sync против checkout не v7.0.1 (якорь изменился/исчез) | FAIL loudly (exit 1), staging не подменяет назначение |
| RF2 | `--skills` без сети и без клона/источника | FAIL с готовой подсказкой, не «пустой успех» |
| RF3 | Повторный `--skills` (идемпотентность) | Повторный прогон не ломает установленное; нет мусора staging |
| RF4 | Бандл активируется без `ctx.tools.guard` (сломан контракт DSH) | Самопроверка помечает guards not-ok, остальное живо; бандл не падает целиком |
| RF5 | Недоклонированный каталог на месте клона | FAIL без оставленного полусостояния |
---
### Task 1: Вендор sync-from-upstream
**Files:**
- Create: `dsh/sync/sync-from-upstream.mjs`
- Create: `dsh/tests/fixtures/superpowers-min/` (минимальное дерево: `using-superpowers/SKILL.md` с якорями transform-ов, `brainstorming/SKILL.md` с `superpowers:`-префиксами и висячими harness-референсами, `references/claude-code-tools.md`, `.claude-plugin/plugin.json` с `"version": "7.0.1"`)
- Create: `dsh/tests/sync.test.mjs`
**Interfaces:**
- Consumes: `dsh/bundle/dsh-tools.md` (единственный источник маппинга).
- Produces: CLI `node dsh/sync/sync-from-upstream.mjs <upstream-checkout> [--out <dir>] [--ref <tag>]`; exit 0 — печать `synced N files…`, exit 1 — FAIL с именем упавшего transform/якоря; `--out` по умолчанию `<checkout>/.sync-out`; staging-каталог `<out>.tmp-<pid>`; `--out` подменяется только после всех ассертов; в `--out/.sync-meta.json` пишется `{"ref": "<tag|unknown>", "version": "<plugin.json version>"}`.
- [ ] **Step 1: Напиши падающие тесты** в `dsh/tests/sync.test.mjs` (helpers: `tempDir`, `write`, `spawnSync` из `./helpers.mjs`; фикстура — копия `dsh/tests/fixtures/superpowers-min/`):
```js
test('sync happy path: transforms, namespace strip, override, meta', ...)
// assert: exit 0; out/using-superpowers/SKILL.md содержит 'references/dsh-tools.md'
// и НЕ содержит 'superpowers:'; out/using-superpowers/references/dsh-tools.md
// == dsh/bundle/dsh-tools.md; out/.sync-meta.json.ref === 'v7.0.1'
// (запуск с --ref v7.0.1); references/claude-code-tools.md отсутствует (SKIP).
test('sync fails loud on broken anchor', ...)
// испортить якорь в копии фикстуры (replace уникального фрагмента);
// assert: exit 1, stderr содержит имя transform-а; существующий --out не тронут.
test('sync fails on changed skill set', ...)
// удалить один skill-каталог из копии; assert: exit 1, stderr содержит added/removed.
test('sync fails when no skills/ dir', ...) // assert: exit 1.
```
- [ ] **Step 2: Прогони — ожидай FAIL** (скрипта нет)
Run: `node --test dsh/tests/sync.test.mjs`
Expected: FAIL (module not found / exit не 1).
- [ ] **Step 3: Реализуй `dsh/sync/sync-from-upstream.mjs`** — перенос из LayneChai/superpowers-dsh `scripts/sync-from-upstream.mjs` с адаптациями:
- без `package.json`-привязки: `upstreamRoot = resolve(argv[2])`, `--out` из argv;
- `SKIP`-список чужих harness-референсов — как у оригинала (7 файлов: copilot-tools.md в v7.0.1 нет; T1.3);
- `TRANSFORMS` — как у оригинала, кроме: их `using-superpowers`-rewrite сохраняется как есть (указывает на `references/dsh-tools.md`);
- override: после копирования дерева — скопировать `dsh/bundle/dsh-tools.md` → `using-superpowers/references/dsh-tools.md` (путь до бандла — `resolve(import.meta.url, '../../bundle/dsh-tools.md')`);
- namespace-strip `superpowers:` → `''` и сверка набора скиллов (15 имён) — как у оригинала;
- staging-swap и «unfired transforms» — как у оригинала; `--ref`/`plugin.json`-версия — в `.sync-meta.json`.
- Только Node builtins.
- [ ] **Step 4: Прогони — ожидай PASS**
Run: `node --test dsh/tests/sync.test.mjs`
Expected: все 4 теста PASS.
- [ ] **Step 5: Весь набор + коммит**
Run: `bash dsh/tests/run.sh`
Expected: весь набор зелёный.
```bash
git add dsh/sync/sync-from-upstream.mjs dsh/tests/sync.test.mjs dsh/tests/fixtures/superpowers-min
git commit -m "feat(dsh): вендор sync-from-upstream — ассертные transform-ы, staging-swap, override dsh-tools.md (#69)"
```
### Task 2: Вендор verify-skills
**Files:**
- Create: `dsh/sync/verify-skills.mjs`
- Create: `dsh/tests/verify.test.mjs`
**Interfaces:**
- Consumes: synced-каталог из Task 1 (формат `<skill>/SKILL.md`).
- Produces: CLI `node dsh/sync/verify-skills.mjs <skills-dir>`; exit 0 — `ok: <name>` на каждый скилл; exit 1 — первая ошибка с `<skill>/SKILL.md: <reason>`; проверки: YAML frontmatter с `name`+`description`, без остаточного `superpowers:`-префикса в теле, все относительные ссылки вида `../<skill>/…`/`references/…` из тела резолвятся в `<skills-dir>`.
- [ ] **Step 1: Напиши падающие тесты** в `dsh/tests/verify.test.mjs` (фикстура — результат sync из Task 1, плюс испорченные копии):
```js
test('verify ok on synced tree', ...) // exit 0, stdout содержит 'ok: using-superpowers'.
test('verify fails on missing description', ...) // exit 1, stderr содержит путь и reason.
test('verify fails on leftover superpowers: prefix', ...)
test('verify fails on dangling relative reference', ...) // ссылка ../brainstorming/x.md без файла.
```
- [ ] **Step 2: Прогони — ожидай FAIL**
Run: `node --test dsh/tests/verify.test.mjs`
Expected: FAIL.
- [ ] **Step 3: Реализуй `dsh/sync/verify-skills.mjs`** — перенос идеи LayneChai `verify-skills.mjs`, без provider-части: только проверки выше, чистый Node. Парсер frontmatter — как в их `parseFrontmatter` (скалярные поля).
- [ ] **Step 4: Прогони — ожидай PASS**
Run: `node --test dsh/tests/verify.test.mjs`
Expected: все тесты PASS.
- [ ] **Step 5: Весь набор + коммит**
Run: `bash dsh/tests/run.sh`
Expected: зелёный.
```bash
git add dsh/sync/verify-skills.mjs dsh/tests/verify.test.mjs
git commit -m "feat(dsh): вендор verify-skills — frontmatter, префиксы, висячие ссылки (#69)"
```
### Task 3: install.sh — пин, fetch, sync-оркестрация
**Files:**
- Modify: `dsh/install.sh`
- Modify: `dsh/tests/install.test.mjs`
- Modify: `dsh/README.md`, `dsh/PORT.md` (разделы установки/источника)
**Interfaces:**
- Consumes: `dsh/sync/sync-from-upstream.mjs` (Task 1), `dsh/sync/verify-skills.mjs` (Task 2); env `DSH_SUPERPOWERS_REF` (default `v7.0.1`), `DSH_SUPERPOWERS_REPO` (default `https://github.com/obra/superpowers`), `DSH_SUPERPOWERS_SOURCE`, `--superpowers-from`.
- Produces: поток `--skills`: resolve (explicit/env → клон `$DSH_HOME/superpowers` на пине → legacy сырой+WARN) → `node dsh/sync/sync-from-upstream.mjs <source> --out "$DSH_HOME/superpowers/skills.synced" --ref "$REF"` → `node dsh/sync/verify-skills.mjs <out>` → копирование `<out>/*` в `$DSH_HOME/skills` (как текущий цикл) → local skills (role, sdd) без изменений. `--check`: строка `ok: superpowers source (<dir>, git <sha>, ref <ref>)`, сети нет.
- [ ] **Step 1: Напиши падающие тесты** в `dsh/tests/install.test.mjs` (helpers уже есть; локальный git-репозиторий: `git init` + коммит фикстуры `dsh/tests/fixtures/superpowers-min` + `git tag v7.0.1`; env `DSH_SUPERPOWERS_REPO=file://<repo>`):
```js
test('--skills clones pinned ref, syncs, verifies and installs adapted skills', ...)
// fresh HOME: exit 0; $DSH_HOME/skills/using-superpowers/SKILL.md содержит 'references/dsh-tools.md'
// и НЕ содержит 'superpowers:'; references/dsh-tools.md установлен; stdout содержит 'synced' и 'copied N superpowers skills'.
test('--skills offline without source fails with hint', ...)
// fresh HOME, DSH_SUPERPOWERS_REPO=file:///nonexistent, без explicit: exit 1,
// stderr содержит 'git clone' hint; $DSH_HOME/skills не создан с пустыми скиллами.
test('--skills removes failed empty clone dir (RF5)', ...)
// fresh HOME, DSH_SUPERPOWERS_REPO=file:///nonexistent, предварительно mkdir $DSH_HOME/superpowers (пустой):
// exit 1; existsSync($DSH_HOME/superpowers) === false (полусостояния не остаётся).
test('--skills re-run updates from moved tag', ...)
// второй прогон после 'git commit --amend' + 'git tag -f v7.0.1': установленный текст обновился (маркер из amend).
test('--skills leaves no staging garbage', ...)
// после прогона: в $DSH_HOME/superpowers нет *.tmp-*; existsSync skills.synced — допустимо, но .sync-meta.json.ref==='v7.0.1'.
test('--check is offline and reports ref', ...)
// env DSH_SUPERPOWERS_REPO=file:///nonexistent: exit 0; stdout содержит 'ref v7.0.1' или 'no superpowers source'.
test('.claude/code-paths содержит dsh/', ...)
// readFileSync('../../.claude/code-paths') содержит строку 'dsh/' (D-5).
```
- [ ] **Step 2: Прогони — ожидай FAIL**
Run: `node --test dsh/tests/install.test.mjs`
Expected: новые тесты FAIL (clone-логики нет), старые — PASS.
- [ ] **Step 3: Реализуй в `dsh/install.sh`** — только `superpowers_source()`/`check()`/`install_skills()`:
- новая ветка resolve: explicit (`--superpowers-from`/`DSH_SUPERPOWERS_SOURCE`) → **sync от этого каталога без сети**; иначе `ensure_pinned_clone`: если `$DSH_HOME/superpowers` уже клон на `$REF` — использовать (WARN при отсутствии сети); иначе `git clone --depth 1 --branch "$REF" "$REPO" "$DSH_HOME/superpowers"`; при неудаче и существующем каталоге без `.git` — FAIL с подсказкой, каталог не трогать; при неудаче и пустом месте — удалить недоклон и FAIL (RF5);
- `install_skills()`: sync → verify → копирование из `--out`; legacy-источник — прежний сырой путь с WARN (без sync);
- `--check`: офлайн, печатает источник/ref/sha из `.sync-meta.json` или git;
- `usage()` — новые переменные.
- Bash с python3-вставками в прежнем стиле.
- [ ] **Step 4: Прогони — ожидай PASS**
Run: `node --test dsh/tests/install.test.mjs`
Expected: все тесты (старые и новые) PASS.
- [ ] **Step 5: Доки + весь набор + коммит** — обнови `dsh/README.md` (Quick start: `--skills` сам тянет пин, переменные, офлайн-кейсы) и `dsh/PORT.md` (§3 убрать «Vendored superpowers»-абзац как устаревший, §6 команды установки).
Run: `bash dsh/tests/run.sh && bash tests/run.sh`
Expected: оба набора зелёные.
```bash
git add dsh/install.sh dsh/tests/install.test.mjs dsh/README.md dsh/PORT.md
git commit -m "feat(dsh): --skills гоняет sync/verify по пину v7.0.1, офлайн-кейсы (#69)"
```
### Task 4: Самопроверка бандла + команда dsh-harness-check
**Files:**
- Create: `dsh/bundle/lib/selfcheck.js`
- Modify: `dsh/bundle/index.js` (аддитивно)
- Create: `dsh/bundle/command-check.js`
- Create: `dsh/tests/selfcheck.test.mjs`
- Modify: `dsh/README.md` (Known limits → самопроверка), `dsh/PORT.md` (§7 риск → чем закрыт)
**Interfaces:**
- Consumes: `ctx`-форма из `dsh/bundle/index.js` (`tools.guard`, `on`, `inject`, `commands`, `workingDirectory.get`).
- Produces: `collectSelfCheck(ctxLike) -> [{ feature: 'guards'|'bootstrap'|'commands'|'workingDirectory', ok: boolean, detail: string }]` — чистая функция, без вызовов ctx; `registerCheckCommand({ commands, collect, logger })` — регистрирует команду `dsh-harness-check`, которая собирает чек и возвращает текст-отчёт; в `index.js`: конфиг-ключи `selfCheck: true` (default) и `checkCommand: true` (default); после wiring — прогон `collectSelfCheck`, при not-ok критической службы (guards) — `ctx.logger.error` + пропуск только этой фичи (существующие фичи при их службах на месте работают как раньше).
- [ ] **Step 1: Напиши падающие тесты** в `dsh/tests/selfcheck.test.mjs`:
```js
test('collectSelfCheck ok on full ctx-like', ...) // все features ok:true.
test('collectSelfCheck flags missing tools.guard', ...) // guards ok:false, detail содержит 'tools.guard'; остальные ok:true.
test('collectSelfCheck flags missing on()', ...) // bootstrap ok:false.
test('registerCheckCommand builds report text', ...)
// fake commands.register(name, handler) — вызвать handler с fake collect, вернуть текст:
// содержит 'guards: not-ok' и имя фичи; ok-ветка содержит 'ok'.
```
- [ ] **Step 2: Прогони — ожидай FAIL**
Run: `node --test dsh/tests/selfcheck.test.mjs`
Expected: FAIL.
- [ ] **Step 3: Реализуй** `lib/selfcheck.js` и `command-check.js` по интерфейсам; в `index.js` добавь в `DEFAULTS` ключи и вызовы после существующих блоков (порядок: guards → bootstrap → roleCommand → selfcheck/command), чтобы при отсутствии `ctx.tools.guard` guard-блок пропускался с `ctx.logger.error('[dsh-harness] self-check: guards not-ok: …')`, а bootstrap/command оставались.
- [ ] **Step 4: Прогони — ожидай PASS**
Run: `node --test dsh/tests/selfcheck.test.mjs`
Expected: PASS.
- [ ] **Step 5: Весь набор + доки + коммит** — `dsh/README.md` (Known limits: самопроверка при активации + команда `dsh-harness-check`), `dsh/PORT.md` §7 (риск контракта → закрыт самопроверкой).
Run: `bash dsh/tests/run.sh`
Expected: зелёный (package.test.mjs без `@deepseek-ai/*` тоже).
```bash
git add dsh/bundle/lib/selfcheck.js dsh/bundle/command-check.js dsh/bundle/index.js dsh/tests/selfcheck.test.mjs dsh/README.md dsh/PORT.md
git commit -m "feat(dsh): самопроверка контракта бандла при активации + команда dsh-harness-check (#69)"
```
### Task 5: Спека-дельта dsh-install и проверка code-paths
**Files:**
- Modify: `openspec/changes/dsh-port-v2/specs/dsh-install/spec.md` (delta)
- Modify: `dsh/PORT.md` (§1 таблица, §3: строка «Vendored superpowers» → «Pinned sync (v7.0.1)»)
- Modify: `openspec/changes/dsh-port-v2/design.md` (D-5 — статус «выполнено в этой ветке» после Task 3; D-6 — ссылки на selfcheck)
**Interfaces:**
- Consumes: поведение из Tasks 3–4.
- Produces: spec-delta: REQUIREMENT `sync-по-пину` (ADDED: `--skills` без явного источника клонирует `DSH_SUPERPOWERS_REPO` на `DSH_SUPERPOWERS_REF`, sync+verify обязательны, FAIL на несовпадении якорей; MODIFIED: сценарии «нет сети» — FAIL с подсказкой вместо тихого legacy; legacy — только WARN-путь), `selfcheck` (ADDED: активационный чек + команда).
- [ ] **Step 1: Напиши падающую проверку** — в `dsh/tests/install.test.mjs` уже есть тест `.claude/code-paths содержит dsh/` (Task 3); добавь в `dsh/tests/sync.test.mjs`:
```js
test('sync meta records ref and version', ...) // .sync-meta.json: ref==='v7.0.1', version==='7.0.1'.
```
Run: `node --test dsh/tests/sync.test.mjs`
Expected: FAIL, если Task 1 не писал `version` (иначе шаг помечается как уже зелёный и пропускается).
- [ ] **Step 2: Напиши дельту** `openspec/changes/dsh-port-v2/specs/dsh-install/spec.md` — прочитай `openspec/specs/dsh-install/spec.md` и оформи ADDED/MODIFIED по формату; BREAKING отметить в `## MODIFIED Requirements` (сеть для `--skills`).
Run: `openspec validate dsh-port-v2 --strict`
Expected: `Change 'dsh-port-v2' is valid`.
- [ ] **Step 3: Обнови** `dsh/PORT.md` §1/§3, `dsh/README.md` (раздел «Обновление»: каденс D-7 — issue на каждый релиз obra/superpowers, команда sync→verify→--skills, проверка `--check`) и `design.md` (D-5/D-6 статусы).
- [ ] **Step 4: Оба набора + коммит**
Run: `bash dsh/tests/run.sh && bash tests/run.sh && openspec validate dsh-port-v2 --strict`
Expected: всё зелёное.
```bash
git add openspec/changes/dsh-port-v2/specs/dsh-install/spec.md openspec/changes/dsh-port-v2/design.md dsh/PORT.md dsh/tests/sync.test.mjs
git commit -m "docs(openspec): дельта dsh-install под sync-по-пину и самопроверку (#69)"
```
## Предусловия окружения (проверяются контроллером один раз до Task 1)
- `python3`, `git`, `node` в PATH; `bash dsh/tests/run.sh` зелёный на входной точке ветки.
- Сеть не нужна для тестов: все прогоны используют `DSH_SUPERPOWERS_REPO=file://…` или temp-HOME; реальный `~/.dsh` не трогается (тесты — temp `DSH_HOME`).
- Ключи Gitea доступны только для `tea` (PR), не в переменных окружения сессий.
- Ветка: `feat/69-dsh-port-v2`, worktree `.claude/worktrees/dsh-port-v2`; `#63` идёт отдельной веткой `feat/63-guard-worktree-chain` и не пересекается по файлам.