diff --git a/docs/superpowers/plans/2026-10-24-dsh-port-v2.md b/docs/superpowers/plans/2026-10-24-dsh-port-v2.md new file mode 100644 index 0000000..b2d1a4c --- /dev/null +++ b/docs/superpowers/plans/2026-10-24-dsh-port-v2.md @@ -0,0 +1,286 @@ +# 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)`). +- Синхронизация апстрима — только по тегу; 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 [--out ] [--ref ]`; exit 0 — печать `synced N files…`, exit 1 — FAIL с именем упавшего transform/якоря; `--out` по умолчанию `/.sync-out`; staging-каталог `.tmp-`; `--out` подменяется только после всех ассертов; в `--out/.sync-meta.json` пишется `{"ref": "", "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-референсов — как у оригинала (8 файлов); + - `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.md`). +- Produces: CLI `node dsh/sync/verify-skills.mjs `; exit 0 — `ok: ` на каждый скилл; exit 1 — первая ошибка с `/SKILL.md: `; проверки: YAML frontmatter с `name`+`description`, без остаточного `superpowers:`-префикса в теле, все относительные ссылки вида `..//…`/`references/…` из тела резолвятся в ``. + +- [ ] **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 --out "$DSH_HOME/superpowers/skills.synced" --ref "$REF"` → `node dsh/sync/verify-skills.mjs ` → копирование `/*` в `$DSH_HOME/skills` (как текущий цикл) → local skills (role, sdd) без изменений. `--check`: строка `ok: superpowers source (, git , 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://`): + +```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` и не пересекается по файлам. diff --git a/openspec/changes/dsh-port-v2/design.md b/openspec/changes/dsh-port-v2/design.md index 0619e35..dd2bc77 100644 --- a/openspec/changes/dsh-port-v2/design.md +++ b/openspec/changes/dsh-port-v2/design.md @@ -44,7 +44,7 @@ RV-6 (legacy-фолбэки как техдолг), RV-11 (нет compat-чек | D-2 | Пин на тег (v7.0.1) через `DSH_SUPERPOWERS_REF`; #67 — rejected | Воспроизводимость; unpinned HEAD = случайная семантика | Каждый релиз upstream — свой issue+PR; пропустили релиз = молча старый текст (митигируется `--check`) | | D-3 | Результат sync — в `$DSH_HOME/skills`; bootstrap бандла не меняется | Один источник текста; install.sh остаётся инсталлятором, а не публикуемым пакетом | Нет | | D-4 | Legacy-фолбэки `~/.claude/*` живут до #66, удаляются им | #49 уже ввёл их с WARN; рубить сейчас = сломать машины без переустановки | Техдолг висит дольше; контракт удаления — в #66 | -| D-5 | `dsh/sync/` и `dsh/bundle/` добавляются в `.claude/code-paths` | Сейчас require-plan защищает только `hooks/` и `skills/`; вендоримый код обязан быть под хук | Без этого правки sync-скриптов не гейтятся планом | +| D-5 | `dsh/` уже в `.claude/code-paths` (проверено 24.10.2026 — файл содержит `dsh/`); require-plan защищает весь `dsh/`, включая `dsh/sync/` | Вендоримый код обязан быть под хук | Без проверки правки sync-скриптов не гейтятся планом — в план добавлен тест-assert на строку `dsh/` в code-paths | | D-6 | Compat-чек (решение владельца 24.10.2026): **самопроверка бандла внутри DSH** — при активации и по команде проверяет контракт, на который бандл опирается (`ctx.tools.guard`, `agent/pre-step`, `session.surface.nodes`, `ctx.commands`, `workingDirectory`); терминальный `--check` гоняет только verify synced-скиллов (frontmatter, префиксы, висячие ссылки) и окружение | SDK DSH лежит в app.asar, терминал до него не дотягивается — проверка контракта возможна только изнутри DSH; это закрывает RV-11 по-настоящему | Бандл тронут аддитивно (активационный assert + команда) — «не трогаем бандл» смягчено до «без изменения существующего поведения» | | D-7 | Каденс: issue на каждый релиз obra/superpowers; compat — после апгрейда DSH | Ритм без напоминаний; влезает в обычный цикл фичи | Пропущенный релиз не виден между ретро — добавить строку в `--check`/doctor (#44) | | D-8 | Болевые точки эксплуатации заранее не фиксируются: ответ владельца 24.10.2026 — «пока трудно сказать, идёт разработка только самого харнеса». План сосредоточен на машинерии порта; первые реальные затыки DSH-сессий копятся в #69 и двигают приоритеты | Не выдумывать боли за владельца, пока нет сигнала | Задачи плана упорядочены без пользовательского приоритета — по зависимостям и P-классам находок |