dsh-expert-group 0.1.1

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.
Files changed (45) hide show
  1. package/LICENSE +38 -0
  2. package/README.md +355 -0
  3. package/THIRD_PARTY_NOTICES.md +35 -0
  4. package/assets/notification-whoosh.wav +0 -0
  5. package/bin/prompt-inspector.mjs +63 -0
  6. package/client-bundle.js +14891 -0
  7. package/client-expert-group-model.js +426 -0
  8. package/client-expert-group-sidebar.js +5401 -0
  9. package/client-freshness.js +364 -0
  10. package/client-model-config.js +2155 -0
  11. package/client-notifications.js +38 -0
  12. package/client.js +6487 -0
  13. package/cordis.patch.yml +10 -0
  14. package/fanout-host.js +1507 -0
  15. package/fanout.js +2981 -0
  16. package/host/composition.js +155 -0
  17. package/host/expert-group-runtime.js +822 -0
  18. package/host/expert-group-snapshot.js +450 -0
  19. package/host/fanout-runtime.js +822 -0
  20. package/host/handoff.js +80 -0
  21. package/host/lineage.js +274 -0
  22. package/host/notifications.js +189 -0
  23. package/host/persistence-health.js +389 -0
  24. package/host/recovery.js +242 -0
  25. package/host/rpc-transport.js +573 -0
  26. package/host/settings-runtime.js +168 -0
  27. package/host/settings-source.js +265 -0
  28. package/host/storage-binding.js +94 -0
  29. package/host/system-toast.js +916 -0
  30. package/image-routing.js +263 -0
  31. package/index.js +9327 -0
  32. package/install.ps1 +219 -0
  33. package/orchestrator.js +239 -0
  34. package/package.json +142 -0
  35. package/prompt-contracts.js +148 -0
  36. package/prompt-inspector.js +584 -0
  37. package/prompt-sidecar.js +610 -0
  38. package/resource-budget.js +540 -0
  39. package/role-registry.js +286 -0
  40. package/rpc-host.js +310 -0
  41. package/scripts/assemble-client.mjs +605 -0
  42. package/scripts/build.mjs +124 -0
  43. package/shared.js +4003 -0
  44. package/storage.js +190 -0
  45. package/uninstall.ps1 +47 -0
package/install.ps1 ADDED
@@ -0,0 +1,219 @@
1
+ # 安装 dsh-expert-group 到 DSH 的全局插件位置($DSH_HOME\profiles\packages\dsh-expert-group),
2
+ # 与工程/源码目录完全解耦:删除本脚本所在的原始目录后插件仍可正常运行。
3
+ # 支持从解压后的包目录直接运行:powershell -ExecutionPolicy Bypass -File .\install.ps1
4
+ # 同时清理同名旧安装位置(profiles\web\packages\*)。
5
+ # 复制面 = package.json 的 files 白名单:test/、test-support/、bench/、.scratch/ 等
6
+ # 开发内容与构建产物 tgz 不会进入用户目录。
7
+ # 维护用干跑模式(不做任何写入,仅供发布门禁/人工核对安装清单):
8
+ # powershell -ExecutionPolicy Bypass -File .\install.ps1 -DryRun [-DryRunListPath <文件>]
9
+ param(
10
+ # 干跑模式:只解析并输出安装白名单,不做任何写入、不构建 bundle、不调用 dsh CLI。
11
+ # 发布门禁测试用它核对「实际安装清单 == package.json 的 files 白名单」。
12
+ [switch]$DryRun,
13
+ # 干跑清单的落盘路径:显式以 UTF-8 写出,避免控制台代码页差异影响测试读取。
14
+ [string]$DryRunListPath
15
+ )
16
+
17
+ $ErrorActionPreference = "Stop"
18
+
19
+ $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
20
+ $sourcePath = $scriptDir
21
+ $packageName = "dsh-expert-group"
22
+
23
+ <#
24
+ 解析安装白名单:以 package.json 的 files 字段(npm 发布面的唯一事实源)为准。
25
+ 设计要点:
26
+ - 旧实现整目录复制(只排除 node_modules),会把 test/、test-support/、bench/、
27
+ .scratch/ 开发草稿与构建产物 tgz 一并带进用户 profiles,既膨胀体积又泄露开发内容,
28
+ 因此这里改为逐项复制 files 声明的条目。
29
+ - files 缺省或为空时直接抛错而不是回退整目录:宁可安装中断,也不放大安装面。
30
+ - npm 语义下 package.json 自身始终随包发布(files 里通常不写它),显式补入,
31
+ 否则 dsh plugin add 与 Node 的包解析都无法识别安装目录。
32
+ - 每项必须是仓库内相对路径:绝对路径、drive 前缀与 .. 越界项一律拒绝。
33
+ - 该函数被 DryRun 与真实复制共用,保证发布门禁验证的就是实际安装行为。
34
+ #>
35
+ function Get-InstallWhitelistEntries {
36
+ param([string]$Source)
37
+
38
+ $manifestPath = Join-Path $Source "package.json"
39
+ if (-not (Test-Path -LiteralPath $manifestPath)) {
40
+ throw "源目录缺少 package.json,无法解析安装白名单:$Source"
41
+ }
42
+ try {
43
+ # -Encoding UTF8:WinPS 5.1 的 Get-Content 默认按 ANSI 读取,UTF-8 manifest 的
44
+ # 中文 description 会被读成乱码导致 ConvertFrom-Json 失败;pwsh 7 默认 UTF-8 无此
45
+ # 问题,显式声明让两个宿主行为一致。
46
+ $sourceManifest = Get-Content -LiteralPath $manifestPath -Raw -Encoding UTF8 | ConvertFrom-Json -ErrorAction Stop
47
+ } catch {
48
+ throw "无法解析源 package.json $manifestPath:$($_.Exception.Message)"
49
+ }
50
+
51
+ $declared = @($sourceManifest.files | Where-Object { $_ -is [string] -and -not [string]::IsNullOrWhiteSpace($_) })
52
+ if ($declared.Count -eq 0) {
53
+ throw "源 package.json 的 files 白名单为空,拒绝整目录复制。请先在 package.json 中声明发布面。"
54
+ }
55
+
56
+ $entries = @($declared + @("package.json"))
57
+ $normalized = @()
58
+ foreach ($entry in $entries) {
59
+ # 统一分隔符为 /(npm 约定)并剥掉 ./ 前缀与尾部斜杠,得到仓库内相对路径。
60
+ $relative = ([string]$entry).Trim() -replace '\\', '/'
61
+ while ($relative.StartsWith("./")) { $relative = $relative.Substring(2) }
62
+ $relative = $relative.TrimEnd('/')
63
+ if ($relative -eq "" -or $relative.StartsWith("/") -or $relative -match "(^|/)\.\.(/|$)" -or $relative.Contains(":")) {
64
+ throw "package.json files 含越界或非相对路径项,拒绝安装:$entry"
65
+ }
66
+ $normalized += $relative
67
+ }
68
+ # 去重,避免同一路径被复制两次(例如 files 同时列了目录与其子项)。
69
+ return @($normalized | Select-Object -Unique)
70
+ }
71
+
72
+ if ($DryRun) {
73
+ # 干跑:只输出白名单相对路径(每行一项,/ 分隔),不做任何文件系统写入、
74
+ # 不构建 client bundle、不调用 dsh CLI,因此在测试与 CI 中可安全执行。
75
+ $dryRunEntries = @(Get-InstallWhitelistEntries -Source $sourcePath)
76
+ if (-not [string]::IsNullOrWhiteSpace($DryRunListPath)) {
77
+ # 显式 UTF-8 落盘(PS 5.1 会带 BOM,读取方需容忍 BOM):非 ASCII 条目也不会因
78
+ # 控制台代码页差异被读成乱码,测试因此可在任意 Windows/locale 下稳定断言。
79
+ $dryRunEntries | Set-Content -LiteralPath $DryRunListPath -Encoding UTF8
80
+ }
81
+ foreach ($dryRunEntry in $dryRunEntries) { Write-Output $dryRunEntry }
82
+ exit 0
83
+ }
84
+
85
+ # 源码目录和正式 npm 包都携带组合脚本;安装前统一刷新唯一的 classic client bundle,
86
+ # 保证复制到 profile 的生成物与三个源文件严格同步。
87
+ $clientAssembler = Join-Path $sourcePath "scripts\assemble-client.mjs"
88
+ if (Test-Path -LiteralPath $clientAssembler) {
89
+ Write-Host "生成 Expert Group 客户端 bundle ..." -ForegroundColor Cyan
90
+ & node $clientAssembler
91
+ if ($LASTEXITCODE -ne 0) { throw "客户端 bundle 生成失败,退出码 $LASTEXITCODE" }
92
+ }
93
+
94
+ # 全局安装位置必须位于 $DSH_HOME\profiles 目录树内:这样 Node 的父目录查找能找到
95
+ # $DSH_HOME\profiles\node_modules 的平坦依赖回退(@deepseek-ai/schemastery 等 Host
96
+ # 运行时依赖都在那里)。直接 link 工程源码目录会让 index.js 的嵌套 import 无法解析,
97
+ # 且工程目录删除后插件即失效——全局安装正是为了避免这一点。
98
+ $dshHome = if ($env:DSH_HOME) { $env:DSH_HOME } else { Join-Path $env:USERPROFILE ".dsh" }
99
+ $profileDir = Join-Path $dshHome "profiles\web"
100
+ $installTarget = Join-Path $dshHome "profiles\packages\$packageName"
101
+
102
+ Write-Host "全局安装 $packageName ..." -ForegroundColor Cyan
103
+ Write-Host " 源目录:$sourcePath"
104
+ Write-Host " 全局安装位置:$installTarget"
105
+
106
+ # 使用解析后的 profile 对象判断插件是否已经完成正式登记;不靠原始文本猜测。
107
+ $profileManifest = Join-Path $profileDir "package.json"
108
+ $manifest = $null
109
+ if (Test-Path -LiteralPath $profileManifest) {
110
+ try {
111
+ # 同上:profile manifest 由 dsh/插件写入 UTF-8,WinPS 5.1 需显式编码读取。
112
+ $manifest = Get-Content -LiteralPath $profileManifest -Raw -Encoding UTF8 | ConvertFrom-Json -ErrorAction Stop
113
+ } catch {
114
+ throw "无法解析 DSH profile manifest $profileManifest:$($_.Exception.Message)"
115
+ }
116
+ }
117
+
118
+ $dependencyProperty = $null
119
+ if ($null -ne $manifest -and $null -ne $manifest.dependencies) {
120
+ $dependencyProperty = $manifest.dependencies.PSObject.Properties[$packageName]
121
+ }
122
+ $dependencyValue = if ($null -ne $dependencyProperty) { [string]$dependencyProperty.Value } else { $null }
123
+ $bundleValues = @()
124
+ if ($null -ne $manifest -and $null -ne $manifest.dsh -and $null -ne $manifest.dsh.profile -and $null -ne $manifest.dsh.profile.bundles) {
125
+ $bundleValues = @($manifest.dsh.profile.bundles)
126
+ }
127
+ $hasDependency = $null -ne $dependencyProperty -and -not [string]::IsNullOrWhiteSpace($dependencyValue)
128
+ $hasBundle = @($bundleValues | Where-Object { $_ -is [string] -and $_ -eq $packageName }).Count -gt 0
129
+ $profileRegistered = $hasDependency -and $hasBundle
130
+
131
+ # CLI 可用时仍由 DSH 正式管理注册;CLI 不在 PATH 时只能升级已登记插件。
132
+ $dsh = Get-Command dsh -ErrorAction SilentlyContinue
133
+ $dshCommand = $null
134
+ if ($null -ne $dsh) {
135
+ $dshCommand = if (-not [string]::IsNullOrWhiteSpace([string]$dsh.Source)) {
136
+ [string]$dsh.Source
137
+ } else {
138
+ [string]$dsh.Name
139
+ }
140
+ }
141
+ $hasDshCommand = -not [string]::IsNullOrWhiteSpace($dshCommand)
142
+ if (-not $hasDshCommand -and -not $profileRegistered) {
143
+ throw "未找到 dsh 命令,且 profile 尚未有效登记 $packageName。请先安装 @deepseek-ai/dsh 并确保其 bin 目录在 PATH 中,再通过 DSH CLI 执行首次注册;脚本不会手工修改 profile。"
144
+ }
145
+ if ($hasDshCommand) {
146
+ Write-Host " 已检测到 DSH CLI,将执行正式 remove/add 注册流程。" -ForegroundColor DarkGray
147
+ } else {
148
+ Write-Host " 未找到 DSH CLI,但 profile 已有效登记 $packageName。将仅同步源码。" -ForegroundColor Yellow
149
+ }
150
+
151
+ # 卸载本包已有依赖,避免同一行重复注册导致工具重名。
152
+ if ($hasDshCommand) {
153
+ foreach ($pkg in @($packageName)) {
154
+ $installed = $false
155
+ if ($null -ne $manifest -and $null -ne $manifest.dependencies) {
156
+ $installed = $null -ne $manifest.dependencies.PSObject.Properties[$pkg]
157
+ }
158
+ if ($installed) {
159
+ Write-Host " 卸载已有依赖 $pkg ..." -ForegroundColor Yellow
160
+ & $dshCommand plugin --profile web remove $pkg
161
+ if ($LASTEXITCODE -ne 0) { throw "$dshCommand plugin remove $pkg 失败,退出码 $LASTEXITCODE" }
162
+ }
163
+ }
164
+ }
165
+
166
+ # 清理目标安装副本(profiles\web\packages\*)。
167
+ foreach ($legacy in @("$profileDir\packages\$packageName")) {
168
+ if (Test-Path $legacy) {
169
+ Remove-Item $legacy -Recurse -Force
170
+ Write-Host " 已删除旧安装副本 $legacy" -ForegroundColor DarkGray
171
+ }
172
+ }
173
+
174
+ # 同步全局安装副本:先清理旧副本,再严格按 package.json 的 files 白名单逐项复制。
175
+ # 白名单外的一切(test/、test-support/、bench/、.scratch/、tgz、docs/、node_modules)
176
+ # 都不会进入用户目录;目录项递归复制,文件项直接复制。
177
+ # 若脚本本身就运行在安装目标目录内(重新安装场景),跳过自复制。
178
+ if ($sourcePath -ne $installTarget) {
179
+ if (Test-Path $installTarget) { Remove-Item $installTarget -Recurse -Force }
180
+ New-Item -ItemType Directory -Path $installTarget -Force | Out-Null
181
+
182
+ $whitelist = @(Get-InstallWhitelistEntries -Source $sourcePath)
183
+ foreach ($entry in $whitelist) {
184
+ # 白名单在解析阶段已统一为 / 分隔并校验过相对性;这里再转成本机分隔符拼路径。
185
+ $relativePath = $entry -replace '/', '\'
186
+ $sourceItem = Join-Path $sourcePath $relativePath
187
+ if (-not (Test-Path -LiteralPath $sourceItem)) {
188
+ throw "安装白名单项在源目录中不存在:$relativePath(请同步修正 package.json 的 files)"
189
+ }
190
+
191
+ # 关键:先确保父目录存在,再把条目复制到父目录里,让 Copy-Item 自行拼接叶子名。
192
+ # 若直接以该条目自身的完整目标路径作为 -Destination 且该路径已存在,Copy-Item 会
193
+ # 在目标下多套一层同名目录,导致安装目录结构错位。
194
+ $targetParent = Split-Path -Parent (Join-Path $installTarget $relativePath)
195
+ if (-not (Test-Path -LiteralPath $targetParent)) {
196
+ New-Item -ItemType Directory -Path $targetParent -Force | Out-Null
197
+ }
198
+
199
+ # 白名单可能出现重叠(既列目录又列其子项)或重复项:先删除同名目标,避免
200
+ # Copy-Item 把源目录合并进已有同名目录而产生「新旧混合」的安装副本。
201
+ $copied = Join-Path $targetParent (Split-Path -Leaf $relativePath)
202
+ if (Test-Path -LiteralPath $copied) { Remove-Item -LiteralPath $copied -Recurse -Force }
203
+ Copy-Item -LiteralPath $sourceItem -Destination $targetParent -Recurse -Force
204
+ }
205
+ Write-Host (" 已按 files 白名单复制 {0} 项到安装目录(node_modules 与未声明内容不复制)" -f $whitelist.Count) -ForegroundColor DarkGray
206
+ } else {
207
+ Write-Host " 脚本位于安装目标目录内,跳过复制" -ForegroundColor DarkGray
208
+ }
209
+
210
+ # CLI 可用时通过 DSH 的正式流程加入 web profile;无 CLI 时仅保留现有登记并同步源码。
211
+ if ($hasDshCommand) {
212
+ & $dshCommand plugin --profile web add $installTarget
213
+ if ($LASTEXITCODE -ne 0) {
214
+ throw "$dshCommand plugin add 失败,退出码 $LASTEXITCODE"
215
+ }
216
+ Write-Host "完成 CLI 注册与源码同步。请重启 DSH Web(dsh web)后刷新页面。验证:$dshCommand --profile web --dump-config" -ForegroundColor Green
217
+ } else {
218
+ Write-Host "已跳过 CLI 注册,保留现有 profile 登记。源码已同步。请重启 DSH Web(dsh web)后刷新页面。" -ForegroundColor Green
219
+ }
@@ -0,0 +1,239 @@
1
+ /**
2
+ * DSH 根会话的主代理(orchestrator)提示词。
3
+ *
4
+ * 该模块只生成插件拥有的 root system section。root workflow 必须保留在 system
5
+ * 层级;动态 routing 和 child persona 只携带各自 scope 所需的短契约,避免把同一
6
+ * 段等待/委派说明复制到每个 tool schema 后触发宿主展示或上下文上限。
7
+ *
8
+ * @module dsh-expert-group/orchestrator
9
+ */
10
+
11
+ import { getRole, listRoles } from "./role-registry.js";
12
+
13
+ /**
14
+ * DSH 中由当前插件注册到 ctx.tools 的唯一命名空间专家工具。
15
+ * 角色集合与顺序来自统一角色注册表:这里不再维护第二份清单。
16
+ */
17
+ export const ORCHESTRATOR_ROLES = Object.freeze(listRoles().map((entry) => entry.id));
18
+
19
+ /**
20
+ * 每个角色的 prompt lane 文案(英文)。首行 "@role" 标记与职责摘要由注册表派生,
21
+ * 因此这里只保留角色特有的派发/不派发指引;缺失任一注册角色的 lane 会在模块加载时
22
+ * 显式失败,避免新增角色静默漏进 root prompt。
23
+ */
24
+ const ROLE_LANES = {
25
+ explorer: [
26
+ "- Lane: fast, read-only codebase reconnaissance",
27
+ "- Capabilities: locate files, symbols, and patterns, then return compressed factual context",
28
+ "- Delegate when: the scope is broad or uncertain, or independent searches can run in parallel",
29
+ "- Do not delegate when: you already know the exact path and need its contents",
30
+ ],
31
+ librarian: [
32
+ "- Lane: read-only external documentation and library research",
33
+ "- Capabilities: verify current APIs, official examples, version-specific behavior, and upstream fixes",
34
+ "- Delegate when: a library API or current external fact materially affects the implementation",
35
+ "- Do not delegate when: the answer is stable, local, or already established in the conversation",
36
+ ],
37
+ oracle: [
38
+ "- Lane: read-only architecture, risk, debugging strategy, and code review",
39
+ "- Capabilities: assess tradeoffs, persistent failures, security, scalability, and simplification opportunities",
40
+ "- Delegate when: the decision is high-risk or the root cause remains unclear after ordinary diagnosis",
41
+ "- Do not delegate when: the change is routine and the correct approach is already clear",
42
+ ],
43
+ designer: [
44
+ "- Lane: UI/UX and visual implementation",
45
+ "- Capabilities: own layout, hierarchy, spacing, responsive behavior, accessibility, and interaction quality",
46
+ "- Delegate when: users see the result and visual or interaction polish matters",
47
+ "- Do not delegate when: the work is headless logic with no visual decision",
48
+ ],
49
+ fixer: [
50
+ "- Lane: bounded implementation and test completion",
51
+ "- Capabilities: execute a clear scoped change across files and report validation results",
52
+ "- Delegate when: implementation is non-trivial, multi-file, mechanical, or benefits from parallel ownership",
53
+ "- Do not delegate when: the action is one small, isolated, low-risk edit or requires unresolved design judgment",
54
+ ],
55
+ observer: [
56
+ "- Lane: read-only image, screenshot, chart, diagram, and PDF analysis",
57
+ "- Capabilities: inspect visual files with read_image and return structured observations",
58
+ "- Delegate when: understanding visual content is required; always include the full workspace file path",
59
+ "- Do not delegate when: plain text inspection is sufficient",
60
+ ],
61
+ };
62
+
63
+ const FANOUT_DESCRIPTION = [
64
+ "fanout",
65
+ "- Generic same-role fan-out: submit one role with an ordered shards array.",
66
+ "- Each shard is an independent child attempt with bounded per-role concurrency, wait-all aggregation, cancellation, and manual retry.",
67
+ "- Use explicit unique shardId values and affectedPaths for write-role conflict checks; the call returns a parentTaskId immediately.",
68
+ ];
69
+
70
+ for (const entry of listRoles()) {
71
+ if (!Array.isArray(ROLE_LANES[entry.id])) {
72
+ throw new Error("orchestrator 缺少注册角色的 lane 描述: " + entry.id);
73
+ }
74
+ }
75
+
76
+ /** 角色的插件工具名来自注册表;未知角色显式失败而不是回落到原始字符串。 */
77
+ function toolNameOf(role) {
78
+ const entry = getRole(role);
79
+ if (entry === undefined) throw new Error("orchestrator 收到未注册的角色: " + String(role));
80
+ return entry.toolName;
81
+ }
82
+
83
+ /** 完整 root system section 的结构边界;诊断只读这些 marker,不修改实际文本。 */
84
+ export const ORCHESTRATOR_SECTION_MARKERS = Object.freeze([
85
+ "<Identity>",
86
+ "</Identity>",
87
+ "<Role>",
88
+ "</Role>",
89
+ "<Scope>",
90
+ "</Scope>",
91
+ "<Agents>",
92
+ "</Agents>",
93
+ "<Notifications>",
94
+ "</Notifications>",
95
+ "<Workflow>",
96
+ "</Workflow>",
97
+ "<Communication>",
98
+ "</Communication>",
99
+ "<DelegationContract>",
100
+ "</DelegationContract>",
101
+ ]);
102
+
103
+ /**
104
+ * 判断一次 DSH 提示词组装是否属于根 Agent。只有带 agent 且没有 parentSession
105
+ * 的在线会话才是根上下文;无 agent 的诊断组装保持不注入主代理提示词。
106
+ * @param {object} [assemblyContext] - DSH AssembleContext。
107
+ * @returns {boolean} 仅在线根 Agent 返回 true。
108
+ */
109
+ export function isRootAgentContext(assemblyContext) {
110
+ const agent = assemblyContext?.agent;
111
+ return agent !== void 0 && agent.session?.header?.parentSession === void 0;
112
+ }
113
+
114
+ /**
115
+ * 对已生成的 root system 文本提供轻量完整性诊断。插件不能知道宿主或模型的外部
116
+ * 上限,因此这里不截断、不重写文本,只报告 UTF-8 长度、section marker 完整性和
117
+ * 常见截断痕迹,供测试与宿主诊断使用。
118
+ * @param {string} [prompt] - buildOrchestratorPrompt() 的完整结果。
119
+ * @returns {{utf8Bytes:number, markersPresent:boolean, markerCounts:object, hasTruncationMarker:boolean, complete:boolean}}
120
+ */
121
+ export function inspectOrchestratorPrompt(prompt = "") {
122
+ const text = typeof prompt === "string" ? prompt : "";
123
+ const encoder = typeof TextEncoder === "function" ? new TextEncoder() : void 0;
124
+ const utf8Bytes = encoder === void 0 ? text.length : encoder.encode(text).length;
125
+ const markerCounts = {};
126
+ for (const marker of ORCHESTRATOR_SECTION_MARKERS) {
127
+ markerCounts[marker] = text.split(marker).length - 1;
128
+ }
129
+ const markersPresent = ORCHESTRATOR_SECTION_MARKERS.every((marker) => markerCounts[marker] === 1);
130
+ // 这些标记不属于插件契约;若宿主将它们混入结果,诊断应明确提示而非猜测修复。
131
+ const hasTruncationMarker = /(?:\.\.\.|TRUNCAT(?:ED|ION)|截断)/i.test(text);
132
+ return {
133
+ utf8Bytes,
134
+ markersPresent,
135
+ markerCounts,
136
+ hasTruncationMarker,
137
+ complete: text.length > 0 && markersPresent && !hasTruncationMarker,
138
+ };
139
+ }
140
+
141
+ /**
142
+ * 构建根 Agent 的完整编排提示词,作为 system-prompt section 使用。
143
+ * routing section 不再复制这里的 workflow;child 也不会收到这里的内容。
144
+ * @param {object} [options]
145
+ * @param {string[]} [options.roles] - 当前实际可用的专家角色。
146
+ * @param {boolean} [options.providerReady] - spawn provider 是否就绪。
147
+ * @param {boolean} [options.observerPreferred] - 是否将带图请求优先交给 Observer。
148
+ * @param {string} [options.rootSessionId] - 当前根 Agent 的 session id,用于绑定第一人称身份。
149
+ * @returns {string} 可直接注册为 system-prompt section 的完整文本。
150
+ */
151
+ export function buildOrchestratorPrompt({ roles = ORCHESTRATOR_ROLES, providerReady = true, observerPreferred = false, rootSessionId } = {}) {
152
+ const enabledRoles = ORCHESTRATOR_ROLES.filter((role) => roles.includes(role));
153
+ const available = enabledRoles.map((role) => toolNameOf(role));
154
+ const fanoutAvailable = providerReady && enabledRoles.length > 0;
155
+ if (fanoutAvailable) available.push("fanout");
156
+ // 每个角色段落:注册表派生的 "@toolName" 标记与"显示名 — 职责摘要",
157
+ // 再加上该角色特有的 lane 指引。注册表是角色身份与职责的唯一来源。
158
+ const roleText = enabledRoles.map((role) => {
159
+ const entry = getRole(role);
160
+ return ["@" + entry.toolName, "- Role: " + entry.label + " — " + entry.description, ...ROLE_LANES[role]].join("\n");
161
+ }).join("\n\n")
162
+ + (fanoutAvailable ? "\n\n" + FANOUT_DESCRIPTION.join("\n") : "");
163
+ const unavailable = providerReady
164
+ ? ""
165
+ : "\n\nThe expert subagent team is currently unavailable because the spawn provider is not ready. Do not call expert tools until the provider becomes available.\n";
166
+ const availableText = providerReady && available.length > 0
167
+ ? "\n\nAvailable expert tools: " + available.join(", ") + "."
168
+ + (fanoutAvailable ? " The fanout tool accepts one role and an ordered shards array, returns immediately with a parentTaskId, and exposes wait-all results through the task list/RPC." : "")
169
+ + "\n"
170
+ : "";
171
+ const imageRoutingText = enabledRoles.includes("observer")
172
+ ? observerPreferred === true
173
+ ? "\n\nImage routing: Observer优先 is enabled. For image input, use Observer only when its model explicitly declares image support; any role-named tool may receive the image directly when its selected model explicitly declares image support. Unknown capability is an error. Only pass the image parameter when forwarding a real image attachment from the current conversation; omit it entirely otherwise and never fabricate placeholder attachment metadata. Use a real .png durable copy for extensionless images; never change a suffix in place.\n"
174
+ : "\n\nImage routing: Observer优先 is disabled. Keep the main model first; use Observer only after the main model explicitly declares image input is unsupported. Any role-named tool may receive an image directly when its selected model explicitly declares image support. Unknown capability remains main-first. Only pass the image parameter when forwarding a real image attachment from the current conversation; omit it entirely otherwise and never fabricate placeholder attachment metadata. Use a real .png durable copy for extensionless images; never change a suffix in place.\n"
175
+ : "";
176
+ // session id 来自 Host 组装上下文。去掉换行,避免动态值破坏 system 分段。
177
+ const identitySessionId = rootSessionId === void 0
178
+ ? "(unavailable)"
179
+ : String(rootSessionId).replace(/[\r\n]+/g, " ");
180
+
181
+ const identity = [
182
+ "<Identity>",
183
+ "YOU are the current root/parent orchestrator for this conversation. There is no separate outer main agent above you. When the user says ‘主代理’, ‘parent’, or ‘root agent’, they mean YOU, the model executing this turn.",
184
+ "Your current root session id is " + identitySessionId + ". Treat that id as your own identity, never as a remote controller to prompt.",
185
+ "Never call session.prompt, HTTP/RPC, browser automation, or CLI commands against your own root session. That is self-messaging, not parent-to-child delegation.",
186
+ "To control a plugin-managed child for an explicit user-requested new pause, stop, redirect, or follow-up task, invoke the same role-named plugin tool again with a different prompt. queue=true is delivered through the public subagents.sendMessage API; queue=false may first call public subagents.interrupt, then also uses public subagents.sendMessage. RC1 sendMessage may steer a running child at its nearest step boundary or start/resume an idle child; do not await whenIdle or direct-steer a child, and do not claim an unpublished FIFO queue. A child in ready state has no current turn: do not send it a fake completion/stop prompt. dsh_expert_group_finalize is a root-only semantic action; it never ends a child and finalize ends only the current root turn. If terminal feedback arrived but the DEG scope is stale, use dsh_expert_group_release_scope with the exact childId; that tool releases only the DEG scope and does not stop the child. Never use tools.send_message, send_message, interrupt_agent, a generic subagent tool, Harness outer subagent_<role>, or raw Agent messages. Never use this control path merely to wait for a background result; the normal later-turn feedback mechanism delivers it. provider cancellation or teardown can affect delivery.",
187
+ "</Identity>",
188
+ ].join("\n");
189
+
190
+ const role = [
191
+ "<Role>",
192
+ "You are the primary workflow manager for coding work: understand the request, choose the lightest reliable plan, delegate bounded lanes, integrate findings, and verify the result. For non-trivial or uncertain work, default to delegating a bounded lane to an enabled expert when that expert can materially contribute. Handle work directly only when it is isolated, clear, low-risk, and delegation overhead materially exceeds the benefit, or when no enabled expert fits; verify proportionately with project tests and diagnostics.",
193
+ "</Role>",
194
+ ].join("\n");
195
+
196
+ const scope = [
197
+ "<Scope>",
198
+ "This complete workflow is root-only. Expert children receive only their own persona and tool limits: each is a leaf worker and must not delegate, and reports to its parent. Never treat a child or routing note as another root orchestrator.",
199
+ "</Scope>",
200
+ ].join("\n");
201
+
202
+ const notifications = [
203
+ "<Notifications>",
204
+ "Notifications are fully automatic and have no tool: the plugin Host keeps this root run's authoritative termination snapshot (completed, blocked, error, aborted, interrupted, max-tokens) together with the running state of every recursive descendant expert, and the browser client surfaces pending approval, plan-review, or question operations on their own. Do not call, invent, or emulate a notify/announce tool, and never send a task-level notification just because an individual child finished. Integrate the results and reach your normal user-facing conclusion.",
205
+ "</Notifications>",
206
+ ].join("\n");
207
+
208
+ const workflow = [
209
+ "<Workflow>",
210
+ "1. Understand explicit requirements and acceptance criteria; choose the lightest path balancing quality, speed, cost, and reliability.",
211
+ "2. For non-trivial work, identify independent lanes, dependencies, validation ownership, and allowed affectedPaths.",
212
+ "3. Dispatch the intended expert calls first. Summarize the accepted tasks and later integration/review/verification, then inspect every result, including started:false advisory or failure outcomes. Preserve each admissionEpoch and complete admissionToken set.",
213
+ "4. Dispatch only independent, non-overlapping lanes; admission is non-blocking. Set continue_parent_turn:true only for explicitly confirmed immediate safe non-overlapping work. When no safe work remains, call root-only dsh_expert_group_finalize last with summaryCompleted:true, outcomesChecked:true, safeParallelWork:false, current admissionEpoch, and every token. Finalize pauses the root goal before concluding only the current root turn; it never waits for, stops, resumes, or concludes a child. For ask_user_question or exit_plan_mode, use that same user-wait handoff and resume the saved exact goal ref after the root user/message when all waits release; grill-me/to-spec user replies use it too. Do not use update_goal pause because it injects context and continues the root. A pure continue_parent_turn batch has no handoff to finalize and may remain open only for the explicitly safe work it authorized. Do not continue with another tool call, wait, poll, list_agents, status query, planning step, or child intervention after finalize.",
214
+ "5. A dependent task waits for prerequisite feedback: dispatch the prerequisite first, return DSH ready, let DSH deliver its feedback in a later turn, then dispatch the dependent task with a compressed result. Never poll, use plugin-internal waiting or an ordinary waiting timeout, or intervene in a child; use DSH's existing feedback mechanism.",
215
+ "6. Keep at most one active continuable child per parent session per role. An identical task is reused; an explicit user-requested new pause/stop/redirect/follow-up task uses the same role tool with a different prompt. run_in_background is a compatibility input only: omitted, true, and false all use non-blocking admission. queue=true is delivered by public subagents.sendMessage and is not a waiting mechanism; queue=false may request cancellation through public subagents.interrupt before that same sendMessage admission. Do not call Agent.whenIdle/Agent.steer directly or claim an unpublished FIFO queue. A ready child is already between turns; never use finalize or a role tool merely to end it. Do not call the same role-named tool again merely to wait, confirm, or restate the same result. Non-success non-cancellation turns use bounded manager sendMessage before definitive failure; only a destroyed, not-resumable, or route-changed child is rebuilt.",
216
+ "7. After independent work, process only feedback already delivered in this turn; otherwise finalize the checked batch. Do not call a role tool again merely to wait, confirm, or restate the same result. After normal completion, abnormal completion, or expert report, reassess work and return DSH ready again when nothing is executable.",
217
+ "8. Route only to enabled roles. Read-only findings return for root integration; the root owns synthesis, review, tests, and user-facing completion. Verify proportionately with project test/build/diagnostic commands.",
218
+ "9. Every new delegation declares affectedPaths. Missing scope is not conflict-free; affected paths intersect an active expert task when their normalized ranges overlap. The root must also avoid direct work whose affected paths intersect an active expert task; this restriction applies even when the root is not creating a new delegation. An overlap advisory means do not start, steer, or follow up unless allowOverlap:true is explicitly safe. Independent scopes may run in parallel; prerequisite and dependent work may not share a round or run in parallel. Keep todo_write current for multi-step work; preserve existing order/statuses and append new work unless the user explicitly replaces or reprioritizes it.",
219
+ "</Workflow>",
220
+ ].join("\n");
221
+
222
+ const communication = [
223
+ "<Communication>",
224
+ "Ask a concise targeted question when a critical requirement, permission, or choice blocks execution. Do not guess critical paths or architecture; state minor assumptions briefly. Keep updates brief and final output focused on result and verification.",
225
+ "</Communication>",
226
+ ].join("\n");
227
+
228
+ const delegationContract = [
229
+ "<DelegationContract>",
230
+ "每次向专家子代理派发任务时,prompt 必须统一使用中文,第一句必须是“你是 XXX 子代理。”,且必须作为第一行;任务正文必须自包含,写明相关 workspace 路径、允许范围和预期验证。固定 child 纪律由 persona、toolFilter 和 runtime guard 注入和执行,主代理不要在任务正文重复写入 send_message、叶子 worker 或最终 assistant 输出规则。",
231
+ "每条派发 prompt 必须自包含,写明相关 workspace 路径、允许范围和预期验证;只能使用本插件启用的 role-named 工具,禁止使用 Harness outer subagent_<role>、PTC run_code 或 generic subagent 替代;不要把 root workflow 或固定 child 纪律重复写入任务正文。",
232
+ "</DelegationContract>",
233
+ ].join("\n");
234
+
235
+ return identity + "\n\n" + role + "\n\n" + scope + "\n\n<Agents>\n"
236
+ + (roleText || "No expert agents are currently enabled.") + "\n</Agents>"
237
+ + availableText + imageRoutingText + unavailable + "\n" + notifications + "\n\n"
238
+ + workflow + "\n\n" + communication + "\n\n" + delegationContract;
239
+ }
package/package.json ADDED
@@ -0,0 +1,142 @@
1
+ {
2
+ "name": "dsh-expert-group",
3
+ "version": "0.1.1",
4
+ "description": "DSH 专家子代理、Expert Group 任务侧边栏与设置页(双面 bundle)",
5
+ "type": "module",
6
+ "main": "./index.js",
7
+ "bin": {
8
+ "prompt-inspector": "bin/prompt-inspector.mjs"
9
+ },
10
+ "exports": {
11
+ ".": "./index.js",
12
+ "./client": "./client-bundle.js",
13
+ "./client-freshness": "./client-freshness.js",
14
+ "./shared": "./shared.js",
15
+ "./role-registry": "./role-registry.js",
16
+ "./resource-budget": "./resource-budget.js",
17
+ "./orchestrator": "./orchestrator.js",
18
+ "./image-routing": "./image-routing.js",
19
+ "./storage": "./storage.js",
20
+ "./prompt-inspector": "./prompt-inspector.js",
21
+ "./prompt-sidecar": "./prompt-sidecar.js",
22
+ "./cordis.patch.yml": "./cordis.patch.yml",
23
+ "./package.json": "./package.json"
24
+ },
25
+ "files": [
26
+ "index.js",
27
+ "client-bundle.js",
28
+ "client.js",
29
+ "client-notifications.js",
30
+ "client-expert-group-model.js",
31
+ "client-expert-group-sidebar.js",
32
+ "client-model-config.js",
33
+ "client-freshness.js",
34
+ "shared.js",
35
+ "role-registry.js",
36
+ "resource-budget.js",
37
+ "host/composition.js",
38
+ "host/expert-group-snapshot.js",
39
+ "host/expert-group-runtime.js",
40
+ "host/fanout-runtime.js",
41
+ "host/handoff.js",
42
+ "host/lineage.js",
43
+ "host/notifications.js",
44
+ "host/rpc-transport.js",
45
+ "host/settings-source.js",
46
+ "host/recovery.js",
47
+ "host/settings-runtime.js",
48
+ "host/storage-binding.js",
49
+ "host/persistence-health.js",
50
+ "host/system-toast.js",
51
+ "orchestrator.js",
52
+ "prompt-contracts.js",
53
+ "image-routing.js",
54
+ "storage.js",
55
+ "prompt-inspector.js",
56
+ "prompt-sidecar.js",
57
+ "fanout.js",
58
+ "fanout-host.js",
59
+ "rpc-host.js",
60
+ "assets/notification-whoosh.wav",
61
+ "scripts/assemble-client.mjs",
62
+ "scripts/build.mjs",
63
+ "bin/prompt-inspector.mjs",
64
+ "cordis.patch.yml",
65
+ "README.md",
66
+ "THIRD_PARTY_NOTICES.md",
67
+ "LICENSE",
68
+ "install.ps1",
69
+ "uninstall.ps1"
70
+ ],
71
+ "dsh": {
72
+ "bundle": {
73
+ "patch": "./cordis.patch.yml"
74
+ },
75
+ "client": {
76
+ "platform": "web",
77
+ "inject": [
78
+ "@deepseek-ai/dsh-client-modules",
79
+ "@deepseek-ai/dsh-client-connection",
80
+ "@deepseek-ai/dsh-client-ui-primitives",
81
+ "@deepseek-ai/dsh-client-ui-settings",
82
+ "@deepseek-ai/dsh-client-ui-workspace",
83
+ "@deepseek-ai/dsh-client-ui-sidebar-right",
84
+ "dsh-better-sidebar"
85
+ ]
86
+ }
87
+ },
88
+ "peerDependencies": {
89
+ "@deepseek-ai/cordis": "^4.0.2",
90
+ "@deepseek-ai/dsh-agent": "^0.2.0-rc.1",
91
+ "@deepseek-ai/dsh-attachment": "^0.2.0-rc.1",
92
+ "@deepseek-ai/dsh-client-modules": "^0.2.0-rc.1",
93
+ "@deepseek-ai/dsh-client-connection": "^0.2.0-rc.1",
94
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.2.0-rc.1",
95
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.2.0-rc.1",
96
+ "@deepseek-ai/dsh-client-ui-settings": "^0.2.0-rc.1",
97
+ "@deepseek-ai/dsh-client-ui-workspace": "^0.2.0-rc.1",
98
+ "@deepseek-ai/dsh-client-ui-sidebar-right": "^0.2.0-rc.1",
99
+ "@deepseek-ai/dsh-invariants": "^0.2.0-rc.1",
100
+ "@deepseek-ai/dsh-llm": "^0.2.0-rc.1",
101
+ "@deepseek-ai/dsh-scope": "^0.2.0-rc.1",
102
+ "@deepseek-ai/dsh-session": "^0.2.0-rc.1",
103
+ "@deepseek-ai/dsh-session-projection": "^0.2.0-rc.1",
104
+ "@deepseek-ai/dsh-settings": "^0.2.0-rc.1",
105
+ "@deepseek-ai/dsh-storage": "^0.2.0-rc.1",
106
+ "@deepseek-ai/dsh-storage-domain": "^0.2.0-rc.1",
107
+ "@deepseek-ai/dsh-subagent": "^0.2.0-rc.1",
108
+ "@deepseek-ai/dsh-system-prompt": "^0.2.0-rc.1",
109
+ "@deepseek-ai/dsh-tools": "^0.2.0-rc.1",
110
+ "@deepseek-ai/dsh-user-approval": "^0.2.0-rc.1",
111
+ "@deepseek-ai/schemastery": "^3.18.1",
112
+ "dsh-better-sidebar": "^0.19.1",
113
+ "react": "^18.2.0",
114
+ "zod": "^4.4.3"
115
+ },
116
+ "peerDependenciesMeta": {
117
+ "@deepseek-ai/dsh-client-ui-sidebar-right": {
118
+ "optional": true
119
+ },
120
+ "dsh-better-sidebar": {
121
+ "optional": true
122
+ }
123
+ },
124
+ "engines": {
125
+ "node": "^22.19.0 || >=24.0.0"
126
+ },
127
+ "scripts": {
128
+ "build": "node scripts/build.mjs",
129
+ "prepare": "npm run build",
130
+ "test": "node --test \"test/*.test.mjs\"",
131
+ "release-gate": "node --test test/release-gate.test.mjs test/package-portability.test.mjs test/client-shared-boundary.test.mjs test/model-config.test.mjs",
132
+ "release": "node scripts/sync-dsh-version.mjs && pnpm run build && pnpm run test && pnpm run release-gate"
133
+ },
134
+ "license": "MIT",
135
+ "repository": {
136
+ "type": "git",
137
+ "url": "git+https://github.com/dimensionli95-ship-it/dsh-expert-group.git"
138
+ },
139
+ "optionalDependencies": {
140
+ "sharp": "^0.35.4"
141
+ }
142
+ }