claude-harness/docs/superpowers/plans/2026-10-24-dsh-openspec.md
2026-10-24 19:44:38 +04:00

399 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 не участвует.