DSH: model-invocable role loader (#39) #40
60
docs/superpowers/plans/2026-10-24-dsh-role-loader.md
Normal file
60
docs/superpowers/plans/2026-10-24-dsh-role-loader.md
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
# DSH role loader — план
|
||||||
|
|
||||||
|
Ветка: `feat/39-dsh-role-loader` (issue #39).
|
||||||
|
Спека/дизайн: `dsh/PORT.md`.
|
||||||
|
|
||||||
|
## Цель
|
||||||
|
|
||||||
|
Дать модели DSH штатный способ загрузить роль: skill `role`, который читает
|
||||||
|
`~/.dsh/roles/<r>.md`. `/role` остаётся пользовательской командой; глобальные
|
||||||
|
инструкции больше не требуют от модели вызвать slash-команду.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
1. `dsh/skills/role/SKILL.md` — model-invocable инструкция: определить роль,
|
||||||
|
прочитать файл, объявить роль, следовать процессу.
|
||||||
|
2. `dsh/install.sh --skills`: копировать локальные `dsh/skills/*` в
|
||||||
|
`$DSH_HOME/skills` вместе с superpowers.
|
||||||
|
3. `install_global`: adapter-строка «модель использует skill `role`; `/role` —
|
||||||
|
пользовательская команда».
|
||||||
|
4. Тесты: наличие/содержимое skill, install-копирование, `--check`.
|
||||||
|
5. README/PORT.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Кастомный `ctx.tools.register('role')` — отдельный вариант, если skill
|
||||||
|
окажется недостаточно.
|
||||||
|
- Автоматическая инъекция роли в `agent/pre-step` — возможный следующий шаг.
|
||||||
|
- Изменение `global/CLAUDE.md` (он общий с Claude Code и ограничен 3 КБ).
|
||||||
|
|
||||||
|
## Задачи
|
||||||
|
|
||||||
|
- [x] **Task 1: skill `role`**
|
||||||
|
- Create: `dsh/skills/role/SKILL.md`
|
||||||
|
- Frontmatter: `name: role`, description для каталога.
|
||||||
|
- Body: роль из первого сообщения; пути `~/.dsh/roles`, `~/.claude/roles`,
|
||||||
|
repo `roles/`; прочитать файл; объявить «Роль: <r>; если не так — скажи».
|
||||||
|
- Не вызывать `/role`; это команда пользователя.
|
||||||
|
- [x] **Task 2: install.sh**
|
||||||
|
- Modify: `dsh/install.sh`
|
||||||
|
- `install_local_skills()` копирует `$ROOT/dsh/skills/*` в `$DSH_HOME/skills`.
|
||||||
|
- `--skills` вызывает superpowers + local; `--all` уже включает.
|
||||||
|
- `check` проверяет `$DSH_HOME/skills/role/SKILL.md`.
|
||||||
|
- `install_global` добавляет adapter-строку про skill `role`.
|
||||||
|
- [x] **Task 3: тесты**
|
||||||
|
- Modify: `dsh/tests/install.test.mjs` — role skill скопирован.
|
||||||
|
- Add: `dsh/tests/role-skill.test.mjs` — frontmatter, пути, запрет `/role`.
|
||||||
|
- [x] **Task 4: docs**
|
||||||
|
- Modify: `dsh/README.md`, `dsh/PORT.md` — skill `role` vs `/role`.
|
||||||
|
- [x] **Task 5: проверки и коммит**
|
||||||
|
- `bash dsh/tests/run.sh`
|
||||||
|
- `bash tests/run.sh`
|
||||||
|
- Commit, push, PR #40 (base main).
|
||||||
|
|
||||||
|
## Acceptance
|
||||||
|
|
||||||
|
Свежая сессия DSH, «Let's make a react todo list»:
|
||||||
|
|
||||||
|
- модель вызывает `skill({name:"role"})`;
|
||||||
|
- читает `~/.dsh/roles/dev.md`;
|
||||||
|
- объявляет роль и следует процессу, без ручного `/role`.
|
||||||
26
dsh/PORT.md
26
dsh/PORT.md
@ -1,9 +1,10 @@
|
|||||||
# DSH port — design and spike notes
|
# DSH port — design and spike notes
|
||||||
|
|
||||||
Status: **spike 0.1.0, live guard/require-plan/bootstrap verified; `/role` and
|
Status: **spike 0.2.0, live guard/require-plan/bootstrap verified; `/role`
|
||||||
fresh-session acceptance pending**. This directory is the
|
command works, the model-invocable `role` skill is added but its live
|
||||||
DeepSeek Harness adapter for `claude-harness`; the Claude Code installation and
|
acceptance is pending**. This directory is the DeepSeek Harness adapter for
|
||||||
the repository's source of truth are unchanged.
|
`claude-harness`; the Claude Code installation and the repository's source of
|
||||||
|
truth are unchanged.
|
||||||
|
|
||||||
## 1. Verdict
|
## 1. Verdict
|
||||||
|
|
||||||
@ -13,7 +14,7 @@ This is a port, not a copy. Three layers map differently:
|
|||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| Instructions | `~/.claude/CLAUDE.md`, project `CLAUDE.md` | `$DSH_HOME/AGENTS.md`, project `CLAUDE.md`/`AGENTS.md`, nested | almost 1:1 |
|
| Instructions | `~/.claude/CLAUDE.md`, project `CLAUDE.md` | `$DSH_HOME/AGENTS.md`, project `CLAUDE.md`/`AGENTS.md`, nested | almost 1:1 |
|
||||||
| Skills | `~/.claude/skills`, `.claude/skills` | `~/.dsh/skills`, `.agents/skills`, `customSkillDirs` | 1:1 format |
|
| Skills | `~/.claude/skills`, `.claude/skills` | `~/.dsh/skills`, `.agents/skills`, `customSkillDirs` | 1:1 format |
|
||||||
| Roles | `roles/`, `/role` skill | same files + a real `/role` command | close |
|
| 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 |
|
| 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 | skills + `agent/pre-step` bootstrap | content 1:1, bootstrap new |
|
||||||
| SDD | `Task` (synchronous) | `subagent` (asynchronous) / `workflow` | semantics differ |
|
| SDD | `Task` (synchronous) | `subagent` (asynchronous) / `workflow` | semantics differ |
|
||||||
@ -41,12 +42,14 @@ file has to be installed as `$DSH_HOME/AGENTS.md` because DSH does not read
|
|||||||
`bundle/dsh-tools.md`, wraps the result in `<EXTREMELY_IMPORTANT>`, and
|
`bundle/dsh-tools.md`, wraps the result in `<EXTREMELY_IMPORTANT>`, and
|
||||||
injects it once per session. A content-hash marker handles dedupe; scanning
|
injects it once per session. A content-hash marker handles dedupe; scanning
|
||||||
`session.surface.nodes` re-injects after compaction.
|
`session.surface.nodes` re-injects after compaction.
|
||||||
3. **`/role`** as a real command (`ctx.commands.register`). It reads
|
3. **Roles** — `/role` is a real command (`ctx.commands.register`); it reads
|
||||||
`~/.dsh/roles/<role>.md`, falls back to `~/.claude/roles/<role>.md`, then to
|
`~/.dsh/roles/<role>.md`, falls back to `~/.claude/roles/<role>.md`, then to
|
||||||
the repository's `roles/`, and reads the project `.claude/roles/<role>.md`.
|
the repository's `roles/`, and reads the project `.claude/roles/<role>.md`.
|
||||||
It injects the role text as a user message and starts a turn for the
|
It injects the role text as a user message and starts a turn for the
|
||||||
five-line summary. Auto-memory is explicitly dropped; the role's "Читать на
|
five-line summary. Because a model cannot call a slash command,
|
||||||
старте" section is left to the model.
|
`skills/role/SKILL.md` is the model-invocable loader: it infers the role,
|
||||||
|
reads the file, and tells the model to follow it. Auto-memory is explicitly
|
||||||
|
dropped; the role's "Читать на старте" section is left to the model.
|
||||||
4. **Display metadata and an optional CodeGraph MCP overlay** so the bundle can
|
4. **Display metadata and an optional CodeGraph MCP overlay** so the bundle can
|
||||||
be installed through the Plugins page and codegraph stays available.
|
be installed through the Plugins page and codegraph stays available.
|
||||||
|
|
||||||
@ -88,7 +91,8 @@ dsh/
|
|||||||
locale/{en,ru}.json
|
locale/{en,ru}.json
|
||||||
icon.svg
|
icon.svg
|
||||||
tests/ # Node tests, no DSH runtime needed
|
tests/ # Node tests, no DSH runtime needed
|
||||||
install.sh # --check / --global / --skills / --all
|
skills/role/SKILL.md # model-invocable role loader (installed to ~/.dsh)
|
||||||
|
install.sh # --check / --global / --skills / --roles / --all
|
||||||
mcp.codegraph.yml # optional CodeGraph MCP row
|
mcp.codegraph.yml # optional CodeGraph MCP row
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -282,8 +286,8 @@ profile was removed; the real `desktop` profile was not touched.
|
|||||||
|
|
||||||
## 8. Next phases
|
## 8. Next phases
|
||||||
|
|
||||||
1. Finish the live smoke test: `/role dev`, the fresh-session superpowers
|
1. Finish the live smoke test: the fresh-session `role` skill acceptance, plus
|
||||||
acceptance, and bootstrap re-injection after `/compact`.
|
the bootstrap and `/compact` checks already verified.
|
||||||
2. Add `harness-check` and optional `role-hint` as non-blocking pre-step
|
2. Add `harness-check` and optional `role-hint` as non-blocking pre-step
|
||||||
injections.
|
injections.
|
||||||
3. Build `briefing` as an agent preset (read-only tools + model route).
|
3. Build `briefing` as an agent preset (read-only tools + model route).
|
||||||
|
|||||||
@ -11,8 +11,10 @@ separate Cordis bundle plus a thin install helper.
|
|||||||
Claude Code;
|
Claude Code;
|
||||||
- superpowers bootstrap — `using-superpowers` is injected once per session from
|
- superpowers bootstrap — `using-superpowers` is injected once per session from
|
||||||
`agent/pre-step` and re-injected after compaction;
|
`agent/pre-step` and re-injected after compaction;
|
||||||
- `/role dev|admin|analyst|qa` — reads both role layers and injects the role
|
- roles — the model loads them with the `role` skill (`~/.dsh/roles`, then
|
||||||
text, without Claude's auto-memory step.
|
`~/.claude/roles`, then repo `roles/`); `/role dev|admin|analyst|qa` remains
|
||||||
|
the user-facing command and injects the same text, without Claude's
|
||||||
|
auto-memory step.
|
||||||
|
|
||||||
**Not yet:** SDD (DSH subagents are asynchronous), `briefing`,
|
**Not yet:** SDD (DSH subagents are asynchronous), `briefing`,
|
||||||
`harness-check`, `role-hint`, `harness-doctor`, auto-memory, caveman. See
|
`harness-check`, `role-hint`, `harness-doctor`, auto-memory, caveman. See
|
||||||
@ -24,7 +26,7 @@ separate Cordis bundle plus a thin install helper.
|
|||||||
# Static check
|
# Static check
|
||||||
bash dsh/install.sh --check
|
bash dsh/install.sh --check
|
||||||
|
|
||||||
# Global instructions, superpowers skills, and roles
|
# Global instructions, superpowers skills, local role skill, and roles
|
||||||
# (run from a terminal; these write outside the session workspace)
|
# (run from a terminal; these write outside the session workspace)
|
||||||
bash dsh/install.sh --global
|
bash dsh/install.sh --global
|
||||||
bash dsh/install.sh --skills
|
bash dsh/install.sh --skills
|
||||||
@ -47,7 +49,8 @@ manager owns those writes.
|
|||||||
`BLOCKED [guard-main]`.
|
`BLOCKED [guard-main]`.
|
||||||
3. Create a worktree on a feature branch, ask for an edit under `src/` — expect
|
3. Create a worktree on a feature branch, ask for an edit under `src/` — expect
|
||||||
`BLOCKED [require-plan]` until `.bounded` or a plan for the branch exists.
|
`BLOCKED [require-plan]` until `.bounded` or a plan for the branch exists.
|
||||||
4. `/role dev` — expect the five-line role summary.
|
4. The model should load a role with `skill({name:"role"})`; `/role dev` is the
|
||||||
|
user-facing command with the same file. Expect the five-line role summary.
|
||||||
5. Prompt `Let's make a react todo list` — `brainstorming` must trigger before
|
5. Prompt `Let's make a react todo list` — `brainstorming` must trigger before
|
||||||
any code.
|
any code.
|
||||||
|
|
||||||
@ -124,6 +127,9 @@ repository layout:
|
|||||||
plugin cache is only a fallback, so a partial Claude Code rollback does not
|
plugin cache is only a fallback, so a partial Claude Code rollback does not
|
||||||
break the bootstrap. Set `bootstrapSkillPath` or `DSH_SUPERPOWERS_SKILL` for
|
break the bootstrap. Set `bootstrapSkillPath` or `DSH_SUPERPOWERS_SKILL` for
|
||||||
another layout.
|
another layout.
|
||||||
|
- Role loading is model-driven through the `role` skill; there is no automatic
|
||||||
|
`agent/pre-step` injection. If the model skips the skill, use the `/role`
|
||||||
|
command or ask it to load the role explicitly.
|
||||||
- Model names in roles and superpowers (`sonnet`, `haiku`, `opus`) are not DSH
|
- Model names in roles and superpowers (`sonnet`, `haiku`, `opus`) are not DSH
|
||||||
models. Map them to live provider/model routes before porting SDD.
|
models. Map them to live provider/model routes before porting SDD.
|
||||||
- SDD is intentionally out of scope for this spike; see
|
- SDD is intentionally out of scope for this spike; see
|
||||||
|
|||||||
@ -32,7 +32,7 @@ Usage: bash dsh/install.sh [--check] [--global] [--skills] [--roles] [--all]
|
|||||||
|
|
||||||
--check verify python3, git, node, hooks, bundle, skills, and roles
|
--check verify python3, git, node, hooks, bundle, skills, and roles
|
||||||
--global back up and write \$DSH_HOME/AGENTS.md from global/CLAUDE.md
|
--global back up and write \$DSH_HOME/AGENTS.md from global/CLAUDE.md
|
||||||
--skills copy installed superpowers skills into \$DSH_HOME/skills
|
--skills copy superpowers skills and local dsh/skills into \$DSH_HOME/skills
|
||||||
--roles copy roles/ into \$DSH_HOME/roles
|
--roles copy roles/ into \$DSH_HOME/roles
|
||||||
--all --check + --global + --skills + --roles, then print the bundle step
|
--all --check + --global + --skills + --roles, then print the bundle step
|
||||||
|
|
||||||
@ -109,6 +109,11 @@ check() {
|
|||||||
else
|
else
|
||||||
echo "WARN: superpowers plugin not found; the bootstrap will be skipped" >&2
|
echo "WARN: superpowers plugin not found; the bootstrap will be skipped" >&2
|
||||||
fi
|
fi
|
||||||
|
if [ -f "$DSH_HOME/skills/role/SKILL.md" ]; then
|
||||||
|
echo "ok: role skill"
|
||||||
|
else
|
||||||
|
echo "WARN: role skill not installed; run --skills" >&2
|
||||||
|
fi
|
||||||
if [ -f "$DSH_HOME/roles/dev.md" ]; then
|
if [ -f "$DSH_HOME/roles/dev.md" ]; then
|
||||||
echo "ok: roles ($DSH_HOME/roles)"
|
echo "ok: roles ($DSH_HOME/roles)"
|
||||||
elif [ -f "$HOME/.claude/roles/dev.md" ]; then
|
elif [ -f "$HOME/.claude/roles/dev.md" ]; then
|
||||||
@ -134,9 +139,10 @@ install_global() {
|
|||||||
echo "# DeepSeek Harness global instructions"
|
echo "# DeepSeek Harness global instructions"
|
||||||
echo
|
echo
|
||||||
echo "Installed from $ROOT/global/CLAUDE.md by dsh/install.sh."
|
echo "Installed from $ROOT/global/CLAUDE.md by dsh/install.sh."
|
||||||
echo "The dsh-harness bundle enforces guard-main/require-plan and provides /role."
|
echo "The dsh-harness bundle enforces guard-main/require-plan and provides the /role command."
|
||||||
echo "DSH adapter: role hints come from the first message or /role; there is no SessionStart hint."
|
echo "DSH adapter: the model loads roles with skill role; /role is the user-facing command."
|
||||||
echo "DSH adapter: /role reads ~/.dsh/roles, then ~/.claude/roles, then $ROOT/roles."
|
echo "DSH adapter: there is no SessionStart role hint; infer the role from the first message."
|
||||||
|
echo "DSH adapter: role files are ~/.dsh/roles, then ~/.claude/roles, then $ROOT/roles."
|
||||||
echo "DSH adapter: bootstrap prefers ~/.dsh/skills; the ~/.claude cache is a fallback."
|
echo "DSH adapter: bootstrap prefers ~/.dsh/skills; the ~/.claude cache is a fallback."
|
||||||
echo "DSH adapter: codegraph MCP tools are named mcp__codegraph__codegraph_explore."
|
echo "DSH adapter: codegraph MCP tools are named mcp__codegraph__codegraph_explore."
|
||||||
echo
|
echo
|
||||||
@ -165,6 +171,23 @@ install_skills() {
|
|||||||
echo "copied $count superpowers skills into $DSH_HOME/skills"
|
echo "copied $count superpowers skills into $DSH_HOME/skills"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
install_local_skills() {
|
||||||
|
local src="$ROOT/dsh/skills"
|
||||||
|
[ -d "$src" ] || return 0
|
||||||
|
mkdir -p "$DSH_HOME/skills"
|
||||||
|
local count=0
|
||||||
|
local dir name dest
|
||||||
|
for dir in "$src"/*; do
|
||||||
|
[ -f "$dir/SKILL.md" ] || continue
|
||||||
|
name="$(basename "$dir")"
|
||||||
|
dest="$DSH_HOME/skills/$name"
|
||||||
|
rm -rf "$dest"
|
||||||
|
cp -R "$dir" "$dest"
|
||||||
|
count=$((count + 1))
|
||||||
|
done
|
||||||
|
echo "copied $count local skills into $DSH_HOME/skills"
|
||||||
|
}
|
||||||
|
|
||||||
install_roles() {
|
install_roles() {
|
||||||
mkdir -p "$DSH_HOME/roles"
|
mkdir -p "$DSH_HOME/roles"
|
||||||
local src="$ROOT/roles"
|
local src="$ROOT/roles"
|
||||||
@ -206,6 +229,7 @@ if [ "$DO_GLOBAL" = 1 ]; then
|
|||||||
fi
|
fi
|
||||||
if [ "$DO_SKILLS" = 1 ]; then
|
if [ "$DO_SKILLS" = 1 ]; then
|
||||||
install_skills
|
install_skills
|
||||||
|
install_local_skills
|
||||||
fi
|
fi
|
||||||
if [ "$DO_ROLES" = 1 ]; then
|
if [ "$DO_ROLES" = 1 ]; then
|
||||||
install_roles
|
install_roles
|
||||||
|
|||||||
43
dsh/skills/role/SKILL.md
Normal file
43
dsh/skills/role/SKILL.md
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
name: role
|
||||||
|
description: Загрузить роль сессии (dev, admin, analyst, qa) из ~/.dsh/roles и следовать её процессу. Вызывать в начале сессии, когда роль очевидна из первого сообщения, или по просьбе владельца.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Роль сессии
|
||||||
|
|
||||||
|
`/role` — пользовательская команда; модель её не вызывает. Этот skill — штатный
|
||||||
|
способ загрузить роль.
|
||||||
|
|
||||||
|
## 1. Определи роль
|
||||||
|
|
||||||
|
По первому сообщению:
|
||||||
|
|
||||||
|
- Jira, Гант, спринт → `admin`;
|
||||||
|
- стенд, прогон, скриншот, макет → `qa`;
|
||||||
|
- ТЗ, OQ, спека, методика → `analyst`;
|
||||||
|
- срез, issue, PR, тест, миграция, «сделай фичу» → `dev`.
|
||||||
|
|
||||||
|
Если роль не очевидна — задай один вопрос с четырьмя вариантами и остановись.
|
||||||
|
|
||||||
|
## 2. Прочитай файл роли
|
||||||
|
|
||||||
|
Порядок:
|
||||||
|
|
||||||
|
1. `~/.dsh/roles/<r>.md` — основной слой DSH;
|
||||||
|
2. `~/.claude/roles/<r>.md` — fallback, если DSH-копии нет;
|
||||||
|
3. `./.claude/roles/<r>.md` — проектный слой, если есть; его правила важнее общего.
|
||||||
|
|
||||||
|
Если ни один файл не найден — скажи одной строкой «файла роли <r> нет» и
|
||||||
|
продолжай без роли. Строки `brief:` пропускай: агент `briefing` в DSH не
|
||||||
|
портирован.
|
||||||
|
|
||||||
|
## 3. Объяви и следуй
|
||||||
|
|
||||||
|
Ответь коротко:
|
||||||
|
|
||||||
|
- `Роль: <r>; если не так — скажи.`
|
||||||
|
- процесс: одна строка из раздела «Процесс»;
|
||||||
|
- прочитано: какие файлы роли загружены;
|
||||||
|
- не делать: одна строка из раздела «Не делать».
|
||||||
|
|
||||||
|
Дальше следуй файлу роли. Новая роль заменяет предыдущую целиком.
|
||||||
@ -32,6 +32,7 @@ test('install.sh copies skills and roles instead of linking into ~/.claude', ()
|
|||||||
const first = run()
|
const first = run()
|
||||||
assert.equal(first.status, 0, first.stderr)
|
assert.equal(first.status, 0, first.stderr)
|
||||||
assert.match(first.stdout, /copied 2 superpowers skills/)
|
assert.match(first.stdout, /copied 2 superpowers skills/)
|
||||||
|
assert.match(first.stdout, /copied 1 local skills/)
|
||||||
assert.match(first.stdout, /installed 4 roles/)
|
assert.match(first.stdout, /installed 4 roles/)
|
||||||
|
|
||||||
const skill = join(dshHome, 'skills', 'using-superpowers')
|
const skill = join(dshHome, 'skills', 'using-superpowers')
|
||||||
@ -39,6 +40,10 @@ test('install.sh copies skills and roles instead of linking into ~/.claude', ()
|
|||||||
assert.equal(lstatSync(skill).isDirectory(), true)
|
assert.equal(lstatSync(skill).isDirectory(), true)
|
||||||
assert.match(readFileSync(join(skill, 'SKILL.md'), 'utf8'), /body/)
|
assert.match(readFileSync(join(skill, 'SKILL.md'), 'utf8'), /body/)
|
||||||
|
|
||||||
|
const roleSkill = join(dshHome, 'skills', 'role', 'SKILL.md')
|
||||||
|
assert.ok(existsSync(roleSkill), 'role skill was not installed')
|
||||||
|
assert.match(readFileSync(roleSkill, 'utf8'), /name: role/)
|
||||||
|
|
||||||
for (const role of ['dev', 'admin', 'analyst', 'qa']) {
|
for (const role of ['dev', 'admin', 'analyst', 'qa']) {
|
||||||
assert.ok(existsSync(join(dshHome, 'roles', `${role}.md`)), `missing ${role}.md`)
|
assert.ok(existsSync(join(dshHome, 'roles', `${role}.md`)), `missing ${role}.md`)
|
||||||
}
|
}
|
||||||
@ -46,6 +51,7 @@ test('install.sh copies skills and roles instead of linking into ~/.claude', ()
|
|||||||
const second = run()
|
const second = run()
|
||||||
assert.equal(second.status, 0, second.stderr)
|
assert.equal(second.status, 0, second.stderr)
|
||||||
assert.equal(lstatSync(skill).isSymbolicLink(), false)
|
assert.equal(lstatSync(skill).isSymbolicLink(), false)
|
||||||
|
assert.ok(existsSync(roleSkill))
|
||||||
} finally {
|
} finally {
|
||||||
cleanup(home)
|
cleanup(home)
|
||||||
}
|
}
|
||||||
|
|||||||
17
dsh/tests/role-skill.test.mjs
Normal file
17
dsh/tests/role-skill.test.mjs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { 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 skillPath = join(here, '..', 'skills', 'role', 'SKILL.md')
|
||||||
|
|
||||||
|
test('role skill is model-invocable and names the DSH role paths', () => {
|
||||||
|
const text = readFileSync(skillPath, 'utf8')
|
||||||
|
assert.match(text, /^---\nname: role\n/)
|
||||||
|
assert.match(text, /\ndescription: .+/)
|
||||||
|
assert.match(text, /~\/\.dsh\/roles/)
|
||||||
|
assert.match(text, /\.claude\/roles/)
|
||||||
|
assert.match(text, /модель её не вызывает/)
|
||||||
|
})
|
||||||
Loading…
x
Reference in New Issue
Block a user