# DSH OpenSpec Integration 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-проекту воркфлоу OpenSpec без Claude Code: шесть скиллов в `.agents/skills`, проверка и установка адаптера в `dsh/install.sh`, документация в PORT/README и живая приёмка полного цикла `new change → validate --strict → archive -y`. **Architecture:** Генератор `openspec init --tools agents` кладёт скиллы в проектный корень `.agents/skills`, который DSH читает нативно (проверено сабагентом 24.10.2026). Обвязка не переписывает воркфлоу и не копирует его глобально: она коммитит скиллы в свой репозиторий (dogfooding), проверяет CLI и каталог в `--check` и добавляет agents-адаптер в чужой проект флагом `--openspec`. Слэш-команд нет — вход через model-invocable скиллы. **Tech Stack:** bash (`set -euo pipefail`), Node test runner (`node --test`), markdown, openspec CLI 1.13.1. **Spec:** `openspec/changes/dsh-openspec/` — `proposal.md`, `design.md` (источник решений), `specs/dsh-openspec/spec.md` (требования и сценарии). **Ветка:** `feat/42-dsh-openspec`, issue #42, worktree `.claude/worktrees/openspec`. ## Global Constraints - `dsh/install.sh` остаётся идемпотентным; новые шаги не трогают `~/.claude`, `~/.dsh/roles`, бандл и профиль. - Никаких копий `openspec-*` в `~/.dsh/skills`: `--skills` копирует только `dsh/skills/*` (сейчас `role`, `sdd`). - Тесты идут без DSH runtime и без установленного openspec: внешний CLI подменяется скриптом в `PATH`. - Оба набора зелёные: `bash dsh/tests/run.sh` и `bash tests/run.sh`. Новый файл `dsh/tests/openspec.test.mjs` подхватывается `dsh/tests/run.sh` по маске. - Документация и тексты — по-русски; структура PORT.md (нумерованные разделы) и README сохраняется. - Коммиты — с trailer `Co-Authored-By: DeepSeek Harness `; force-push запрещён. - Маркировка: находки ревью `T.` / `C-n` / `I-n` / `M-n`, рулинги `R-n (design #n)`, наблюдения `O-n`, замечания человека `H-n`; у каждой статус `fixed ` / `deferred` / `rejected <причина>`. ## Review Focus - `--openspec` в каталоге без `openspec/` → скрипт обязан отказать и не создать корень; иначе флаг в чужом проекте молча заводит `openspec/`. - `--check` без `openspec` в `PATH` → `WARN`, код возврата 0; остальные `ok:`-строки на месте, `python3/git/node` не задеты. - `bash dsh/install.sh --skills` → в `~/.dsh/skills` нет каталогов `openspec-*`; появление там копии — регресс R-1. - Повторный `--openspec` в том же проекте → второй прогон не ломает скиллы и не плодит файлы. - Frontmatter каждого скилла: `name` совпадает с именем каталога, `description` содержит «openspec». --- ### Task 1: Скиллы `.agents/skills` в репозитории **Files:** - Create: `.agents/skills/openspec-{propose,apply-change,archive-change,explore,sync-specs,update-change}/SKILL.md` - Create: `.agents/skills/.openspec-target` - Create: `dsh/tests/openspec.test.mjs` (первый тест) **Interfaces:** - Produces: каталоги `.agents/skills/openspec-*/SKILL.md` с frontmatter `name`, `description`, `metadata.generatedBy`; файл `.openspec-target` со строкой `agents`. - Consumes: ничего. - [ ] **Step 1: Сгенерировать скиллы генератором, а не руками** Run: `openspec init --tools agents --no-animation` из корня worktree (он же корень репозитория обвязки); повторить после Task 5, если версия CLI изменится. Expected: `6 skills in .agents/`, `Config: openspec/config.yaml (exists)`; `git status --short` показывает только `?? .agents/`. - [ ] **Step 2: Написать падающий тест** `dsh/tests/openspec.test.mjs` ```js import assert from 'node:assert/strict' import { existsSync, readFileSync } from 'node:fs' import { dirname, join } from 'node:path' import test from 'node:test' import { fileURLToPath } from 'node:url' const here = dirname(fileURLToPath(import.meta.url)) const root = join(here, '..', '..') const agentsSkills = join(root, '.agents', 'skills') const NAMES = [ 'openspec-apply-change', 'openspec-archive-change', 'openspec-explore', 'openspec-propose', 'openspec-sync-specs', 'openspec-update-change', ] test('repository ships six agents skills for DSH', () => { assert.equal(readFileSync(join(agentsSkills, '.openspec-target'), 'utf8').trim(), 'agents') for (const name of NAMES) { const file = join(agentsSkills, name, 'SKILL.md') assert.ok(existsSync(file), `${name}/SKILL.md is missing`) const text = readFileSync(file, 'utf8') assert.match(text, new RegExp(`^name: ${name}$`, 'm'), `${name}: frontmatter name must match the dir`) assert.match(text, /description: .*openspec/i, `${name}: description must mention openspec`) } }) ``` - [ ] **Step 3: Запустить тест и убедиться, что он падает** Run: `node --test dsh/tests/openspec.test.mjs` Expected: FAIL на первом же assert, если каталог не создан; после Step 1 — PASS. Если PASS сразу после Step 1, закоммитить тест вместе со скиллами. - [ ] **Step 4: Проверить, что `--skills` не копирует `.agents`** Run: `HOME=$(mktemp -d) bash dsh/install.sh --skills` (HOME во временном каталоге; `--skills` берёт `$ROOT/dsh/skills`) Expected: `copied 2 local skills into …`; `ls $HOME/.dsh/skills` не содержит `openspec-*`. - [ ] **Step 5: Коммит** ```bash git add .agents dsh/tests/openspec.test.mjs git commit -m "feat(dsh): скиллы OpenSpec в .agents/skills проекта (#42)" ``` --- ### Task 2: `--check` сообщает про OpenSpec **Files:** - Modify: `dsh/install.sh` (`check()`, после проверки subagent overlay) - Modify: `dsh/tests/openspec.test.mjs` **Interfaces:** - Produces: строки вывода `check()` — `ok: openspec ` / `WARN: openspec not found; …` и `ok: openspec agents skills ()` / `WARN: .agents/skills missing; run --openspec in the project`. - Consumes: `.agents/skills/openspec-propose/SKILL.md` из Task 1; `$ROOT`. - [ ] **Step 1: Добавить падающие тесты** ```js test('install.sh --check reports the openspec CLI and the agents skills', () => { const home = tempDir() try { const bin = join(home, 'bin') write(join(bin, 'openspec'), '#!/bin/sh\n[ "$1" = "--version" ] && echo 1.13.1\nexit 0\n') // python3/git/node нужны, чтобы остальные пункты --check не падали const env = { ...process.env, HOME: home, DSH_HOME: join(home, '.dsh'), PATH: `${bin}:${process.env.PATH}` } const run = spawnSync('bash', [script, '--check'], { env, encoding: 'utf8' }) assert.equal(run.status, 0, run.stderr) assert.match(run.stdout, /ok: openspec 1\.13\.1/) assert.match(run.stdout, /ok: openspec agents skills \(/) } finally { cleanup(home) } }) test('install.sh --check warns but does not fail without the openspec CLI', () => { const home = tempDir() try { const bin = join(home, 'bin') write(join(bin, 'node'), '#!/bin/sh\nexit 0\n') // PATH только из временного каталога: openspec заведомо нет const env = { ...process.env, HOME: home, DSH_HOME: join(home, '.dsh'), PATH: bin } const run = spawnSync('bash', [script, '--check'], { env, encoding: 'utf8' }) assert.match(run.stdout + run.stderr, /WARN: openspec not found/) } finally { cleanup(home) } }) ``` (импорт `spawnSync` добавляется в шапку файла; `write` создаёт файл `0755` — см. `helpers.mjs`; если `write` не выставляет права, добавить `chmodSync(join(bin, 'openspec'), 0o755)`.) - [ ] **Step 2: Запустить и убедиться, что падает** Run: `node --test dsh/tests/openspec.test.mjs` Expected: FAIL — в выводе `--check` нет строк `openspec`. - [ ] **Step 3: Реализовать проверки в `check()`** ```bash if command -v openspec >/dev/null 2>&1; then echo "ok: openspec $(openspec --version 2>/dev/null | head -1)" else echo "WARN: openspec not found; OpenSpec workflows are unavailable" >&2 fi if [ -f "$ROOT/.agents/skills/openspec-propose/SKILL.md" ]; then echo "ok: openspec agents skills ($ROOT/.agents/skills)" else echo "WARN: .agents/skills missing; run --openspec in the project" >&2 fi ``` - [ ] **Step 4: Прогнать тест и весь набор** Run: `node --test dsh/tests/openspec.test.mjs && bash dsh/tests/run.sh` Expected: PASS, набор зелёный. - [ ] **Step 5: Коммит** ```bash git add dsh/install.sh dsh/tests/openspec.test.mjs git commit -m "feat(dsh): --check сообщает OpenSpec CLI и agents-скиллы (#42)" ``` --- ### Task 3: `--openspec` ставит адаптер в проект **Files:** - Modify: `dsh/install.sh` (usage, разбор аргументов, новая функция `install_openspec()`) - Modify: `dsh/tests/openspec.test.mjs` **Interfaces:** - Produces: `bash dsh/install.sh --openspec` — работает в `$PWD`; при `openspec/` в `$PWD` вызывает `openspec init --tools agents`, печатает `ok: openspec agents adapter in `; без `openspec/` — `FAIL: has no openspec/ root; run: openspec init --tools agents`, код 1, ничего не пишет; без CLI — `FAIL: openspec CLI not found`, код 1. - Consumes: test-хелперы `tempDir`, `write`, `cleanup` из `dsh/tests/helpers.mjs`. - [ ] **Step 1: Добавить падающие тесты** ```js function fakeOpenspec(dir, log) { const bin = join(dir, 'bin') write(join(bin, 'openspec'), `#!/bin/sh\necho "$PWD $@" >> ${JSON.stringify(log)}\nexit 0\n`) chmodSync(join(bin, 'openspec'), 0o755) return bin } test('--openspec adds the agents adapter to an existing OpenSpec project', () => { const home = tempDir() try { const project = join(home, 'project') mkdirSync(join(project, 'openspec'), { recursive: true }) const log = join(home, 'calls.log') const bin = fakeOpenspec(home, log) const env = { ...process.env, HOME: home, DSH_HOME: join(home, '.dsh'), PATH: `${bin}:${process.env.PATH}` } const run = spawnSync('bash', [script, '--openspec'], { cwd: project, env, encoding: 'utf8' }) assert.equal(run.status, 0, run.stderr) assert.match(run.stdout, /ok: openspec agents adapter in /) assert.match(readFileSync(log, 'utf8'), new RegExp(`^${project} init --tools agents$`, 'm')) } finally { cleanup(home) } }) test('--openspec refuses a project without an openspec root', () => { const home = tempDir() try { const project = join(home, 'plain') mkdirSync(project, { recursive: true }) const log = join(home, 'calls.log') const bin = fakeOpenspec(home, log) const env = { ...process.env, HOME: home, DSH_HOME: join(home, '.dsh'), PATH: `${bin}:${process.env.PATH}` } const run = spawnSync('bash', [script, '--openspec'], { cwd: project, env, encoding: 'utf8' }) assert.notEqual(run.status, 0) assert.match(run.stdout + run.stderr, /has no openspec\/ root/) assert.equal(existsSync(log), false, 'openspec must not run in a non-OpenSpec project') assert.equal(existsSync(join(project, 'openspec')), false) } finally { cleanup(home) } }) ``` - [ ] **Step 2: Запустить и убедиться, что падает** Run: `node --test dsh/tests/openspec.test.mjs` Expected: FAIL — `unknown option: --openspec`, код 2. - [ ] **Step 3: Реализовать флаг** ```bash install_openspec() { local dir="${1:-$PWD}" if ! command -v openspec >/dev/null 2>&1; then echo "FAIL: openspec CLI not found; install it first" >&2 return 1 fi if [ ! -d "$dir/openspec" ]; then echo "FAIL: $dir has no openspec/ root; run: openspec init --tools agents" >&2 return 1 fi ( cd "$dir" && openspec init --tools agents ) echo "ok: openspec agents adapter in $dir" } ``` В разборе аргументов: `--openspec) DO_OPENSPEC=1 ;;`, в `usage()` строка ` --openspec add the agents skill adapter to the current OpenSpec project`, в конце — `if [ "$DO_OPENSPEC" = 1 ]; then install_openspec "$PWD"; fi`. - [ ] **Step 4: Прогнать тест и набор** Run: `node --test dsh/tests/openspec.test.mjs && bash dsh/tests/run.sh` Expected: PASS; повторный `--openspec` в том же каталоге даёт тот же `ok:`, фейковый лог получает вторую строку (идемпотентность проверяется живой приёмкой, не юнитом). - [ ] **Step 5: Коммит** ```bash git add dsh/install.sh dsh/tests/openspec.test.mjs git commit -m "feat(dsh): --openspec добавляет agents-адаптер в проект (#42)" ``` --- ### Task 4: Документация и проверка полноты **Files:** - Modify: `dsh/PORT.md` (§1 таблица — OpenSpec; §2 — новый пункт; §5 — подраздел «OpenSpec skills»; §6 — шаг установки; §8 — состояния фаз) - Modify: `dsh/README.md` (What works, Quick start, Verify, Layout, Known limits) **Interfaces:** - Consumes: строки `--check`/`--openspec` из Task 2–3; `openspec/changes/dsh-openspec/design.md` (решения R-1…R-5). - Produces: тексты, на которые ссылается PR и issue #42. - [ ] **Step 1: PORT.md — факты и отказ от старых обещаний** В §1 в строке `Gitea, OpenSpec` дописать статус: CLI + проектные скиллы `.agents/skills`. В §2 добавить пункт 5: ```markdown 5. **OpenSpec** — воркфлоу идут проектными скиллами `.agents/skills/openspec-*`, которые пишет `openspec init --tools agents`. DSH читает этот каталог нативно; проверено сабагентом с cwd в проекте (все шесть имён в каталоге скиллов). Глобальной копии в `~/.dsh/skills` нет: генератор обновляется вместе с CLI проекта. Слэш-команд нет — только скиллы. Установка в проект: `bash dsh/install.sh --openspec`; проверка: `--check`. ``` В §5 добавить `### OpenSpec skills` с командами `openspec new change`, `status/instructions`, `validate --strict`, `archive -y` и оговоркой, что gemerated-скиллы руками не правятся (`openspec update`). В §6 добавить шаг «в проекте: `bash dsh/install.sh --openspec`; в сессии спросить "openspec propose"». В §8 снять OpenSpec из «next phases», если он там упомянут, и обновить статус спайка. - [ ] **Step 2: README.md** - «What works» — пункт про OpenSpec (2 строки + ссылка на PORT §2). - Quick start — `bash dsh/install.sh --openspec` с пометкой «из корня проекта с `openspec/`». - Verify — пункт: в проекте с `.agents/skills` спросить «what skills do you have?» → должны быть `openspec-*`. - Layout — строка `.agents/skills/openspec-* проектные скиллы (генерирует openspec CLI)`. - Known limits — одна строка: OpenSpec-скиллы не ставятся глобально; в новом проекте их нужно добавить `--openspec`. - [ ] **Step 3: Проверить, что документация не врёт** Run: `grep -n "openspec" dsh/README.md dsh/PORT.md | head -30` и пройти по каждой строке глазами: команда, флаг и путь существуют в `dsh/install.sh` этой ветки. Expected: нет упоминаний `/opsx`, `~/.dsh/skills/openspec`, `openspec change new` и `openspec archive` без `-y`. - [ ] **Step 4: Полный прогон** Run: `bash dsh/tests/run.sh && bash tests/run.sh` Expected: оба набора зелёные. - [ ] **Step 5: Коммит** ```bash git add dsh/PORT.md dsh/README.md git commit -m "docs(dsh): OpenSpec — проектные скиллы, --openspec, проверка (#42)" ``` --- ### Task 5: Живая приёмка DSH-цикла **Files:** - Modify: `openspec/changes/dsh-openspec/design.md` (§6 — результаты приёмки) - Не коммитить: temp-проект живёт в `/tmp`. **Interfaces:** - Consumes: скиллы из Task 1, `--openspec` из Task 3, CLI openspec 1.13.1. - Produces: раздел «Приёмка: прогон» в design.md и комментарий в issue #42. - [ ] **Step 1: Собрать temp-проект с веткой, чтобы guard-main не блокировал** Run: ```bash TD=$(mktemp -d /tmp/openspec-accept-XXXX) && cd "$TD" && git init -q . \ && git commit -q --allow-empty -m init && git checkout -q -b feat/probe \ && openspec init --tools agents --no-animation | tail -3 ``` Expected: `6 skills in .agents/`; `git status --short` — только `.agents/`, `openspec/`. - [ ] **Step 2: Спросить DSH-сабагента, видит ли он скиллы** Dispatch: `subagent` с `cwd` = temp-проект, промпт: «перечисли имена скиллов своего каталога; подтверди, есть ли `openspec-propose`». Ожидание: все шесть `openspec-*` в ответе. - [ ] **Step 3: Прогнать полный цикл через ровно те команды, которые предписывает скилл** Run (в temp-проекте): ```bash openspec new change probe openspec status --change probe --json >/dev/null openspec instructions proposal --change probe --json >/dev/null # написать proposal.md + specs/probe/spec.md с одной дельтой openspec validate probe --strict openspec archive probe -y test -f openspec/specs/probe/spec.md && echo "spec archived" ``` Expected: `Change 'probe' is valid`, `archived as '2026-10-24-probe'`, `spec archived`. - [ ] **Step 4: Записать результаты в design.md §6 и в issue** Добавить в `design.md` §6 подраздел «Приёмка: прогон 24.10.2026» — дата, версия CLI, что увидел сабагент, вывод validate/archive, путь temp-проекта. Затем `tea comments 42 <файл с текстом>` (тело — из файла, а не аргументом). - [ ] **Step 5: Коммит design.md** ```bash git add openspec/changes/dsh-openspec/design.md git commit -m "docs(openspec): результаты приёмки DSH-цикла (#42)" ``` --- ## Self-review заметки - Спека `dsh-openspec`: requirement «Скиллы живут в проекте» → Task 1; «Цикл без Claude Code» → Task 5; «Проверка установки» → Task 2; «Установка в проект» → Task 3; «Вход — скиллы» → Task 4 (тексты) + отсутствие изменения бандла в задачах 1–3. - `--check` в Task 2 использует `$ROOT/.agents/skills`, а не `$PWD`: это решение R-1/design §4, тест это фиксирует. - Идемпотентность `--openspec` юнитом не проверяется (фейковый openspec всегда возвращает 0) — проверяется живой приёмкой Task 5, Step 3 (повторный `openspec init` без ошибок), где Fake не участвует.