@twinsearth/dsh-master 3.3.2 → 3.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -6
- package/client.js +47 -1
- package/cordis.patch.yml +49 -0
- package/dist-host/agents/pjm-agent.js +46 -0
- package/dist-host/agents/pm-agent.js +35 -0
- package/dist-host/agents/prompts.js +61 -0
- package/dist-host/agents/toolkit.js +64 -0
- package/dist-host/dual/documents.js +107 -0
- package/dist-host/dual/schema.js +214 -0
- package/dist-host/index.js +40 -1
- package/dist-host/kernel.js +37 -9
- package/dist-host/plugins/cost-optimizer.js +3 -3
- package/dist-host/plugins/dual-manager.js +152 -79
- package/docs/AGENTS-PM-PJM.md +123 -0
- package/docs/ARCHITECTURE.md +5 -3
- package/docs/CHANGELOG.md +29 -0
- package/docs/DESIGN-v3.3.0-cost-optimization.md +6 -5
- package/docs/REVIEW-PM-PJM-3.3.4.md +153 -0
- package/docs/SECURITY.md +10 -8
- package/docs/SUPERAGENT.md +1 -1
- package/package.json +3 -1
- package/src/agents/pjm-agent.ts +46 -0
- package/src/agents/pm-agent.ts +38 -0
- package/src/agents/prompts.ts +64 -0
- package/src/agents/toolkit.ts +64 -0
- package/src/dual/documents.ts +107 -0
- package/src/dual/schema.ts +208 -0
- package/src/index.ts +38 -2
- package/src/kernel.ts +38 -9
- package/src/plugins/cost-optimizer.ts +3 -3
- package/src/plugins/dual-manager.ts +134 -78
|
@@ -3,19 +3,26 @@
|
|
|
3
3
|
// @ts-nocheck — host JS→TS 迁移阶段:运行时行为优先,类型注解逐步补齐(worker/ 仍严格检查)。
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* 双智能体共享服务(PM + PJM)。
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* *
|
|
8
|
+
* `master-pm` / `master-pjm` 是两个独立的 DSH Agent preset;本服务保存它们协作的
|
|
9
|
+
* 结构化 PRD、六类盘点、WBS、节点与结案状态,并负责文档落盘。
|
|
10
|
+
*
|
|
11
|
+
* 边界:
|
|
12
|
+
* * PM 只做需求与证据盘点,不执行代码、不派发分身;
|
|
13
|
+
* * PJM 只做计划、隔离派发、进度与结案,不替用户决定需求;
|
|
14
|
+
* * 没有 fangForker.spawn 时 dispatch fail-closed,绝不在宿主本地降级执行;
|
|
15
|
+
* * 歧义澄清复用瓮城决策中心,不另造一套审批;
|
|
16
|
+
* * 文档只写当前 Session workspace 下的 docs/pm 或 docs/pjm,不接受任意路径。
|
|
13
17
|
*/
|
|
14
18
|
|
|
19
|
+
import fs from 'node:fs/promises'
|
|
20
|
+
import path from 'node:path'
|
|
21
|
+
|
|
15
22
|
import {
|
|
16
23
|
FANG_SERVICE,
|
|
24
|
+
MASTER_VERSION,
|
|
17
25
|
ROW,
|
|
18
|
-
ROLE,
|
|
19
26
|
SERVICE,
|
|
20
27
|
createIdFactory,
|
|
21
28
|
detectRole,
|
|
@@ -24,34 +31,26 @@ import {
|
|
|
24
31
|
roleAllowed,
|
|
25
32
|
safeGet,
|
|
26
33
|
} from '../kernel.ts'
|
|
34
|
+
import { closePlan, normalizeAssessment, normalizePlan, normalizePrd, planProgress } from '../dual/schema.ts'
|
|
35
|
+
import { renderAssessmentDocument, renderClosureDocument, renderPrdDocument, renderProjectDocument } from '../dual/documents.ts'
|
|
27
36
|
|
|
28
37
|
export const name = 'dsh-master-dual-manager'
|
|
29
38
|
export const inject = ['dshMaster']
|
|
30
39
|
export const ROW_ID = ROW.dual
|
|
31
40
|
|
|
32
|
-
const PRIORITIES = Object.freeze(['low', 'normal', 'high', 'critical'])
|
|
33
|
-
const DEFAULT_STEP_KIND = 'development'
|
|
34
|
-
|
|
35
|
-
function stringList(value) {
|
|
36
|
-
if (!Array.isArray(value)) return []
|
|
37
|
-
return value.map((item) => String(item ?? '').trim()).filter(Boolean)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
41
|
function missing(name) {
|
|
41
42
|
return masterError(`${name}-required`, `${name} is required`)
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
/**
|
|
45
46
|
* @param {object} ctx
|
|
46
|
-
* @param {{enabled?:boolean}} [config]
|
|
47
|
+
* @param {{enabled?:boolean, dispatch?:object}} [config]
|
|
47
48
|
*/
|
|
48
49
|
export function apply(ctx, config = {}) {
|
|
49
50
|
if (!enabledOf(config)) return { skipped: true, reason: 'disabled-by-config' }
|
|
50
51
|
const role = detectRole()
|
|
51
52
|
if (!roleAllowed(ROW_ID, role)) {
|
|
52
|
-
try {
|
|
53
|
-
ctx?.logger?.info?.(`[dsh-master] dual-manager skipped: role=${role} not in mother/essence`)
|
|
54
|
-
} catch { /* ignore */ }
|
|
53
|
+
try { ctx?.logger?.info?.(`[dsh-master] dual-manager skipped: role=${role}`) } catch { /* ignore */ }
|
|
55
54
|
return { skipped: true, reason: `role-not-allowed:${role}` }
|
|
56
55
|
}
|
|
57
56
|
const master = safeGet(ctx, SERVICE.master)
|
|
@@ -60,8 +59,10 @@ export function apply(ctx, config = {}) {
|
|
|
60
59
|
}
|
|
61
60
|
|
|
62
61
|
const newPrdId = createIdFactory('prd')
|
|
62
|
+
const newAssessmentId = createIdFactory('review')
|
|
63
63
|
const newPlanId = createIdFactory('plan')
|
|
64
64
|
const prds = new Map()
|
|
65
|
+
const assessments = new Map()
|
|
65
66
|
const plans = new Map()
|
|
66
67
|
|
|
67
68
|
const decisionCenter = () => safeGet(ctx, FANG_SERVICE.decisionCenter)
|
|
@@ -70,27 +71,35 @@ export function apply(ctx, config = {}) {
|
|
|
70
71
|
return service && typeof service.spawn === 'function' ? service : null
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
/** 文档写口:只接受代码内生成的相对路径,根目录来自当前 Session。 */
|
|
75
|
+
async function writeWorkspaceDocument(agent, relativePath, content) {
|
|
76
|
+
const workingDirectory = safeGet(ctx, 'workingDirectory')
|
|
77
|
+
if (!workingDirectory) return masterError('working-directory-unavailable', 'workingDirectory service is required to write agent documents')
|
|
78
|
+
try {
|
|
79
|
+
const root = agent && typeof workingDirectory.ensure === 'function'
|
|
80
|
+
? await workingDirectory.ensure(agent)
|
|
81
|
+
: String(workingDirectory.defaultDirectory ?? '')
|
|
82
|
+
if (!root) return masterError('working-directory-unavailable', 'no active Session workspace')
|
|
83
|
+
const base = path.resolve(String(root))
|
|
84
|
+
const target = path.resolve(base, relativePath)
|
|
85
|
+
if (target !== base && !target.startsWith(`${base}${path.sep}`)) {
|
|
86
|
+
return masterError('document-path-outside-workspace', 'document path must stay inside the Session workspace')
|
|
87
|
+
}
|
|
88
|
+
await fs.mkdir(path.dirname(target), { recursive: true })
|
|
89
|
+
await fs.writeFile(target, String(content), 'utf8')
|
|
90
|
+
return { ok: true, path: target, bytes: Buffer.byteLength(String(content), 'utf8') }
|
|
91
|
+
} catch (error) {
|
|
92
|
+
return masterError('document-write-failed', String(error?.message ?? error))
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
73
96
|
const pm = {
|
|
74
|
-
/** 需求 → 结构化 PRD
|
|
97
|
+
/** 需求 → 结构化 PRD(模型通过 pm-agent 工具填充;服务本身不调用模型)。 */
|
|
75
98
|
createPrd(input = {}) {
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const priority = PRIORITIES.includes(String(input.priority ?? 'normal')) ? String(input.priority ?? 'normal') : 'normal'
|
|
81
|
-
const prd = Object.freeze({
|
|
82
|
-
id: newPrdId(),
|
|
83
|
-
title,
|
|
84
|
-
goal,
|
|
85
|
-
priority,
|
|
86
|
-
constraints: Object.freeze(stringList(input.constraints)),
|
|
87
|
-
acceptance: Object.freeze(stringList(input.acceptance)),
|
|
88
|
-
requester: String(input.requester ?? 'user'),
|
|
89
|
-
status: 'draft',
|
|
90
|
-
createdAt: Date.now(),
|
|
91
|
-
})
|
|
92
|
-
prds.set(prd.id, prd)
|
|
93
|
-
return { ok: true, prd }
|
|
99
|
+
const made = normalizePrd(input, { id: newPrdId() })
|
|
100
|
+
if (!made.ok) return made
|
|
101
|
+
prds.set(made.prd.id, made.prd)
|
|
102
|
+
return made
|
|
94
103
|
},
|
|
95
104
|
getPrd(id) {
|
|
96
105
|
const prd = prds.get(String(id ?? ''))
|
|
@@ -99,6 +108,39 @@ export function apply(ctx, config = {}) {
|
|
|
99
108
|
listPrds() {
|
|
100
109
|
return { ok: true, items: [...prds.values()] }
|
|
101
110
|
},
|
|
111
|
+
/** 六类现状盘点;completed 必须由调用方给出可复核证据。 */
|
|
112
|
+
createAssessment(input = {}) {
|
|
113
|
+
const prdId = String(input.prdId ?? '').trim()
|
|
114
|
+
if (prdId && !prds.has(prdId)) return masterError('prd-not-found', 'PRD not found', { id: prdId })
|
|
115
|
+
const made = normalizeAssessment(input, { id: newAssessmentId() })
|
|
116
|
+
if (!made.ok) return made
|
|
117
|
+
assessments.set(made.assessment.id, made.assessment)
|
|
118
|
+
return made
|
|
119
|
+
},
|
|
120
|
+
getAssessment(id) {
|
|
121
|
+
const assessment = assessments.get(String(id ?? ''))
|
|
122
|
+
return assessment ? { ok: true, assessment } : masterError('assessment-not-found', 'Assessment not found', { id: String(id ?? '') })
|
|
123
|
+
},
|
|
124
|
+
listAssessments() {
|
|
125
|
+
return { ok: true, items: [...assessments.values()] }
|
|
126
|
+
},
|
|
127
|
+
renderPrd(id, { assessmentId = '' } = {}) {
|
|
128
|
+
const prd = prds.get(String(id ?? ''))
|
|
129
|
+
if (!prd) return masterError('prd-not-found', 'PRD not found', { id: String(id ?? '') })
|
|
130
|
+
const assessment = assessmentId ? assessments.get(String(assessmentId)) : null
|
|
131
|
+
if (assessmentId && !assessment) return masterError('assessment-not-found', 'Assessment not found', { id: String(assessmentId) })
|
|
132
|
+
return { ok: true, markdown: renderPrdDocument(prd, assessment) }
|
|
133
|
+
},
|
|
134
|
+
async writePrd(id, { assessmentId = '', agent } = {}) {
|
|
135
|
+
const rendered = pm.renderPrd(id, { assessmentId })
|
|
136
|
+
if (!rendered.ok) return rendered
|
|
137
|
+
return await writeWorkspaceDocument(agent, path.join('docs', 'pm', `PRD-${id}.md`), rendered.markdown)
|
|
138
|
+
},
|
|
139
|
+
async writeAssessment(id, { agent } = {}) {
|
|
140
|
+
const assessment = assessments.get(String(id ?? ''))
|
|
141
|
+
if (!assessment) return masterError('assessment-not-found', 'Assessment not found', { id: String(id ?? '') })
|
|
142
|
+
return await writeWorkspaceDocument(agent, path.join('docs', 'pm', `REVIEW-${id}.md`), renderAssessmentDocument(assessment))
|
|
143
|
+
},
|
|
102
144
|
/** 歧义澄清:复用瓮城决策中心,返回 ticket 或明确的 unavailable。 */
|
|
103
145
|
async askUser({ question, context = '', options = [], level = 'yellow' } = {}) {
|
|
104
146
|
const q = String(question ?? '').trim()
|
|
@@ -124,54 +166,38 @@ export function apply(ctx, config = {}) {
|
|
|
124
166
|
}
|
|
125
167
|
|
|
126
168
|
const pjm = {
|
|
127
|
-
/** PRD →
|
|
128
|
-
plan(prdId,
|
|
169
|
+
/** PRD → 七类项目计划与可调度 WBS。 */
|
|
170
|
+
plan(prdId, input = {}) {
|
|
129
171
|
const prd = prds.get(String(prdId ?? ''))
|
|
130
172
|
if (!prd) return masterError('prd-not-found', 'PRD not found', { id: String(prdId ?? '') })
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const prompt = String(step?.prompt ?? '').trim()
|
|
136
|
-
if (!title) return missing(`steps[${index}].title`)
|
|
137
|
-
if (!prompt) return missing(`steps[${index}].prompt`)
|
|
138
|
-
normalized.push({
|
|
139
|
-
id: String(step?.id ?? `step-${index + 1}`),
|
|
140
|
-
title,
|
|
141
|
-
prompt,
|
|
142
|
-
kind: String(step?.kind ?? DEFAULT_STEP_KIND),
|
|
143
|
-
status: 'pending',
|
|
144
|
-
fork: null,
|
|
145
|
-
})
|
|
146
|
-
}
|
|
147
|
-
const plan = {
|
|
148
|
-
id: newPlanId(),
|
|
149
|
-
prdId: prd.id,
|
|
150
|
-
status: 'planned',
|
|
151
|
-
steps: normalized,
|
|
152
|
-
createdAt: Date.now(),
|
|
153
|
-
}
|
|
154
|
-
plans.set(plan.id, plan)
|
|
155
|
-
return { ok: true, plan }
|
|
173
|
+
const made = normalizePlan(prd, input, { id: newPlanId() })
|
|
174
|
+
if (!made.ok) return made
|
|
175
|
+
plans.set(made.plan.id, made.plan)
|
|
176
|
+
return made
|
|
156
177
|
},
|
|
157
178
|
getPlan(id) {
|
|
158
179
|
const plan = plans.get(String(id ?? ''))
|
|
159
180
|
return plan ? { ok: true, plan } : masterError('plan-not-found', 'Plan not found', { id: String(id ?? '') })
|
|
160
181
|
},
|
|
182
|
+
progress(id) {
|
|
183
|
+
const plan = plans.get(String(id ?? ''))
|
|
184
|
+
if (!plan) return masterError('plan-not-found', 'Plan not found', { id: String(id ?? '') })
|
|
185
|
+
return { ok: true, planId: plan.id, status: plan.status, progress: planProgress(plan), steps: plan.steps }
|
|
186
|
+
},
|
|
161
187
|
/** 调瓮城 fangForker.spawn 派生执行分身;无 forker 时 fail-closed。 */
|
|
162
188
|
async dispatch(planId, { stepId } = {}) {
|
|
163
189
|
const plan = plans.get(String(planId ?? ''))
|
|
164
190
|
if (!plan) return masterError('plan-not-found', 'Plan not found', { id: String(planId ?? '') })
|
|
191
|
+
if (plan.status === 'closed') return masterError('plan-already-closed', 'Closed plans cannot dispatch new steps')
|
|
165
192
|
const step = plan.steps.find((item) => item.id === String(stepId ?? ''))
|
|
166
193
|
if (!step) return masterError('step-not-found', 'Step not found', { id: String(stepId ?? '') })
|
|
167
194
|
if (step.fork) return masterError('step-already-dispatched', 'Step already dispatched', { stepId: step.id, fork: step.fork })
|
|
168
195
|
const fangForker = forker()
|
|
169
|
-
if (!fangForker)
|
|
170
|
-
return masterError('fang-forker-unavailable', 'dsh-fang fangForker.spawn is required to dispatch a step')
|
|
171
|
-
}
|
|
196
|
+
if (!fangForker) return masterError('fang-forker-unavailable', 'dsh-fang fangForker.spawn is required to dispatch a step')
|
|
172
197
|
// 先占位再 await,避免两个并发 dispatch 同时看到 step.fork=null 而重复派生。
|
|
173
198
|
step.status = 'dispatching'
|
|
174
199
|
step.fork = { pending: true }
|
|
200
|
+
plan.updatedAt = Date.now()
|
|
175
201
|
try {
|
|
176
202
|
const spawnConfig = config.dispatch && typeof config.dispatch === 'object' ? config.dispatch : {}
|
|
177
203
|
const fork = await fangForker.spawn({
|
|
@@ -194,26 +220,54 @@ export function apply(ctx, config = {}) {
|
|
|
194
220
|
step.status = 'running'
|
|
195
221
|
step.fork = fork ?? null
|
|
196
222
|
plan.status = 'running'
|
|
223
|
+
plan.updatedAt = Date.now()
|
|
197
224
|
return { ok: true, planId: plan.id, stepId: step.id, step, fork }
|
|
198
225
|
} catch (error) {
|
|
199
226
|
step.status = 'pending'; step.fork = null
|
|
200
227
|
return masterError('fang-spawn-threw', String(error?.message ?? error))
|
|
201
228
|
}
|
|
202
229
|
},
|
|
203
|
-
completeStep(planId, stepId, { status = 'done', summary = '' } = {}) {
|
|
230
|
+
completeStep(planId, stepId, { status = 'done', summary = '', evidence = [], actualMinutes = null } = {}) {
|
|
204
231
|
const plan = plans.get(String(planId ?? ''))
|
|
205
232
|
if (!plan) return masterError('plan-not-found', 'Plan not found', { id: String(planId ?? '') })
|
|
233
|
+
if (plan.status === 'closed') return masterError('plan-already-closed', 'Closed plans cannot be modified')
|
|
206
234
|
const step = plan.steps.find((item) => item.id === String(stepId ?? ''))
|
|
207
235
|
if (!step) return masterError('step-not-found', 'Step not found', { id: String(stepId ?? '') })
|
|
208
|
-
|
|
209
|
-
step.status = next
|
|
236
|
+
step.status = status === 'failed' ? 'failed' : 'done'
|
|
210
237
|
step.summary = String(summary)
|
|
238
|
+
step.evidence = Array.isArray(evidence) ? evidence.map(String).filter(Boolean) : []
|
|
239
|
+
const minutes = Number(actualMinutes)
|
|
240
|
+
step.actualMinutes = Number.isFinite(minutes) && minutes >= 0 ? minutes : null
|
|
211
241
|
const states = plan.steps.map((item) => item.status)
|
|
212
242
|
plan.status = states.every((state) => state === 'done')
|
|
213
243
|
? 'done'
|
|
214
|
-
: (states.some((state) => state === 'running') ? 'running' : 'planned')
|
|
244
|
+
: (states.some((state) => state === 'running' || state === 'dispatching') ? 'running' : 'planned')
|
|
245
|
+
plan.updatedAt = Date.now()
|
|
215
246
|
return { ok: true, plan }
|
|
216
247
|
},
|
|
248
|
+
close(planId, input = {}) {
|
|
249
|
+
const plan = plans.get(String(planId ?? ''))
|
|
250
|
+
if (!plan) return masterError('plan-not-found', 'Plan not found', { id: String(planId ?? '') })
|
|
251
|
+
if (plan.status === 'closed') return masterError('plan-already-closed', 'Plan is already closed')
|
|
252
|
+
return closePlan(plan, input)
|
|
253
|
+
},
|
|
254
|
+
renderProject(planId) {
|
|
255
|
+
const plan = plans.get(String(planId ?? ''))
|
|
256
|
+
if (!plan) return masterError('plan-not-found', 'Plan not found', { id: String(planId ?? '') })
|
|
257
|
+
return { ok: true, markdown: renderProjectDocument(plan) }
|
|
258
|
+
},
|
|
259
|
+
async writeProject(planId, { agent } = {}) {
|
|
260
|
+
const rendered = pjm.renderProject(planId)
|
|
261
|
+
if (!rendered.ok) return rendered
|
|
262
|
+
return await writeWorkspaceDocument(agent, path.join('docs', 'pjm', `PROJECT-${planId}.md`), rendered.markdown)
|
|
263
|
+
},
|
|
264
|
+
async writeClosure(planId, { agent } = {}) {
|
|
265
|
+
const plan = plans.get(String(planId ?? ''))
|
|
266
|
+
if (!plan) return masterError('plan-not-found', 'Plan not found', { id: String(planId ?? '') })
|
|
267
|
+
if (plan.closure.status !== 'closed') return masterError('plan-not-closed', 'Project must be closed before writing its closure report')
|
|
268
|
+
const markdown = `# 项目结案 · ${plan.title}\n\n- 计划 ID:\`${plan.id}\`\n- 关联 PRD:\`${plan.prdId}\`\n\n${renderClosureDocument(plan)}\n`
|
|
269
|
+
return await writeWorkspaceDocument(agent, path.join('docs', 'pjm', `CLOSURE-${planId}.md`), markdown)
|
|
270
|
+
},
|
|
217
271
|
listPlans() {
|
|
218
272
|
return { ok: true, items: [...plans.values()] }
|
|
219
273
|
},
|
|
@@ -221,18 +275,20 @@ export function apply(ctx, config = {}) {
|
|
|
221
275
|
|
|
222
276
|
const service = {
|
|
223
277
|
id: SERVICE.dual,
|
|
224
|
-
version:
|
|
278
|
+
version: MASTER_VERSION,
|
|
225
279
|
role,
|
|
226
280
|
pm,
|
|
227
281
|
pjm,
|
|
282
|
+
agents: Object.freeze({ pm: 'master-pm', pjm: 'master-pjm' }),
|
|
228
283
|
status() {
|
|
229
284
|
return {
|
|
230
285
|
ok: true,
|
|
231
286
|
id: 'dual-manager',
|
|
232
|
-
version:
|
|
287
|
+
version: MASTER_VERSION,
|
|
233
288
|
role,
|
|
234
|
-
|
|
235
|
-
|
|
289
|
+
agents: { pm: 'master-pm', pjm: 'master-pjm' },
|
|
290
|
+
pm: { prds: prds.size, assessments: assessments.size },
|
|
291
|
+
pjm: { plans: plans.size, closed: [...plans.values()].filter((plan) => plan.status === 'closed').length },
|
|
236
292
|
cooperation: {
|
|
237
293
|
decisionCenter: Boolean(decisionCenter()),
|
|
238
294
|
forker: Boolean(forker()),
|
|
@@ -245,8 +301,8 @@ export function apply(ctx, config = {}) {
|
|
|
245
301
|
master.registerSubplugin({
|
|
246
302
|
id: 'dual-manager',
|
|
247
303
|
name: 'dsh-master-dual-manager',
|
|
248
|
-
capabilities: ['pm-prd', 'pm-ask-user', 'pjm-plan', 'pjm-dispatch'],
|
|
249
|
-
description: 'PM
|
|
304
|
+
capabilities: ['pm-prd', 'pm-assessment', 'pm-document', 'pm-ask-user', 'pjm-plan', 'pjm-dispatch', 'pjm-progress', 'pjm-close'],
|
|
305
|
+
description: 'PM/PJM 双智能体共享服务;master-pm 与 master-pjm preset 复用此服务',
|
|
250
306
|
})
|
|
251
307
|
return service
|
|
252
308
|
}
|