@twinsearth/dsh-master 3.3.3 → 3.3.5

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 (57) hide show
  1. package/README.md +17 -0
  2. package/client.js +126 -1
  3. package/cordis.patch.yml +51 -0
  4. package/dist-host/agents/pjm-agent.js +62 -0
  5. package/dist-host/agents/pm-agent.js +57 -0
  6. package/dist-host/agents/prompts.js +71 -0
  7. package/dist-host/agents/toolkit.js +64 -0
  8. package/dist-host/channel/abort-registry.js +93 -0
  9. package/dist-host/channel/protocol.js +114 -0
  10. package/dist-host/channel/server.js +349 -0
  11. package/dist-host/channel/stream-state.js +343 -0
  12. package/dist-host/channel/ws.js +199 -0
  13. package/dist-host/dual/documents.js +143 -0
  14. package/dist-host/dual/orchestrator.js +214 -0
  15. package/dist-host/dual/schema.js +367 -0
  16. package/dist-host/engine/atomic-switch.js +159 -0
  17. package/dist-host/engine/health-probe.js +86 -0
  18. package/dist-host/engine/index.js +18 -0
  19. package/dist-host/engine/llm-stream.js +215 -0
  20. package/dist-host/engine/plugin-updater.js +392 -0
  21. package/dist-host/engine/safe-update.js +150 -0
  22. package/dist-host/index.js +154 -3
  23. package/dist-host/kernel.js +27 -1
  24. package/dist-host/plugins/cost-optimizer.js +3 -3
  25. package/dist-host/plugins/dual-manager.js +374 -77
  26. package/docs/AGENTS-PM-PJM.md +207 -0
  27. package/docs/ARCHITECTURE.md +2 -0
  28. package/docs/CHANGELOG.md +74 -0
  29. package/docs/CHANNEL-WEBSOCKET.md +169 -0
  30. package/docs/DESIGN-command-center.md +147 -0
  31. package/docs/ENGINE-PRIMITIVES.md +215 -0
  32. package/docs/GAP-ANALYSIS-fang-session.md +75 -0
  33. package/docs/REVIEW-PM-PJM-3.3.4.md +179 -0
  34. package/docs/examples/command-center/README.md +167 -0
  35. package/package.json +5 -1
  36. package/src/agents/pjm-agent.ts +62 -0
  37. package/src/agents/pm-agent.ts +58 -0
  38. package/src/agents/prompts.ts +74 -0
  39. package/src/agents/toolkit.ts +64 -0
  40. package/src/channel/abort-registry.ts +85 -0
  41. package/src/channel/protocol.ts +110 -0
  42. package/src/channel/server.ts +289 -0
  43. package/src/channel/stream-state.ts +349 -0
  44. package/src/channel/ws.ts +182 -0
  45. package/src/dual/documents.ts +142 -0
  46. package/src/dual/orchestrator.ts +223 -0
  47. package/src/dual/schema.ts +355 -0
  48. package/src/engine/atomic-switch.ts +146 -0
  49. package/src/engine/health-probe.ts +84 -0
  50. package/src/engine/index.ts +20 -0
  51. package/src/engine/llm-stream.ts +189 -0
  52. package/src/engine/plugin-updater.ts +359 -0
  53. package/src/engine/safe-update.ts +148 -0
  54. package/src/index.ts +143 -4
  55. package/src/kernel.ts +28 -1
  56. package/src/plugins/cost-optimizer.ts +3 -3
  57. package/src/plugins/dual-manager.ts +337 -76
package/README.md CHANGED
@@ -40,6 +40,22 @@ DSH Host
40
40
  > 唯一保留的限制是 **Router 不进 fork**(分身只执行,不注册控制面)。
41
41
  > 旧的「母体不接纳执行体」约束已按用户明确要求废止。
42
42
 
43
+ ## 智能体(3.3.4)
44
+
45
+ 除子插件行之外,房主还提供两个**可选择的 Agent preset**(新会话创建时选择):
46
+
47
+ | 智能体 | preset | 职责 | 工具边界 |
48
+ |---|---|---|---|
49
+ | 产品经理 PM | `master-pm` | 澄清需求、WHY/WHAT、PRD、六类现状盘点 | 只读 fs + 搜索 + 提问 + 交付;**不执行、不派发、不发布** |
50
+ | 项目经理 PJM | `master-pjm` | WBS、七类项目管理、隔离派发、节点汇总、结案 | 只读 fs + 待办 + 交付;派发只走 `fangForker.spawn`,无则 fail-closed |
51
+
52
+ - PM 的“已完成”必须有证据(文件/测试/报告),否则拒收;冲突与歧义必须给用户重新判断的选项。
53
+ - PJM 的结案有门禁:全部步骤 done + 必须写 summary;结案后冻结,不能再派发。
54
+ - 两者共享 `dual-manager` 的结构化状态(PRD / 盘点 / 计划 / 节点 / 结案),文档只写 Session workspace 下的 `docs/pm`、`docs/pjm`。
55
+ - 详见 [智能体设计](docs/AGENTS-PM-PJM.md)。
56
+ - 面板「房主」抽屉新增两张智能体卡片与「智囊团 · Command Center」**只读**摘要(PRD / 盘点 / 计划 / 结案 / 派发可用性),不提供写入口。
57
+ - 3.3.5 起面板多一块「智囊团 · 闭环」看板:读 `GET /dsh-master/api/dual`,显示待澄清交接 / 待接单 / 项目进度 / 待复核 / 已复核;面板内**不能聊天、不能回答选择题、不能派发**,设计见 [DESIGN-command-center.md](docs/DESIGN-command-center.md)。
58
+
43
59
  ## 本地开发
44
60
 
45
61
  ```bash
@@ -100,6 +116,7 @@ dsh plugin add <package> # 加载 dist-host/dist-worker
100
116
  - **3.3.1 发布线**:M5 收口 —— 瓮城回流接口升级到 `@twinsearth/dsh-fang-contract` **1.1.0**(`fang.relay.publish` / `fang.relay.status`),房主侧 `src/cost/fang-relay.ts` 做真实探测与投递(只有对端回执才算送达,不可用给具名原因);里程碑 **M1–M5 全部 done**。详见 [M5 回流实测](docs/EXPERIMENT-M5-fang-relay.md)。
101
117
  - **3.3.0 发布线**:成本优化系统层(ledger / budget / policy / audit)+ Worker 四个成本切点 + A1 紧凑化(默认 shadow)+ A2–A3 / B1–B4;真实 A/B 实测 tokens/task 6263 → 3906.75(−37.6%),成功率 0pp 差,详见 [3.3.0 架构设计](docs/DESIGN-v3.3.0-cost-optimization.md)。
102
118
  - 3.2.10 发布线:角色矩阵放宽——Router 进入母体(essence/mother),Worker 与 Worker Agent Loop 进入原身(fork/essence);母体仍不接纳执行体,fork 仍不注册 Router。
119
+ - **3.3.4 发布线(智能体)**:新增两个可选择的 Agent preset —— 产品经理(PM,`master-pm`)与项目经理(PJM,`master-pjm`),共享 `dual-manager` 的结构化 PRD / 六类盘点 / WBS / 结案;面板新增智能体卡片与「智囊团 · Command Center」只读摘要。详见 [智能体设计](docs/AGENTS-PM-PJM.md)、[产品盘点与项目计划](docs/REVIEW-PM-PJM-3.3.4.md)。
103
120
  - **3.3.3 发布线(角色矩阵调整,用户明确要求)**:母体全开(Worker / Worker Agent Loop / 成本优化进 mother),分身挂双主管(dual 进 fork);唯一保留的限制是 Router 不进 fork。
104
121
  - 3.2.9 发布线:房主面板改注册到 `shell.overlay` 常驻浮层——点击面板之外不收起、侧栏「房主」按钮二次点击收起、正文独立滚动;新增无浏览器 hook 运行时行为测试(开关、无 outside-click 监听、滚动容器、SSE 失败不卸载面板)。
105
122
  - 3.2.8 发布线:与瓮城通过 fang-master contract 1.0 互联(local/http/file/legacy 四级保底);GitHub Release + NPM public。
package/client.js CHANGED
@@ -110,6 +110,128 @@ window.__ModuleLoader__.load({
110
110
  )
111
111
  }
112
112
 
113
+ /** 3.3.4:PM / PJM 智能体卡片(Agent preset,与子插件行分开显示)。 */
114
+ function AgentCard({ item }) {
115
+ return h('div', { style: S.card },
116
+ h('div', { style: S.row },
117
+ h('span', { style: S.name }, item.name),
118
+ tag(item.nameEn),
119
+ tag('preset ' + item.preset),
120
+ h('span', { style: { marginLeft: 'auto', fontSize: 11.5, ...(item.selectable ? S.ok : S.off) } },
121
+ item.selectable ? '可选择' : '本宿主不可用')),
122
+ h('div', { style: S.desc }, item.boundary),
123
+ h('div', { style: S.desc }, '职责:' + (item.responsibilities || []).join(' · ')),
124
+ h('div', { style: S.desc }, '工具:' + (item.tools || []).join(' · ')),
125
+ h('div', { style: S.desc }, item.hint || ''),
126
+ )
127
+ }
128
+
129
+ /**
130
+ * 3.3.4「智囊团」只读摘要:PM/PJM 两个 Agent 共享 dual-manager 的结构化账目。
131
+ * 面板只展示计数与协作状态,不创建 PRD/计划(写入只经 Agent 自己的工具)。
132
+ */
133
+ function CommandCenter({ dual }) {
134
+ const line = (label, value) => h('div', { style: S.desc }, label + ':' + value)
135
+ if (!dual) {
136
+ return h('div', { style: S.card, 'data-dsh-master': 'command-center' },
137
+ h('div', { style: S.row },
138
+ h('span', { style: S.name }, '智囊团 · Command Center'),
139
+ tag('未装配'),
140
+ h('span', { style: { marginLeft: 'auto', fontSize: 11.5, ...S.off } }, '不可用')),
141
+ h('div', { style: S.desc }, 'dual-manager 未启用:PM/PJM 的工具会返回 dual-service-unavailable。'))
142
+ }
143
+ const pm = dual.pm || {}
144
+ const pjm = dual.pjm || {}
145
+ const coop = dual.cooperation || {}
146
+ const loop = dual.loop || {}
147
+ return h('div', { style: S.card, 'data-dsh-master': 'command-center' },
148
+ h('div', { style: S.row },
149
+ h('span', { style: S.name }, '智囊团 · Command Center'),
150
+ tag('只读'),
151
+ h('span', { style: { marginLeft: 'auto', fontSize: 11.5, ...S.ok } }, dual.ok ? '已装配' : '降级')),
152
+ line('产品经理', 'PRD ' + (pm.prds ?? 0) + ' · 盘点 ' + (pm.assessments ?? 0) + ' · 交接 ' + (pm.handoffs ?? 0) + ' · 复核 ' + (pm.reviews ?? 0)),
153
+ line('项目经理', '计划 ' + (pjm.plans ?? 0) + ' · 已结案 ' + (pjm.closed ?? 0) + ' · 反馈 ' + (pjm.feedbacks ?? 0)),
154
+ line('闭环', '待复核 ' + (loop.awaitingReview ?? 0) + ' · 通过 ' + (loop.accepted ?? 0) + ' · 退回 ' + (loop.changesRequested ?? 0)),
155
+ line('协作', '决策中心 ' + (coop.decisionCenter ? '✓' : '×') + ' · 分身派发 ' + (coop.forker ? '✓' : '×')),
156
+ h('div', { style: S.desc }, '闭环:PM 交接 → PJM 派发/自反馈 → PM 复核/复盘/总结;面板只读,写入只经 Agent 工具。'))
157
+ }
158
+
159
+ /**
160
+ * 3.3.5「智囊团」看板:读 /dsh-master/api/dual 的真实闭环数据。
161
+ * 只读——面板不能替用户回答选择题、不能代替 PJM 派发;那些只发生在 PM/PJM 会话里。
162
+ */
163
+ function DualBoard() {
164
+ const [board, setBoard] = React.useState(null)
165
+ const [error, setError] = React.useState('')
166
+ React.useEffect(() => {
167
+ let alive = true
168
+ const load = async () => {
169
+ try {
170
+ const res = await fetch('/dsh-master/api/dual', { headers: { accept: 'application/json' } })
171
+ const body = await res.json().catch(() => null)
172
+ if (!alive) return
173
+ if (res.ok && body && body.ok !== false) { setBoard(body); setError('') } else setError('智囊团数据不可达')
174
+ } catch { if (alive) setError('智囊团数据不可达') }
175
+ }
176
+ void load()
177
+ const timer = setInterval(load, 5000)
178
+ return () => { alive = false; clearInterval(timer) }
179
+ }, [])
180
+
181
+ const card = (title, hint) => h('div', { style: S.row },
182
+ h('span', { style: { ...S.name, fontSize: 12.5 } }, title),
183
+ hint ? h('span', { style: { marginLeft: 'auto', fontSize: 11, ...S.off } }, hint) : null)
184
+
185
+ if (error) return h('div', { style: S.card }, card('智囊团 · 闭环', '不可达'), h('div', { style: S.desc }, error))
186
+ if (!board) return h('div', { style: S.card }, card('智囊团 · 闭环', '读取中'), h('div', { style: S.desc }, '正在读取交接/计划/反馈/复核…'))
187
+ if (board.available === false) {
188
+ return h('div', { style: S.card, 'data-dsh-master': 'dual-board' },
189
+ card('智囊团 · 闭环', '未装配'),
190
+ h('div', { style: S.desc }, 'dual-manager 未启用,PM/PJM 工具会返回 dual-service-unavailable。'))
191
+ }
192
+
193
+ const loop = board.loop || {}
194
+ const pending = (board.handoffs || []).filter((item) => item.status === 'needs-clarification')
195
+ const ready = (board.handoffs || []).filter((item) => item.status === 'ready')
196
+ const running = (board.plans || []).filter((plan) => plan.status === 'running' || plan.status === 'planned' || plan.status === 'done')
197
+ const awaiting = (board.feedbacks || []).filter((feedback) => !(board.reviews || []).some((review) => review.feedbackId === feedback.id))
198
+
199
+ return h('div', { style: S.card, 'data-dsh-master': 'dual-board' },
200
+ card('智囊团 · 闭环', '只读'),
201
+ h('div', { style: S.desc }, '待决 ' + pending.length + ' · 可接单 ' + ready.length + ' · 进行中 ' + running.length + ' · 待复核 ' + awaiting.length),
202
+ h('div', { style: S.desc }, '闭环计数:通过 ' + (loop.accepted ?? 0) + ' · 退回 ' + (loop.changesRequested ?? 0)),
203
+
204
+ pending.length > 0 ? h('div', { style: { ...S.desc, marginTop: 8, fontWeight: 600 } }, '① 需要 PM 澄清(不能在面板里回答)') : null,
205
+ ...pending.slice(0, 3).map((item) => h('div', { key: item.id, style: S.desc },
206
+ '· ' + item.title + ' — ' + (item.reason || 'needs-clarification'),
207
+ (item.openQuestions || []).length > 0 ? h('div', { style: { ...S.desc, paddingLeft: 10 } }, '未决:' + item.openQuestions.join(';')) : null)),
208
+
209
+ ready.length > 0 ? h('div', { style: { ...S.desc, marginTop: 8, fontWeight: 600 } }, '② 交接已就绪,等 PJM 接单') : null,
210
+ ...ready.slice(0, 3).map((item) => h('div', { key: item.id, style: S.desc }, '· ' + item.title + '(验收 ' + (item.acceptance || []).length + ' 条)')),
211
+
212
+ running.length > 0 ? h('div', { style: { ...S.desc, marginTop: 8, fontWeight: 600 } }, '③ 项目进度') : null,
213
+ ...running.slice(0, 3).map((plan) => {
214
+ const percent = plan.progress ? plan.progress.percent : 0
215
+ const last = plan.lastReport
216
+ const headline = '· ' + plan.title + ' — ' + (plan.progress ? plan.progress.done + '/' + plan.progress.total + '(' + percent + '%)' : plan.status)
217
+ const detail = last
218
+ ? h('div', { style: { ...S.desc, paddingLeft: 10 } }, '最近节点:' + (last.status || 'progress') + ' · ' + String(last.summary || '').slice(0, 60) + (last.blocker ? ' · 阻塞:' + String(last.blocker).slice(0, 40) : ''))
219
+ : null
220
+ return h('div', { key: plan.id, style: S.desc }, headline, detail)
221
+ }),
222
+
223
+ awaiting.length > 0 ? h('div', { style: { ...S.desc, marginTop: 8, fontWeight: 600 } }, '④ 等 PM 复核') : null,
224
+ ...awaiting.slice(0, 3).map((feedback) => h('div', { key: feedback.id, style: S.desc },
225
+ '· ' + String(feedback.summary || '').slice(0, 60) + (feedback.openIssues && feedback.openIssues.length ? '(遗留 ' + feedback.openIssues.length + ')' : ''))),
226
+
227
+ (board.reviews || []).length > 0 ? h('div', { style: { ...S.desc, marginTop: 8, fontWeight: 600 } }, '⑤ 已复核') : null,
228
+ ...(board.reviews || []).slice(-3).reverse().map((review) => h('div', { key: review.id, style: S.desc },
229
+ '· ' + (review.verdict === 'accepted' ? '通过' : '退回') + (review.failed && review.failed.length ? ':' + review.failed.join(';') : ''))),
230
+
231
+ h('div', { style: { ...S.desc, marginTop: 8 } }, '面板只读:回答选择题请在 PM 会话;派发/结案请在 PJM 会话。'),
232
+ )
233
+ }
234
+
113
235
  function FangCard({ cooperation }) {
114
236
  if (!cooperation) return null
115
237
  const entries = Object.entries(cooperation.services || {})
@@ -275,7 +397,7 @@ window.__ModuleLoader__.load({
275
397
  return h('div', { style: S.card, 'data-dsh-master': 'cost-block' },
276
398
  h('div', { style: S.row },
277
399
  h('span', { style: S.name }, '成本(只读)'),
278
- tag(block ? '3.3.3' : '未启用', block ? undefined : '#B45309')),
400
+ tag(block ? '3.3.5' : '未启用', block ? undefined : '#B45309')),
279
401
  failed ? h('div', { style: S.error }, '成本接口不可达') : null,
280
402
  !block && !failed ? h('div', { style: S.desc }, '成本系统未装配(cost-optimizer 未启用或角色不匹配)') : null,
281
403
  columns ? h('div', { style: S.desc },
@@ -332,6 +454,9 @@ window.__ModuleLoader__.load({
332
454
  h('div', { style: S.desc }, '运行:' + Math.max(1, Math.round((status.uptimeMs || 0) / 60000)) + ' 分钟'),
333
455
  ) : null,
334
456
  ...items.map((item) => h(SubpluginCard, { key: item.id, item })),
457
+ ...(status && Array.isArray(status.agents) ? status.agents : []).map((item) => h(AgentCard, { key: item.id, item })),
458
+ h(CommandCenter, { dual: status && status.dual }),
459
+ h(DualBoard),
335
460
  h(CostCard),
336
461
  h(FangCard, { cooperation: status && status.cooperation }),
337
462
  h(LiveFeed),
package/cordis.patch.yml CHANGED
@@ -69,3 +69,54 @@
69
69
  name: '@twinsearth/dsh-master/dual-manager'
70
70
  config:
71
71
  enabled: true
72
+ # ── 3.3.4:两个可被新会话选择的智能体 preset ────────────────────────────────
73
+ # PM 只拿“读 + 问 + 交付文档”,PJM 只拿“读 + 待办 + 交付 + 共享服务工具”。
74
+ # 两者都不拿 shell / 写文件 / 发布会话,避免智能体绕过 Role Matrix 自行改代码。
75
+ - id: preset-master-pm
76
+ name: '@deepseek-ai/dsh-agent-preset'
77
+ config:
78
+ id: master-pm
79
+ name: 产品经理(PM)
80
+ description: 把需求翻译成 PRD 与六类现状盘点;只读证据、出选择题、写文档,不执行代码。
81
+ order: 20
82
+ plugins:
83
+ - id: pm-agent
84
+ name: '@twinsearth/dsh-master/pm-agent'
85
+ - id: tool-fs
86
+ name: '@deepseek-ai/dsh-tool-fs'
87
+ config:
88
+ read: true
89
+ readImage: true
90
+ write: false
91
+ edit: false
92
+ applyPatch: false
93
+ - id: tool-fs-search
94
+ name: '@deepseek-ai/dsh-tool-fs-search'
95
+ - id: tool-ask-user
96
+ name: '@deepseek-ai/dsh-tool-ask-user'
97
+ - id: tool-present
98
+ name: '@deepseek-ai/dsh-tool-present'
99
+ - id: preset-master-pjm
100
+ name: '@deepseek-ai/dsh-agent-preset'
101
+ config:
102
+ id: master-pjm
103
+ name: 项目经理(PJM)
104
+ description: 把 PRD 拆成 WBS 与七类项目管理内容;隔离派发、节点汇总、结案汇报。
105
+ order: 21
106
+ plugins:
107
+ - id: pjm-agent
108
+ name: '@twinsearth/dsh-master/pjm-agent'
109
+ - id: tool-fs
110
+ name: '@deepseek-ai/dsh-tool-fs'
111
+ config:
112
+ read: true
113
+ readImage: true
114
+ write: false
115
+ edit: false
116
+ applyPatch: false
117
+ - id: tool-fs-search
118
+ name: '@deepseek-ai/dsh-tool-fs-search'
119
+ - id: tool-todo
120
+ name: '@deepseek-ai/dsh-tool-todo'
121
+ - id: tool-present
122
+ name: '@deepseek-ai/dsh-tool-present'
@@ -0,0 +1,62 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 dsh-Master contributors — see LICENSE-CORE.
3
+ // @ts-nocheck
4
+ import { PJM_SYSTEM_PROMPT } from "./prompts.js";
5
+ import { dualService, dualUnavailable, id, installAgent, jsonTool, objectSchema, objects, string, strings } from "./toolkit.js";
6
+ export const name = 'dsh-master-pjm-agent';
7
+ export const inject = ['systemPrompt', 'tools'];
8
+ /** PJM preset:项目计划、节点、隔离派发、结案;不替 PM 决定需求。 */
9
+ export function apply(ctx) {
10
+ const dual = () => dualService(ctx);
11
+ const tools = [
12
+ jsonTool('pjm_create_plan', '把已确认 PRD 拆成 WBS 与七类项目计划;给 handoffId 时同时完成接单。', objectSchema({
13
+ prdId: id('PRD id'), handoffId: id('可选:PM 交接包 id'), title: string('项目计划标题'), how: string('实施路径'), where: string('执行场景'),
14
+ projectReview: strings('项目会审结论'), requirements: strings('项目需求与内容'),
15
+ steps: objects('WBS:id/title/prompt/owner/where/estimateMinutes/dependsOn/acceptance/kind'),
16
+ schedule: { type: 'object', description: 'startAt 与 milestones[](title/owner/estimateMinutes/dependsOn)', additionalProperties: true },
17
+ team: objects('人员:role/owner/responsibility'),
18
+ quality: { type: 'object', description: 'tests[] / deployment[] / acceptance[]', additionalProperties: true },
19
+ }, ['prdId', 'steps']), async (args) => dual() ? dual().pjm.plan(args.prdId, args) : dualUnavailable()),
20
+ jsonTool('pjm_take_handoff', '接单:只有 PM 标记 ready 的交接包可以接走;needs-clarification 必须退回 PM。', objectSchema({
21
+ handoffId: id('交接包 id'), planId: id('可选:关联计划 id'),
22
+ }, ['handoffId']), async (args) => dual() ? dual().pjm.takeHandoff(args.handoffId, args) : dualUnavailable()),
23
+ jsonTool('pjm_report', '节点自反馈:执行中随时汇报进展/阻塞,blocked 必须写原因。', objectSchema({
24
+ planId: id('计划 id'), summary: string('节点摘要'),
25
+ status: { type: 'string', enum: ['progress', 'blocked', 'done'] },
26
+ stepId: id('可选:关联步骤'), blocker: string('阻塞原因(blocked 必填)'), evidence: strings('证据'),
27
+ }, ['planId', 'summary']), async (args) => dual() ? dual().pjm.report(args.planId, args) : dualUnavailable()),
28
+ jsonTool('pjm_submit_feedback', '项目级自反馈:计划全部步骤完成后提交给 PM 复核,写清交付、偏差、遗留问题与测试。', objectSchema({
29
+ planId: id('计划 id'), summary: string('执行总结(必填)'),
30
+ deviations: strings('相对计划的偏差'), openIssues: strings('遗留问题'), tests: strings('测试证据'),
31
+ }, ['planId', 'summary']), async (args) => dual() ? dual().pjm.submitFeedback(args.planId, args) : dualUnavailable()),
32
+ jsonTool('pjm_dispatch_step', '通过 fangForker.spawn 在隔离分身中执行一个 WBS 步骤;不可用会 fail-closed,不在宿主本地执行。', objectSchema({
33
+ planId: id('计划 id'), stepId: id('步骤 id'),
34
+ }, ['planId', 'stepId']), async (args) => dual() ? await dual().pjm.dispatch(args.planId, { stepId: args.stepId }) : dualUnavailable()),
35
+ jsonTool('pjm_complete_step', '记录步骤完成/失败、证据与实际用时;不得把失败写成完成。', objectSchema({
36
+ planId: id('计划 id'), stepId: id('步骤 id'),
37
+ status: { type: 'string', enum: ['done', 'failed'] }, summary: string('结果摘要'),
38
+ evidence: strings('测试/文件/报告证据'), actualMinutes: { type: 'number', description: '实际用时(分钟)' },
39
+ }, ['planId', 'stepId', 'status']), async (args) => dual() ? dual().pjm.completeStep(args.planId, args.stepId, args) : dualUnavailable()),
40
+ jsonTool('pjm_check_step', '查单个步骤真实状态:派发/执行/汇报、证据、用时与关联报告。结果来自计划与节点汇报,不是 VM 作业队列。', objectSchema({
41
+ planId: id('计划 id'), stepId: id('步骤 id'),
42
+ }, ['planId', 'stepId']), async (args) => dual() ? dual().pjm.checkStep(args.planId, args.stepId) : dualUnavailable()),
43
+ jsonTool('pjm_next_action', '读计划状态,返回 PJM 下一步唯一推荐动作与理由(纯建议,不执行)。', objectSchema({ planId: id('计划 id') }, ['planId']), async (args) => dual() ? dual().pjm.nextAction(args.planId) : dualUnavailable()),
44
+ jsonTool('pjm_project_status', '读取项目看板:完成/进行/待办/失败、估时、实际用时、是否可结案。', objectSchema({ planId: id('计划 id') }, ['planId']), async (args) => dual() ? dual().pjm.progress(args.planId) : dualUnavailable()),
45
+ jsonTool('pjm_write_project', '把项目计划与当前节点写成 workspace/docs/pjm 下的 Markdown。', objectSchema({ planId: id('计划 id') }, ['planId']), async (args, exec) => dual() ? await dual().pjm.writeProject(args.planId, { agent: exec.agent }) : dualUnavailable()),
46
+ jsonTool('pjm_close_project', '全部步骤完成后结案:写入总结、评审报告、验收与资料清单,并可生成结案文档。', objectSchema({
47
+ planId: id('计划 id'), summary: string('结案总结(必填)'), reviewReport: string('评审报告'),
48
+ acceptance: strings('验收记录'), artifacts: strings('项目资料路径'), write: { type: 'boolean', description: '是否同时写结案 Markdown,默认 true' },
49
+ }, ['planId', 'summary']), async (args, exec) => {
50
+ if (!dual())
51
+ return dualUnavailable();
52
+ const closed = dual().pjm.close(args.planId, args);
53
+ if (!closed.ok)
54
+ return closed;
55
+ if (args.write === false)
56
+ return closed;
57
+ const document = await dual().pjm.writeClosure(args.planId, { agent: exec.agent });
58
+ return { ...closed, document };
59
+ }),
60
+ ];
61
+ return installAgent(ctx, { prompt: PJM_SYSTEM_PROMPT, tools });
62
+ }
@@ -0,0 +1,57 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 dsh-Master contributors — see LICENSE-CORE.
3
+ // @ts-nocheck
4
+ import { PM_SYSTEM_PROMPT } from "./prompts.js";
5
+ import { dualService, dualUnavailable, id, installAgent, jsonTool, objectSchema, objects, string, strings } from "./toolkit.js";
6
+ export const name = 'dsh-master-pm-agent';
7
+ export const inject = ['systemPrompt', 'tools'];
8
+ /** PM preset:只提供需求/盘点/文档工具,不提供任何执行或派生能力。 */
9
+ export function apply(ctx) {
10
+ const dual = () => dualService(ctx);
11
+ const tools = [
12
+ jsonTool('pm_create_prd', '创建结构化产品需求文档(WHY/WHAT/模块/约束/验收),返回 PRD id。', objectSchema({
13
+ title: string('需求标题'), goal: string('要解决的问题与目标'),
14
+ why: string('背景与价值'), what: strings('功能范围'), architecture: string('架构说明'),
15
+ outcomes: strings('预期结果'), modules: objects('模块:name/result/architecture/status'),
16
+ priority: { type: 'string', enum: ['low', 'normal', 'high', 'critical'] },
17
+ constraints: strings('约束'), acceptance: strings('验收标准'), nonGoals: strings('非目标'),
18
+ assumptions: strings('假设'), openQuestions: strings('待澄清问题'), requester: string('请求方'),
19
+ }, ['title', 'goal']), async (args) => dual() ? dual().pm.createPrd(args) : dualUnavailable()),
20
+ jsonTool('pm_create_assessment', '保存六类产品现状盘点。completed 每项必须附 evidence;其它类给原因、计划、冲突或歧义。', objectSchema({
21
+ prdId: id('可选:关联的 PRD id'), title: string('盘点标题'), why: string('目标'), what: strings('范围'),
22
+ completed: objects('已完成:title/progress/evidence[](证据必填)'),
23
+ unfinished: objects('未完成:title/reason/nextStep/progress'),
24
+ blocked: objects('受阻:title/reason/blocker/nextStep'),
25
+ planned: objects('已计划:title/plan/estimateMinutes/priority'),
26
+ conflicts: objects('冲突:title/question/options[{value,label,consequence}]'),
27
+ ambiguities: objects('歧义:title/question/currentInterpretation'),
28
+ }, ['title']), async (args) => dual() ? dual().pm.createAssessment(args) : dualUnavailable()),
29
+ jsonTool('pm_write_prd', '把 PRD(可带六类盘点)写成 workspace/docs/pm 下的 Markdown,返回文档路径。', objectSchema({
30
+ prdId: id('PRD id'), assessmentId: id('可选:关联的 Assessment id'),
31
+ }, ['prdId']), async (args, exec) => dual() ? await dual().pm.writePrd(args.prdId, { assessmentId: args.assessmentId, agent: exec.agent }) : dualUnavailable()),
32
+ jsonTool('pm_handoff_to_pjm', '把 PRD(可带盘点)打成交接包交给 PJM。有未决问题或冲突时不给 ready,必须先澄清。', objectSchema({
33
+ prdId: id('PRD id'), assessmentId: id('可选:关联的盘点 id'),
34
+ acceptance: strings('可选:覆盖 PRD 的验收标准'), openQuestions: strings('可选:补充未决问题'),
35
+ }, ['prdId']), async (args) => dual() ? dual().pm.createHandoff(args.prdId, args) : dualUnavailable()),
36
+ jsonTool('pm_review_feedback', '复核 PJM 的执行反馈:逐条验收给 met 布尔与证据,写复盘与总结;结论必须与验收一致。', objectSchema({
37
+ feedbackId: id('PJM 反馈 id'),
38
+ acceptanceChecks: objects('逐条验收:criterion/met(boolean)/evidence[]/note'),
39
+ verdict: { type: 'string', enum: ['accepted', 'changes-requested'] },
40
+ retrospective: { type: 'object', description: 'kept[] / improve[] / lessons[]', additionalProperties: true },
41
+ summary: string('复核总结(必填)'),
42
+ write: { type: 'boolean', description: '是否同时写复核文档,默认 true' },
43
+ }, ['feedbackId', 'acceptanceChecks', 'summary']), async (args, exec) => {
44
+ if (!dual())
45
+ return dualUnavailable();
46
+ const reviewed = dual().pm.reviewFeedback(args.feedbackId, args);
47
+ if (!reviewed.ok || args.write === false)
48
+ return reviewed;
49
+ const document = await dual().pm.writeReview(reviewed.review.id, { agent: exec.agent });
50
+ return { ...reviewed, document };
51
+ }),
52
+ jsonTool('pm_pipeline', '读一个 PRD 的闭环状态:drafted → handoff-ready / needs-clarification → planning → feedback → accepted / changes-requested。', objectSchema({ prdId: id('PRD id') }, ['prdId']), async (args) => dual() ? dual().pm.pipeline(args.prdId) : dualUnavailable()),
53
+ jsonTool('pm_next_action', '读闭环状态,返回 PM 下一步唯一推荐动作与理由(纯建议,不执行)。', objectSchema({ prdId: id('PRD id') }, ['prdId']), async (args) => dual() ? dual().pm.nextAction(args.prdId) : dualUnavailable()),
54
+ jsonTool('pm_list_prds', '列出当前 Host 中的 PRD、六类盘点、交接包与复核索引。', objectSchema({}), async () => dual() ? ({ ok: true, prds: dual().pm.listPrds().items, assessments: dual().pm.listAssessments().items, handoffs: dual().pm.listHandoffs().items, reviews: dual().pm.listReviews().items }) : dualUnavailable()),
55
+ ];
56
+ return installAgent(ctx, { prompt: PM_SYSTEM_PROMPT, tools });
57
+ }
@@ -0,0 +1,71 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 dsh-Master contributors — see LICENSE-CORE.
3
+ export const PM_AGENT_ID = 'master-pm';
4
+ export const PJM_AGENT_ID = 'master-pjm';
5
+ export const PM_SYSTEM_PROMPT = String.raw `# Role
6
+ 你是 DSH-Master「房主」的产品经理 Agent(PM,preset: master-pm)。你把用户和开发者的大白话转换成可核对、可验收的产品需求与文档。
7
+
8
+ # Goals — WHY / WHAT
9
+ 1. WHY:说清解决什么问题、为什么值得做、达成什么目标。
10
+ 2. WHAT:说清功能、模块、架构、预期结果、约束、非目标和验收标准。
11
+ 3. 对现状做证据驱动的六类盘点:已完成、未完成、无法实现/受阻、已计划、冲突、歧义。
12
+
13
+ # Workflow
14
+ 1. 先识别目标、范围、约束和未决项;重大歧义或不可逆选择必须用 ask_user_question 提出一个具体决策点,给 2–4 个选项并标推荐项。不要为琐碎实现细节打断用户。
15
+ 2. 盘点前先用 read/grep/glob 读取真实代码、测试和文档。没有证据的“完成”不得写 100%,不得把设计稿当成已交付。
16
+ 3. 用 pm_create_prd 保存结构化 PRD;用 pm_create_assessment 保存六类盘点。每个 completed 项必须附文件、测试或报告证据。
17
+ 4. 用 pm_write_prd 生成 Markdown 文档;需要用户打开时用 present 交付。
18
+ 5. **交接**:确认 PRD 有验收标准、且没有未决问题/冲突后,用 pm_handoff_to_pjm 交接给 PJM;不 ready 时先回去澄清,不得硬塞。
19
+ 6. **复核闭环**:PJM 提交反馈后用 pm_review_feedback 逐条核对验收(met + 证据),写复盘(kept/improve/lessons)与总结;结论必须与验收一致(有未达标就 changes-requested)。
20
+ 7. 用 pm_pipeline 查闭环阶段(drafted → handoff-ready → planning → feedback → accepted / changes-requested)。
21
+ 8. 不确定下一步时先调 pm_next_action:它给出唯一推荐动作(create_prd / create_assessment / clarify / handoff / review / done / blocked)与理由;按它走,不要自己跳步。
22
+
23
+ # Rules
24
+ - 不臆测、不伪造证据、不把“可以实现”写成“已经完成”。
25
+ - 冲突必须列出冲突双方、影响、可选方案和需要用户重新判断的问题。
26
+ - 歧义必须写出当前理解与待确认问题;用户未回答前不得把该理解当成事实。
27
+ - PM 不写生产代码、不跑 shell、不发布、不安装、不调度 VM/分身。
28
+ - 只有 PM 能复核 PJM 的交付;不得自己给自己复核,也不得替 PJM 编造执行结果。
29
+ - 只写 PRD/盘点/复核文档;不改需求范围外的文件。
30
+
31
+ # Output Format
32
+ 默认用简洁 Markdown 汇报,必须按任务需要包含:
33
+ - WHY & WHAT
34
+ - 六类现状矩阵:completed / unfinished / blocked / planned / conflicts / ambiguities
35
+ - 冲突与歧义的用户决策点
36
+ - 已保存的 PRD/Assessment id 与文档路径
37
+ 只输出已核实内容;未知、未验证、受限分别标明。`;
38
+ export const PJM_SYSTEM_PROMPT = String.raw `# Role
39
+ 你是 DSH-Master「房主」的项目经理 Agent(PJM,preset: master-pjm)。你把已确认的 PRD 变成可执行、可追踪、可验收的项目计划,并按节点汇报直至结案。
40
+
41
+ # Goals — HOW / WHO / WHERE / WHEN
42
+ 1. HOW:把 PRD 拆成 WBS,每步写清 prompt、依赖、验收、估时。
43
+ 2. WHO:给每一步分配 owner 与责任;未分配必须写 unassigned,不得虚构人员。
44
+ 3. WHERE:明确执行场景(宿主、fork、workspace、DSH Web);脏任务只能在隔离执行体中运行。
45
+ 4. WHEN:给里程碑与估时;估时是计划,不是承诺工期。
46
+
47
+ # Workflow
48
+ 1. 项目会审:核对 PRD、验收标准、约束、未决问题;未决项未解决不得自行扩权。
49
+ 2. 接单:收到 PM 交接包后用 pjm_take_handoff 或 pjm_create_plan({handoffId}) 接单;交接包 needs-clarification 时退回 PM,不得硬开工。
50
+ 3. 用 pjm_create_plan 建立七类项目内容:项目会审、WBS、需求内容、时间、人员、测试部署验收、结案。
51
+ 4. 执行前申请隔离资源,通过 pjm_dispatch_step 走 fangForker.spawn;不可用就 fail-closed,绝不在母体/原身本地降级执行,不虚构 vm_create/vm_exec。
52
+ 5. **节点自反馈**:执行中随时用 pjm_report 汇报进展/阻塞(blocked 必须写原因)。
53
+ 6. **提交反馈**:全部步骤完成后用 pjm_submit_feedback 把交付、偏差、遗留问题、测试证据交给 PM 复核;不得在未完成时提交。
54
+ 7. 用 pjm_project_status 汇总节点,用 todo_write 跟踪当前工作;完成步骤时记录 summary/evidence/actualMinutes。
55
+ 8. 用 pjm_write_project 保存计划与节点报告。全部步骤完成后才能 pjm_close_project;结案必须有总结、验收记录、评审报告和资料清单,并交付文档。
56
+ 9. 不确定下一步时先调 pjm_next_action:它给出唯一推荐动作(take_handoff / return_to_pm / create_plan / dispatch / report / complete_feedback / close / done)与理由;按它走,不要跳步或重复派发。
57
+
58
+ # Rules
59
+ - 按时、按质、按量报告真实状态;失败、阻塞、未测量不得包装成完成。
60
+ - 不发布、不部署到生产、不修改 profile、不触碰凭据,除非用户对该动作另行明确授权。
61
+ - 不代替 PM 决定需求,不替用户回答选择题;阻塞时给出原因、影响、可选方案和所需决策。
62
+ - 不给自己复核:交付是否正确由 PM 复核;PM 判 changes-requested 时按新的验收项重做。
63
+ - 不做无限轮询;节点汇报写完成/进行/待办/失败及下一步。
64
+
65
+ # Output Format
66
+ - 项目会审与范围
67
+ - WBS 看板(Todo / In Progress / Done / Failed)
68
+ - Owner、场景、估时、实际用时与依赖
69
+ - 测试/部署/交付/验收证据
70
+ - 结案:评审报告、验收记录、资料汇总
71
+ - 仍需人类决策的事项(如有)`;
@@ -0,0 +1,64 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 dsh-Master contributors — see LICENSE-CORE.
3
+ // @ts-nocheck
4
+ import { SERVICE, masterError, safeGet } from "../kernel.js";
5
+ export function dualService(ctx) {
6
+ const service = safeGet(ctx, SERVICE.dual);
7
+ return service && service.pm && service.pjm ? service : null;
8
+ }
9
+ export function dualUnavailable() {
10
+ return masterError('dual-service-unavailable', 'dshMasterDual is required; enable master-dual-manager in this Host');
11
+ }
12
+ /** 注册一个返回结构化 JSON 的模型工具;异常也转成具名错误,不吞掉。 */
13
+ export function jsonTool(name, description, parameters, execute) {
14
+ return {
15
+ name,
16
+ description,
17
+ parameters,
18
+ output: {
19
+ schema: { type: 'object' },
20
+ render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }],
21
+ },
22
+ async execute(args, exec) {
23
+ try {
24
+ return await execute(args && typeof args === 'object' ? args : {}, exec);
25
+ }
26
+ catch (error) {
27
+ return masterError('agent-tool-threw', String(error?.message ?? error), { tool: name });
28
+ }
29
+ },
30
+ };
31
+ }
32
+ /** 在 preset scope 内挂 persona 与 scoped tools,全部纳入 Cordis effect 生命周期。 */
33
+ export function installAgent(ctx, { prompt, tools }) {
34
+ const systemPrompt = safeGet(ctx, 'systemPrompt');
35
+ const registry = safeGet(ctx, 'tools');
36
+ if (!systemPrompt || typeof systemPrompt.section !== 'function') {
37
+ return { ok: false, code: 'system-prompt-unavailable', error: 'ctx.systemPrompt with section() is required' };
38
+ }
39
+ if (!registry || typeof registry.register !== 'function') {
40
+ return { ok: false, code: 'tools-unavailable', error: 'ctx.tools with register() is required' };
41
+ }
42
+ const registered = [];
43
+ try {
44
+ ctx.effect(() => systemPrompt.section({ name: 'deployment:persona-prefix', order: 0, text: prompt }));
45
+ }
46
+ catch (error) {
47
+ return { ok: false, code: 'prompt-registration-failed', error: String(error?.message ?? error) };
48
+ }
49
+ for (const definition of tools) {
50
+ try {
51
+ ctx.effect(() => registry.register(definition));
52
+ registered.push(definition.name);
53
+ }
54
+ catch (error) {
55
+ return { ok: false, code: 'tool-registration-failed', error: String(error?.message ?? error), tool: definition.name, registered };
56
+ }
57
+ }
58
+ return { ok: true, tools: registered };
59
+ }
60
+ export const objectSchema = (properties = {}, required = []) => ({ type: 'object', properties, required, additionalProperties: true });
61
+ export const string = (description = '') => ({ type: 'string', description });
62
+ export const strings = (description = '') => ({ type: 'array', items: { type: 'string' }, description });
63
+ export const objects = (description = '') => ({ type: 'array', items: { type: 'object', additionalProperties: true }, description });
64
+ export const id = (description) => ({ type: 'string', description });
@@ -0,0 +1,93 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 dsh-Master contributors — see LICENSE-CORE.
3
+ // @ts-nocheck
4
+ /**
5
+ * 流式生成的取消登记表(3.3.5)。
6
+ *
7
+ * 为什么单独抽出来:中断的正确性取决于「谁在跑、谁负责停、停完是否清理」。
8
+ * 把它写成可测原语后,通道、会话桥、演示生产者都能复用同一套语义。
9
+ *
10
+ * 语义:
11
+ * * `begin(key)` 拿到 `{ key, signal, abort, end }`;同一个 key 重复 begin 会**先中止旧的**(新的一轮覆盖旧的)。
12
+ * * `abort(key)` 触发 signal 并保留登记,直到 `end()` 才真正清理 —— 这样“已中止但尚未收尾”可被观察。
13
+ * * `end(key)` 幂等清理;调用后 `has(key)` 为 false。
14
+ * * `abortAll()` 用于连接关闭 / 插件卸载,保证没有僵尸流。
15
+ * * 所有方法都不抛(除了传入非法 key 返回 false)。
16
+ */
17
+ export function createAbortRegistry() {
18
+ const entries = new Map();
19
+ function begin(key, { onAbort } = {}) {
20
+ const id = String(key ?? '');
21
+ if (!id)
22
+ throw new Error('abort-registry: key is required');
23
+ // 同一 key 上已有生成:先中止旧的,避免两路流同时往同一个消息里写
24
+ const previous = entries.get(id);
25
+ if (previous) {
26
+ previous.abort('superseded');
27
+ }
28
+ const controller = new AbortController();
29
+ const entry = {
30
+ key: id,
31
+ controller,
32
+ signal: controller.signal,
33
+ aborted: false,
34
+ reason: null,
35
+ startedAt: Date.now(),
36
+ endedAt: null,
37
+ };
38
+ controller.signal.addEventListener('abort', () => {
39
+ entry.aborted = true;
40
+ try {
41
+ onAbort?.(entry.reason ?? controller.signal.reason ?? 'aborted');
42
+ }
43
+ catch { /* 回调异常不影响中止 */ }
44
+ }, { once: true });
45
+ entry.abort = (reason = 'user-interrupt') => {
46
+ if (controller.signal.aborted)
47
+ return false;
48
+ try {
49
+ controller.abort(reason);
50
+ }
51
+ catch { /* ignore */ }
52
+ entry.reason = String(reason);
53
+ return true;
54
+ };
55
+ entry.end = () => {
56
+ if (entries.get(id) === entry)
57
+ entries.delete(id);
58
+ entry.endedAt = Date.now();
59
+ return true;
60
+ };
61
+ entries.set(id, entry);
62
+ return entry;
63
+ }
64
+ return {
65
+ begin,
66
+ has: (key) => entries.has(String(key ?? '')),
67
+ get: (key) => entries.get(String(key ?? '')) ?? null,
68
+ /** 中止并返回是否真的发生了中止。 */
69
+ abort: (key, reason = 'user-interrupt') => {
70
+ const entry = entries.get(String(key ?? ''));
71
+ if (!entry)
72
+ return false;
73
+ return entry.abort(reason);
74
+ },
75
+ abortAll: (reason = 'shutdown') => {
76
+ const aborted = [];
77
+ for (const entry of entries.values()) {
78
+ if (entry.abort(reason))
79
+ aborted.push(entry.key);
80
+ }
81
+ return aborted;
82
+ },
83
+ /** 结束并清理(幂等)。 */
84
+ end: (key) => {
85
+ const entry = entries.get(String(key ?? ''));
86
+ if (!entry)
87
+ return false;
88
+ return entry.end();
89
+ },
90
+ keys: () => [...entries.keys()],
91
+ size: () => entries.size,
92
+ };
93
+ }