dsh-roles-zeta
dsh-roles-zeta gives a dsh agent one delegate tool and a folder of role
files. A call names a role; the role file supplies the child's persona, model
route, reasoning effort, and tool surface. It sits on the same ctx.subagents
service the built-in subagent tool uses, so providers, delegation depth,
continuation, and background settlement keep working unchanged. The built-in
subagent and subagent_fork tools stay mounted: they let the model pick a
route per call, while a role is fixed when the file is written.
Each role reaches its child as an Agent Preset: a copy of the calling agent's own preset with the role's persona and only the plugins its tools need (see Roles as agent presets). This needs dsh 0.2.1-rc.2 or later, where a delegation carries no persona or tool filter of its own.
Around the tool sit three surfaces, each mounted only where its host service exists: one slash command per role in the Web input box, a Role agents page in the Web settings, and a watcher on the role directory so an edit reaches the next call without recomposing the profile.
Install
dsh plugin --profile web add dsh-roles-zeta
# restart the host
Profiles are separate install roots, so install into each profile that should
have it. github:zeta987/dsh-roles-zeta and file:<path> specs work too. On
the first load the eight role files are written to $DSH_HOME/agents
($DSH_HOME defaults to ~/.dsh), and a new session has a delegate tool
whose description lists them.
~/.dsh/agents
├── code-mapper-lite.md explorer.md log-distiller.md triage.md
├── deep-coordinator.md hypothesis-debate.md reviewer.md worker.md
└── .seeded.json
Updating
Nothing updates on its own: dsh plugin forwards to pnpm inside the profile,
starting the host installs nothing, and no page checks a registry. To move to
a release:
dsh plugin --profile web add dsh-roles-zeta@0.4.0
# restart the host
Name the version. update stays inside the ^0.x range pnpm wrote at install
time, and pnpm 11+ hides a version published less than 24 hours ago from
outdated and @latest (minimumReleaseAge); an explicit version installs
right away. Role files you edited are left alone; shipped ones you never
touched are refreshed on the next load.
Slash commands
Every loaded role is a command in the Web input box. Type / to see them next
to /plan and /goal; /reviewer look at lib/index.js queues a turn asking
the agent to call delegate with role reviewer and that text as the task,
run it in the foreground, and relay the result. The delegation therefore stays
in the transcript as an ordinary tool call, where /plan <message> puts its
message too.
Commands follow the roster: adding, renaming, or removing a role re-registers
them and the menu refreshes. A role whose id collides with another plugin's
command is skipped as a command (logged) and still works through the tool.
commands: false mounts none; commandPrefix: "r-" gives /r-reviewer.

Settings page
Settings → Role agents lists the roster with a badge per role (shipped,
yours, edited) and edits one role at a time: id, the when line, the route
(with the session's allowed routes as suggestions), the effort, the tools as a
checklist of what is registered right now (or "inherit everything" plus the
delegation toggle), and the persona. Saving writes <id>.md into
$DSH_HOME/agents, the same file you would write by hand.
| Button | On disk |
|---|---|
| Delete, on a role you added | Removes the file |
| Delete, on a shipped role | Leaves a disabled: true stub so the packaged copy stops loading; listed under Disabled shipped roles until restored |
| Restore this role | Copies the shipped file back over yours |
| Restore the shipped roles | The same for all eight; roles you added are never touched |
The page is a browser module over JSON routes under /__dsh/roles-zeta/,
gated by the same browser-session cookie and Host/Origin fence as the rest of
the Web API. settingsPage: false mounts neither.

Role files
One Markdown file per role: frontmatter for the machine fields, body as the child's persona prefix.
---
id: reviewer
when: Deep read-only reviewer for correctness, regressions, edge cases, and missing tests.
route: deepseek-official/deepseek-flash
effort: high
allow: [read, glob, grep, web_search, web_fetch, skill]
---
Review like an owner.
Prioritize correctness, regressions, edge cases, and concurrency hazards.
| Field | Required | Meaning |
|---|---|---|
id |
yes | Role id; the _ spelling resolves to the same role |
when |
yes | One line; the model's menu entry and the command description |
route |
no | A model id (deepseek-flash) or an exact provider/model |
effort |
no | off/minimal/low/medium/high/xhigh/max, clamped per route |
allow |
no | Tool names the child needs; the plugins that register them are kept (per plugin, see below) and the child is then masked to exactly these names; omitted inherits everything, [] keeps no tool plugin |
delegation |
no | true keeps the delegation plugins in a child without an allow list |
disabled |
no | true removes a shipped role |
A role with an allow list can delegate only if the list names the delegation
tools; one without an allow list only with delegation: true.
Where roles load from
Two roots, later winning: the package's own roles/*.md, then
$DSH_HOME/agents. The package seeds its roster into your directory on first
load and keeps it managed while you leave it alone:
| What you do | Next load |
|---|---|
| Nothing | Shipped files follow the package; a release that changes a role updates it |
| Edit one | It is yours and is never overwritten again |
| Delete one | It is not seeded again, but the packaged copy still loads; remove the role itself with a disabled stub or the settings page |
| Add one | It loads alongside the shipped roles |
.seeded.json records what the package wrote. A file you drop in replaces the
shipped role by id; a stub with only id and disabled: true removes it. The
directory is watched, so any change is re-read after a short quiet period.
seedRolesDir: false stops the package from writing into your directory;
watchRolesDir: false reads it once per composition.
Configuration
- insert:
- id: roles-zeta
name: dsh-roles-zeta
config:
provider: spawn
toolName: delegate
backgroundMode: continuable
maxDepth: 2
No model and no path appears here: role files name their route and the plugin resolves it. The row is host plane, so every agent in every preset sees the tool.
| Field | Default | Meaning |
|---|---|---|
dshHome |
$DSH_HOME, else ~/.dsh |
Harness home the role directory is derived from |
rolesDir |
$DSH_HOME/agents |
Your role directory: seed target and override root |
seedRolesDir |
true |
Write the shipped roster into rolesDir and keep it current while unedited |
watchRolesDir |
true |
Reload the roster when a file in rolesDir changes |
commands |
true |
One /<id> slash command per role |
commandPrefix |
(empty) | Text placed before every command name |
settingsPage |
true |
Mount the settings API and page |
provider |
spawn |
ctx.subagents provider name |
toolName |
delegate |
Model-facing tool name |
backgroundMode |
continuable |
continuable returns a durable child id; one-shot a job id |
enableRunInBackground |
true |
Expose run_in_background |
maxDepth |
2 |
Delegation-depth cap for a started child |
defaultRoute |
(empty) | Used when a role names no route: |
routes |
{} |
Alias table: deep: { provider: deepseek-official, model: deepseek-flash } lets roles say route: deep |
How a route resolves
A route: is looked up in this order: the row's routes aliases; the
session's subagent-model-selection allowed list (the models ticked in the
DSH settings); the registered providers' catalogs; an inline
model: { provider, model } on the role. The catalog step is what lets a
headless profile resolve without any settings surface. When the allowed list
is enabled it also gates the role's route: a registered model the list
excludes is rejected with a message, not run quietly.
With no route named anywhere the child inherits the calling agent's provider
and model, and the result says so; the plugin never picks from the allowed
list by itself. A bare model id registered under two providers must be written
as provider/model.
Effort is clamped onto the levels the route advertises, the higher one on a
tie: deepseek-flash offers off/low/high/max, so medium becomes
high and xhigh becomes max. The shipped roles all say high. A
substitution is reported in the delegation result.
Roles as agent presets
A delegation request names an Agent Preset for the child to mount, and that preset alone decides the child's persona and tools. On its first call for a role, the plugin registers one preset for that role and the caller's preset:
- Id
roles-zeta/<role>@<parent preset>, for exampleroles-zeta/reviewer@standard, namedRole: reviewer (standard). - Rows are the parent preset's own plugin rows, copied. The
@deepseek-ai/dsh-personarow'sprefixbecomes the role body; its suffix and other settings stay. A parent without a persona row gets one. - Tool plugins are filtered by plugin row, not by tool. dsh does not
record which plugin registered a tool, so the plugin carries a table of the
shipped tool plugins and the names each one registers (
dsh-tool-fs:read,write,edit,read_image;dsh-tool-fs-search:glob,grep; and so on). With anallowlist, a tool plugin stays when it registers at least one allowed name. Without one, every plugin stays except those registering a delegation tool (subagent,subagent_fork,workflow,ralph), unlessdelegation: true.allow: []keeps no tool plugin. - Rows the table does not know (third-party plugins, infrastructure such
as
dsh-skill-filesystemor compaction) are kept as they are, because another row may depend on them. A group that loses every row is dropped. - A per-tool mask is then applied to the child itself as it is created
(
tools.restrict()on its own scope). With anallowlist it sees exactly those names; without one, and withoutdelegation: true, it losesdelegate,subagent,subagent_fork,workflow, andralph. This also covers sibling tools of a kept plugin and host-plane tools no preset owns.
The preset is reused while the role file and the parent preset stay the same; editing either registers a fresh one on the next call. A role child that delegates again clones the original parent preset, so personas never stack. The log says which rows each role preset left out.
What the model sees
One tool schema per session: a roster line per role and the allowed routes in
the description; parameters role, description, prompt, an optional
route alias, provider/model when the session enables model selection,
and run_in_background. A call resolves the role, preflights the route,
registers or reuses the role's preset, and starts one child on it. A
foreground call returns the child's final text; a continuable background call
returns started subagent <id> and settles through the runtime's notice.
Known limitations
- The preset still loads whole plugins; the mask hides the extras. A kept
dsh-tool-fsrow still mountswrite,edit, andread_imagefor a reviewer allowedread, and host-plane tools (delegate,working_directory, the MCP resource tools, every connected MCP server'smcp__*tools) still exist outside the preset. Neither reaches the child: the per-tool mask hides every name itsallowlist does not give, and a role without one losesdelegateand the delegation tools unlessdelegation: true. The mask is computed when the child is created, so an allowed tool that registers later (an MCP server that connects afterwards) stays hidden from that child, and a tool the child registers in its own scope (its structured-output tool) is never masked. - Role presets show up next to the shipped ones. They are ordinary Agent
Presets, so the Web preset picker lists them after
standard,cordis, and the rest. - Resuming a background child after a restart needs its preset again. Role presets are registered on first use. Messaging a role child that was started before the host restarted fails with an unknown-preset error until the same role has been delegated once from the same parent preset.
- The tool table follows dsh 0.2.1-rc.2. A tool plugin the table does not list is kept whole, and a tool renamed in a later dsh release stops being recognized until the table is updated.
- A role cannot change the workspace instruction chain. Every child loads
the same
AGENTS.md/CLAUDE.mdchain as its parent. - A slash command is a request, not a guarantee. The model performs the delegation; the transcript shows whether it did.
- The settings page edits files, not sessions. A running child is unaffected by a later edit; the next start reads the new file.
- No per-role memory.
No comments yet. Be the first to write one.