@twinsearth/dsh-master 3.3.4 → 3.3.6

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 (54) hide show
  1. package/README.md +1 -0
  2. package/client.js +83 -4
  3. package/cordis.patch.yml +2 -0
  4. package/dist-host/agents/pjm-agent.js +18 -2
  5. package/dist-host/agents/pm-agent.js +23 -1
  6. package/dist-host/agents/prompts.js +16 -6
  7. package/dist-host/channel/abort-registry.js +93 -0
  8. package/dist-host/channel/protocol.js +114 -0
  9. package/dist-host/channel/server.js +349 -0
  10. package/dist-host/channel/stream-state.js +343 -0
  11. package/dist-host/channel/ws.js +199 -0
  12. package/dist-host/dual/documents.js +36 -0
  13. package/dist-host/dual/orchestrator.js +214 -0
  14. package/dist-host/dual/schema.js +153 -0
  15. package/dist-host/engine/atomic-switch.js +159 -0
  16. package/dist-host/engine/health-probe.js +90 -0
  17. package/dist-host/engine/index.js +18 -0
  18. package/dist-host/engine/llm-stream.js +215 -0
  19. package/dist-host/engine/plugin-updater.js +392 -0
  20. package/dist-host/engine/safe-update.js +150 -0
  21. package/dist-host/index.js +114 -2
  22. package/dist-host/kernel.js +7 -7
  23. package/dist-host/plugins/cost-optimizer.js +3 -3
  24. package/dist-host/plugins/dual-manager.js +230 -6
  25. package/docs/AGENTS-PM-PJM.md +90 -6
  26. package/docs/CHANGELOG.md +63 -0
  27. package/docs/CHANNEL-WEBSOCKET.md +169 -0
  28. package/docs/DESIGN-command-center.md +147 -0
  29. package/docs/ENGINE-PRIMITIVES.md +215 -0
  30. package/docs/GAP-ANALYSIS-fang-session.md +75 -0
  31. package/docs/REVIEW-PM-PJM-3.3.4.md +32 -6
  32. package/docs/examples/command-center/README.md +167 -0
  33. package/package.json +3 -1
  34. package/src/agents/pjm-agent.ts +18 -2
  35. package/src/agents/pm-agent.ts +21 -1
  36. package/src/agents/prompts.ts +16 -6
  37. package/src/channel/abort-registry.ts +85 -0
  38. package/src/channel/protocol.ts +110 -0
  39. package/src/channel/server.ts +289 -0
  40. package/src/channel/stream-state.ts +349 -0
  41. package/src/channel/ws.ts +182 -0
  42. package/src/dual/documents.ts +35 -0
  43. package/src/dual/orchestrator.ts +223 -0
  44. package/src/dual/schema.ts +148 -1
  45. package/src/engine/atomic-switch.ts +146 -0
  46. package/src/engine/health-probe.ts +88 -0
  47. package/src/engine/index.ts +20 -0
  48. package/src/engine/llm-stream.ts +189 -0
  49. package/src/engine/plugin-updater.ts +359 -0
  50. package/src/engine/safe-update.ts +148 -0
  51. package/src/index.ts +105 -2
  52. package/src/kernel.ts +7 -7
  53. package/src/plugins/cost-optimizer.ts +3 -3
  54. package/src/plugins/dual-manager.ts +211 -6
package/README.md CHANGED
@@ -54,6 +54,7 @@ DSH Host
54
54
  - 两者共享 `dual-manager` 的结构化状态(PRD / 盘点 / 计划 / 节点 / 结案),文档只写 Session workspace 下的 `docs/pm`、`docs/pjm`。
55
55
  - 详见 [智能体设计](docs/AGENTS-PM-PJM.md)。
56
56
  - 面板「房主」抽屉新增两张智能体卡片与「智囊团 · Command Center」**只读**摘要(PRD / 盘点 / 计划 / 结案 / 派发可用性),不提供写入口。
57
+ - 3.3.5 起面板多一块「智囊团 · 闭环」看板:读 `GET /dsh-master/api/dual`,显示待澄清交接 / 待接单 / 项目进度 / 待复核 / 已复核;面板内**不能聊天、不能回答选择题、不能派发**,设计见 [DESIGN-command-center.md](docs/DESIGN-command-center.md)。
57
58
 
58
59
  ## 本地开发
59
60
 
package/client.js CHANGED
@@ -143,15 +143,93 @@ window.__ModuleLoader__.load({
143
143
  const pm = dual.pm || {}
144
144
  const pjm = dual.pjm || {}
145
145
  const coop = dual.cooperation || {}
146
+ const loop = dual.loop || {}
146
147
  return h('div', { style: S.card, 'data-dsh-master': 'command-center' },
147
148
  h('div', { style: S.row },
148
149
  h('span', { style: S.name }, '智囊团 · Command Center'),
149
150
  tag('只读'),
150
151
  h('span', { style: { marginLeft: 'auto', fontSize: 11.5, ...S.ok } }, dual.ok ? '已装配' : '降级')),
151
- line('产品经理', 'PRD ' + (pm.prds ?? 0) + ' · 盘点 ' + (pm.assessments ?? 0)),
152
- line('项目经理', '计划 ' + (pjm.plans ?? 0) + ' · 已结案 ' + (pjm.closed ?? 0)),
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)),
153
155
  line('协作', '决策中心 ' + (coop.decisionCenter ? '✓' : '×') + ' · 分身派发 ' + (coop.forker ? '✓' : '×')),
154
- h('div', { style: S.desc }, '面板只读:PRD/计划由 PM/PJM 会话里的工具创建;派发必须经 fangForker。'))
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
+ )
155
233
  }
156
234
 
157
235
  function FangCard({ cooperation }) {
@@ -319,7 +397,7 @@ window.__ModuleLoader__.load({
319
397
  return h('div', { style: S.card, 'data-dsh-master': 'cost-block' },
320
398
  h('div', { style: S.row },
321
399
  h('span', { style: S.name }, '成本(只读)'),
322
- tag(block ? '3.3.4' : '未启用', block ? undefined : '#B45309')),
400
+ tag(block ? '3.3.6' : '未启用', block ? undefined : '#B45309')),
323
401
  failed ? h('div', { style: S.error }, '成本接口不可达') : null,
324
402
  !block && !failed ? h('div', { style: S.desc }, '成本系统未装配(cost-optimizer 未启用或角色不匹配)') : null,
325
403
  columns ? h('div', { style: S.desc },
@@ -378,6 +456,7 @@ window.__ModuleLoader__.load({
378
456
  ...items.map((item) => h(SubpluginCard, { key: item.id, item })),
379
457
  ...(status && Array.isArray(status.agents) ? status.agents : []).map((item) => h(AgentCard, { key: item.id, item })),
380
458
  h(CommandCenter, { dual: status && status.dual }),
459
+ h(DualBoard),
381
460
  h(CostCard),
382
461
  h(FangCard, { cooperation: status && status.cooperation }),
383
462
  h(LiveFeed),
package/cordis.patch.yml CHANGED
@@ -89,6 +89,7 @@
89
89
  readImage: true
90
90
  write: false
91
91
  edit: false
92
+ applyPatch: false
92
93
  - id: tool-fs-search
93
94
  name: '@deepseek-ai/dsh-tool-fs-search'
94
95
  - id: tool-ask-user
@@ -112,6 +113,7 @@
112
113
  readImage: true
113
114
  write: false
114
115
  edit: false
116
+ applyPatch: false
115
117
  - id: tool-fs-search
116
118
  name: '@deepseek-ai/dsh-tool-fs-search'
117
119
  - id: tool-todo
@@ -9,14 +9,26 @@ export const inject = ['systemPrompt', 'tools'];
9
9
  export function apply(ctx) {
10
10
  const dual = () => dualService(ctx);
11
11
  const tools = [
12
- jsonTool('pjm_create_plan', '把已确认 PRD 拆成 WBS 与七类项目计划,返回 plan id;估时必须以分钟给出。', objectSchema({
13
- prdId: id('PRD id'), title: string('项目计划标题'), how: string('实施路径'), where: string('执行场景'),
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
14
  projectReview: strings('项目会审结论'), requirements: strings('项目需求与内容'),
15
15
  steps: objects('WBS:id/title/prompt/owner/where/estimateMinutes/dependsOn/acceptance/kind'),
16
16
  schedule: { type: 'object', description: 'startAt 与 milestones[](title/owner/estimateMinutes/dependsOn)', additionalProperties: true },
17
17
  team: objects('人员:role/owner/responsibility'),
18
18
  quality: { type: 'object', description: 'tests[] / deployment[] / acceptance[]', additionalProperties: true },
19
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()),
20
32
  jsonTool('pjm_dispatch_step', '通过 fangForker.spawn 在隔离分身中执行一个 WBS 步骤;不可用会 fail-closed,不在宿主本地执行。', objectSchema({
21
33
  planId: id('计划 id'), stepId: id('步骤 id'),
22
34
  }, ['planId', 'stepId']), async (args) => dual() ? await dual().pjm.dispatch(args.planId, { stepId: args.stepId }) : dualUnavailable()),
@@ -25,6 +37,10 @@ export function apply(ctx) {
25
37
  status: { type: 'string', enum: ['done', 'failed'] }, summary: string('结果摘要'),
26
38
  evidence: strings('测试/文件/报告证据'), actualMinutes: { type: 'number', description: '实际用时(分钟)' },
27
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()),
28
44
  jsonTool('pjm_project_status', '读取项目看板:完成/进行/待办/失败、估时、实际用时、是否可结案。', objectSchema({ planId: id('计划 id') }, ['planId']), async (args) => dual() ? dual().pjm.progress(args.planId) : dualUnavailable()),
29
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()),
30
46
  jsonTool('pjm_close_project', '全部步骤完成后结案:写入总结、评审报告、验收与资料清单,并可生成结案文档。', objectSchema({
@@ -29,7 +29,29 @@ export function apply(ctx) {
29
29
  jsonTool('pm_write_prd', '把 PRD(可带六类盘点)写成 workspace/docs/pm 下的 Markdown,返回文档路径。', objectSchema({
30
30
  prdId: id('PRD id'), assessmentId: id('可选:关联的 Assessment id'),
31
31
  }, ['prdId']), async (args, exec) => dual() ? await dual().pm.writePrd(args.prdId, { assessmentId: args.assessmentId, agent: exec.agent }) : dualUnavailable()),
32
- jsonTool('pm_list_prds', '列出当前 Host 中的 PRD 与六类盘点索引。', objectSchema({}), async () => dual() ? ({ ok: true, prds: dual().pm.listPrds().items, assessments: dual().pm.listAssessments().items }) : 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()),
33
55
  ];
34
56
  return installAgent(ctx, { prompt: PM_SYSTEM_PROMPT, tools });
35
57
  }
@@ -14,14 +14,19 @@ export const PM_SYSTEM_PROMPT = String.raw `# Role
14
14
  1. 先识别目标、范围、约束和未决项;重大歧义或不可逆选择必须用 ask_user_question 提出一个具体决策点,给 2–4 个选项并标推荐项。不要为琐碎实现细节打断用户。
15
15
  2. 盘点前先用 read/grep/glob 读取真实代码、测试和文档。没有证据的“完成”不得写 100%,不得把设计稿当成已交付。
16
16
  3. 用 pm_create_prd 保存结构化 PRD;用 pm_create_assessment 保存六类盘点。每个 completed 项必须附文件、测试或报告证据。
17
- 4. 用 pm_write_prd 生成 Markdown 文档;需要用户打开时用 present 交付。需要 PJM 执行时,只移交 PRD id、验收标准、未决问题,不代替 PJM 派工。
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)与理由;按它走,不要自己跳步。
18
22
 
19
23
  # Rules
20
24
  - 不臆测、不伪造证据、不把“可以实现”写成“已经完成”。
21
25
  - 冲突必须列出冲突双方、影响、可选方案和需要用户重新判断的问题。
22
26
  - 歧义必须写出当前理解与待确认问题;用户未回答前不得把该理解当成事实。
23
27
  - PM 不写生产代码、不跑 shell、不发布、不安装、不调度 VM/分身。
24
- - 只写 PRD/盘点文档;不改需求范围外的文件。
28
+ - 只有 PM 能复核 PJM 的交付;不得自己给自己复核,也不得替 PJM 编造执行结果。
29
+ - 只写 PRD/盘点/复核文档;不改需求范围外的文件。
25
30
 
26
31
  # Output Format
27
32
  默认用简洁 Markdown 汇报,必须按任务需要包含:
@@ -41,15 +46,20 @@ export const PJM_SYSTEM_PROMPT = String.raw `# Role
41
46
 
42
47
  # Workflow
43
48
  1. 项目会审:核对 PRD、验收标准、约束、未决问题;未决项未解决不得自行扩权。
44
- 2. 用 pjm_create_plan 建立七类项目内容:项目会审、WBS、需求内容、时间、人员、测试部署验收、结案。
45
- 3. 执行前申请隔离资源,通过 pjm_dispatch_step 走 fangForker.spawn;不可用就 fail-closed,绝不在母体/原身本地降级执行,不虚构 vm_create/vm_exec。
46
- 4. 用 pjm_project_status 汇总节点,用 todo_write 跟踪当前工作;完成步骤时记录 summary/evidence/actualMinutes。
47
- 5. 用 pjm_write_project 保存计划与节点报告。全部步骤完成后才能 pjm_close_project;结案必须有总结、验收记录、评审报告和资料清单,并交付文档。
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)与理由;按它走,不要跳步或重复派发。
48
57
 
49
58
  # Rules
50
59
  - 按时、按质、按量报告真实状态;失败、阻塞、未测量不得包装成完成。
51
60
  - 不发布、不部署到生产、不修改 profile、不触碰凭据,除非用户对该动作另行明确授权。
52
61
  - 不代替 PM 决定需求,不替用户回答选择题;阻塞时给出原因、影响、可选方案和所需决策。
62
+ - 不给自己复核:交付是否正确由 PM 复核;PM 判 changes-requested 时按新的验收项重做。
53
63
  - 不做无限轮询;节点汇报写完成/进行/待办/失败及下一步。
54
64
 
55
65
  # Output Format
@@ -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
+ }
@@ -0,0 +1,114 @@
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
+ * { type: 'ping' }
9
+ * { type: 'resync' } → 重新推一次快照
10
+ * { type: 'chat', content: string, agent?: 'pm'|'pjm' }
11
+ * { type: 'answer', callId: string, value: string } ← 目前会返回 session-prompt-unavailable
12
+ *
13
+ * 服务端 → 客户端:
14
+ * { type: 'hello', ... } 连接建立(含协议版本与可用性)
15
+ * { type: 'dual', snapshot } 交接/计划/反馈/复核快照
16
+ * { type: 'pong', at }
17
+ * { type: 'error', code, message, detail? }
18
+ *
19
+ * 为什么 chat/answer 现在只返回错误:见 docs/CHANNEL-WEBSOCKET.md。
20
+ * 插件侧没有「向指定 preset 会话提交用户消息」的 Host API;假装能聊就是假聊天。
21
+ */
22
+ export const CHANNEL_PROTOCOL_VERSION = 1;
23
+ export const CHANNEL_MAX_MESSAGE_BYTES = 64 * 1024;
24
+ export const CLIENT_MESSAGE_TYPES = Object.freeze(['ping', 'resync', 'chat', 'answer', 'interrupt']);
25
+ function text(value) { return String(value ?? '').trim(); }
26
+ /**
27
+ * 解析并校验一条客户端消息;非法输入给出具名错误而不是抛。
28
+ */
29
+ export function parseClientMessage(raw) {
30
+ if (typeof raw === 'string' && raw.length > CHANNEL_MAX_MESSAGE_BYTES) {
31
+ return { ok: false, code: 'message-too-large', error: `message exceeds ${CHANNEL_MAX_MESSAGE_BYTES} bytes` };
32
+ }
33
+ let body = null;
34
+ if (typeof raw === 'string') {
35
+ try {
36
+ body = JSON.parse(raw);
37
+ }
38
+ catch {
39
+ return { ok: false, code: 'invalid-json', error: 'message must be JSON' };
40
+ }
41
+ }
42
+ else if (raw && typeof raw === 'object') {
43
+ body = raw;
44
+ }
45
+ else {
46
+ return { ok: false, code: 'invalid-message', error: 'message must be a JSON object' };
47
+ }
48
+ const type = text(body.type);
49
+ if (!CLIENT_MESSAGE_TYPES.includes(type)) {
50
+ return { ok: false, code: 'unknown-type', error: `unknown message type: ${type || '(empty)'}`, types: CLIENT_MESSAGE_TYPES };
51
+ }
52
+ if (type === 'chat') {
53
+ const content = text(body.content);
54
+ if (!content)
55
+ return { ok: false, code: 'content-required', error: 'chat requires content' };
56
+ if (content.length > 8_000)
57
+ return { ok: false, code: 'content-too-long', error: 'chat content exceeds 8000 characters' };
58
+ const agent = text(body.agent) || 'pm';
59
+ if (!['pm', 'pjm'].includes(agent))
60
+ return { ok: false, code: 'agent-invalid', error: 'agent must be pm or pjm' };
61
+ return { ok: true, message: { type, content, agent } };
62
+ }
63
+ if (type === 'answer') {
64
+ const callId = text(body.callId);
65
+ const value = text(body.value);
66
+ if (!callId || !value)
67
+ return { ok: false, code: 'answer-incomplete', error: 'answer requires callId and value' };
68
+ return { ok: true, message: { type, callId, value } };
69
+ }
70
+ if (type === 'interrupt') {
71
+ // messageId 可选:省略表示“这一连接上所有正在生成的流都停”
72
+ const messageId = text(body.messageId);
73
+ return { ok: true, message: { type, messageId: messageId || null } };
74
+ }
75
+ return { ok: true, message: { type } };
76
+ }
77
+ /** 中断确认:`aborted` 表示是否真的掐断了一条在跑的流。 */
78
+ export function interruptedMessage({ messageId, savedContent = '', aborted = true } = {}) {
79
+ return {
80
+ type: 'interrupted',
81
+ messageId: text(messageId) || null,
82
+ savedContent: String(savedContent ?? ''),
83
+ aborted: aborted === true,
84
+ at: Date.now(),
85
+ };
86
+ }
87
+ export function serverHello({ channelId, tokenRequired = true } = {}) {
88
+ return {
89
+ type: 'hello',
90
+ protocol: CHANNEL_PROTOCOL_VERSION,
91
+ channelId: text(channelId) || 'dsh-master-command-center',
92
+ capabilities: {
93
+ dualSnapshot: true,
94
+ chat: false,
95
+ answer: false,
96
+ },
97
+ limitations: {
98
+ chat: 'plugin cannot submit a user message into a preset agent session',
99
+ answer: 'user questions are answered inside their owning session',
100
+ },
101
+ tokenRequired: tokenRequired === true,
102
+ serverTime: Date.now(),
103
+ };
104
+ }
105
+ export function serverError(code, message, detail) {
106
+ const out = { type: 'error', code: text(code) || 'unknown', message: String(message ?? '') };
107
+ if (detail !== undefined)
108
+ out.detail = detail;
109
+ return out;
110
+ }
111
+ /** 把 dual 快照包成通道消息(保持字段原样,不在这里加工业务语义)。 */
112
+ export function dualMessage(snapshot) {
113
+ return { type: 'dual', snapshot: snapshot ?? null };
114
+ }