[English](README.md) | 繁體中文 | [简体中文](README.zh-CN.md)

# dsh-roles-zeta

`dsh-roles-zeta` 給 dsh agent 一支 `delegate` 工具與一個角色檔資料夾。呼叫時指定角色，角色檔提供該 child 的 persona、模型路線、reasoning effort 與工具範圍。它架在內建 `subagent` 工具用的同一個 `ctx.subagents` 服務上，所以 provider、委派深度、續談與背景結算全部照舊。內建的 `subagent` 與 `subagent_fork` 繼續掛著：它們讓模型每次呼叫自己挑路線，而角色是寫檔時就固定的。

每個角色以 Agent Preset 的形式交給 child：複製呼叫者自己的 preset，換上角色的 persona，只保留角色工具需要的插件（見[角色即 agent preset](#角色即-agent-preset)）。需要 dsh 0.2.1-rc.2 以上；從這版起，委派請求本身不再帶 persona 或工具過濾器。

工具旁邊有三個介面，各自只在對應的 host 服務存在時掛載：Web 輸入框裡每個角色一條斜線指令、Web 設定裡的「角色代理」頁，以及角色目錄的監看，改動不必重新組合 profile 就會進到下一次呼叫。

## 安裝

```sh
dsh plugin --profile web add dsh-roles-zeta
# 重啟 host
```

profile 是各自獨立的安裝根，要用的每個 profile 都裝一次。`github:zeta987/dsh-roles-zeta` 與 `file:<path>` 也可以。第一次載入時八個角色檔會寫進 `$DSH_HOME/agents`（`$DSH_HOME` 預設 `~/.dsh`），新 session 就有一支 `delegate` 工具，描述裡列出它們。

```
~/.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
```

### 更新

不會自動更新：`dsh plugin` 是在 profile 目錄裡轉發給 pnpm，啟動 host 不會安裝任何東西，也沒有任何頁面會查 registry。要換到某個版本：

```sh
dsh plugin --profile web add dsh-roles-zeta@0.4.0
# 重啟 host
```

版本要明講。`update` 只在安裝時 pnpm 寫下的 `^0.x` 範圍內移動；pnpm 11 起會把發布未滿 24 小時的版本從 `outdated` 與 `@latest` 裡藏起來（`minimumReleaseAge`），明確指定版本則立刻能裝。你改過的角色檔不會被動；沒碰過的內建角色會在下次載入時刷新。

## 斜線指令

每個載入的角色都是 Web 輸入框裡的一條指令。輸入 `/` 就會看到它們和 `/plan`、`/goal` 並列；`/reviewer look at lib/index.js` 會排進一輪對話，要求 agent 以 `reviewer` 角色呼叫 `delegate`、把這段文字當任務、在前景跑完並轉述結果。所以委派會留在對話紀錄裡，就是一次普通的工具呼叫，跟 `/plan <message>` 放訊息的位置相同。

指令跟著名冊走：新增、改名或移除角色都會重新註冊，選單自己更新。角色 id 撞到其他 plugin 的指令名時，該角色不註冊成指令（log 會說明），但仍可透過工具使用。`commands: false` 就完全不掛；`commandPrefix: "r-"` 得到 `/r-reviewer`。

![Web 輸入框輸入 `/`：角色與內建指令並列](https://raw.githubusercontent.com/zeta987/dsh-roles-zeta/main/docs/images/commands-zh.png)

## 設定頁

設定 → **角色代理** 列出名冊，每個角色有標籤（內建、自訂、已修改），一次編輯一個角色：id、`when` 說明、路線（session 允許的路線會列成建議）、推理強度、工具（目前已註冊工具的勾選清單，或「沿用全部」加上委派開關）以及角色指示。儲存會把 `<id>.md` 寫進 `$DSH_HOME/agents`，跟你手寫的是同一個檔案。

| 按鈕 | 對磁碟做的事 |
|---|---|
| 刪除（自訂角色） | 移除檔案 |
| 刪除（內建角色） | 留下 `disabled: true` 停用檔，套件內的副本不再載入；恢復前列在「已停用的內建角色」 |
| 恢復此角色 | 把套件內的檔案複製回來覆蓋你的版本 |
| 恢復內建角色 | 八個一起做；你自己新增的角色一律不動 |

這頁是 `/__dsh/roles-zeta/` 底下幾條 JSON 路由之上的瀏覽器模組，跟其他 Web API 一樣受瀏覽器 session cookie 與 Host／Origin 信任檢查把關。`settingsPage: false` 就兩者都不掛。

![設定 → 角色代理：上方是名冊，下方開著 reviewer 的編輯器](https://raw.githubusercontent.com/zeta987/dsh-roles-zeta/main/docs/images/settings-zh.png)

## 角色檔

一個角色一個 Markdown 檔：frontmatter 放機器欄位，正文是 child 的 persona prefix。

```markdown
---
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.
```

| 欄位 | 必填 | 語意 |
|---|---|---|
| `id` | 是 | 角色 id；底線寫法解析到同一個角色 |
| `when` | 是 | 一行；模型的選單項目，也是指令說明 |
| `route` | 否 | 模型 id（`deepseek-flash`）或完整的 `provider/model` |
| `effort` | 否 | `off`／`minimal`／`low`／`medium`／`high`／`xhigh`／`max`，依路線夾到可用層級 |
| `allow` | 否 | child 需要的工具名；註冊這些工具的插件會被保留（以插件為單位，見下文），之後 child 會被遮罩成只剩這些名稱；省略則繼承全部，`[]` 不保留任何工具插件 |
| `delegation` | 否 | `true` 讓沒有 `allow` 的 child 保留委派插件 |
| `disabled` | 否 | `true` 移除一個內建角色 |

有 `allow` 的角色只有清單裡列了委派工具才能委派；沒有 `allow` 的只有 `delegation: true` 才能。

### 角色從哪裡載入

兩層來源，後者覆蓋前者：套件自己的 `roles/*.md`，然後是 `$DSH_HOME/agents`。套件第一次載入時把名冊植入你的目錄，你不動它就一直由套件管理：

| 你做了什麼 | 下次載入 |
|---|---|
| 什麼都不做 | 內建檔跟著套件；新版本改了角色就更新 |
| 改了一個 | 它變成你的，不再被覆寫 |
| 刪了一個 | 不會再植入，但套件內的副本照樣載入；要真的拿掉角色，用 `disabled` stub 或設定頁 |
| 自己加一個 | 與內建角色一起載入 |

`.seeded.json` 記著套件寫了什麼。你放進去的檔案會依 id 取代內建角色；只有 `id` 與 `disabled: true` 的 stub 會移除它。目錄是被監看的，任何變動在安靜一小段時間後重新讀取。`seedRolesDir: false` 讓套件不寫入你的目錄；`watchRolesDir: false` 改成每次組合只讀一次。

## 設定

```yaml
- insert:
    - id: roles-zeta
      name: dsh-roles-zeta
      config:
        provider: spawn
        toolName: delegate
        backgroundMode: continuable
        maxDepth: 2
```

這裡沒有任何模型也沒有任何路徑：角色檔自己指名路線，插件負責解析。這條 row 在 host plane，所以每個 preset 的每個 agent 都看得到工具。

| 欄位 | 預設 | 語意 |
|---|---|---|
| `dshHome` | `$DSH_HOME`，否則 `~/.dsh` | 角色目錄所依據的 harness home |
| `rolesDir` | `$DSH_HOME/agents` | 你的角色目錄：植入目標，也是覆蓋根 |
| `seedRolesDir` | `true` | 把內建名冊寫進 `rolesDir`，未改動的跟著套件更新 |
| `watchRolesDir` | `true` | `rolesDir` 有檔案變動時重新載入名冊 |
| `commands` | `true` | 每個角色一條 `/<id>` 斜線指令 |
| `commandPrefix` | （空） | 放在每個指令名前面的文字 |
| `settingsPage` | `true` | 掛載設定 API 與設定頁 |
| `provider` | `spawn` | `ctx.subagents` 的 provider 名 |
| `toolName` | `delegate` | 模型面工具名 |
| `backgroundMode` | `continuable` | `continuable` 回傳 durable child id；`one-shot` 回傳 job id |
| `enableRunInBackground` | `true` | 是否暴露 `run_in_background` |
| `maxDepth` | `2` | 被啟動 child 的委派深度上限 |
| `defaultRoute` | （空） | 角色沒寫 `route:` 時使用 |
| `routes` | `{}` | 別名表：`deep: { provider: deepseek-official, model: deepseek-flash }` 後角色可寫 `route: deep` |

## 路線怎麼解析

`route:` 依序查：row 的 `routes` 別名表；這個 session 的 `subagent-model-selection` 允許清單（你在 dsh 設定裡勾的模型）；已註冊 provider 的模型目錄；角色檔行內的 `model: { provider, model }`。目錄那一步讓沒有設定介面的 headless profile 也能解析。允許清單開著時它也把關角色的路線：已註冊但不在清單裡的模型會得到明確錯誤，不會偷偷跑。

哪裡都沒指名路線時，child 沿用呼叫者的 provider 與 model，結果會註明；插件從不自己從允許清單挑。同一個模型 id 註冊在兩個 provider 底下時必須寫成 `provider/model`。

effort 會夾到該路線宣告的層級上，同距離取較高：`deepseek-flash` 提供 `off`／`low`／`high`／`max`，所以 `medium` 變成 `high`、`xhigh` 變成 `max`。內建角色一律寫 `high`。發生替換時會在委派結果裡註明。

## 角色即 agent preset

委派請求會指名一個讓 child 掛載的 Agent Preset，child 的 persona 與工具完全由那個 preset 決定。某個角色第一次被呼叫時，插件會針對「這個角色＋呼叫者的 preset」註冊一個 preset：

- **id** 是 `roles-zeta/<角色>@<父 preset>`，例如 `roles-zeta/reviewer@standard`，名稱為 `Role: reviewer (standard)`。
- **row** 複製自父 preset 自己的插件 row。`@deepseek-ai/dsh-persona` row 的 `prefix` 換成角色正文，suffix 與其他設定保留；父 preset 沒有 persona row 時會補一條。
- **工具插件以插件 row 為單位篩選，不是以工具為單位。** dsh 沒有記錄哪個插件註冊了哪支工具，所以插件內建一張表，列出內建工具插件與各自註冊的名稱（`dsh-tool-fs`：`read`、`write`、`edit`、`read_image`；`dsh-tool-fs-search`：`glob`、`grep`；其餘類推）。有 `allow` 時，只要某個工具插件註冊了至少一個允許的名稱就保留。沒有 `allow` 時全部保留，但註冊委派工具（`subagent`、`subagent_fork`、`workflow`、`ralph`）的插件會拿掉，除非 `delegation: true`。`allow: []` 不保留任何工具插件。
- **表裡沒有的 row**（第三方插件，或 `dsh-skill-filesystem`、compaction 這類基礎設施）原樣保留，因為其他 row 可能依賴它們。一個 group 的 row 全被拿掉時，整個 group 也拿掉。
- **逐工具遮罩**：child 建立時，再在它自己的 scope 上套一層 `tools.restrict()`。有 `allow` 的 child 只看得到清單裡的名稱；沒有 `allow` 也沒設 `delegation: true` 的，會拿掉 `delegate`、`subagent`、`subagent_fork`、`workflow`、`ralph`。保留插件的同組工具，以及不屬於任何 preset 的 host plane 工具，也一併涵蓋。

只要角色檔與父 preset 沒變，preset 就會重複使用；兩者任一改動，下次呼叫時會註冊新的。角色 child 再委派時，複製的是最初的父 preset，所以 persona 不會疊加。log 會寫出每個角色 preset 拿掉了哪些 row。

## 模型看到什麼

每個 session 一份工具 schema：描述裡每個角色一行，加上允許的路線；參數是 `role`、`description`、`prompt`、選配的 `route` 別名、session 開啟模型選擇時的 `provider`／`model`，以及 `run_in_background`。一次呼叫會解析角色、預檢路線、註冊或沿用該角色的 preset，然後在它上面啟動一個 child。前景呼叫回傳 child 的最終文字；continuable 背景呼叫回傳 `started subagent <id>`，再由 runtime 的通知結算。

## 已知限制

- **preset 仍以整個插件載入，多出的工具由遮罩藏起來。** 只允許 `read` 的 reviewer，保留下來的 `dsh-tool-fs` row 仍會掛上 `write`、`edit`、`read_image`；host plane 的工具（`delegate`、`working_directory`、MCP resource 工具、每個已連線 MCP server 的 `mcp__*` 工具）也仍存在於 preset 之外。兩者都到不了 child：逐工具遮罩會藏起 `allow` 沒給的每個名稱，沒有 `allow` 的角色則會失去 `delegate` 與委派工具，除非設了 `delegation: true`。遮罩在 child 建立當下計算，所以之後才註冊的允許工具（例如晚一點才連上的 MCP server）對該 child 仍是隱藏的；child 在自己 scope 註冊的工具（它的結構化輸出工具）則永遠不會被遮罩。
- **角色 preset 會和內建 preset 並列。** 它們是一般的 Agent Preset，所以 Web 的 preset 選單會把它們列在 `standard`、`cordis` 等之後。
- **host 重啟後恢復背景 child，需要先有它的 preset。** 角色 preset 是第一次使用時才註冊。host 重啟前啟動的角色 child，在同一個角色從同一個父 preset 再委派一次之前，對它發訊息會得到 unknown preset 錯誤。
- **工具對照表依 dsh 0.2.1-rc.2 建立。** 表裡沒列的工具插件會整個保留；之後的 dsh 版本若改了工具名，要等表更新才認得。
- **角色改變不了工作區指示鏈。** 每個 child 與父載入同一條 `AGENTS.md`／`CLAUDE.md` 鏈。
- **斜線指令是請求，不是保證。** 委派由模型執行，對話紀錄會顯示它有沒有做。
- **設定頁改的是檔案，不是 session。** 已啟動的 child 不受之後的修改影響；下一次啟動才讀新檔。
- **沒有 per-role 記憶。**
