dsh-paoding 0.3.9 → 0.3.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,7 +14,7 @@ dsh-paoding(中文品牌「庖丁」,典出《庄子》——庖丁顺纹理
14
14
  - **帮手能继承对话开工**:把某个角色的「启动方式」切成继承对话(fork),帮手带着主 agent 已完成的对话轮次直接开工,省得在委派时把上下文复述一遍;默认全员全新启动,不开启就一切照旧。
15
15
  - **帮手能就地续修**:把某个角色的「会话模式」切成可续(continuable),活干砸了或干得不满意,主 agent 用 `send_message` 在同一个子会话接着修,不用从头重派;默认全员一次性(one-shot)用完即弃,不开启就一切照旧。
16
16
  - **省钱省上下文**:帮手的工具只在被叫到时才加载,用一次付一次;主 agent 每次请求的工具开销约降三分之二,上下文只收摘要,不再被搜索结果和代码改动撑爆。
17
- - **角色随改随用**:删掉用不上的内置角色、给角色改显示名、给主 agent 配显示名,都是配置里一行的事;改完在面板点一次「保存并应用」,预设即时重生成,重启 DSH / 新建会话后生效。
17
+ - **角色随改随用**:删掉用不上的内置角色、给角色改显示名、给主 agent 配显示名,都是配置里一行的事;配了显示名的角色,派出的子代理标签自动带上角色名(「民工码农 · 修复标签显示」),任务管理页、子代理目录树、后台任务列表一眼可辨。改完在面板点一次「保存并应用」,预设即时重生成,重启 DSH / 新建会话后生效。
18
18
  - **装了什么都能认出来**:MCP 服务器、本地插件、已装技能,安装与配置时全程检测;默认只装 dsh 基础工具,检测到的 host 工具在「庖丁配置」里按需勾选;停用哪个,重新应用后相关工具自动剔除。
19
19
  - **按工作区各有各的班底**:不同项目可以配不同的主/子 agent——「庖丁配置」里选定工作区单独配置,每个工作区生成自己的预设(`orchestrator-<目录名>`),新建会话时选对应预设即可,全局默认不受影响。
20
20
  - **图形界面配置**:不习惯改配置文件的话,点开左侧栏底部动作条(设置行上方)的「庖丁配置」入口,整页点选即可,「保存并应用」即时重生成预设——与首装自动化同一条生成管线。
package/README_en.md CHANGED
@@ -14,7 +14,7 @@ dsh-paoding (Chinese brand 「庖丁」/ Pao Ding, from the Zhuangzi parable of
14
14
  - **Helpers that inherit the conversation**: flip a role's start mode to fork and the helper starts from the main agent's already-completed turns — no restating the context inside every delegation prompt. Every role defaults to a fresh start, so nothing changes until you flip it.
15
15
  - **Helpers that finish the job in place**: flip a role's session mode to continuable and, when a run fails or disappoints, the main agent resumes the same child conversation with `send_message` instead of re-delegating from scratch. Every role defaults to one-shot — run and discard — so nothing changes until you flip it.
16
16
  - **Cheaper requests, cleaner context**: a helper's tools load only when it is called — pay per use. The main agent's per-request tool overhead drops by roughly two-thirds, and it keeps only summaries instead of accumulating search dumps and diffs.
17
- - **Roles bend to your setup**: delete built-in roles you don't need, give roles display names, set a display name for the main agent — all one line of config; hit Save & Apply in the panel once and the preset regenerates on the spot, taking effect after a DSH restart or a new session.
17
+ - **Roles bend to your setup**: delete built-in roles you don't need, give roles display names, set a display name for the main agent — all one line of config; a role with a display name also stamps its sub-agent labels with that name (e.g.「民工码农 · 修复标签显示」), so the task-management page, the sub-agent tree and the background-job list all show who is doing what at a glance. Hit Save & Apply in the panel once and the preset regenerates on the spot, taking effect after a DSH restart or a new session.
18
18
  - **Whatever you've installed, it's recognized**: MCP servers, local plugins, installed skills — detected throughout install and configuration; the default install carries only DSH base tools, and detected host tools are opted into per tool in **Paoding Config**; disable one, re-apply, and its tools drop out on their own.
19
19
  - **A different lineup per workspace**: each project can carry its own main/sub-agent setup — pick the workspace in 庖丁配置, configure it separately, and it gets its own preset (`orchestrator-<basename>`); choose that preset when starting a session. The global default stays untouched.
20
20
  - **A visual configurator**: prefer clicking to editing YAML? the 庖丁配置 (Paoding Config) entry in the sidebar bottom action bar (above the Settings row) covers everything; Save & Apply regenerates the preset on the spot — the same generation pipeline as first-install automation.
@@ -48,7 +48,7 @@ dsh-paoding 把「按角色分工的编排 preset」做成**配置驱动**:你
48
48
 
49
49
  - `roles.<toolName>.persona`:非空字符串**整体替换**该角色的部署 persona(含 `''` 与缺省 = 保留静态默认)。自定义角色缺省 persona 为 `You are the <toolName> agent. Handle tasks delegated to this role.`
50
50
  - `roles.<toolName>.tools`:`toolName` 属内置三角色时**全量覆盖**该角色 `toolFilter.allow` 的意图(安装器再对 host 依赖的名字做检测求交);缺省 = 保留静态 allow。三角色若最终 allow 为空,安装直接报错(拒绝安装零工具角色)。
51
- - `roles.<toolName>.name`:仅对内置三角色生效的可选 `string | null` 显示名(缺省 `null` = 用该角色内置默认身份)。只替换该角色**默认** persona 首行身份句的主语——design 默认 `You are the design agent.`,配 `name: UI 设计` 后为 `You are the UI 设计 agent.`,句子其余文字不动;约束、边界与示例见 2.4。
51
+ - `roles.<toolName>.name`:可选 `string | null` 显示名(内置与自定义角色条目同收;缺省 `null` = 内置三角色用默认身份)。内置角色用它替换该角色**默认** persona 首行身份句的主语——design 默认 `You are the design agent.`,配 `name: UI 设计` 后为 `You are the UI 设计 agent.`;凡配了名字的角色,委派标签一律加前缀(「角色名 · 任务短语」,见 2.4)——自定义角色只有标签效果(persona 本就是你写的原文)。约束、边界与示例见 2.4。
52
52
  - `roles.<toolName>.model`:该角色的专用模型 id(可选 `string`)。配置后,该角色派出的每个子 agent 都固定用这个模型,不再随主 agent 会话当前模型走;缺省 = 不注入 `agentOptions`,子 agent 继承主 agent 的模型(详见 2.5)。
53
53
  - `roles.<toolName>.provider`:该角色模型所在的 provider 路由(可选 `string`)。须与 `model` 成对配置才生效,只配 `provider` 会被安装器告警并忽略;只配 `model` 时沿用主 agent 的 provider 路由(详见 2.5)。
54
54
  - `roles.<toolName>.background_mode`:该角色的会话模式(可选 `'one-shot' | 'continuable'`,缺省 `one-shot`)——一次性用完即弃,或可续:子会话跨轮保留,主 agent 用 `send_message` 就地续修。内置角色与自定义角色同语义;continuable 的前置、代价与面板路径见 2.6。
@@ -201,24 +201,36 @@ roles_remove:
201
201
 
202
202
  ### 2.4 内置角色显示名:roles.\<toolName>.name
203
203
 
204
- 内置三角色(`search_external` / `design` / `implement`)的条目下可写可选子键 `name`(`string | null`;缺省 / `null` = 用该角色**内置默认身份**)。它的作用窄而明确:**只替换该角色默认 persona 首行身份句的主语**——身份句其余文字(职责描述、行为边界等)原样不动,工具面与委派机制一概不碰。
204
+ 内置三角色(`search_external` / `design` / `implement`)的条目下可写可选子键 `name`(`string | null`;缺省 / `null` = 用该角色**内置默认身份**)。它有两处生效点,共用同一个配置:
205
205
 
206
- 例:design 默认身份句是 `You are the design agent.`,配 `name: UI 设计` 后生成 `You are the UI 设计 agent.`:
206
+ - **persona 身份句**:只替换该角色默认 persona 首行身份句的主语——身份句其余文字(职责描述、行为边界等)原样不动,工具面与委派机制一概不碰。
207
+ - **委派标签前缀**:安装器把名字写进预设的 `delegation-labels` 插件行,该角色派出的每个子会话,其持久标签都会变成 `角色名 · 原任务短语`(详见下文「委派标签带角色名」)。
208
+
209
+ 例:design 默认身份句是 `You are the design agent.`,配 `name: UI 设计` 后生成 `You are the UI 设计 agent.`,同时该角色的委派标签形如「UI 设计 · 首页视觉方案」:
207
210
 
208
211
  ```yaml
209
212
  roles:
210
213
  design:
211
- name: UI 设计 # 只换 persona 身份句主语;tools 可省(回落默认工具面)
214
+ name: UI 设计 # 换 persona 身份句主语 + 委派标签前缀;tools 可省(回落默认工具面)
212
215
  ```
213
216
 
214
- 约束:trim 后 1–60 字符、不可含换行;空字符串 / 缺省 = 用该角色内置默认身份(不写此子键时,身份句是 SRC 原文,安装器不做任何替换)。
217
+ 约束:trim 后 1–60 字符、不可含换行;空字符串 / 缺省 = 用该角色内置默认身份、标签不加前缀(不写此子键时,身份句是 SRC 原文,安装器不做任何替换)。
215
218
 
216
219
  边界:
217
220
 
218
- - **只动 persona 身份句**:工具注册名与委派调用名(toolName)不变——主 agent 仍按 `search_external` / `design` / `implement` 委派;`restrict allow` 与主 agent persona 里的委派指引(如 `delegate to design`)也都不变。
219
- - **persona 被自定义时 `name` 不生效**:同一条目写了非空 `persona`(覆盖默认 persona)时,安装器在生成时忽略 `name` 并告警——想把名字显出来,直接写进自定义 persona 首行(如 `persona: |-` 块第一句 `You are the UI 设计 agent.`),此时无需 `name` 子键。
220
- - **被删除的角色不涉及**:`roles_remove` 整条删除的角色没有 persona 可改(见 2.3)。
221
- - **静态角色不在列**:`search_internal_deep` 不在安装器管理的三角色内,无此子键(见 2.2)。
221
+ - **标签只加前缀,不动正文**:任务管理页、会话头子代理目录树与后台任务列表展示的就是委派时的 `description`,安装器不在其中改写任何任务语义。
222
+ - **改名不碰机制面**:工具注册名与委派调用名(toolName)不变——主 agent 仍按 `search_external` / `design` / `implement` 委派;`restrict allow` 与主 agent persona 里的委派指引(如 `delegate to design`)也都不变。
223
+ - **persona 被自定义时身份句不生效、标签前缀照常生效**:同一条目写了非空 `persona`(覆盖默认 persona)时,安装器在生成时忽略 `name` 的身份句替换并告警——想让名字显出来,直接写进自定义 persona 首行;但标签前缀不依赖 persona,配置后照常生效。
224
+ - **被删除的角色不涉及**:`roles_remove` 整条删除的角色没有 persona,也没有委派可打标签(见 2.3)。
225
+ - **静态角色不在列**:`search_internal_deep` 不在安装器管理的三角色内,无此子键,标签保持原样(见 2.2)。
226
+
227
+ #### 委派标签带角色名
228
+
229
+ 委派工具调用的 `description` 参数会随子会话持久化,成为三处展示面的标签:侧栏任务管理页、会话头的子代理目录树、后台任务列表。编排模型随手写的 description 只是一句任务短语,多角色并行时扫一眼分不清谁在干活。安装器据此把配置了名字的角色的委派调用统一改写成 `角色名 · 原任务短语`——改写发生在工具执行的瀑布上(`delegation-labels.mjs`,随预设一起安装),不依赖提示词配合,主 agent 无感知。几点口径:
230
+
231
+ - **幂等**:已带前缀的标签不会重复加前缀;
232
+ - **只对新委派生效**:改配置之前已经开出去的子会话不追溯;
233
+ - **自定义角色同享**:`roles` 键下任何角色(内置或自定义,见第 6 节)配了 `name` 都进这套前缀。
222
234
 
223
235
  生效方式与主 agent 显示名同款(见 5.1):
224
236
 
@@ -453,7 +465,7 @@ main_agent_display_name: 庖丁 # 仅 preset 显示名换成「庖丁」,per
453
465
 
454
466
  - **手编**:写键后在「庖丁配置」点「保存并应用」,再重启 GUI / 新会话生效。
455
467
  - **面板**:左侧栏「庖丁配置」→ 主 agent 卡「主 agent 显示名(仅 preset 显示名)」输入框(带「恢复默认」按钮),点「保存并应用」后**重启 GUI / 新会话**生效。
456
- - **对照**:显示名线共两条、互不牵连:`main_agent_display_name`(仅主 agent 的 preset 显示名,见 5.1)、`roles.<toolName>.name`(内置角色默认 persona 首行身份句的主语,不改工具调用名,见 2.4),可各自独立配置。
468
+ - **对照**:显示名线共两条、互不牵连:`main_agent_display_name`(仅主 agent 的 preset 显示名,见 5.1)、`roles.<toolName>.name`(角色 persona 身份句主语 + 委派标签前缀,不改工具调用名,见 2.4),可各自独立配置。
457
469
 
458
470
  ### 5.2 追加三态语义:main_agent_persona_extra
459
471
 
@@ -497,6 +509,10 @@ main_agent_display_name: 庖丁 # 仅 preset 显示名换成「庖丁」,per
497
509
  <toolName>.`,紧跟既有委派行之后)——主 agent 全靠这行知道什么活该派给谁,因此建议把 persona 首行
498
510
  写成一句简短职责(例如「你的职责就是查找符合用户要求的技能」)。
499
511
 
512
+ 另有一个可选子键:自定义角色条目同样支持 `name`——它不参与 persona 身份句替换(自定义角色的
513
+ persona 就是你写的原文),唯一作用是**委派标签前缀**(见 2.4「委派标签带角色名」):配了名字,这个
514
+ 角色派出的子会话标签就是「名字 · 任务短语」,多个自定义角色并行时一眼可辨。
515
+
500
516
  ### 6.1 路径一:庖丁配置面板(推荐)
501
517
 
502
518
  面板流程(左侧栏「庖丁配置」→「Agent 工具分配」→ 底部「新建自定义 agent 角色」):
@@ -33,7 +33,7 @@ The key names below match `normalizeConfig` in `tools/lib/config.mjs` — the si
33
33
  | Top-level key | Type | Semantics |
34
34
  |---|---|---|
35
35
  | `profile` | string | Which profile's patch layer (`$DSH_HOME/profiles/<profile>/cordis.patch.yml`) is scanned during detection; default `web` |
36
- | `roles` | map | `toolName` → `{ persona?, name?, tools[], model?, provider?, start_mode?, background_mode? }`. For the three built-in roles (`search_external` / `design` / `implement`): overrides the persona and the allow intent, and may carry a `name` display name (rewrites only the subject of that role's default persona identity sentence — see 2.4), a dedicated model (`model` / `provider`, see 2.5), a session mode (`background_mode`, see 2.6) and a start mode (`start_mode`, see 2.7). Any other key is a **custom role**: the installer emits a fresh `delegation-<toolName>` block and injects the toolName into the main agent's `config.allow` (sections 2 and 6) |
36
+ | `roles` | map | `toolName` → `{ persona?, name?, tools[], model?, provider?, start_mode?, background_mode? }`. For the three built-in roles (`search_external` / `design` / `implement`): overrides the persona and the allow intent, and may carry a `name` display name (rewrites the subject of that role's default persona identity sentence and prefixes its delegation labels — see 2.4), a dedicated model (`model` / `provider`, see 2.5), a session mode (`background_mode`, see 2.6) and a start mode (`start_mode`, see 2.7). Any other key is a **custom role**: the installer emits a fresh `delegation-<toolName>` block and injects the toolName into the main agent's `config.allow` (sections 2 and 6) |
37
37
  | `roles_remove` | string[] | toolNames of built-in roles deleted wholesale (only `search_external` / `design` / `implement` are accepted; other names are ignored with a warning; default `[]`). Deleting means the delegation block / tool surface / role persona / main-agent delegation guidance are no longer generated (see 2.3) |
38
38
  | `skills` | map | `skill` → the list of role toolNames the skill is assigned to; the installer appends an `Available skills: …` guidance sentence to those roles' personas (section 7) |
39
39
  | `main_agent_extra` | string[] | Tools appended to the main-agent allow list (host tools such as `mcp__codegraph__codegraph_explore`, `memory_search`, `mnemon_*`); names are checked against the preset's own tool face ∪ the detection inventory — a name in neither is not injected |
@@ -48,7 +48,7 @@ The key names below match `normalizeConfig` in `tools/lib/config.mjs` — the si
48
48
 
49
49
  - `roles.<toolName>.persona`: a non-empty string **replaces the role's deployment persona wholesale** (`''` or absent keeps the static default). A custom role without a persona gets `You are the <toolName> agent. Handle tasks delegated to this role.`
50
50
  - `roles.<toolName>.tools`: when `toolName` is one of the three built-ins this **fully overrides the intent** of that role's `toolFilter.allow` (the installer then intersects host-dependent names with what was actually detected); absent keeps the static allow. If a built-in role would end up with an empty allow, the install aborts (a zero-tool role is refused).
51
- - `roles.<toolName>.name`: an optional `string | null` display name for the built-in trio only (absent / `null` = the role's built-in default identity). It rewrites only the **subject of that role's default persona identity sentence** — design's default `You are the design agent.` with `name: UI 设计` becomes `You are the UI 设计 agent.`, with the rest of the sentence untouched; constraints, boundaries and an example are in 2.4.
51
+ - `roles.<toolName>.name`: an optional `string | null` display name (built-in and custom role entries alike; absent / `null` = the built-in trio keeps its default identity). For built-ins it rewrites the **subject of that role's default persona identity sentence** — design's default `You are the design agent.` with `name: UI 设计` becomes `You are the UI 设计 agent.` — and for every named role it prefixes the role's delegation labels (`role name · task phrase`, see 2.4); custom roles only get the label effect, since their persona is your text. Constraints, boundaries and an example are in 2.4.
52
52
  - `roles.<toolName>.model`: a dedicated model id for the role (optional `string`). When set, every sub-agent the role spawns runs on that model instead of following the main agent's current session model; absent = nothing is injected and the child inherits the main agent's model (see 2.5).
53
53
  - `roles.<toolName>.provider`: the provider route the role's model lives on (optional `string`). It only takes effect paired with `model`; a lone `provider` is warned about and ignored by the installer, while a lone `model` rides the main agent's provider route (see 2.5).
54
54
  - `roles.<toolName>.background_mode`: the role's session mode (optional `'one-shot' | 'continuable'`; default `one-shot`) — run-and-discard, or continuable: the child conversation is kept across turns so the main agent can resume in place with `send_message`. Built-in and custom role entries share the semantics; the continuable prerequisites, costs and panel path are in 2.6.
@@ -201,24 +201,36 @@ Restoring:
201
201
 
202
202
  ### 2.4 Built-in role display names: roles.\<toolName>.name
203
203
 
204
- Entries of the built-in trio (`search_external` / `design` / `implement`) may carry an optional `name` sub-key (`string | null`; absent / `null` = the role's **built-in default identity**). Its effect is narrow and explicit: **it rewrites only the subject of that role's default persona identity sentence at the head of the persona** — the rest of the sentence (job description, behavior boundaries, …) stays verbatim, and the tool surface / delegation machinery are never touched.
204
+ Entries of the built-in trio (`search_external` / `design` / `implement`) may carry an optional `name` sub-key (`string | null`; absent / `null` = the role's **built-in default identity**). It takes effect in two places off a single setting:
205
205
 
206
- Example: design's default identity sentence is `You are the design agent.`; with `name: UI 设计` it is generated as `You are the UI 设计 agent.`:
206
+ - **Persona identity sentence**: rewrites only the subject of that role's default persona identity sentence at the head of the persona — the rest of the sentence (job description, behavior boundaries, …) stays verbatim, and the tool surface / delegation machinery are never touched.
207
+ - **Delegation label prefix**: the installer writes the name into the preset's `delegation-labels` plugin row; every sub-session the role spawns gets a persistent label of the form `role name · original task phrase` (see "Delegation labels carry the role name" below).
208
+
209
+ Example: design's default identity sentence is `You are the design agent.`; with `name: UI 设计` it is generated as `You are the UI 设计 agent.`, and that role's delegation labels look like「UI 设计 · 首页视觉方案」:
207
210
 
208
211
  ```yaml
209
212
  roles:
210
213
  design:
211
- name: UI 设计 # rewrites only the subject of the persona identity sentence; tools may be omitted (the default tool surface applies)
214
+ name: UI 设计 # rewrites the persona identity sentence subject + delegation label prefix; tools may be omitted (the default tool surface applies)
212
215
  ```
213
216
 
214
- Constraints: trimmed, 1–60 characters, no newlines; an empty string / an absent key = the role's built-in default identity (without the sub-key the identity sentence stays verbatim from the SRC — the installer substitutes nothing).
217
+ Constraints: trimmed, 1–60 characters, no newlines; an empty string / an absent key = the role's built-in default identity and no label prefix (without the sub-key the identity sentence stays verbatim from the SRC — the installer substitutes nothing).
215
218
 
216
219
  Boundaries:
217
220
 
218
- - **Only the persona identity sentence moves**: the tool registration name and the delegation call name (toolName) stay unchanged — the main agent still delegates to `search_external` / `design` / `implement`; `restrict allow` and the delegation guidance in the main-agent persona (e.g. `delegate to design`) are untouched too.
219
- - **`name` does not apply once the persona is customized**: when the same entry carries a non-empty `persona` (overriding the default), the installer ignores `name` at generation with a warning — to surface the name, write it directly into the first line of the custom persona (e.g. the first sentence of the `persona: |-` block: `You are the UI 设计 agent.`); the `name` sub-key is then unnecessary.
220
- - **Deleted roles are not involved**: a role deleted wholesale by `roles_remove` has no persona left to rewrite (see 2.3).
221
- - **The static role is out of scope**: `search_internal_deep` is outside the installer-managed trio and has no such sub-key (see 2.2).
221
+ - **Labels only get a prefix; the body is never rewritten**: the task-management page, the sub-agent tree in the session header and the background-job list all show the `description` captured at delegation time — the installer rewrites no task semantics inside it.
222
+ - **Renaming never touches the machinery**: the tool registration name and the delegation call name (toolName) stay unchanged — the main agent still delegates to `search_external` / `design` / `implement`; `restrict allow` and the delegation guidance in the main-agent persona (e.g. `delegate to design`) are untouched too.
223
+ - **With a custom persona the identity sentence is skipped but the label prefix still applies**: when the same entry carries a non-empty `persona` (overriding the default), the installer skips the `name` identity-sentence rewrite at generation with a warning — to surface the name, write it directly into the first line of the custom persona; the label prefix does not depend on the persona and applies as configured.
224
+ - **Deleted roles are not involved**: a role deleted wholesale by `roles_remove` has no persona left and no delegations left to label (see 2.3).
225
+ - **The static role is out of scope**: `search_internal_deep` is outside the installer-managed trio and has no such sub-key; its labels stay as written (see 2.2).
226
+
227
+ #### Delegation labels carry the role name
228
+
229
+ The `description` argument of a delegation tool call persists with the sub-session and becomes the label shown in three places: the sidebar task-management page, the sub-agent tree in the session header, and the background-job list. A task phrase the orchestrator jots down says nothing about who is doing the work once several roles run in parallel. For every role with a configured name, the installer therefore rewrites its delegation calls to `role name · original task phrase` — the rewrite happens in-place on the tools/pre-execute waterfall (`delegation-labels.mjs`, installed with the preset), with no reliance on prompt cooperation and no awareness required from the main agent. Ground rules:
230
+
231
+ - **Idempotent**: a label that already carries the prefix never gets a second one;
232
+ - **New delegations only**: sub-sessions spawned before the configuration change are not relabeled;
233
+ - **Custom roles included**: any role under the `roles` key (built-in or custom, see section 6) with a `name` joins this prefixing.
222
234
 
223
235
  Applying it works just like the main-agent display name (see 5.1):
224
236
 
@@ -453,7 +465,7 @@ Where to set it:
453
465
 
454
466
  - **Hand-editing**: write the key and hit Save & Apply in 庖丁配置, then restart the GUI or open a new session.
455
467
  - **Panel**: 庖丁配置 (left sidebar) → main-agent card, the「主 agent 显示名(仅 preset 显示名)」("main agent display name (preset display name only)") input, with its「恢复默认」("restore default") button; after「保存并应用」(save & apply) it takes effect on a **GUI restart / new session**.
456
- - For contrast: there are two independent naming lines that never interfere with each other: `main_agent_display_name` (the main agent's preset display name only, see 5.1) and `roles.<toolName>.name` (the subject of a built-in role's default persona identity sentence, never the tool call name — see 2.4). Configure each on its own.
468
+ - For contrast: there are two independent naming lines that never interfere with each other: `main_agent_display_name` (the main agent's preset display name only, see 5.1) and `roles.<toolName>.name` (a role's persona identity-sentence subject + delegation label prefix, never the tool call name — see 2.4). Configure each on its own.
457
469
 
458
470
  ### 5.2 Persona extra: three-state semantics
459
471
 
@@ -503,6 +515,12 @@ row** (`- <duty>: delegate to <toolName>.`, right after the existing delegation
503
515
  main agent's only routing hint for the role, so make the persona's first line a short duty sentence
504
516
  (e.g. "Find the skill that matches the user's need").
505
517
 
518
+ One optional sub-key on top: custom role entries accept `name` too — it takes no part in the persona
519
+ identity-sentence rewrite (a custom role's persona is verbatim what you wrote) and its only effect is
520
+ the **delegation label prefix** (see 2.4, "Delegation labels carry the role name"): with a name set,
521
+ this role's sub-session labels read `name · task phrase`, easy to tell apart when several custom roles
522
+ run in parallel.
523
+
506
524
  ### 6.1 Path 1: the 庖丁配置 panel (recommended)
507
525
 
508
526
  The panel flow (庖丁配置 in the left sidebar → agent tool assignment → "Create a custom agent role" at the bottom):
@@ -19,13 +19,13 @@
19
19
  |---|---|---|
20
20
  | `$DSH_HOME` | `~/.dsh` | DSH 家目录(认环境变量 `DSH_HOME`,缺省 `$HOME/.dsh`)。host 层配置 `cordis.patch.yml`、`node_modules`、`profiles/<name>/cordis.patch.yml`、插件 profile 目录都在其下 |
21
21
  | preset roster 根目录 | `$DSH_HOME/.agent-presets/` | DSH ≤ 0.1.6 由 `dsh-agent-presets` 扫描此目录发现本地 authored presets;DSH ≥ 0.1.7 宿主不再扫目录,此目录退为预设文件存放处(见第 2 节「版本支持与预设落点双轨」) |
22
- | 本预设安装目标 | `$DSH_HOME/.agent-presets/orchestrator` | 生成产物:`agent.cordis.yml`(重写后的角色配置)、`preset.yml`、`restrict.mjs`、`.generator-version` 版本与轨道标记,是一个静态目录;按工作区配置生成的预设落同一父目录下的 `orchestrator-<slug>` |
22
+ | 本预设安装目标 | `$DSH_HOME/.agent-presets/orchestrator` | 生成产物:`agent.cordis.yml`(重写后的角色配置)、`preset.yml`、`restrict.mjs`、`delegation-labels.mjs`、`.generator-version` 版本与轨道标记,是一个静态目录;按工作区配置生成的预设落同一父目录下的 `orchestrator-<slug>` |
23
23
  | 声明行托管块(仅 DSH ≥ 0.1.7) | `$DSH_HOME/cordis.patch.yml` 内的托管块 | 以 `# --- dsh-paoding presets (auto-generated; do not edit) ---` 起始、配套 end 标记收尾,块内 `- insert:` 写 `@deepseek-ai/dsh-agent-preset` 声明行;安装器自动维护,详见第 2 节 |
24
24
  | 配置文件 | `$DSH_HOME/dsh-paoding.config.yml` | 「庖丁配置」面板「保存并应用」写入的角色与工具分配(`0o600`,可手编);首装自动化同样读写它 |
25
25
 
26
26
  - **环境变量**:`DSH_HOME` 覆盖 DSH 家目录(默认 `~/.dsh`);`DSH_PAODING_CONFIG` 覆盖配置文件路径(面板认它;仓库内兜底 CLI 则用 `--config <file>` 指定)。
27
27
 
28
- 仓库内 `presets/orchestrator/` 是静态预设源(生成管线读取并重写 `agent.cordis.yml`;`preset.yml`、`restrict.mjs` 原样复制——唯一例外是配置了 `main_agent_display_name`(或工作区派生显示名)时会改写 `preset.yml` 的 `name:` 行),`tools/` 是生成器本体(面板与兜底 CLI 共用),`plugins/paoding-config-ui/` 是「庖丁配置」面板插件本体。只想正常使用的话,这些都不用碰——见下一节。
28
+ 仓库内 `presets/orchestrator/` 是静态预设源(生成管线读取并重写 `agent.cordis.yml`;`preset.yml`、`restrict.mjs`、`delegation-labels.mjs` 原样复制——唯一例外是配置了 `main_agent_display_name`(或工作区派生显示名)时会改写 `preset.yml` 的 `name:` 行),`tools/` 是生成器本体(面板与兜底 CLI 共用),`plugins/paoding-config-ui/` 是「庖丁配置」面板插件本体。只想正常使用的话,这些都不用碰——见下一节。
29
29
 
30
30
  ## 2. 安装:唯一官方通道
31
31
 
@@ -56,8 +56,8 @@ dsh plugin --profile web add dsh-paoding
56
56
 
57
57
  两代宿主发现「本地 authored preset」的机制不同,安装器据此分双轨落点,宿主检测全自动、用户无感:
58
58
 
59
- - **目录轨(DSH ≤ 0.1.6)**:宿主扫描 `$DSH_HOME/.agent-presets/` 下的目录来发现 preset,目录即注册。安装器照旧把产物写成 `$DSH_HOME/.agent-presets/orchestrator/`(`agent.cordis.yml` + `preset.yml` + `restrict.mjs`);按工作区配置生成的预设落同目录下的 `orchestrator-<slug>`。
60
- - **声明行轨(DSH ≥ 0.1.7)**:宿主不再扫目录,改为读取 patch 里的 `@deepseek-ai/dsh-agent-preset` 声明行。安装器在 `$DSH_HOME/cordis.patch.yml` 里维护一个托管块——以 `# --- dsh-paoding presets (auto-generated; do not edit) ---` 起始、以配套的 end 标记收尾——块内用 `- insert:` 写声明行:全局预设 `config.id: orchestrator`,工作区预设 `config.id: orchestrator-<slug>`,各自的 `plugins` 内联全部插件行;`restrict.mjs` 以绝对 `file:` URL 引用。产物三件套仍落在 `.agent-presets/<id>/`,只是从「目录即注册」变成「文件存放处」。托管块之外的用户内容一字节不动;块内带 do not edit 标记,手改块内内容会在下次保存时被覆盖。
59
+ - **目录轨(DSH ≤ 0.1.6)**:宿主扫描 `$DSH_HOME/.agent-presets/` 下的目录来发现 preset,目录即注册。安装器照旧把产物写成 `$DSH_HOME/.agent-presets/orchestrator/`(`agent.cordis.yml` + `preset.yml` + `restrict.mjs` + `delegation-labels.mjs`);按工作区配置生成的预设落同目录下的 `orchestrator-<slug>`。
60
+ - **声明行轨(DSH ≥ 0.1.7)**:宿主不再扫目录,改为读取 patch 里的 `@deepseek-ai/dsh-agent-preset` 声明行。安装器在 `$DSH_HOME/cordis.patch.yml` 里维护一个托管块——以 `# --- dsh-paoding presets (auto-generated; do not edit) ---` 起始、以配套的 end 标记收尾——块内用 `- insert:` 写声明行:全局预设 `config.id: orchestrator`,工作区预设 `config.id: orchestrator-<slug>`,各自的 `plugins` 内联全部插件行;`restrict.mjs` / `delegation-labels.mjs` 以绝对 `file:` URL 引用。产物四件套仍落在 `.agent-presets/<id>/`,只是从「目录即注册」变成「文件存放处」。托管块之外的用户内容一字节不动;块内带 do not edit 标记,手改块内内容会在下次保存时被覆盖。
61
61
 
62
62
  安装器按下面的顺序判定走哪条轨:
63
63
 
@@ -19,13 +19,13 @@ Check the following before installing:
19
19
  |---|---|---|
20
20
  | `$DSH_HOME` | `~/.dsh` | The DSH home directory (the `DSH_HOME` environment variable is honored, defaulting to `$HOME/.dsh`). The home-layer `cordis.patch.yml`, `node_modules`, `profiles/<name>/cordis.patch.yml`, and plugin profile directories live under it |
21
21
  | preset roster root | `$DSH_HOME/.agent-presets/` | Under DSH ≤ 0.1.6, `dsh-agent-presets` scans this directory for locally authored presets; under DSH ≥ 0.1.7 the host no longer scans it and the directory is just where the preset files live (see §2 "Version support and the dual preset landing") |
22
- | Install target of this preset | `$DSH_HOME/.agent-presets/orchestrator` | The generated artifact: `agent.cordis.yml` (rewritten role config), `preset.yml`, `restrict.mjs`, and the `.generator-version` version-and-track marker — a static directory; per-workspace presets land in sibling `orchestrator-<slug>` directories |
22
+ | Install target of this preset | `$DSH_HOME/.agent-presets/orchestrator` | The generated artifact: `agent.cordis.yml` (rewritten role config), `preset.yml`, `restrict.mjs`, `delegation-labels.mjs`, and the `.generator-version` version-and-track marker — a static directory; per-workspace presets land in sibling `orchestrator-<slug>` directories |
23
23
  | Declaration managed block (DSH ≥ 0.1.7 only) | a managed block inside `$DSH_HOME/cordis.patch.yml` | Opens with `# --- dsh-paoding presets (auto-generated; do not edit) ---` and closes with the matching end marker; the `- insert:` rows inside carry `@deepseek-ai/dsh-agent-preset` declarations, maintained by the installer — see §2 |
24
24
  | Config file | `$DSH_HOME/dsh-paoding.config.yml` | Role/tool assignments written by the Paoding Config panel's Save & Apply (`0o600`, hand-editable); first-install automation reads and writes it too |
25
25
 
26
26
  - **Environment variables**: `DSH_HOME` overrides the DSH home directory (default `~/.dsh`); `DSH_PAODING_CONFIG` overrides the config-file path (honored by the panel; the in-repo fallback CLI uses `--config <file>` instead).
27
27
 
28
- Inside the repository, `presets/orchestrator/` is the static preset source (the pipeline reads and rewrites `agent.cordis.yml`; `preset.yml` and `restrict.mjs` are copied as-is — the one exception: when `main_agent_display_name` (or a workspace-derived display name) is configured, the `name:` line of `preset.yml` is rewritten), `tools/` is the generator itself (shared by the panel and the fallback CLI), and `plugins/paoding-config-ui/` is the Paoding Config panel plugin. For plain usage you never need to touch any of these — see the next section.
28
+ Inside the repository, `presets/orchestrator/` is the static preset source (the pipeline reads and rewrites `agent.cordis.yml`; `preset.yml`, `restrict.mjs` and `delegation-labels.mjs` are copied as-is — the one exception: when `main_agent_display_name` (or a workspace-derived display name) is configured, the `name:` line of `preset.yml` is rewritten), `tools/` is the generator itself (shared by the panel and the fallback CLI), and `plugins/paoding-config-ui/` is the Paoding Config panel plugin. For plain usage you never need to touch any of these — see the next section.
29
29
 
30
30
  ## 2. Install: the single official channel
31
31
 
@@ -56,8 +56,8 @@ The plugin supports two host generations: **DSH 0.1.5 / 0.1.6 / 0.1.7-rc.1 / 0.1
56
56
 
57
57
  The two generations discover "local authored presets" differently, so the installer maintains a dual landing, detecting the host automatically — nothing for the user to do:
58
58
 
59
- - **Directory track (DSH ≤ 0.1.6)**: the host discovers presets by scanning directories under `$DSH_HOME/.agent-presets/` — the directory is the registration. The installer writes the artifacts to `$DSH_HOME/.agent-presets/orchestrator/` (`agent.cordis.yml` + `preset.yml` + `restrict.mjs`) as before; workspace-specific presets land in sibling `orchestrator-<slug>` directories.
60
- - **Declaration track (DSH ≥ 0.1.7)**: the host no longer scans directories; it reads `@deepseek-ai/dsh-agent-preset` declaration rows from the patch instead. The installer maintains a managed block in `$DSH_HOME/cordis.patch.yml` — opening with `# --- dsh-paoding presets (auto-generated; do not edit) ---` and closing with the matching end marker — whose `- insert:` rows write the declarations: the global preset gets `config.id: orchestrator`, a workspace preset gets `config.id: orchestrator-<slug>`, each with all plugin rows inlined under `plugins`; `restrict.mjs` is referenced by an absolute `file:` URL. The artifact trio still lands in `.agent-presets/<id>/` — demoted from "the directory is the registration" to "a place where the files live". User content outside the block is left untouched, byte for byte; the block carries a do-not-edit marker, and anything hand-edited inside it is overwritten by the next save.
59
+ - **Directory track (DSH ≤ 0.1.6)**: the host discovers presets by scanning directories under `$DSH_HOME/.agent-presets/` — the directory is the registration. The installer writes the artifacts to `$DSH_HOME/.agent-presets/orchestrator/` (`agent.cordis.yml` + `preset.yml` + `restrict.mjs` + `delegation-labels.mjs`) as before; workspace-specific presets land in sibling `orchestrator-<slug>` directories.
60
+ - **Declaration track (DSH ≥ 0.1.7)**: the host no longer scans directories; it reads `@deepseek-ai/dsh-agent-preset` declaration rows from the patch instead. The installer maintains a managed block in `$DSH_HOME/cordis.patch.yml` — opening with `# --- dsh-paoding presets (auto-generated; do not edit) ---` and closing with the matching end marker — whose `- insert:` rows write the declarations: the global preset gets `config.id: orchestrator`, a workspace preset gets `config.id: orchestrator-<slug>`, each with all plugin rows inlined under `plugins`; `restrict.mjs` / `delegation-labels.mjs` are referenced by absolute `file:` URLs. The artifact quartet still lands in `.agent-presets/<id>/` — demoted from "the directory is the registration" to "a place where the files live". User content outside the block is left untouched, byte for byte; the block carries a do-not-edit marker, and anything hand-edited inside it is overwritten by the next save.
61
61
 
62
62
  The installer decides which track to take in this order:
63
63
 
@@ -26,6 +26,10 @@ dsh-paoding 把 DSH 从「单体 agent 全量加载工具」改造成「主 agen
26
26
 
27
27
  每请求工具开销(估算,随注册面浮动):主 agent 全量 ~16.2k tokens → 白名单后 ~5.5k;`search_external` ~3.0k / `design` ~3.3k / `implement` ~3.8k,仅在真正委派时发生(数值来源:`agent.cordis.yml` 头部注释)。
28
28
 
29
+ ### 子代理标签带角色名
30
+
31
+ 委派时主 agent 写下的任务短语(委派工具的 `description` 参数)会随子会话持久化,成为三处展示面的标签:侧栏任务管理页、会话头的子代理目录树、后台任务列表。给角色配了显示名(`roles.<toolName>.name`,见[配置](configuration.md) 2.4)后,安装器在预设里带上 `delegation-labels` 插件行:该角色派出的每个子会话,标签统一为 `角色名 · 原任务短语`(如「民工码农 · 修复标签显示」)——多角色并行时扫一眼就知道谁在干活,不用点进会话核对。改写在工具执行的瀑布上原地完成,主 agent 与子 agent 都无感知;只对配置后新发起的委派生效,已有子会话不追溯。
32
+
29
33
  ### 主 agent persona(编排人设)原文摘录
30
34
 
31
35
  来自 `presets/orchestrator/agent.cordis.yml` 的 `- id: persona` 行(YAML `prefix: |-` 块标量,内容行缩进 6 空格;v0.3.0 起 persona 配置键为 `prefix`,`text` 是生成器兼容的旧名)。编排与委派的总指引(persona 原文为英文,照录如下):
@@ -26,6 +26,10 @@ The main agent's model-facing surface is narrowed by `restrict.mjs` on the `syst
26
26
 
27
27
  Per-request tool tax (estimates; they float with the registered surface): full monolithic main agent ~16.2k tokens → ~5.5k after the allow list; `search_external` ~3.0k / `design` ~3.3k / `implement` ~3.8k, incurred only when actually delegated (figures from the header comment of `agent.cordis.yml`).
28
28
 
29
+ ### Sub-agent labels carry the role name
30
+
31
+ The task phrase the main agent writes when delegating (the delegation tool's `description` argument) persists with the sub-session and becomes the label shown in three places: the sidebar task-management page, the sub-agent tree in the session header, and the background-job list. Once a role has a display name configured (`roles.<toolName>.name`, see [Configuration](configuration_en.md) 2.4), the installer ships a `delegation-labels` plugin row with the preset: every sub-session that role spawns gets a uniform label of `role name · original task phrase` (e.g.「民工码农 · 修复标签显示」) — with several roles in flight, one glance tells you who is doing what, no session-digging required. The rewrite happens in-place on the tools waterfall; neither the main agent nor the sub-agent needs to cooperate or even know. It applies only to delegations spawned after the configuration change — existing sub-sessions are not relabeled.
32
+
29
33
  ### Main-agent persona (excerpt, verbatim)
30
34
 
31
35
  Taken from the `- id: persona` row of `presets/orchestrator/agent.cordis.yml` (a YAML `prefix: |-` block scalar whose content lines are indented 6 spaces; as of v0.3.0 the persona config key is `prefix`, with `text` kept as a legacy alias the generator still accepts). The overall orchestration-and-delegation guidance:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-paoding",
3
- "version": "0.3.9",
3
+ "version": "0.3.10",
4
4
  "description": "庖丁 — DSH (DeepSeek Harness) 编排模式安装器:host 感知的角色工具过滤 + Web 设置页可视化配置器",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paoding-config-ui",
3
- "version": "0.3.9",
3
+ "version": "0.3.10",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "main": "index.mjs",
@@ -7,6 +7,9 @@
7
7
  # · orchestrator-restrict → restrict.mjs 在 system-prompt/assemble 瀑布上把
8
8
  # 主 agent 的工具面裁剪为「编排 + 内部搜索」白名单;子 agent(origin:
9
9
  # 'subagent')放行,由各自 toolFilter 决定工具面
10
+ # · delegation-labels → delegation-labels.mjs 在 tools/pre-execute 瀑布上把
11
+ # 命中角色的委派调用 description 原地改写为「角色名 · 原任务短语」,任务
12
+ # 管理页 / 子代理目录树 / 后台任务列表的标签统一带角色名
10
13
  # · delegation-* → 4 个角色委派工具(dsh-tool-subagent,search_internal_deep 为静态委派),子 agent 创建时
11
14
  # 以 per-child tools.restrict() 只保留各自 allow 的工具
12
15
  #
@@ -68,6 +71,16 @@
68
71
  - id: orchestrator-restrict
69
72
  name: ./restrict.mjs
70
73
 
74
+ # ── 委派标签角色名前缀 ───────────────────────────────────────────────────────
75
+ # delegation-labels.mjs 挂 tools/pre-execute 瀑布,把命中角色的委派调用
76
+ # description 原地改写为「角色名 · 原任务短语」——子会话 label 持久化后,任务
77
+ # 管理页、会话头子代理目录树与后台任务列表统一显示角色名。roles 由生成器按
78
+ # roles.<toolName>.name 填充;空对象 = 没有角色配了名字,插件不注册任何处理器。
79
+ - id: delegation-labels
80
+ name: ./delegation-labels.mjs
81
+ config:
82
+ roles: {} # 生成器按 roles.<toolName>.name 填充
83
+
71
84
  # ── shell ───────────────────────────────────────────────────────────────────
72
85
 
73
86
  - id: tool-bash
@@ -0,0 +1,80 @@
1
+ /**
2
+ * 委派子代理标签带角色名(delegation-labels.mjs)
3
+ *
4
+ * Part of the dsh-paoding 编排模式 (orchestrator) preset. 主 agent 委派角色
5
+ * (implement / design / search_external 及自定义角色)时,委派工具调用的
6
+ * `description` 参数会成为子会话的持久 label——任务管理页(better-sidebar)、
7
+ * 原生会话头的子代理目录树、后台任务列表展示的都是它。编排模型随手写的
8
+ * description 只是一句任务短语,扫一眼分不清哪条子代理是谁在干活;本插件在
9
+ * `tools/pre-execute` 瀑布上把命中角色的委派调用改写成 `角色名 · 原任务短语`
10
+ * (如「民工码农 · 修复标签显示」),三处展示面统一带上角色名。
11
+ *
12
+ * 机制(提示词约束做不到的确定性改写):
13
+ * dsh-tools 调度器在每次工具执行前跑 tools/pre-execute 瀑布,随后把同一个
14
+ * exec.arguments 对象原样交给工具本体。dsh-tool-subagent 的子会话 label、
15
+ * jobs.start 的 job label、startContinuable 的 label 三者都取自
16
+ * args.description,所以在这里原地改写该字段,下游一并生效,并随子会话
17
+ * descriptor 持久化。处理完必须 `return await next()` 放行——本插件只管
18
+ * 标签,永不拦截委派。
19
+ *
20
+ * config.roles 由生成器按 dsh-paoding.config.yml 的 roles.<toolName>.name 填充
21
+ * (toolName → 显示名):没配名字的角色不进映射、标签保持原样;全空时本插件
22
+ * 不注册任何处理器。幂等:已带 `角色名 · ` 前缀的 description 不重复加前缀
23
+ * (同一次委派重复过瀑布也不会双写)。
24
+ *
25
+ * 失败面:整段 try/catch 包住,任何异常只 warn 一次后照常放行——标签出错顶多
26
+ * 退回旧观感,绝不能拦掉一次委派。
27
+ */
28
+
29
+ /** Cordis plugin name used by loader diagnostics. */
30
+ export const name = 'orchestrator-delegation-labels'
31
+
32
+ /** The waterfall rides on ctx events; tools guarantees the registry exists first. */
33
+ export const inject = ['tools']
34
+
35
+ /** 角色名与任务短语之间的缺省分隔符。 */
36
+ const DEFAULT_SEPARATOR = ' · '
37
+
38
+ /** 标签改写出错只提醒一次,避免每次委派都刷一条日志。 */
39
+ let warned = false
40
+
41
+ /**
42
+ * @param {object} ctx - cordis context(事件瀑布 / logger)。
43
+ * @param {{ separator?: string, roles?: Record<string, string> }} config
44
+ * 生成器写入的角色显示名映射与可选分隔符。
45
+ */
46
+ export function apply(ctx, config = {}) {
47
+ const separator =
48
+ typeof config.separator === 'string' && config.separator !== '' ? config.separator : DEFAULT_SEPARATOR
49
+ // 只收非空显示名;一个名字都没有就什么都不注册(无事可做,零开销)。
50
+ const displayNames = new Map()
51
+ for (const [toolName, displayName] of Object.entries(config.roles ?? {})) {
52
+ if (typeof toolName !== 'string' || toolName === '') continue
53
+ if (typeof displayName !== 'string' || displayName.trim() === '') continue
54
+ displayNames.set(toolName, displayName.trim())
55
+ }
56
+ if (displayNames.size === 0) return
57
+
58
+ ctx.on('tools/pre-execute', async (exec, next) => {
59
+ try {
60
+ const toolName = exec?.name
61
+ if (typeof toolName === 'string' && displayNames.has(toolName)) {
62
+ const args = exec.arguments
63
+ if (args !== null && typeof args === 'object') {
64
+ const prefix = `${displayNames.get(toolName)}${separator}`
65
+ const description = args.description
66
+ // 非空字符串且未带前缀才改写(幂等:再次进瀑布不双写前缀)。
67
+ if (typeof description === 'string' && description.trim() !== '' && !description.startsWith(prefix)) {
68
+ args.description = `${prefix}${description}`
69
+ }
70
+ }
71
+ }
72
+ } catch (error) {
73
+ if (!warned) {
74
+ warned = true
75
+ ctx.logger?.warn?.(`${name}: 标签前缀注入失败,本次按原 description 放行(后续不再提醒): ${String(error)}`)
76
+ }
77
+ }
78
+ return await next()
79
+ })
80
+ }
package/tools/lib/cli.mjs CHANGED
@@ -8,7 +8,7 @@
8
8
  * 自带的 Web 配置器(侧栏底部「庖丁配置」入口)。
9
9
  *
10
10
  * 依赖 util(ROLES / SRC_DIR / PROFILE_NAME_RE)、config(normalizeRoleName)、
11
- * host(removedReason)、state(collectState /
11
+ * host(removedReason)、state(PRESET_ASIS_FILES / collectState /
12
12
  * generateAndInstall)、alloc(autoAssignments,--auto 语义与插件首装共享)、
13
13
  * version(getVersionInfo,main 收尾的新版提示)以及 node:os / node:path。
14
14
  */
@@ -17,7 +17,7 @@ import path from 'node:path'
17
17
  import { PROFILE_NAME_RE, ROLES, SRC_DIR } from './util.mjs'
18
18
  import { normalizeRoleName } from './config.mjs'
19
19
  import { removedReason } from './host.mjs'
20
- import { collectState, generateAndInstall } from './state.mjs'
20
+ import { PRESET_ASIS_FILES, collectState, generateAndInstall } from './state.mjs'
21
21
  import { autoAssignments } from './alloc.mjs'
22
22
  import { getVersionInfo } from './version.mjs'
23
23
 
@@ -196,7 +196,7 @@ export function printReport({ scanned, mcpReports, pluginReports, roleResults, d
196
196
  `${dryRun ? '[dry-run] would write' : 'wrote'} ${path.join(dstDir, 'agent.cordis.yml')}` +
197
197
  (dryRun ? '' : ' (allow lists rewritten)'),
198
198
  )
199
- console.log(`${dryRun ? '[dry-run] would copy' : 'copied'} preset.yml, restrict.mjs as-is`)
199
+ console.log(`${dryRun ? '[dry-run] would copy' : 'copied'} ${PRESET_ASIS_FILES.join(', ')} as-is`)
200
200
  if (!dryRun) {
201
201
  console.log()
202
202
  console.log('next steps:')
@@ -16,7 +16,10 @@
16
16
  * 行行尾兜底注入;'one-shot' 不动源行 —— 源模板四块各预置
17
17
  * `backgroundMode: one-shot`(可见性),全默认产物与源零 diff)、
18
18
  * restrict config.allow 注入(computeRestrictAllow /
19
- * injectRestrictAllow / filterUsableTools)与整文 compose(composeGenerated)。
19
+ * injectRestrictAllow / filterUsableTools)、委派标签角色名映射注入
20
+ * (injectDelegationLabelRoles:roles.<toolName>.name → delegation-labels 行的
21
+ * config.roles,运行期给委派调用的 description 加「角色名 · 」前缀)与整文
22
+ * compose(composeGenerated)。
20
23
  * allow 过滤走白名单语义(usableWith / presetUniverse ∪ inventory,见
21
24
  * filterUsableTools 上方说明):手写进配置、既不在 preset 自带工具面也不在
22
25
  * 检测库存的名字一律从 allow 丢弃,不再「非 host 依赖名直通」。
@@ -328,6 +331,45 @@ export function injectRestrictAllow(srcText, allowList) {
328
331
  return srcText.slice(0, insertAt) + block + srcText.slice(insertAt)
329
332
  }
330
333
 
334
+ /**
335
+ * 委派标签角色名映射注入(delegation-labels 行的 config.roles):把
336
+ * delegation-labels.mjs 模板行的 `roles: {}` 占位改写为实际映射
337
+ * (toolName → 显示名,键值均过 yamlScalar 落安全标量)。运行期
338
+ * delegation-labels.mjs 据此在 tools/pre-execute 瀑布上给委派调用的
339
+ * description 加「角色名 · 」前缀(子会话 label / job label 的持久展示名)。
340
+ * 无任何名字时直接返回原文(产物与源零 diff,插件侧拿到空映射也不注册
341
+ * 处理器);找不到 delegation-labels 行或 `roles: {}` 占位属模板漂移,硬
342
+ * 错误(与 injectRestrictAllow 的失败口径一致)。键按字典序落盘,同一配置
343
+ * 反复生成产物稳定。
344
+ */
345
+ export function injectDelegationLabelRoles(srcText, roleNames) {
346
+ const entries = Object.entries(roleNames ?? {})
347
+ .filter(
348
+ ([toolName, displayName]) =>
349
+ typeof toolName === 'string' && toolName !== '' && typeof displayName === 'string' && displayName.trim() !== '',
350
+ )
351
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
352
+ if (entries.length === 0) return srcText
353
+ const marker = '- id: delegation-labels'
354
+ const idx = srcText.indexOf(marker)
355
+ if (idx === -1) throw new Error('delegation-labels row not found')
356
+ // 行块边界 = 下一条顶层 `- id: ` 行(本块只有 id / name / config.roles 三件),
357
+ // 占位匹配限定在块内,防误吞后续行的同名键。
358
+ const nextRow = srcText.indexOf('\n- id: ', idx + marker.length)
359
+ const blockEnd = nextRow === -1 ? srcText.length : nextRow
360
+ const rolesIdx = srcText.indexOf('roles: {}', idx)
361
+ if (rolesIdx === -1 || rolesIdx >= blockEnd) {
362
+ throw new Error('delegation-labels row: `roles: {}` placeholder not found')
363
+ }
364
+ const lineStart = srcText.lastIndexOf('\n', rolesIdx - 1) + 1
365
+ const lineEnd = srcText.indexOf('\n', rolesIdx)
366
+ const rendered = entries
367
+ .map(([toolName, displayName]) => ` ${yamlScalar(toolName)}: ${yamlScalar(displayName.trim())}`)
368
+ .join('\n')
369
+ const end = lineEnd === -1 ? srcText.length : lineEnd
370
+ return srcText.slice(0, lineStart) + ` roles:\n${rendered}` + srcText.slice(end)
371
+ }
372
+
331
373
  /**
332
374
  * Append text to the main-agent persona (`- id: persona` row) block scalar,
333
375
  * used to inline the selected skills' full SKILL.md bodies (composeGenerated,
@@ -874,6 +916,18 @@ export function composeGenerated(srcText, blocks, personaBlocks, assignments, in
874
916
  }
875
917
  if (restrictAllow !== null) out = injectRestrictAllow(out, restrictAllow)
876
918
 
919
+ // 委派标签角色名映射(delegation-labels 行的 config.roles):roles.<toolName>.name
920
+ // 的第二处生效点——内置与自定义角色一律收(配了名字才进映射,标签才带前缀);
921
+ // roles_remove 删除的角色委派行已整条不存在,跳过;全空则产物与源零 diff,
922
+ // 插件侧对空映射也不注册任何处理器。
923
+ const delegationLabelRoles = {}
924
+ for (const [toolName, role] of Object.entries(assignments.roles)) {
925
+ if (removedRoles.includes(toolName)) continue
926
+ const name = typeof role?.name === 'string' && role.name.trim() !== '' ? role.name.trim() : null
927
+ if (name !== null) delegationLabelRoles[toolName] = name
928
+ }
929
+ out = injectDelegationLabelRoles(out, delegationLabelRoles)
930
+
877
931
  // Main-agent skills (dual mode, mirrors the roles' skill soft guidance):
878
932
  // - main_agent_skills (soft): one compact "# skill: <name> — <description>"
879
933
  // row + "Read full rules: <file>" per skill appended to the main-agent
@@ -12,8 +12,9 @@
12
12
  * directory(安全侧:声明行误落 0.1.6 会炸 profile 启动,目录轨误落
13
13
  * 0.1.7 只是预设不显示);
14
14
  * 2) 声明行构造 buildPresetRowText:把生成的 agent.cordis.yml 全文内联成
15
- * 一条声明行的 config.plugins(restrict 行 name 换成 restrict.mjs 的绝对
16
- * file: URL——声明轨没有「相对 preset 目录」语义,必须绝对化);
15
+ * 一条声明行的 config.plugins(restrict 行与 delegation-labels 行的本地
16
+ * name 换成对应 .mjs 的绝对 file: URL——声明轨没有「相对 preset 目录」
17
+ * 语义,必须绝对化);
17
18
  * 3) home patch 托管块手术 upsertHomePatchRows / stripHomePatchRows /
18
19
  * readHomePatchRows:对 $DSH_HOME/cordis.patch.yml 的 MANAGED 标记块做
19
20
  * 行级增删查,块外用户内容一字节不动;托管块损坏(缺结束标记)时拒绝
@@ -155,11 +156,15 @@ function dshVersionText() {
155
156
  * 缩进基准:4 空格起排(`- insert:` 列表项层级由 upsertHomePatchRows 拼),
156
157
  * plugins: 落 8 空格、插件条目行落 10 空格。name/description 用 JSON.stringify
157
158
  * 落值——JSON 字符串即合法 YAML 双引号标量,换行/引号自动转义。agentYmlText
158
- * 先把 restrict 行的 `name: ./restrict.mjs` 替换为 restrictFileUrl(绝对 file:
159
- * URL),再整体 +10 缩进;空行保持空行(块标量相对缩进不变),`!!js` 表达式
160
- * 行原样保留(宿主 patch schema 支持,求值仍由 cordis 装载时完成)。
159
+ * 先把 restrict 行的 `name: ./restrict.mjs` 与 delegation-labels 行的
160
+ * `name: ./delegation-labels.mjs` 替换为对应绝对 file: URL(两个本地插件文件
161
+ * 均无「相对 preset 目录」语义),再整体 +10 缩进;空行保持空行(块标量相对
162
+ * 缩进不变),`!!js` 表达式行原样保留(宿主 patch schema 支持,求值仍由
163
+ * cordis 装载时完成)。文本里出现 delegation-labels 行而未提供
164
+ * delegationLabelsFileUrl 时硬错误——漏传会让相对名字静默进 patch,宿主
165
+ * 解析不了、preset 直接挂掉。
161
166
  */
162
- export function buildPresetRowText({ presetId, name = null, description = null, order = null, agentYmlText, restrictFileUrl }) {
167
+ export function buildPresetRowText({ presetId, name = null, description = null, order = null, agentYmlText, restrictFileUrl, delegationLabelsFileUrl = null }) {
163
168
  if (typeof presetId !== 'string' || !/^[a-z0-9][a-z0-9-]*$/.test(presetId)) {
164
169
  throw new Error(`buildPresetRowText: 非法 presetId: ${presetId}`)
165
170
  }
@@ -169,6 +174,12 @@ export function buildPresetRowText({ presetId, name = null, description = null,
169
174
  if (typeof restrictFileUrl !== 'string' || restrictFileUrl === '') {
170
175
  throw new Error('buildPresetRowText: restrictFileUrl 不能为空')
171
176
  }
177
+ const referencesLabelsRow = String(agentYmlText)
178
+ .split(/\r?\n/)
179
+ .some((line) => line.trim() === 'name: ./delegation-labels.mjs')
180
+ if (referencesLabelsRow && (typeof delegationLabelsFileUrl !== 'string' || delegationLabelsFileUrl === '')) {
181
+ throw new Error('buildPresetRowText: agentYmlText 引用了 ./delegation-labels.mjs,但未提供 delegationLabelsFileUrl')
182
+ }
172
183
  const lines = [
173
184
  ` - id: ${presetRowId(presetId)}`,
174
185
  ` name: '${PRESET_ROW_NAME}'`,
@@ -184,10 +195,13 @@ export function buildPresetRowText({ presetId, name = null, description = null,
184
195
  lines.push('')
185
196
  continue
186
197
  }
187
- // restrict 行换绝对 file: URL(按行精确内容匹配,缩进原样保留)
188
- const replaced = line.trim() === 'name: ./restrict.mjs'
189
- ? `${line.match(/^[ \t]*/)[0]}name: ${restrictFileUrl}`
190
- : line
198
+ // 本地插件行换绝对 file: URL(按行精确内容匹配,缩进原样保留)
199
+ const trimmed = line.trim()
200
+ const fileUrl =
201
+ trimmed === 'name: ./restrict.mjs' ? restrictFileUrl
202
+ : trimmed === 'name: ./delegation-labels.mjs' ? delegationLabelsFileUrl
203
+ : null
204
+ const replaced = fileUrl !== null ? `${line.match(/^[ \t]*/)[0]}name: ${fileUrl}` : line
191
205
  lines.push(` ${replaced}`)
192
206
  }
193
207
  while (lines.length > 0 && lines[lines.length - 1] === '') lines.pop() // 去尾部空行,段拼接才紧凑
@@ -503,7 +517,8 @@ export function readPresetDirMeta(dir) {
503
517
  /**
504
518
  * 把磁盘上已有的编排类 preset 目录补写进 home patch 托管块(缺行才补,已有行
505
519
  * 原样不动);行在、目录没了的撤行——目录与行同生同灭。产物不全(缺
506
- * agent.cordis.yml / restrict.mjs)的目录跳过并 warn,不拖垮其余补写。
520
+ * agent.cordis.yml / restrict.mjs / delegation-labels.mjs)的目录跳过并 warn,
521
+ * 不拖垮其余补写(整盘重生成可修复)。
507
522
  * 返回实际补写成功的 presetId 列表(既没补也没撤时为空数组)。
508
523
  */
509
524
  export function backfillHomePatchRows(dshHome, presetIds) {
@@ -518,6 +533,8 @@ export function backfillHomePatchRows(dshHome, presetIds) {
518
533
  const agentYmlText = readFileSync(path.join(dir, 'agent.cordis.yml'), 'utf8')
519
534
  const restrictFile = path.join(dir, 'restrict.mjs')
520
535
  if (!existsSync(restrictFile)) throw new Error('restrict.mjs 缺失')
536
+ const labelsFile = path.join(dir, 'delegation-labels.mjs')
537
+ if (!existsSync(labelsFile)) throw new Error('delegation-labels.mjs 缺失')
521
538
  const meta = readPresetDirMeta(dir)
522
539
  rows.push(
523
540
  buildPresetRowText({
@@ -527,6 +544,7 @@ export function backfillHomePatchRows(dshHome, presetIds) {
527
544
  order: meta.order,
528
545
  agentYmlText,
529
546
  restrictFileUrl: pathToFileURL(restrictFile).href,
547
+ delegationLabelsFileUrl: pathToFileURL(labelsFile).href,
530
548
  }),
531
549
  )
532
550
  backfilled.push(presetId)
@@ -58,6 +58,25 @@ import {
58
58
  upsertHomePatchRows,
59
59
  } from './preset-system.mjs'
60
60
 
61
+ /**
62
+ * 预设源文件清单(generateAndInstall 原样复制件;agent.cordis.yml 走重写,
63
+ * 不在此列)。collectState 的存在性检查与物化复制共用同一份,加源文件只改
64
+ * 这一处。delegation-labels.mjs 与 restrict.mjs 同生命周期:都是本地 cordis
65
+ * 插件,声明行轨内联时按绝对 file: URL 引用。
66
+ */
67
+ export const PRESET_ASIS_FILES = ['preset.yml', 'restrict.mjs', 'delegation-labels.mjs']
68
+
69
+ /**
70
+ * 预设源文件存在性检查(collectState 第一步):全部源文件缺任一件即硬错误,
71
+ * 报错点名缺的文件——delegation-labels.mjs 与 restrict.mjs 同口径。
72
+ */
73
+ export function assertPresetSourceFiles(srcDir = SRC_DIR) {
74
+ for (const name of ['agent.cordis.yml', ...PRESET_ASIS_FILES]) {
75
+ const file = path.join(srcDir, name)
76
+ if (!existsSync(file)) throw new Error(`missing preset source file: ${file}`)
77
+ }
78
+ }
79
+
61
80
  /**
62
81
  * Reusable state collection for the CLI and the visual config UI.
63
82
  * Runs the whole detection pipeline (patch scan, MCP handshakes, plugin and
@@ -78,10 +97,7 @@ export async function collectState({ dshHome, configFile, profile = null, patche
78
97
  const dstAgentFile = path.join(dstDir, 'agent.cordis.yml')
79
98
 
80
99
  // Source preset files must exist before anything else.
81
- const sourceFiles = ['agent.cordis.yml', 'preset.yml', 'restrict.mjs'].map((f) => path.join(SRC_DIR, f))
82
- for (const file of sourceFiles) {
83
- if (!existsSync(file)) throw new Error(`missing preset source file: ${file}`)
84
- }
100
+ assertPresetSourceFiles()
85
101
 
86
102
  const yamlMod = loadYaml(dshHome)
87
103
  const srcText = readFileSync(path.join(SRC_DIR, 'agent.cordis.yml'), 'utf8')
@@ -437,7 +453,7 @@ export function generateAndInstall(state, assignments, { dryRun = false, saveCon
437
453
  mkdirSync(path.join(dshHome, '.agent-presets'), { recursive: true })
438
454
  rmSync(dstDir, { recursive: true, force: true })
439
455
  mkdirSync(dstDir, { recursive: true })
440
- for (const name of ['preset.yml', 'restrict.mjs']) {
456
+ for (const name of PRESET_ASIS_FILES) {
441
457
  const src = path.join(SRC_DIR, name)
442
458
  const dst = path.join(dstDir, name)
443
459
  if (name === 'preset.yml' && displayName !== null) {
@@ -517,7 +533,8 @@ export function generateAndInstall(state, assignments, { dryRun = false, saveCon
517
533
  // declarative(0.1.7+):目录扫描机制已删,必须把本 preset 以一条 `- insert:`
518
534
  // 声明行写进 home patch 托管块才算安装完成(preset 元信息与写 preset.yml 同
519
535
  // 源:显示名取 displayName ?? SRC preset.yml 的 name,description / order 取
520
- // SRC;restrict.mjs 用绝对 file: URL——声明行没有「相对 preset 目录」语义)。
536
+ // SRC;restrict.mjs / delegation-labels.mjs 用绝对 file: URL——声明行没有
537
+ // 「相对 preset 目录」语义)。
521
538
  // directory(≤0.1.6):上面落盘即完成;home patch 里若有残留托管声明块
522
539
  //(宿主刚从 0.1.7 降级等场景)整块撤下自愈——残留声明行会让 profile 启动失败。
523
540
  if (wrote) {
@@ -531,6 +548,7 @@ export function generateAndInstall(state, assignments, { dryRun = false, saveCon
531
548
  order: meta.order,
532
549
  agentYmlText: generatedText,
533
550
  restrictFileUrl: pathToFileURL(path.join(dstDir, 'restrict.mjs')).href,
551
+ delegationLabelsFileUrl: pathToFileURL(path.join(dstDir, 'delegation-labels.mjs')).href,
534
552
  }),
535
553
  ])
536
554
  } else if (readHomePatchRows(dshHome).length > 0) {