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

22 KiB
Raw Blame History

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 + 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/):

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: весь набор зелёный.

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, плюс испорченные копии):

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: зелёный.

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>):

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: оба набора зелёные.

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:

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/* тоже).

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:

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: всё зелёное.

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 и не пересекается по файлам.