21 KiB
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 <noreply@deepseek.com>; force-push запрещён. - Маркировка: находки ревью
T<n>.<k>/C-n/I-n/M-n, рулингиR-n (design #n), наблюденияO-n, замечания человекаH-n; у каждой статусfixed <commit>/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с frontmattername,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
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: Коммит
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 <version>/WARN: openspec not found; …иok: openspec agents skills (<path>)/WARN: .agents/skills missing; run --openspec in the project. -
Consumes:
.agents/skills/openspec-propose/SKILL.mdиз Task 1;$ROOT. -
Step 1: Добавить падающие тесты
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()
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: Коммит
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 <dir>; безopenspec/—FAIL: <dir> 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: Добавить падающие тесты
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: Реализовать флаг
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: Коммит
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:
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: Коммит
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:
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-проекте):
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
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 не участвует.