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
|
||||
|
||||
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 `<EXTREMELY_IMPORTANT>`, 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/<role>.md`, falls back to `~/.claude/roles/<role>.md`, then to
|
||||
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
|
||||
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).
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
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()
|
||||
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)
|
||||
}
|
||||
|
||||
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