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

21 KiB
Raw Blame History

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