DSH port v2: доставка superpowers вариантом C (sync-по-пину, самопроверка бандла) (#69) #72
24
dsh/PORT.md
24
dsh/PORT.md
@ -16,7 +16,7 @@ This is a port, not a copy. Three layers map differently:
|
||||
| Skills | `~/.claude/skills`, `.claude/skills` | `~/.dsh/skills`, `.agents/skills`, `customSkillDirs` | 1:1 format |
|
||||
| Roles | `roles/`, `/role` skill | same files + `/role` command + model-invocable `role` skill | close |
|
||||
| Hard rules | `PreToolUse` shell hooks | `ctx.tools.guard` Cordis guard | logic reused, wrapper new |
|
||||
| Superpowers | plugin + `SessionStart` hook | skills + `agent/pre-step` bootstrap | content 1:1, bootstrap new |
|
||||
| Superpowers | plugin + `SessionStart` hook | pinned sync of tag `v7.0.1` (`DSH_SUPERPOWERS_REF`) into `~/.dsh/skills` + `agent/pre-step` bootstrap | content adapted by sync, bootstrap new |
|
||||
| SDD | `Task` (synchronous) | `subagent` (asynchronous) / `workflow` | semantics differ |
|
||||
| Gitea | CLI + skills | `bash` + skills | 1:1 |
|
||||
| OpenSpec | CLI + `.claude/skills` + `/opsx:*` | CLI + проектные `.agents/skills` | close: нет слэш-команд |
|
||||
@ -81,16 +81,20 @@ file has to be installed as `$DSH_HOME/AGENTS.md` because DSH does not read
|
||||
- **Full installer**: `install.sh` prepares global instructions and skills; the
|
||||
bundle itself is installed by `plugin_manager` because the Desktop profile is
|
||||
Electron-managed.
|
||||
- **Upstream pin (change `dsh-port-v2`, D-2)**: the default install source is
|
||||
the clone at `~/.dsh/superpowers` pinned to `$DSH_SUPERPOWERS_REF` (default
|
||||
`v7.0.1`). `install.sh --skills` clones or updates it, runs the vendored
|
||||
sync + verify, and copies the adapted skills; a broken or empty synced tree
|
||||
is never installed. Explicit sources (`--superpowers-from`,
|
||||
- **Pinned sync of superpowers (change `dsh-port-v2`, D-2)**: "vendored
|
||||
superpowers" is gone — no upstream skill text is copied as-is. The default
|
||||
install source is the clone at `~/.dsh/superpowers` pinned to
|
||||
`$DSH_SUPERPOWERS_REF` (default `v7.0.1`). `install.sh --skills` clones or
|
||||
updates it, runs the vendored `dsh/sync/sync-from-upstream.mjs` (asserted
|
||||
transforms, namespace strip, `dsh-tools.md` override) and
|
||||
`dsh/sync/verify-skills.mjs`, and copies only the adapted tree; a broken or
|
||||
empty synced tree is never installed. Explicit sources (`--superpowers-from`,
|
||||
`$DSH_SUPERPOWERS_SOURCE`) are synced directly with no network. Upstream
|
||||
moves independently, so each release is a deliberate pin bump (an issue per
|
||||
release, D-7). Removing the legacy `~/.claude` fallbacks is deferred until
|
||||
every machine has rerun `--skills --roles`; until then both runtime and
|
||||
installer warn when they use one.
|
||||
release, D-7; the procedure is in `dsh/README.md`). Removing the legacy
|
||||
`~/.claude` fallbacks is deferred until every machine has rerun
|
||||
`--skills --roles`; until then both runtime and installer warn when they use
|
||||
one.
|
||||
|
||||
## 4. Architecture
|
||||
|
||||
@ -170,6 +174,8 @@ for the repository layout:
|
||||
guards: true
|
||||
bootstrap: true
|
||||
roleCommand: true
|
||||
selfCheck: true # activation check of the DSH contract; not-ok is logged
|
||||
checkCommand: true # register the dsh-harness-check command
|
||||
bootstrapSubagents: false
|
||||
hooksDir: '' # empty -> bundle/hooks -> ../../hooks
|
||||
rolesDir: '' # empty -> ~/.dsh/roles, legacy ~/.claude/roles, repo roles/
|
||||
|
||||
@ -23,9 +23,13 @@ separate Cordis bundle plus a thin install helper.
|
||||
(ставит `openspec init --tools agents`); глобальной копии нет, слэш-команд
|
||||
нет. Установка в проект: `bash dsh/install.sh --openspec`.
|
||||
|
||||
**Not yet:** `briefing`, `harness-check`, `role-hint`, `harness-doctor`,
|
||||
auto-memory, caveman. See [PORT.md](PORT.md) for the design, mapping table, and
|
||||
risks.
|
||||
- self-check — at activation the bundle verifies the DSH contract it depends on
|
||||
and logs every missing service; the `dsh-harness-check` command re-runs the
|
||||
same check on demand.
|
||||
|
||||
**Not yet:** `briefing`, the `harness-check` SessionStart hook, `role-hint`,
|
||||
`harness-doctor`, auto-memory, caveman. See [PORT.md](PORT.md) for the design,
|
||||
mapping table, and risks.
|
||||
|
||||
## Quick start
|
||||
|
||||
@ -74,6 +78,29 @@ Do not hand-edit `$DSH_HOME/profiles/desktop/package.json` or
|
||||
`cordis.patch.yml`: the Desktop profile is Electron-managed and the plugin
|
||||
manager owns those writes.
|
||||
|
||||
## Update
|
||||
|
||||
Upstream superpowers moves on its own schedule, so a pin bump is a deliberate
|
||||
step, never a background pull (design D-2, cadence D-7):
|
||||
|
||||
1. One issue per `obra/superpowers` release (sync → tests → PR). Compare the
|
||||
release tags with the pin in `dsh/install.sh`.
|
||||
2. Bump `DSH_SUPERPOWERS_REF` in `dsh/install.sh` and the pin quoted in this
|
||||
file and in `PORT.md`; then run the sync and the verifier by hand against a
|
||||
checkout of the new tag:
|
||||
`node dsh/sync/sync-from-upstream.mjs <checkout> --out <dir> --ref <tag>`,
|
||||
then `node dsh/sync/verify-skills.mjs <dir>`. A changed transform anchor or a
|
||||
changed skill set is a hard `FAIL`: adapt the transform or the expected set,
|
||||
do not bypass it.
|
||||
3. Reinstall and check: `bash dsh/install.sh --skills`, then
|
||||
`bash dsh/install.sh --check`. The source line must name the new ref and the
|
||||
synced version (`ref v7.0.2 (synced, version 7.0.2)`); `--check` prints the
|
||||
configured pin next to the ref recorded by the last sync, so a pin that was
|
||||
bumped without resyncing (or a sync that never ran) is visible instead of
|
||||
silently old text.
|
||||
4. `bash dsh/tests/run.sh` must stay green. The fixtures are a reduced upstream
|
||||
tree, so a transform that stopped matching fails there first.
|
||||
|
||||
## Verify
|
||||
|
||||
1. In a new session, ask `what skills do you have?` — the superpowers catalog
|
||||
@ -148,6 +175,8 @@ repository layout:
|
||||
guards: true
|
||||
bootstrap: true
|
||||
roleCommand: true
|
||||
selfCheck: true # activation check of the DSH contract; not-ok is logged
|
||||
checkCommand: true # register the dsh-harness-check command
|
||||
bootstrapSubagents: false
|
||||
hooksDir: '' # empty -> bundle/hooks -> ../../hooks
|
||||
rolesDir: '' # empty -> ~/.dsh/roles, legacy ~/.claude/roles, repo roles/
|
||||
|
||||
@ -322,10 +322,18 @@ function parseArgs(argv) {
|
||||
}
|
||||
}
|
||||
|
||||
/** True when `parent` is `child` or one of its ancestors (`/a` → `/a/b`). */
|
||||
/**
|
||||
* True when `parent` is `child` or one of its ancestors (`/a` → `/a/b`).
|
||||
*
|
||||
* A parent hop is exactly `..` or `..<sep>…`; a sibling named `..foo` also makes
|
||||
* `relative()` start with `..` but is a descendant, so the collision guard must
|
||||
* still fire for it (T1.9).
|
||||
*/
|
||||
function isAncestorOrSame(parent, child) {
|
||||
const rel = relative(parent, child)
|
||||
return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel))
|
||||
if (rel === '') return true
|
||||
if (rel === '..' || rel.startsWith('..' + sep)) return false
|
||||
return !isAbsolute(rel)
|
||||
}
|
||||
|
||||
/** @returns {Promise<string[]>} repo-relative file paths under `root`. */
|
||||
|
||||
@ -28,8 +28,9 @@
|
||||
* regression this check exists for (T2.1/T2.2). Names that are
|
||||
* illustrative examples rather than real files (`scripts/tool.sh`, …)
|
||||
* are skipped, as in the vendored reference implementation; that
|
||||
* whitelist is a deliberate compromise, not a general escape hatch
|
||||
* (T2.3 in the SDD ledger).
|
||||
* whitelist is a deliberate compromise, not a general escape hatch —
|
||||
* it covers exactly four literals and hides nothing real in v7.0.1
|
||||
* (T2.3/T2.8 in the SDD ledger).
|
||||
*
|
||||
* The prefix check is scoped to SKILL.md bodies on purpose: reference files
|
||||
* such as `using-superpowers/references/dsh-tools.md` quote `superpowers:` in
|
||||
@ -58,10 +59,11 @@ const TRAILING_PUNCTUATION = /[.,;:)\]]+$/
|
||||
* Names that appear only as illustrative examples in prose, never as real
|
||||
* files — the same list the vendored reference implementation keeps.
|
||||
*
|
||||
* Deliberate compromise: upstream `writing-skills` shows `bash scripts/tool.sh`
|
||||
* as an example, and no such file ships (see T2.3 in the SDD ledger). The list
|
||||
* is closed and exact-path matched, so it cannot hide a real dangling link in
|
||||
* another skill.
|
||||
* The protection is deliberately narrow (T2.3/T2.8): it skips exactly these
|
||||
* four literals, matched as whole inline-code tokens — not a prefix, not a
|
||||
* glob, not a path segment. Checked on obra/superpowers v7.0.1: no real
|
||||
* upstream link or path uses any of these names, so nothing that exists is
|
||||
* skipped today. A new name here needs the same evidence.
|
||||
*/
|
||||
const ILLUSTRATIVE = new Set(['scripts/tool.sh', 'scripts/tool.js', 'scripts/helper.py', 'scripts/helper.sh'])
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { cpSync, existsSync, readFileSync, readdirSync, rmSync } from 'node:fs'
|
||||
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from 'node:fs'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import test from 'node:test'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
@ -135,6 +135,34 @@ test('sync refuses --out that collides with the upstream checkout', () => {
|
||||
}
|
||||
})
|
||||
|
||||
test('sync refuses --out that is an ancestor named like a parent hop (T1.9)', () => {
|
||||
const work = tempDir('dsh-sync-collision-dotdot-')
|
||||
try {
|
||||
// `relative(out, upstream)` is `..foo` here: a sibling directory whose name
|
||||
// starts with `..`, not a parent hop. Treating every `..`-prefixed relative
|
||||
// path as unrelated would let the swap delete the checkout it reads.
|
||||
const out = join(work, 'out')
|
||||
const upstream = join(out, '..foo')
|
||||
mkdirSync(upstream, { recursive: true })
|
||||
cpSync(fixture, upstream, { recursive: true })
|
||||
write(join(upstream, 'sentinel.txt'), 'source stays\n')
|
||||
|
||||
const result = runSync([upstream, '--out', out, '--ref', 'v7.0.1'])
|
||||
|
||||
assert.equal(result.status, 1, `--out ${out} must be refused`)
|
||||
assert.match(result.stderr, /--out collides with the upstream checkout/)
|
||||
assert.equal(readFileSync(join(upstream, 'sentinel.txt'), 'utf8'), 'source stays\n')
|
||||
assert.equal(existsSync(join(upstream, 'skills', 'using-superpowers', 'SKILL.md')), true)
|
||||
assert.deepEqual(
|
||||
readdirSync(work).filter((name) => name.includes('.tmp-')),
|
||||
[],
|
||||
'a refused sync must not create its staging tree',
|
||||
)
|
||||
} finally {
|
||||
cleanup(work)
|
||||
}
|
||||
})
|
||||
|
||||
test('sync fails on a transform whose source file upstream dropped', () => {
|
||||
const work = tempDir('dsh-sync-missing-transform-file-')
|
||||
try {
|
||||
|
||||
@ -250,6 +250,23 @@ test('verify ignores prefixes and paths inside fenced examples', () => {
|
||||
}
|
||||
})
|
||||
|
||||
test('verify fails on a dangling inline reference (T2.7)', () => {
|
||||
const work = tempDir('dsh-verify-dangling-inline-')
|
||||
try {
|
||||
const dir = syncedCopy(work, syncedTree(work))
|
||||
const file = join(dir, 'using-superpowers', 'SKILL.md')
|
||||
write(file, `${readFileSync(file, 'utf8')}\nSee \`references/nope.md\` for the full mapping.\n`)
|
||||
|
||||
const result = verify(dir)
|
||||
|
||||
assert.equal(result.status, 1, `stdout: ${result.stdout}`)
|
||||
assert.match(result.stderr, /using-superpowers\/SKILL\.md/)
|
||||
assert.match(result.stderr, /references\/nope\.md/)
|
||||
} finally {
|
||||
cleanup(work)
|
||||
}
|
||||
})
|
||||
|
||||
test('verify does not flag a resolving inline reference', () => {
|
||||
const work = tempDir('dsh-verify-inline-reference-')
|
||||
try {
|
||||
|
||||
@ -51,6 +51,21 @@ RV-6 (legacy-фолбэки как техдолг), RV-11 (нет compat-чек
|
||||
| D-9 | Модели исполнителей (указание владельца 24.10.2026 «простые задачи — дешёвой модели»): механика и простые правки — `deepseek-official/deepseek-flash`, интеграция и ревью — `deepseek-official/deepseek-v4-pro`; фиксируется в брифах плана | Экономия на рутине без потери качества ревью | Сложность недооценена → flash не справился, fix-loop дороже; страхуется ревьюером на v4-pro |
|
||||
| D-10 | #63 (ложный блок worktree-цепочки, O-4) — bounded-задача **до старта плана** (решение владельца 24.10.2026); CC-сторона хуков входит в скоуп только этим исключением | Блок бьёт по каждому чеклисту закрытия; фикс маленький и независим от dsh/ | Два параллельных потока правок в claude-harness (#63 и дизайн #69) — не пересекаются по файлам: #63 — `hooks/` + тесты, #69 — `dsh/` |
|
||||
|
||||
## Статус решений (25.10.2026, ветка `feat/69-dsh-port-v2`)
|
||||
|
||||
- **D-5 — выполнено в этой ветке.** `.claude/code-paths` содержит строку
|
||||
`dsh/` (проверено 24.10.2026), вендоренный `dsh/sync/` попадает под
|
||||
require-plan; строку держит assert в `dsh/tests/install.test.mjs`
|
||||
(`code-paths lists dsh/`), чтобы каталог не выпал из хука молча.
|
||||
- **D-6 — выполнено в этой ветке.** Самопроверка живёт в
|
||||
`dsh/bundle/lib/selfcheck.js` (чистый `collectSelfCheck(ctxLike)`) и
|
||||
`dsh/bundle/command-check.js` (команда `dsh-harness-check`), подключается
|
||||
аддитивно в `dsh/bundle/index.js`; ключи конфига `selfCheck`/`checkCommand`
|
||||
(default `true`); тесты — `dsh/tests/selfcheck.test.mjs`; риск и чем закрыт
|
||||
— PORT.md §7. Терминальный `--check` остался офлайн-проверкой файлов и
|
||||
окружения: SDK внутри `app.asar` ему недоступен, поэтому контракт
|
||||
проверяется только изнутри DSH (требование `selfcheck` в спеке-дельте).
|
||||
|
||||
## Открытые вопросы (к владельцу) — решены 24.10.2026
|
||||
|
||||
1. ~~Момент в DSH-сессии, когда обвязка споткнула~~ — ответ «пока трудно
|
||||
|
||||
@ -31,10 +31,11 @@ Issue: #69. Источники: ревью обвязки 24.10.2026 (наход
|
||||
**Вариант C** — взять машинерию готового порта как форк/вендор, но sync
|
||||
гонять самим по своему каденсу:
|
||||
|
||||
- В `dsh/sync/` вендорятся `scripts/sync-from-upstream.mjs`,
|
||||
`scripts/verify-skills.mjs`, `scripts/check-dsh-compat.mjs` из
|
||||
LayneChai/superpowers-dsh (MIT) с адаптацией путей и transform-ов под
|
||||
наш `dsh-tools.md`.
|
||||
- В `dsh/sync/` вендорятся два скрипта LayneChai/superpowers-dsh (MIT):
|
||||
`scripts/sync-from-upstream.mjs` и `scripts/verify-skills.mjs` — с
|
||||
адаптацией путей и transform-ов под наш `dsh-tools.md`. Их
|
||||
`scripts/check-dsh-compat.mjs` не вендорим: его идея переезжает в
|
||||
самопроверку бандла (см. ниже и design D-6).
|
||||
- Пин на тег obra/superpowers (v7.0.1 на момент плана) живёт в репозитории,
|
||||
а не в голове: `DSH_SUPERPOWERS_REF`, переменная в `dsh/install.sh` и в
|
||||
sync-скрипте. #67 (авто-pull unpinned HEAD) отменяется — rejected.
|
||||
|
||||
241
openspec/changes/dsh-port-v2/specs/dsh-install/spec.md
Normal file
241
openspec/changes/dsh-port-v2/specs/dsh-install/spec.md
Normal file
@ -0,0 +1,241 @@
|
||||
# Spec Delta: dsh-install
|
||||
|
||||
Дельта change `dsh-port-v2` (#69). Доставка superpowers переходит с «сырой
|
||||
копии клона» на sync по пину `v7.0.1`, бандл получает самопроверку контракта.
|
||||
|
||||
**BREAKING:** `--skills` без явного источника теперь требует сеть (клон пина);
|
||||
офлайн-прогон без клона — `FAIL`, а не тихий legacy и не «WARN + код 0».
|
||||
Отмена #67 (авто-pull unpinned HEAD) — решение design.md D-2.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Sync по пину
|
||||
`dsh/install.sh --skills` без явного источника MUST получить superpowers из
|
||||
клона `$DSH_HOME/superpowers` на пине `$DSH_SUPERPOWERS_REF` (default
|
||||
`v7.0.1`) из `$DSH_SUPERPOWERS_REPO` (default
|
||||
`https://github.com/obra/superpowers`) и MUST прогнать sync
|
||||
(`dsh/sync/sync-from-upstream.mjs`) и verify (`dsh/sync/verify-skills.mjs`) до
|
||||
копирования в `$DSH_HOME/skills`.
|
||||
|
||||
- Копирование MUST брать только synced-дерево; сырой текст апстрима при пине
|
||||
MUST NOT попадать в `$DSH_HOME/skills`. Адаптации: `superpowers:`-префиксы
|
||||
сняты, ссылки ведут на `using-superpowers/references/dsh-tools.md` из
|
||||
`dsh/bundle/`, чужие harness-референсы не копируются.
|
||||
- Sync MUST завершаться `FAIL` (код 1) на несовпадении якоря transform-а
|
||||
(ноль или больше одного совпадения), на изменении набора скиллов и на
|
||||
отсутствии `skills/` в checkout. При любом падении назначение (`--out`)
|
||||
MUST остаться прежним, а staging-каталог MUST быть удалён.
|
||||
- Verify MUST проверять YAML frontmatter (`name` и `description` непусты, без
|
||||
дублей ключей), отсутствие остаточных `superpowers:`-префиксов в телах
|
||||
`SKILL.md` и резолв всех относительных ссылок внутри synced-дерева. Дерево,
|
||||
прошедшее verify с нулём скиллов (`verified 0 skills`), MUST трактоваться
|
||||
как `FAIL`: пустое дерево не устанавливается.
|
||||
- В `<out>/.sync-meta.json` MUST записываться `ref` (значение `--ref`) и
|
||||
`version` (версия из `.claude-plugin/plugin.json` апстрима); `--check` MUST
|
||||
печатать ref и version из этого файла, не обращаясь в сеть.
|
||||
- Повторный `--skills` MUST быть идемпотентным: каталоги `*.tmp-*` не
|
||||
остаются, установленные копии заменяются целиком (`rm -rf` + `cp -R`), а не
|
||||
дополняются.
|
||||
|
||||
#### Scenario: Свежий HOME, установка по пину
|
||||
- **WHEN** `HOME` пуст, флаг и `DSH_SUPERPOWERS_SOURCE` не заданы, а
|
||||
`DSH_SUPERPOWERS_REPO` указывает на репозиторий с тегом `v7.0.1`
|
||||
- **THEN** `--skills` клонирует пин, печатает `synced N files` и
|
||||
`verified 15 skills`, копирует адаптированные скиллы; в
|
||||
`using-superpowers/SKILL.md` есть `references/dsh-tools.md` и нет
|
||||
`superpowers:`, а `references/dsh-tools.md` совпадает с
|
||||
`dsh/bundle/dsh-tools.md`
|
||||
|
||||
#### Scenario: Сломанный якорь апстрима
|
||||
- **WHEN** checkout на пине не содержит якоря одного из transform-ов
|
||||
- **THEN** sync печатает `FAIL` с именем transform-а и завершается кодом 1,
|
||||
прежнее `--out` и `$DSH_HOME/skills` не изменены
|
||||
|
||||
#### Scenario: Изменился набор скиллов
|
||||
- **WHEN** в checkout на пине появился или исчез каталог скилла относительно
|
||||
ожидаемого списка
|
||||
- **THEN** sync печатает `added upstream:` / `removed upstream:` и завершается
|
||||
кодом 1; установка ничего не копирует
|
||||
|
||||
#### Scenario: Verify нашёл висячую ссылку
|
||||
- **WHEN** synced-дерево содержит ссылку на несуществующий файл (markdown или
|
||||
inline-код)
|
||||
- **THEN** verify печатает `<skill>/SKILL.md: dangling relative reference: …` и
|
||||
завершается кодом 1; в `$DSH_HOME/skills` ничего не копируется
|
||||
|
||||
#### Scenario: Нулевое дерево
|
||||
- **WHEN** verify завершился кодом 0, но сообщил `verified 0 skills`
|
||||
- **THEN** установка завершается `FAIL`, ни один скилл не скопирован
|
||||
|
||||
#### Scenario: Повторный прогон
|
||||
- **WHEN** `--skills` запущен второй раз на том же пине
|
||||
- **THEN** установленные скиллы заменены, в `$DSH_HOME/superpowers` нет
|
||||
каталогов `*.tmp-*`, а `.sync-meta.json.ref` равен пину
|
||||
|
||||
### Requirement: Самопроверка контракта бандла
|
||||
Бандл `dsh-harness` MUST при активации проверять контракт DSH, на который
|
||||
опирается, и MUST предоставлять команду `dsh-harness-check` для повторного
|
||||
прогона той же проверки.
|
||||
|
||||
- Проверка MUST покрывать `ctx.tools.guard`, `ctx.on` для `agent/pre-step`,
|
||||
`ctx.inject` (регистрация команд) и `workingDirectory.get`. Каждая
|
||||
отсутствующая служба MUST логироваться строкой
|
||||
`[dsh-harness] self-check: <feature> not-ok: <detail>`.
|
||||
- Отсутствие службы MUST отключать только свою фичу: без `ctx.tools.guard`
|
||||
guard-обвязка пропускается, bootstrap и `/role` продолжают работать при
|
||||
своих службах. Активация бандла MUST NOT падать целиком из-за несовпадения
|
||||
контракта.
|
||||
- Команда `dsh-harness-check` MUST возвращать текстовый отчёт с одной строкой
|
||||
`ok`/`not-ok` на каждую проверяемую фичу.
|
||||
- Ключи конфига `selfCheck` и `checkCommand` (default `true`) MUST позволять
|
||||
выключить активационную проверку и команду соответственно.
|
||||
- Терминальный `--check` MUST оставаться офлайн-проверкой файлов и окружения:
|
||||
SDK внутри `app.asar` ему недоступен, контракт проверяется только изнутри
|
||||
DSH.
|
||||
|
||||
#### Scenario: Сломанный контракт при активации
|
||||
- **WHEN** бандл активируется в `ctx` без `tools.guard`
|
||||
- **THEN** в лог попадает `self-check: guards not-ok: tools.guard`, guard-wiring
|
||||
пропускается, bootstrap и команды продолжают работать, бандл не падает
|
||||
|
||||
#### Scenario: Проверка по требованию
|
||||
- **WHEN** пользователь вызывает `/dsh-harness-check`
|
||||
- **THEN** отчёт перечисляет каждую фичу со статусом `ok` или `not-ok`
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Источник superpowers задаётся при установке
|
||||
`dsh/install.sh --skills` SHALL разрешать источник superpowers-скиллов в
|
||||
порядке приоритета. Сеть нужна только ветке пина; явный источник
|
||||
синхронизируется локально.
|
||||
|
||||
- Порядок: `--superpowers-from <dir>` → `$DSH_SUPERPOWERS_SOURCE` →
|
||||
клон `$DSH_HOME/superpowers` на `$DSH_SUPERPOWERS_REF` →
|
||||
legacy-кэш Claude Code → legacy `~/.claude/skills`.
|
||||
- Явный источник (`--superpowers-from`, `$DSH_SUPERPOWERS_SOURCE`) MUST NOT
|
||||
требовать сети и MUST NOT проверять пин. Допускаются две формы: корень
|
||||
checkout (есть `skills/using-superpowers/SKILL.md`) или каталог `skills`
|
||||
такого корня. Синк явного источника MUST идти в staging вне каталога клона
|
||||
(`$DSH_HOME/.skills.synced`), потому что `$DSH_HOME/superpowers` зарезервирован
|
||||
под клон пина.
|
||||
- Каталог скиллов, который не является ни checkout-корнем, ни его `skills/`,
|
||||
MUST быть `FAIL` (код 1) и в `--skills`, и в `--check`: вердикты двух команд
|
||||
MUST совпадать. Тихий откат на legacy запрещён.
|
||||
- Клон-пин: отсутствующий или пустой каталог MUST клонироваться
|
||||
`git clone --depth 1 --branch <ref> <repo> <dir>`; неудачный клон MUST
|
||||
удалять недоклонированный каталог (полусостояние не остаётся).
|
||||
- Существующий клон MUST обновляться fetch'ем от `$DSH_SUPERPOWERS_REPO` (не от
|
||||
origin клона) с force-рефспеком тега
|
||||
(`+refs/tags/<ref>:refs/tags/<ref>`) и checkout `FETCH_HEAD`: смена
|
||||
репозитория и переехавший тег MUST подхватываться следующим прогоном.
|
||||
Офлайн клон MAY переиспользоваться, только если HEAD уже ровно на пине, и
|
||||
тогда MUST печататься `WARN`; иначе — `FAIL` с командой клона.
|
||||
- Непустой каталог на месте клона без рабочего `.git` MUST быть `FAIL` (код 1)
|
||||
без изменения каталога.
|
||||
- Копирование MUST быть заменой каталога назначения (`rm -rf` + `cp -R`), а не
|
||||
симлинком: `~/.dsh/skills` не должен зависеть от каталога-источника.
|
||||
|
||||
#### Scenario: Явный источник без ~/.claude
|
||||
- **WHEN** `HOME` — пустой temp-каталог без `~/.claude`, а `--superpowers-from`
|
||||
указывает на каталог superpowers
|
||||
- **THEN** `--skills` синхронизирует и копирует скиллы в `$DSH_HOME/skills`,
|
||||
печатает источник и завершается кодом 0, не обращаясь в сеть
|
||||
|
||||
#### Scenario: Переменная окружения вместо флага
|
||||
- **WHEN** задан `DSH_SUPERPOWERS_SOURCE=<dir>` и флаг `--superpowers-from` не передан
|
||||
- **THEN** источник берётся из переменной; поведение совпадает с флагом
|
||||
|
||||
#### Scenario: Флаг важнее переменной
|
||||
- **WHEN** заданы и `--superpowers-from`, и `DSH_SUPERPOWERS_SOURCE`
|
||||
- **THEN** используется каталог из флага
|
||||
|
||||
#### Scenario: Невалидный явный источник
|
||||
- **WHEN** `--superpowers-from` указывает на каталог без
|
||||
`using-superpowers/SKILL.md`
|
||||
- **THEN** `--skills` печатает `FAIL` с ожидаемой структурой и завершается
|
||||
кодом 1, ничего не копируя из legacy-источников
|
||||
|
||||
#### Scenario: Голый каталог скиллов
|
||||
- **WHEN** явный источник — каталог `my-skills` с
|
||||
`using-superpowers/SKILL.md`, но без checkout-корня
|
||||
- **THEN** и `--skills`, и `--check` печатают `FAIL: explicit superpowers source
|
||||
must be a checkout root or its skills/ directory` и завершаются кодом 1
|
||||
|
||||
#### Scenario: Пиннинг в ~/.dsh
|
||||
- **WHEN** существует клон `$DSH_HOME/superpowers` с
|
||||
`skills/using-superpowers/SKILL.md` и флаг/переменная не заданы
|
||||
- **THEN** источником служит этот клон, а `--check` офлайн печатает
|
||||
`ok: superpowers source (<path>)` и ref из `.sync-meta.json`
|
||||
|
||||
#### Scenario: Обновление клона от DSH_SUPERPOWERS_REPO
|
||||
- **WHEN** клон уже есть, а `DSH_SUPERPOWERS_REPO` сменился или тег переехал
|
||||
- **THEN** прогон фетчит от `DSH_SUPERPOWERS_REPO` с force-рефспеком, делает
|
||||
checkout `FETCH_HEAD` и устанавливает текст новой версии тега
|
||||
|
||||
#### Scenario: Офлайн-переиспользование клона
|
||||
- **WHEN** клон существует, HEAD ровно на пине, сеть недоступна
|
||||
- **THEN** `--skills` печатает `WARN: offline; reusing the existing clone` и
|
||||
завершается кодом 0
|
||||
|
||||
#### Scenario: Каталог без .git на месте клона
|
||||
- **WHEN** `$DSH_HOME/superpowers` непустой и не является git-клоном
|
||||
- **THEN** `--skills` завершается `FAIL` (код 1), каталог не изменяется
|
||||
|
||||
### Requirement: Отсутствие источника не бывает тихим
|
||||
Если источник superpowers недоступен, `--skills` MUST завершаться `FAIL`, кроме
|
||||
случая, когда существует legacy-источник.
|
||||
|
||||
- Офлайн, без явного источника, без клона на пине и без legacy MUST печататься
|
||||
`FAIL: no superpowers source` с готовой командой клона
|
||||
(`git clone --depth 1 --branch <ref> <repo> <DSH_HOME>/superpowers`) и код
|
||||
возврата 1. Тихий успех запрещён даже при уже установленных копиях.
|
||||
- Уже установленные копии в `$DSH_HOME/skills` MUST NOT удаляться или
|
||||
портиться при таком `FAIL`.
|
||||
- Если legacy-источник существует — скиллы копируются сырым текстом с
|
||||
`WARN: legacy superpowers source` и кодом 0 (путь живёт до #66).
|
||||
- Локальные скиллы обвязки (`dsh/skills/*`: `role`, `sdd`) MUST ставиться в
|
||||
любом исходе: они не зависят от superpowers.
|
||||
|
||||
#### Scenario: Первая установка без источника
|
||||
- **WHEN** `HOME` пуст, `$DSH_HOME/skills` не существует, клон получить не
|
||||
удалось, legacy нет
|
||||
- **THEN** `--skills` печатает `FAIL: no superpowers source` и команду клона,
|
||||
код 1, но `$DSH_HOME/skills/role/SKILL.md` и `$DSH_HOME/skills/sdd/SKILL.md`
|
||||
на месте
|
||||
|
||||
#### Scenario: Повторная установка без источника
|
||||
- **WHEN** `$DSH_HOME/skills/using-superpowers/SKILL.md` уже есть, а клон
|
||||
получить не удалось
|
||||
- **THEN** `--skills` печатает `FAIL: no superpowers source`, код 1,
|
||||
установленный файл не изменён (раньше здесь был `WARN` и код 0)
|
||||
|
||||
### Requirement: ~/.claude — только legacy-fallback с предупреждением
|
||||
superpowers-скиллы и роли MUST иметь рабочий путь `~/.dsh`; пути `~/.claude`
|
||||
SHALL оставаться fallback'ом для машин, где `--skills --roles` ещё не
|
||||
перезапускали, и MUST сопровождаться предупреждением.
|
||||
|
||||
- Установка: legacy-кэш Claude Code и `~/.claude/skills` MAY быть источником
|
||||
только после того, как клон пина получить не удалось; копирование в этом
|
||||
случае — сырое (без sync и verify), с
|
||||
`WARN: legacy superpowers source` и подсказкой перейти на пин/явный источник.
|
||||
- `--check`: legacy-источник — `WARN`, явный/пин — `ok`, отсутствие — `WARN` с
|
||||
подсказкой клона, невалидный явный источник — `FAIL` и код 1
|
||||
(пользователь задал его сам, подсказка «нет источника» здесь неверна).
|
||||
- Runtime: bootstrap и резолвер ролей сохраняют порядок `~/.dsh` → `~/.claude`,
|
||||
но при выборе legacy-пути один раз за процесс пишут в лог предупреждение.
|
||||
- Удаление fallback'ов — отдельное изменение (#66); в этом change они не
|
||||
удаляются.
|
||||
|
||||
#### Scenario: Legacy-кэш как источник установки
|
||||
- **WHEN** в `HOME` есть только кэш Claude Code с superpowers, `$DSH_HOME` пуст
|
||||
- **THEN** `--skills` копирует скиллы и печатает `WARN: legacy superpowers
|
||||
source`
|
||||
|
||||
#### Scenario: Runtime-предупреждение
|
||||
- **WHEN** bootstrap резолвит `using-superpowers` из `~/.claude/...`
|
||||
- **THEN** в лог один раз за процесс попадает предупреждение с предложением
|
||||
выполнить `dsh/install.sh --skills`
|
||||
|
||||
#### Scenario: ~/.dsh приоритетнее
|
||||
- **WHEN** существуют и `~/.dsh/skills/using-superpowers/SKILL.md`, и legacy-копия
|
||||
- **THEN** используется `~/.dsh`, предупреждения нет
|
||||
Loading…
x
Reference in New Issue
Block a user