22 KiB
DSH port v2 (вариант C) Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use the
sddskill (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(defaulthttps://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-dshscripts/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— перенос идеи LayneChaiverify-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); envDSH_SUPERPOWERS_REF(defaultv7.0.1),DSH_SUPERPOWERS_REPO(defaulthttps://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; envDSH_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-вставками в прежнем стиле.
- новая ветка resolve: explicit (
-
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.guardguard-блок пропускался с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не трогается (тесты — tempDSH_HOME). - Ключи Gitea доступны только для
tea(PR), не в переменных окружения сессий. - Ветка:
feat/69-dsh-port-v2, worktree.claude/worktrees/dsh-port-v2;#63идёт отдельной веткойfeat/63-guard-worktree-chainи не пересекается по файлам.