diff --git a/docs/superpowers/plans/2026-10-24-dsh-role-loader.md b/docs/superpowers/plans/2026-10-24-dsh-role-loader.md new file mode 100644 index 0000000..c99fe26 --- /dev/null +++ b/docs/superpowers/plans/2026-10-24-dsh-role-loader.md @@ -0,0 +1,60 @@ +# DSH role loader — план + +Ветка: `feat/39-dsh-role-loader` (issue #39). +Спека/дизайн: `dsh/PORT.md`. + +## Цель + +Дать модели DSH штатный способ загрузить роль: skill `role`, который читает +`~/.dsh/roles/.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/`; прочитать файл; объявить «Роль: ; если не так — скажи». + - Не вызывать `/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`. diff --git a/dsh/PORT.md b/dsh/PORT.md index 5f950c0..6783fe8 100644 --- a/dsh/PORT.md +++ b/dsh/PORT.md @@ -1,9 +1,10 @@ # DSH port — design and spike notes -Status: **spike 0.1.0, live guard/require-plan/bootstrap verified; `/role` and -fresh-session acceptance pending**. This directory is the -DeepSeek Harness adapter for `claude-harness`; the Claude Code installation and -the repository's source of truth are unchanged. +Status: **spike 0.2.0, live guard/require-plan/bootstrap verified; `/role` +command works, the model-invocable `role` skill is added but its live +acceptance is pending**. This directory is the DeepSeek Harness adapter for +`claude-harness`; the Claude Code installation and the repository's source of +truth are unchanged. ## 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 | | 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 | | Superpowers | plugin + `SessionStart` hook | skills + `agent/pre-step` bootstrap | content 1:1, bootstrap new | | 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 ``, and injects it once per session. A content-hash marker handles dedupe; scanning `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/.md`, falls back to `~/.claude/roles/.md`, then to the repository's `roles/`, and reads the project `.claude/roles/.md`. 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 "Читать на - старте" section is left to the model. + five-line summary. Because a model cannot call a slash command, + `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 be installed through the Plugins page and codegraph stays available. @@ -88,7 +91,8 @@ dsh/ locale/{en,ru}.json icon.svg 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 ``` @@ -282,8 +286,8 @@ profile was removed; the real `desktop` profile was not touched. ## 8. Next phases -1. Finish the live smoke test: `/role dev`, the fresh-session superpowers - acceptance, and bootstrap re-injection after `/compact`. +1. Finish the live smoke test: the fresh-session `role` skill acceptance, plus + the bootstrap and `/compact` checks already verified. 2. Add `harness-check` and optional `role-hint` as non-blocking pre-step injections. 3. Build `briefing` as an agent preset (read-only tools + model route). diff --git a/dsh/README.md b/dsh/README.md index 571382a..6373bff 100644 --- a/dsh/README.md +++ b/dsh/README.md @@ -11,8 +11,10 @@ separate Cordis bundle plus a thin install helper. Claude Code; - superpowers bootstrap — `using-superpowers` is injected once per session from `agent/pre-step` and re-injected after compaction; -- `/role dev|admin|analyst|qa` — reads both role layers and injects the role - text, without Claude's auto-memory step. +- roles — the model loads them with the `role` skill (`~/.dsh/roles`, then + `~/.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`, `harness-check`, `role-hint`, `harness-doctor`, auto-memory, caveman. See @@ -24,7 +26,7 @@ separate Cordis bundle plus a thin install helper. # Static 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) bash dsh/install.sh --global bash dsh/install.sh --skills @@ -47,7 +49,8 @@ manager owns those writes. `BLOCKED [guard-main]`. 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. -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 any code. @@ -124,6 +127,9 @@ repository layout: plugin cache is only a fallback, so a partial Claude Code rollback does not break the bootstrap. Set `bootstrapSkillPath` or `DSH_SUPERPOWERS_SKILL` for 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 models. Map them to live provider/model routes before porting SDD. - SDD is intentionally out of scope for this spike; see diff --git a/dsh/install.sh b/dsh/install.sh index 2aff5e6..fdcebd6 100755 --- a/dsh/install.sh +++ b/dsh/install.sh @@ -32,7 +32,7 @@ Usage: bash dsh/install.sh [--check] [--global] [--skills] [--roles] [--all] --check verify python3, git, node, hooks, bundle, skills, and roles --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 --all --check + --global + --skills + --roles, then print the bundle step @@ -109,6 +109,11 @@ check() { else echo "WARN: superpowers plugin not found; the bootstrap will be skipped" >&2 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 echo "ok: roles ($DSH_HOME/roles)" elif [ -f "$HOME/.claude/roles/dev.md" ]; then @@ -134,9 +139,10 @@ install_global() { echo "# DeepSeek Harness global instructions" echo 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 "DSH adapter: role hints come from the first message or /role; there is no SessionStart hint." - echo "DSH adapter: /role reads ~/.dsh/roles, then ~/.claude/roles, then $ROOT/roles." + echo "The dsh-harness bundle enforces guard-main/require-plan and provides the /role command." + echo "DSH adapter: the model loads roles with skill role; /role is the user-facing command." + 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: codegraph MCP tools are named mcp__codegraph__codegraph_explore." echo @@ -165,6 +171,23 @@ install_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() { mkdir -p "$DSH_HOME/roles" local src="$ROOT/roles" @@ -206,6 +229,7 @@ if [ "$DO_GLOBAL" = 1 ]; then fi if [ "$DO_SKILLS" = 1 ]; then install_skills + install_local_skills fi if [ "$DO_ROLES" = 1 ]; then install_roles diff --git a/dsh/skills/role/SKILL.md b/dsh/skills/role/SKILL.md new file mode 100644 index 0000000..bd7ecaf --- /dev/null +++ b/dsh/skills/role/SKILL.md @@ -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/.md` — основной слой DSH; +2. `~/.claude/roles/.md` — fallback, если DSH-копии нет; +3. `./.claude/roles/.md` — проектный слой, если есть; его правила важнее общего. + +Если ни один файл не найден — скажи одной строкой «файла роли нет» и +продолжай без роли. Строки `brief:` пропускай: агент `briefing` в DSH не +портирован. + +## 3. Объяви и следуй + +Ответь коротко: + +- `Роль: ; если не так — скажи.` +- процесс: одна строка из раздела «Процесс»; +- прочитано: какие файлы роли загружены; +- не делать: одна строка из раздела «Не делать». + +Дальше следуй файлу роли. Новая роль заменяет предыдущую целиком. diff --git a/dsh/tests/install.test.mjs b/dsh/tests/install.test.mjs index 4b62f66..1a1e819 100644 --- a/dsh/tests/install.test.mjs +++ b/dsh/tests/install.test.mjs @@ -32,6 +32,7 @@ test('install.sh copies skills and roles instead of linking into ~/.claude', () const first = run() assert.equal(first.status, 0, first.stderr) assert.match(first.stdout, /copied 2 superpowers skills/) + assert.match(first.stdout, /copied 1 local skills/) assert.match(first.stdout, /installed 4 roles/) 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.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']) { 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() assert.equal(second.status, 0, second.stderr) assert.equal(lstatSync(skill).isSymbolicLink(), false) + assert.ok(existsSync(roleSkill)) } finally { cleanup(home) } diff --git a/dsh/tests/role-skill.test.mjs b/dsh/tests/role-skill.test.mjs new file mode 100644 index 0000000..54aa275 --- /dev/null +++ b/dsh/tests/role-skill.test.mjs @@ -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, /модель её не вызывает/) +})