399 lines
21 KiB
Markdown
399 lines
21 KiB
Markdown
# 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` с 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 <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: Добавить падающие тесты**
|
||
|
||
```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 <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: Добавить падающие тесты**
|
||
|
||
```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 не участвует.
|