@twinsearth/dsh-master 3.3.4 → 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.
- package/README.md +1 -0
- package/client.js +83 -4
- package/cordis.patch.yml +2 -0
- package/dist-host/agents/pjm-agent.js +18 -2
- package/dist-host/agents/pm-agent.js +23 -1
- package/dist-host/agents/prompts.js +16 -6
- package/dist-host/channel/abort-registry.js +93 -0
- package/dist-host/channel/protocol.js +114 -0
- package/dist-host/channel/server.js +349 -0
- package/dist-host/channel/stream-state.js +343 -0
- package/dist-host/channel/ws.js +199 -0
- package/dist-host/dual/documents.js +36 -0
- package/dist-host/dual/orchestrator.js +214 -0
- package/dist-host/dual/schema.js +153 -0
- package/dist-host/engine/atomic-switch.js +159 -0
- package/dist-host/engine/health-probe.js +86 -0
- package/dist-host/engine/index.js +18 -0
- package/dist-host/engine/llm-stream.js +215 -0
- package/dist-host/engine/plugin-updater.js +392 -0
- package/dist-host/engine/safe-update.js +150 -0
- package/dist-host/index.js +114 -2
- package/dist-host/kernel.js +7 -7
- package/dist-host/plugins/cost-optimizer.js +3 -3
- package/dist-host/plugins/dual-manager.js +230 -6
- package/docs/AGENTS-PM-PJM.md +90 -6
- package/docs/CHANGELOG.md +58 -0
- package/docs/CHANNEL-WEBSOCKET.md +169 -0
- package/docs/DESIGN-command-center.md +147 -0
- package/docs/ENGINE-PRIMITIVES.md +215 -0
- package/docs/GAP-ANALYSIS-fang-session.md +75 -0
- package/docs/REVIEW-PM-PJM-3.3.4.md +32 -6
- package/docs/examples/command-center/README.md +167 -0
- package/package.json +3 -1
- package/src/agents/pjm-agent.ts +18 -2
- package/src/agents/pm-agent.ts +21 -1
- package/src/agents/prompts.ts +16 -6
- package/src/channel/abort-registry.ts +85 -0
- package/src/channel/protocol.ts +110 -0
- package/src/channel/server.ts +289 -0
- package/src/channel/stream-state.ts +349 -0
- package/src/channel/ws.ts +182 -0
- package/src/dual/documents.ts +35 -0
- package/src/dual/orchestrator.ts +223 -0
- package/src/dual/schema.ts +148 -1
- package/src/engine/atomic-switch.ts +146 -0
- package/src/engine/health-probe.ts +84 -0
- package/src/engine/index.ts +20 -0
- package/src/engine/llm-stream.ts +189 -0
- package/src/engine/plugin-updater.ts +359 -0
- package/src/engine/safe-update.ts +148 -0
- package/src/index.ts +105 -2
- package/src/kernel.ts +7 -7
- package/src/plugins/cost-optimizer.ts +3 -3
- package/src/plugins/dual-manager.ts +211 -6
package/dist-host/index.js
CHANGED
|
@@ -18,12 +18,16 @@
|
|
|
18
18
|
* 不可用时子插件必须 fail-closed,绝不本地降级执行。
|
|
19
19
|
*/
|
|
20
20
|
import { AGENT_CATALOG, FANG_CONTRACT_MIN, FANG_SERVICE, MASTER_VERSION, ROLE_MATRIX, ROW, SERVICE, SUBPLUGIN_CATALOG, bearerOf, detectRole, enabledOf, jsonResponse, masterError, readJsonBody, roleAllowed, safeGet, } from "./kernel.js";
|
|
21
|
+
import { randomBytes } from 'node:crypto';
|
|
21
22
|
import { BRIDGE_PATH, filterEnv } from "./superagent/dispatch.js";
|
|
22
23
|
import { createBridgeTokenManager } from "./superagent/bridge-tokens.js";
|
|
24
|
+
import { attachCommandCenterChannel, CHANNEL_PATH } from "./channel/server.js";
|
|
23
25
|
export const name = 'dsh-master';
|
|
24
26
|
export const inject = [];
|
|
25
27
|
/** 状态路由(Client UI 读取;只读、非敏感、loopback-only 由宿主 webServer 保证)。 */
|
|
26
28
|
export const STATUS_ROUTE = '/dsh-master/api/status';
|
|
29
|
+
/** 3.3.5:智囊团只读快照(交接/计划/反馈/复核)。 */
|
|
30
|
+
export const DUAL_ROUTE = '/dsh-master/api/dual';
|
|
27
31
|
/**
|
|
28
32
|
* 房主核心 apply。
|
|
29
33
|
* @param {object} ctx Cordis context
|
|
@@ -104,8 +108,88 @@ export function apply(ctx, config = {}) {
|
|
|
104
108
|
: '宿主缺少 agentPresets 服务(DSH web bundle 才有),本 Host 无法选择该 preset。',
|
|
105
109
|
}));
|
|
106
110
|
};
|
|
107
|
-
|
|
108
|
-
|
|
111
|
+
/**
|
|
112
|
+
* 3.3.5:智囊团只读快照(面板数据源)。
|
|
113
|
+
* 只暴露“谁在等什么”,不暴露 token、不提供写入口;dual 未装配时给 available:false。
|
|
114
|
+
*/
|
|
115
|
+
const dualSnapshot = () => {
|
|
116
|
+
const dual = safeGet(ctx, SERVICE.dual);
|
|
117
|
+
if (!dual || typeof dual.status !== 'function') {
|
|
118
|
+
return { ok: true, available: false, reason: 'dual-service-unavailable', updatedAt: Date.now(), handoffs: [], plans: [], feedbacks: [], reviews: [], loop: null };
|
|
119
|
+
}
|
|
120
|
+
const safe = (fn, fallback) => { try {
|
|
121
|
+
return fn();
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return fallback;
|
|
125
|
+
} };
|
|
126
|
+
const handoffs = safe(() => dual.pm.listHandoffs().items, []);
|
|
127
|
+
const plans = safe(() => dual.pjm.listPlans().items, []);
|
|
128
|
+
const feedbacks = safe(() => dual.pjm.listFeedback().items, []);
|
|
129
|
+
const reviews = safe(() => dual.pm.listReviews().items, []);
|
|
130
|
+
return {
|
|
131
|
+
ok: true,
|
|
132
|
+
available: true,
|
|
133
|
+
updatedAt: Date.now(),
|
|
134
|
+
agents: { pm: 'master-pm', pjm: 'master-pjm' },
|
|
135
|
+
loop: safe(() => dual.status().loop, null),
|
|
136
|
+
handoffs: handoffs.map((item) => ({
|
|
137
|
+
id: item.id,
|
|
138
|
+
prdId: item.prdId,
|
|
139
|
+
title: item.title,
|
|
140
|
+
status: item.status,
|
|
141
|
+
reason: item.reason ?? null,
|
|
142
|
+
openQuestions: (item.openQuestions ?? []).slice(0, 10),
|
|
143
|
+
acceptance: (item.acceptance ?? []).slice(0, 10),
|
|
144
|
+
planId: item.planId ?? null,
|
|
145
|
+
preparedAt: item.preparedAt ?? null,
|
|
146
|
+
})),
|
|
147
|
+
plans: plans.map((plan) => {
|
|
148
|
+
const progress = safe(() => dual.pjm.progress(plan.id).progress, null);
|
|
149
|
+
const lastReport = Array.isArray(plan.reports) && plan.reports.length > 0 ? plan.reports.at(-1) : null;
|
|
150
|
+
return {
|
|
151
|
+
id: plan.id,
|
|
152
|
+
prdId: plan.prdId,
|
|
153
|
+
title: plan.title,
|
|
154
|
+
status: plan.status,
|
|
155
|
+
handoffId: plan.handoffId ?? null,
|
|
156
|
+
progress,
|
|
157
|
+
lastReport: lastReport ? { status: lastReport.status, summary: lastReport.summary, blocker: lastReport.blocker ?? null, at: lastReport.at } : null,
|
|
158
|
+
pmReview: plan.pmReview ?? null,
|
|
159
|
+
feedbackId: plan.feedbackId ?? null,
|
|
160
|
+
};
|
|
161
|
+
}),
|
|
162
|
+
feedbacks: feedbacks.map((item) => ({
|
|
163
|
+
id: item.id,
|
|
164
|
+
planId: item.planId,
|
|
165
|
+
prdId: item.prdId ?? null,
|
|
166
|
+
createdAt: item.createdAt ?? null,
|
|
167
|
+
summary: String(item.summary ?? '').slice(0, 500),
|
|
168
|
+
metrics: item.metrics ?? null,
|
|
169
|
+
openIssues: (item.openIssues ?? []).slice(0, 10),
|
|
170
|
+
})),
|
|
171
|
+
reviews: reviews.map((review) => ({
|
|
172
|
+
id: review.id,
|
|
173
|
+
feedbackId: review.feedbackId,
|
|
174
|
+
planId: review.planId,
|
|
175
|
+
verdict: review.verdict,
|
|
176
|
+
createdAt: review.createdAt ?? null,
|
|
177
|
+
failed: (review.acceptanceChecks ?? []).filter((check) => check.met !== true).map((check) => check.criterion),
|
|
178
|
+
})),
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
/**
|
|
182
|
+
* 智能体通道 token(3.3.5):每次加载随机生成;只读 status 暴露给同源面板。
|
|
183
|
+
* 跨站页面由 Origin 校验挡住;token 只是纵深防御,不是唯一门槛。
|
|
184
|
+
*/
|
|
185
|
+
const channelToken = String(config.channelToken ?? '').trim() || randomBytes(24).toString('hex');
|
|
186
|
+
const safeOrigin = (fn) => { try {
|
|
187
|
+
return String(fn() ?? '');
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return '';
|
|
191
|
+
} };
|
|
192
|
+
const service = { id: SERVICE.master,
|
|
109
193
|
name: 'dsh-master',
|
|
110
194
|
version: MASTER_VERSION,
|
|
111
195
|
role,
|
|
@@ -174,6 +258,8 @@ export function apply(ctx, config = {}) {
|
|
|
174
258
|
return null;
|
|
175
259
|
}
|
|
176
260
|
})(),
|
|
261
|
+
/** 3.3.5:智囊团 WebSocket 通道(同源面板用;chat 目前返回具名缺口)。 */
|
|
262
|
+
channel: { path: CHANNEL_PATH, token: channelToken },
|
|
177
263
|
roleMatrix: ROLE_MATRIX,
|
|
178
264
|
bridge: {
|
|
179
265
|
enabled: Boolean(String(config.bridgeToken ?? '').trim()) || bridgeTokens.describe().tokens.length > 0,
|
|
@@ -231,6 +317,32 @@ export function apply(ctx, config = {}) {
|
|
|
231
317
|
catch (error) {
|
|
232
318
|
logWarn(`[dsh-master] status route registration failed: ${String(error?.message ?? error)}`);
|
|
233
319
|
}
|
|
320
|
+
// v3.3.5:智囊团只读数据源(交接 / 计划 / 反馈 / 复核)。无写口、无 token。
|
|
321
|
+
try {
|
|
322
|
+
webServer.register({
|
|
323
|
+
kind: 'exact',
|
|
324
|
+
path: DUAL_ROUTE,
|
|
325
|
+
method: 'GET',
|
|
326
|
+
handler: async (req, res) => jsonResponse(res, 200, dualSnapshot()),
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
logWarn(`[dsh-master] dual route registration failed: ${String(error?.message ?? error)}`);
|
|
331
|
+
}
|
|
332
|
+
// v3.3.5:智囊团 WebSocket 通道(双向;当前只推快照,chat 返回具名缺口)。
|
|
333
|
+
try {
|
|
334
|
+
const channel = attachCommandCenterChannel({
|
|
335
|
+
webServer,
|
|
336
|
+
token: channelToken,
|
|
337
|
+
dualSnapshot,
|
|
338
|
+
allowedOrigin: safeOrigin(() => webServer.getOrigin?.()),
|
|
339
|
+
logger: { info: logWarn, warn: logWarn, error: logWarn },
|
|
340
|
+
});
|
|
341
|
+
logWarn(`[dsh-master] command-center channel: ${channel.attached ? channel.path : `not attached (${channel.reason})`}`);
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
logWarn(`[dsh-master] command-center channel failed: ${String(error?.message ?? error)}`);
|
|
345
|
+
}
|
|
234
346
|
// v3.2.3:原身 Router → 母体 forker 的跨进程派发桥。
|
|
235
347
|
try {
|
|
236
348
|
webServer.register({
|
package/dist-host/kernel.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 与瓮城 dsh-Fang 的协作只走 DSH 服务键,不 import fang 代码。
|
|
8
8
|
*/
|
|
9
9
|
import crypto from 'node:crypto';
|
|
10
|
-
export const MASTER_VERSION = '3.3.
|
|
10
|
+
export const MASTER_VERSION = '3.3.5';
|
|
11
11
|
export const FANG_CONTRACT_MIN = '3.1.14';
|
|
12
12
|
export const SERVICE = Object.freeze({
|
|
13
13
|
master: 'dshMaster',
|
|
@@ -69,18 +69,18 @@ export const AGENT_CATALOG = Object.freeze([
|
|
|
69
69
|
preset: 'master-pm',
|
|
70
70
|
name: '产品经理(PM)',
|
|
71
71
|
nameEn: 'Product Manager',
|
|
72
|
-
responsibilities: ['需求澄清与选择题请示', 'WHY/WHAT 与 PRD', '六类现状盘点(证据驱动)'],
|
|
73
|
-
tools: ['pm_create_prd', 'pm_create_assessment', 'pm_write_prd', 'pm_list_prds'],
|
|
74
|
-
boundary: '只读代码与文档 + 向用户提问 + 写 PRD
|
|
72
|
+
responsibilities: ['需求澄清与选择题请示', 'WHY/WHAT 与 PRD', '六类现状盘点(证据驱动)', '交接给 PJM', '复核/复盘/总结'],
|
|
73
|
+
tools: ['pm_create_prd', 'pm_create_assessment', 'pm_write_prd', 'pm_list_prds', 'pm_handoff_to_pjm', 'pm_review_feedback', 'pm_pipeline', 'pm_next_action'],
|
|
74
|
+
boundary: '只读代码与文档 + 向用户提问 + 写 PRD/盘点/复核;不执行、不派发、不发布。',
|
|
75
75
|
}),
|
|
76
76
|
Object.freeze({
|
|
77
77
|
id: 'master-pjm',
|
|
78
78
|
preset: 'master-pjm',
|
|
79
79
|
name: '项目经理(PJM)',
|
|
80
80
|
nameEn: 'Project Manager',
|
|
81
|
-
responsibilities: ['WBS 与七类项目管理', '
|
|
82
|
-
tools: ['pjm_create_plan', 'pjm_dispatch_step', 'pjm_complete_step', 'pjm_project_status', 'pjm_write_project', 'pjm_close_project'],
|
|
83
|
-
boundary: '只读代码 +
|
|
81
|
+
responsibilities: ['WBS 与七类项目管理', '接单与隔离派发', '节点自反馈', '测试/交付/验收与结案'],
|
|
82
|
+
tools: ['pjm_create_plan', 'pjm_take_handoff', 'pjm_dispatch_step', 'pjm_complete_step', 'pjm_project_status', 'pjm_check_step', 'pjm_report', 'pjm_submit_feedback', 'pjm_write_project', 'pjm_close_project', 'pjm_next_action'],
|
|
83
|
+
boundary: '只读代码 + 计划/派发/反馈;派发必须经 fangForker.spawn,无 forker 即 fail-closed;不替 PM 决定需求。',
|
|
84
84
|
}),
|
|
85
85
|
]);
|
|
86
86
|
export const ROLE = Object.freeze({ mother: 'mother', essence: 'essence', fork: 'fork' });
|
|
@@ -108,7 +108,7 @@ export function apply(ctx, config = {}) {
|
|
|
108
108
|
};
|
|
109
109
|
const service = {
|
|
110
110
|
id: SERVICE.cost,
|
|
111
|
-
version: '3.3.
|
|
111
|
+
version: '3.3.5',
|
|
112
112
|
role,
|
|
113
113
|
get mode() {
|
|
114
114
|
return system.policy.describe().mode;
|
|
@@ -354,7 +354,7 @@ export function apply(ctx, config = {}) {
|
|
|
354
354
|
return {
|
|
355
355
|
ok: true,
|
|
356
356
|
id: 'cost-optimizer',
|
|
357
|
-
version: '3.3.
|
|
357
|
+
version: '3.3.5',
|
|
358
358
|
role,
|
|
359
359
|
mode: current.mode,
|
|
360
360
|
requestedMode: current.requestedMode,
|
|
@@ -396,7 +396,7 @@ export function apply(ctx, config = {}) {
|
|
|
396
396
|
doubleCountGuard: report.doubleCountGuard,
|
|
397
397
|
};
|
|
398
398
|
})(),
|
|
399
|
-
note: '3.3.
|
|
399
|
+
note: '3.3.5:系统层记账/预算/灰度/审计 + A1 工具输出紧凑化(默认 shadow)+ 瓮城回流出口(contract 1.1.1 真实投递,回执计数修复);工具面 A2–A3 与决策复用 B1–B4 已接入(shadow 默认);3.3.3 起角色矩阵母体全开、分身挂双主管;3.3.4 起内置 PM/PJM 两个智能体 preset;3.3.5 起补齐引擎原语、可中断 LLM 流、WS 通道与流式管线。',
|
|
400
400
|
};
|
|
401
401
|
},
|
|
402
402
|
};
|
|
@@ -17,8 +17,9 @@
|
|
|
17
17
|
import fs from 'node:fs/promises';
|
|
18
18
|
import path from 'node:path';
|
|
19
19
|
import { FANG_SERVICE, MASTER_VERSION, ROW, SERVICE, createIdFactory, detectRole, enabledOf, masterError, roleAllowed, safeGet, } from "../kernel.js";
|
|
20
|
-
import { closePlan, normalizeAssessment, normalizePlan, normalizePrd, planProgress } from "../dual/schema.js";
|
|
21
|
-
import { renderAssessmentDocument, renderClosureDocument, renderPrdDocument, renderProjectDocument } from "../dual/documents.js";
|
|
20
|
+
import { closePlan, acceptHandoff, normalizeAssessment, normalizeFeedback, normalizeHandoff, normalizePlan, normalizePrd, normalizeReview, planProgress } from "../dual/schema.js";
|
|
21
|
+
import { renderAssessmentDocument, renderClosureDocument, renderPrdDocument, renderProjectDocument, renderReviewDocument } from "../dual/documents.js";
|
|
22
|
+
import { nextPjmAction, nextPmAction } from "../dual/orchestrator.js";
|
|
22
23
|
export const name = 'dsh-master-dual-manager';
|
|
23
24
|
export const inject = ['dshMaster'];
|
|
24
25
|
export const ROW_ID = ROW.dual;
|
|
@@ -47,9 +48,15 @@ export function apply(ctx, config = {}) {
|
|
|
47
48
|
const newPrdId = createIdFactory('prd');
|
|
48
49
|
const newAssessmentId = createIdFactory('review');
|
|
49
50
|
const newPlanId = createIdFactory('plan');
|
|
51
|
+
const newHandoffId = createIdFactory('handoff');
|
|
52
|
+
const newFeedbackId = createIdFactory('feedback');
|
|
53
|
+
const newReviewId = createIdFactory('pmreview');
|
|
50
54
|
const prds = new Map();
|
|
51
55
|
const assessments = new Map();
|
|
52
56
|
const plans = new Map();
|
|
57
|
+
const handoffs = new Map();
|
|
58
|
+
const feedbacks = new Map();
|
|
59
|
+
const reviews = new Map();
|
|
53
60
|
const decisionCenter = () => safeGet(ctx, FANG_SERVICE.decisionCenter);
|
|
54
61
|
const forker = () => {
|
|
55
62
|
const service = safeGet(ctx, FANG_SERVICE.forker);
|
|
@@ -134,6 +141,108 @@ export function apply(ctx, config = {}) {
|
|
|
134
141
|
return masterError('assessment-not-found', 'Assessment not found', { id: String(id ?? '') });
|
|
135
142
|
return await writeWorkspaceDocument(agent, path.join('docs', 'pm', `REVIEW-${id}.md`), renderAssessmentDocument(assessment));
|
|
136
143
|
},
|
|
144
|
+
/** 闭环第一步:PM 把 PRD(+盘点)打成交接包;open questions / 冲突未清则不给 ready。 */
|
|
145
|
+
createHandoff(prdId, input = {}) {
|
|
146
|
+
const prd = prds.get(String(prdId ?? ''));
|
|
147
|
+
if (!prd)
|
|
148
|
+
return masterError('prd-not-found', 'PRD not found', { id: String(prdId ?? '') });
|
|
149
|
+
const assessmentId = String(input.assessmentId ?? '').trim();
|
|
150
|
+
const assessment = assessmentId ? assessments.get(assessmentId) : null;
|
|
151
|
+
if (assessmentId && !assessment)
|
|
152
|
+
return masterError('assessment-not-found', 'Assessment not found', { id: assessmentId });
|
|
153
|
+
const made = normalizeHandoff(prd, assessment, { ...input, prdId: prd.id }, { id: newHandoffId() });
|
|
154
|
+
if (!made.ok)
|
|
155
|
+
return made;
|
|
156
|
+
handoffs.set(made.handoff.id, made.handoff);
|
|
157
|
+
return { ...made, ready: made.handoff.status === 'ready', reason: made.handoff.reason };
|
|
158
|
+
},
|
|
159
|
+
getHandoff(id) {
|
|
160
|
+
const handoff = handoffs.get(String(id ?? ''));
|
|
161
|
+
return handoff ? { ok: true, handoff } : masterError('handoff-not-found', 'Handoff not found', { id: String(id ?? '') });
|
|
162
|
+
},
|
|
163
|
+
listHandoffs() {
|
|
164
|
+
return { ok: true, items: [...handoffs.values()] };
|
|
165
|
+
},
|
|
166
|
+
/** 闭环第三步:PM 复核 PJM 反馈(逐条验收 + 复盘 + 总结),结论必须与验收一致。 */
|
|
167
|
+
reviewFeedback(feedbackId, input = {}) {
|
|
168
|
+
const feedback = feedbacks.get(String(feedbackId ?? ''));
|
|
169
|
+
if (!feedback)
|
|
170
|
+
return masterError('feedback-not-found', 'Feedback not found', { id: String(feedbackId ?? '') });
|
|
171
|
+
const made = normalizeReview(feedback, { ...input, feedbackId: feedback.id }, { id: newReviewId() });
|
|
172
|
+
if (!made.ok)
|
|
173
|
+
return made;
|
|
174
|
+
reviews.set(made.review.id, made.review);
|
|
175
|
+
const plan = plans.get(feedback.planId);
|
|
176
|
+
const handoff = [...handoffs.values()].find((item) => item.planId === feedback.planId) ?? null;
|
|
177
|
+
if (plan) {
|
|
178
|
+
plan.pmReview = Object.freeze({
|
|
179
|
+
reviewId: made.review.id,
|
|
180
|
+
verdict: made.review.verdict,
|
|
181
|
+
accepted: made.review.verdict === 'accepted',
|
|
182
|
+
reviewedAt: made.review.createdAt,
|
|
183
|
+
});
|
|
184
|
+
// 结案后仍保留事实,只标注“是否被 PM 接受”;不伪造重新开场。
|
|
185
|
+
plan.updatedAt = Date.now();
|
|
186
|
+
}
|
|
187
|
+
return { ok: true, review: made.review, planAccepted: made.review.verdict === 'accepted', handoffId: handoff?.id ?? null };
|
|
188
|
+
},
|
|
189
|
+
getReview(id) {
|
|
190
|
+
const review = reviews.get(String(id ?? ''));
|
|
191
|
+
return review ? { ok: true, review } : masterError('pm-review-not-found', 'Review not found', { id: String(id ?? '') });
|
|
192
|
+
},
|
|
193
|
+
listReviews() {
|
|
194
|
+
return { ok: true, items: [...reviews.values()] };
|
|
195
|
+
},
|
|
196
|
+
/** 闭环状态:一个 PRD 走到哪一步(交接/计划/反馈/复核)。 */
|
|
197
|
+
pipeline(prdId) {
|
|
198
|
+
const prd = prds.get(String(prdId ?? ''));
|
|
199
|
+
if (!prd)
|
|
200
|
+
return masterError('prd-not-found', 'PRD not found', { id: String(prdId ?? '') });
|
|
201
|
+
const handoff = [...handoffs.values()].filter((item) => item.prdId === prd.id).at(-1) ?? null;
|
|
202
|
+
const plan = handoff?.planId ? plans.get(handoff.planId) ?? null : [...plans.values()].filter((item) => item.prdId === prd.id).at(-1) ?? null;
|
|
203
|
+
const feedback = plan ? [...feedbacks.values()].filter((item) => item.planId === plan.id).at(-1) ?? null : null;
|
|
204
|
+
const review = feedback ? [...reviews.values()].filter((item) => item.feedbackId === feedback.id).at(-1) ?? null : null;
|
|
205
|
+
const stage = review
|
|
206
|
+
? (review.verdict === 'accepted' ? 'accepted' : 'changes-requested')
|
|
207
|
+
: feedback ? 'feedback-submitted'
|
|
208
|
+
: plan ? (plan.status === 'closed' ? 'closed-without-feedback' : 'planning')
|
|
209
|
+
: handoff ? (handoff.status === 'ready' ? 'handoff-ready' : 'needs-clarification')
|
|
210
|
+
: 'drafted';
|
|
211
|
+
return {
|
|
212
|
+
ok: true,
|
|
213
|
+
prdId: prd.id,
|
|
214
|
+
stage,
|
|
215
|
+
handoffId: handoff?.id ?? null,
|
|
216
|
+
handoffStatus: handoff?.status ?? null,
|
|
217
|
+
planId: plan?.id ?? null,
|
|
218
|
+
planStatus: plan?.status ?? null,
|
|
219
|
+
feedbackId: feedback?.id ?? null,
|
|
220
|
+
reviewId: review?.id ?? null,
|
|
221
|
+
verdict: review?.verdict ?? null,
|
|
222
|
+
};
|
|
223
|
+
},
|
|
224
|
+
/** 闭环第三步落盘:复核 + 复盘 + 总结文档。 */
|
|
225
|
+
async writeReview(reviewId, { agent } = {}) {
|
|
226
|
+
const review = reviews.get(String(reviewId ?? ''));
|
|
227
|
+
if (!review)
|
|
228
|
+
return masterError('pm-review-not-found', 'Review not found', { id: String(reviewId ?? '') });
|
|
229
|
+
const feedback = feedbacks.get(review.feedbackId) ?? null;
|
|
230
|
+
const handoff = feedback ? [...handoffs.values()].find((item) => item.planId === review.planId) ?? null : null;
|
|
231
|
+
const markdown = renderReviewDocument(review, { feedback, handoff });
|
|
232
|
+
return await writeWorkspaceDocument(agent, path.join('docs', 'pm', `REVIEW-${review.id}.md`), markdown);
|
|
233
|
+
},
|
|
234
|
+
/** 编排建议:PM 下一步该做什么(纯推荐,不产生副作用)。 */
|
|
235
|
+
nextAction(prdId) {
|
|
236
|
+
const prd = prds.get(String(prdId ?? ''));
|
|
237
|
+
if (!prd)
|
|
238
|
+
return masterError('prd-not-found', 'PRD not found', { id: String(prdId ?? '') });
|
|
239
|
+
const assessment = [...assessments.values()].filter((item) => item.prdId === prd.id).at(-1) ?? null;
|
|
240
|
+
const handoff = [...handoffs.values()].filter((item) => item.prdId === prd.id).at(-1) ?? null;
|
|
241
|
+
const plan = handoff?.planId ? plans.get(handoff.planId) ?? null : null;
|
|
242
|
+
const feedback = plan ? [...feedbacks.values()].filter((item) => item.planId === plan.id).at(-1) ?? null : null;
|
|
243
|
+
const review = feedback ? [...reviews.values()].filter((item) => item.feedbackId === feedback.id).at(-1) ?? null : null;
|
|
244
|
+
return { ok: true, prdId: prd.id, ...nextPmAction({ prd, assessment, handoff, feedback, review }), pipeline: pm.pipeline(prd.id) };
|
|
245
|
+
},
|
|
137
246
|
/** 歧义澄清:复用瓮城决策中心,返回 ticket 或明确的 unavailable。 */
|
|
138
247
|
async askUser({ question, context = '', options = [], level = 'yellow' } = {}) {
|
|
139
248
|
const q = String(question ?? '').trim();
|
|
@@ -161,17 +270,125 @@ export function apply(ctx, config = {}) {
|
|
|
161
270
|
},
|
|
162
271
|
};
|
|
163
272
|
const pjm = {
|
|
164
|
-
/** PRD → 七类项目计划与可调度 WBS
|
|
273
|
+
/** PRD → 七类项目计划与可调度 WBS;给 handoffId 时同时完成接单。 */
|
|
165
274
|
plan(prdId, input = {}) {
|
|
166
275
|
const prd = prds.get(String(prdId ?? ''));
|
|
167
276
|
if (!prd)
|
|
168
277
|
return masterError('prd-not-found', 'PRD not found', { id: String(prdId ?? '') });
|
|
278
|
+
const handoffId = String(input.handoffId ?? '').trim();
|
|
279
|
+
const handoff = handoffId ? handoffs.get(handoffId) : null;
|
|
280
|
+
if (handoffId && !handoff)
|
|
281
|
+
return masterError('handoff-not-found', 'Handoff not found', { id: handoffId });
|
|
282
|
+
if (handoff && handoff.status === 'needs-clarification') {
|
|
283
|
+
return masterError('handoff-not-ready', 'PM must resolve open questions before PJM plans', { reason: handoff.reason, openQuestions: handoff.openQuestions });
|
|
284
|
+
}
|
|
285
|
+
if (handoff && handoff.prdId !== prd.id)
|
|
286
|
+
return masterError('handoff-prd-mismatch', 'Handoff belongs to a different PRD', { handoffId, prdId: prd.id });
|
|
169
287
|
const made = normalizePlan(prd, input, { id: newPlanId() });
|
|
170
288
|
if (!made.ok)
|
|
171
289
|
return made;
|
|
172
290
|
plans.set(made.plan.id, made.plan);
|
|
291
|
+
if (handoff) {
|
|
292
|
+
const accepted = acceptHandoff(handoff, { planId: made.plan.id });
|
|
293
|
+
if (!accepted.ok)
|
|
294
|
+
return { ...accepted, plan: made.plan };
|
|
295
|
+
made.plan.handoffId = handoff.id;
|
|
296
|
+
}
|
|
173
297
|
return made;
|
|
174
298
|
},
|
|
299
|
+
/** 接单(独立调用):只有 ready 的交接包能被接走。 */
|
|
300
|
+
takeHandoff(handoffId, input = {}) {
|
|
301
|
+
const handoff = handoffs.get(String(handoffId ?? ''));
|
|
302
|
+
if (!handoff)
|
|
303
|
+
return masterError('handoff-not-found', 'Handoff not found', { id: String(handoffId ?? '') });
|
|
304
|
+
return acceptHandoff(handoff, { planId: input.planId });
|
|
305
|
+
},
|
|
306
|
+
listHandoffs() {
|
|
307
|
+
return { ok: true, items: [...handoffs.values()] };
|
|
308
|
+
},
|
|
309
|
+
/** 节点自反馈:执行中随时可报,不等到最后;blocked 必须写原因。 */
|
|
310
|
+
report(planId, input = {}) {
|
|
311
|
+
const plan = plans.get(String(planId ?? ''));
|
|
312
|
+
if (!plan)
|
|
313
|
+
return masterError('plan-not-found', 'Plan not found', { id: String(planId ?? '') });
|
|
314
|
+
const summary = String(input.summary ?? '').trim();
|
|
315
|
+
if (!summary)
|
|
316
|
+
return missing('summary');
|
|
317
|
+
const status = ['progress', 'blocked', 'done'].includes(String(input.status)) ? String(input.status) : 'progress';
|
|
318
|
+
if (status === 'blocked' && !String(input.blocker ?? input.reason ?? '').trim()) {
|
|
319
|
+
return masterError('blocker-required', 'a blocked report must name the blocker');
|
|
320
|
+
}
|
|
321
|
+
plan.reports = Array.isArray(plan.reports) ? plan.reports : [];
|
|
322
|
+
plan.reports.push(Object.freeze({
|
|
323
|
+
stepId: String(input.stepId ?? '') || null,
|
|
324
|
+
status,
|
|
325
|
+
summary,
|
|
326
|
+
blocker: String(input.blocker ?? input.reason ?? '').trim() || null,
|
|
327
|
+
evidence: Array.isArray(input.evidence) ? input.evidence.map(String).filter(Boolean) : [],
|
|
328
|
+
at: Date.now(),
|
|
329
|
+
}));
|
|
330
|
+
plan.updatedAt = Date.now();
|
|
331
|
+
return { ok: true, planId: plan.id, status, reports: plan.reports.length, progress: planProgress(plan) };
|
|
332
|
+
},
|
|
333
|
+
/** 项目级自反馈:计划全部步骤完成后提交给 PM 复核。 */
|
|
334
|
+
submitFeedback(planId, input = {}) {
|
|
335
|
+
const plan = plans.get(String(planId ?? ''));
|
|
336
|
+
if (!plan)
|
|
337
|
+
return masterError('plan-not-found', 'Plan not found', { id: String(planId ?? '') });
|
|
338
|
+
const prd = prds.get(plan.prdId) ?? null;
|
|
339
|
+
const handoff = [...handoffs.values()].find((item) => item.planId === plan.id) ?? null;
|
|
340
|
+
const made = normalizeFeedback(plan, prd, { ...input, planId: plan.id, handoffId: handoff?.id ?? input.handoffId }, { id: newFeedbackId() });
|
|
341
|
+
if (!made.ok)
|
|
342
|
+
return made;
|
|
343
|
+
feedbacks.set(made.feedback.id, made.feedback);
|
|
344
|
+
plan.feedbackId = made.feedback.id;
|
|
345
|
+
return { ...made, handoffId: handoff?.id ?? null };
|
|
346
|
+
},
|
|
347
|
+
listFeedback() {
|
|
348
|
+
return { ok: true, items: [...feedbacks.values()] };
|
|
349
|
+
},
|
|
350
|
+
/** 单步状态:房主没有 VM 作业队列,这里报的是派发/执行/汇报的真实状态。 */
|
|
351
|
+
checkStep(planId, stepId) {
|
|
352
|
+
const plan = plans.get(String(planId ?? ''));
|
|
353
|
+
if (!plan)
|
|
354
|
+
return masterError('plan-not-found', 'Plan not found', { id: String(planId ?? '') });
|
|
355
|
+
const step = plan.steps.find((item) => item.id === String(stepId ?? ''));
|
|
356
|
+
if (!step)
|
|
357
|
+
return masterError('step-not-found', 'Step not found', { id: String(stepId ?? '') });
|
|
358
|
+
const reports = (Array.isArray(plan.reports) ? plan.reports : []).filter((report) => !report.stepId || report.stepId === step.id);
|
|
359
|
+
return {
|
|
360
|
+
ok: true,
|
|
361
|
+
planId: plan.id,
|
|
362
|
+
stepId: step.id,
|
|
363
|
+
title: step.title,
|
|
364
|
+
status: step.status,
|
|
365
|
+
owner: step.owner ?? null,
|
|
366
|
+
where: step.where ?? null,
|
|
367
|
+
estimateMinutes: step.estimateMinutes ?? null,
|
|
368
|
+
actualMinutes: step.actualMinutes ?? null,
|
|
369
|
+
summary: step.summary ?? null,
|
|
370
|
+
evidence: step.evidence ?? [],
|
|
371
|
+
acceptance: step.acceptance ?? [],
|
|
372
|
+
fork: step.fork ?? null,
|
|
373
|
+
reports: reports.slice(-5),
|
|
374
|
+
note: 'no-vm-jobs: 房主经 fangForker 派发隔离执行体,没有 vm_job_submit / vm_job_status 这类作业队列',
|
|
375
|
+
};
|
|
376
|
+
},
|
|
377
|
+
/** 编排建议:PJM 下一步该做什么(纯推荐,不产生副作用)。 */
|
|
378
|
+
nextAction(planId) {
|
|
379
|
+
const plan = plans.get(String(planId ?? ''));
|
|
380
|
+
if (!plan)
|
|
381
|
+
return masterError('plan-not-found', 'Plan not found', { id: String(planId ?? '') });
|
|
382
|
+
const handoff = [...handoffs.values()].find((item) => item.planId === plan.id)
|
|
383
|
+
?? [...handoffs.values()].filter((item) => item.prdId === plan.prdId).at(-1)
|
|
384
|
+
?? null;
|
|
385
|
+
const feedback = [...feedbacks.values()].filter((item) => item.planId === plan.id).at(-1) ?? null;
|
|
386
|
+
return { ok: true, planId: plan.id, ...nextPjmAction({ handoff, plan, feedback }), progress: planProgress(plan) };
|
|
387
|
+
},
|
|
388
|
+
getFeedback(id) {
|
|
389
|
+
const feedback = feedbacks.get(String(id ?? ''));
|
|
390
|
+
return feedback ? { ok: true, feedback } : masterError('feedback-not-found', 'Feedback not found', { id: String(id ?? '') });
|
|
391
|
+
},
|
|
175
392
|
getPlan(id) {
|
|
176
393
|
const plan = plans.get(String(id ?? ''));
|
|
177
394
|
return plan ? { ok: true, plan } : masterError('plan-not-found', 'Plan not found', { id: String(id ?? '') });
|
|
@@ -301,8 +518,15 @@ export function apply(ctx, config = {}) {
|
|
|
301
518
|
version: MASTER_VERSION,
|
|
302
519
|
role,
|
|
303
520
|
agents: { pm: 'master-pm', pjm: 'master-pjm' },
|
|
304
|
-
pm: { prds: prds.size, assessments: assessments.size },
|
|
305
|
-
pjm: { plans: plans.size, closed: [...plans.values()].filter((plan) => plan.status === 'closed').length },
|
|
521
|
+
pm: { prds: prds.size, assessments: assessments.size, handoffs: handoffs.size, reviews: reviews.size },
|
|
522
|
+
pjm: { plans: plans.size, closed: [...plans.values()].filter((plan) => plan.status === 'closed').length, feedbacks: feedbacks.size },
|
|
523
|
+
loop: {
|
|
524
|
+
pendingHandoffs: [...handoffs.values()].filter((item) => item.status !== 'accepted').length,
|
|
525
|
+
acceptedHandoffs: [...handoffs.values()].filter((item) => item.status === 'accepted').length,
|
|
526
|
+
awaitingReview: [...feedbacks.values()].filter((item) => ![...reviews.values()].some((review) => review.feedbackId === item.id)).length,
|
|
527
|
+
accepted: [...reviews.values()].filter((review) => review.verdict === 'accepted').length,
|
|
528
|
+
changesRequested: [...reviews.values()].filter((review) => review.verdict === 'changes-requested').length,
|
|
529
|
+
},
|
|
306
530
|
cooperation: {
|
|
307
531
|
decisionCenter: Boolean(decisionCenter()),
|
|
308
532
|
forker: Boolean(forker()),
|
|
@@ -317,7 +541,7 @@ export function apply(ctx, config = {}) {
|
|
|
317
541
|
master.registerSubplugin({
|
|
318
542
|
id: 'dual-manager',
|
|
319
543
|
name: 'dsh-master-dual-manager',
|
|
320
|
-
capabilities: ['pm-prd', 'pm-assessment', 'pm-document', 'pm-ask-user', 'pjm-plan', 'pjm-dispatch', 'pjm-progress', 'pjm-close'],
|
|
544
|
+
capabilities: ['pm-prd', 'pm-assessment', 'pm-document', 'pm-ask-user', 'pm-handoff', 'pm-review', 'pjm-plan', 'pjm-dispatch', 'pjm-progress', 'pjm-close', 'pjm-feedback'],
|
|
321
545
|
description: 'PM/PJM 双智能体共享服务;master-pm 与 master-pjm preset 复用此服务',
|
|
322
546
|
});
|
|
323
547
|
return service;
|
package/docs/AGENTS-PM-PJM.md
CHANGED
|
@@ -81,16 +81,100 @@ Rules 真实状态汇报,失败/阻塞/未测量不得包装成完成;
|
|
|
81
81
|
│ (新会话选 preset: master-pm)
|
|
82
82
|
▼
|
|
83
83
|
PM: 澄清选择题 → PRD(prd_*) → 六类盘点(review_*) → docs/pm/*.md
|
|
84
|
-
│
|
|
84
|
+
│ pm_handoff_to_pjm:验收标准齐全 + 无未决问题/冲突 ⇒ ready
|
|
85
85
|
▼
|
|
86
|
-
PJM:
|
|
87
|
-
│
|
|
88
|
-
│
|
|
86
|
+
PJM: pjm_take_handoff / pjm_create_plan({handoffId}) 接单
|
|
87
|
+
│ WBS → fangForker.spawn 派生分身 → pjm_report 节点自反馈
|
|
88
|
+
│ 每步记录 evidence / actualMinutes
|
|
89
89
|
▼
|
|
90
|
-
PJM:
|
|
90
|
+
PJM: 全步 done → pjm_submit_feedback(交付/偏差/遗留/测试)
|
|
91
|
+
▼
|
|
92
|
+
PM: pm_review_feedback(逐条 met + 证据 + 复盘 + 总结)
|
|
93
|
+
│ verdict 必须与验收一致:有未达标 ⇒ changes-requested
|
|
94
|
+
▼
|
|
95
|
+
accepted ⇒ 计划标记 pmReview.accepted;changes-requested ⇒ 退回重做
|
|
91
96
|
```
|
|
92
97
|
|
|
93
|
-
|
|
98
|
+
**闭环门禁**(都写在 `src/dual/schema.ts`,有测试):
|
|
99
|
+
|
|
100
|
+
| 环节 | 门禁 |
|
|
101
|
+
|---|---|
|
|
102
|
+
| PM 交接 | 验收标准 ≥1;未决问题/冲突必须清零,否则 `needs-clarification`,PJM 不允许接单 |
|
|
103
|
+
| PJM 接单 | 只有 `ready` 能接;重复接单 `handoff-already-accepted` |
|
|
104
|
+
| 节点自反馈 | `blocked` 必须写 blocker;不等到最后才报 |
|
|
105
|
+
| 项目反馈 | 必须全部步骤 `done`;`summary` 必填;带交付/偏差/遗留/测试 |
|
|
106
|
+
| PM 复核 | 验收逐条 `met` 必须是布尔;`accepted` 不能带未达标项;`changes-requested` 不能带全达标项 |
|
|
107
|
+
| 闭环状态 | `pm_pipeline` 给出 drafted → handoff-ready/needs-clarification → planning → feedback → accepted/changes-requested |
|
|
108
|
+
|
|
109
|
+
**跨角色只传结构**:PRD/盘点/交接包/计划/反馈/复核都是显式记录(`handoff_*`、`feedback_*`、`pmreview_*`),
|
|
110
|
+
不共享隐式上下文;PM 不能替 PJM 编造执行结果,PJM 不能给自己复核。
|
|
111
|
+
|
|
112
|
+
### 4.3 工具清单与「草案 → 实际」对照
|
|
113
|
+
|
|
114
|
+
**PM(8 个)**:`pm_create_prd` · `pm_create_assessment` · `pm_write_prd` · `pm_list_prds` ·
|
|
115
|
+
`pm_handoff_to_pjm` · `pm_review_feedback` · `pm_pipeline` · `pm_next_action`
|
|
116
|
+
|
|
117
|
+
**PJM(11 个)**:`pjm_create_plan` · `pjm_take_handoff` · `pjm_dispatch_step` · `pjm_complete_step` ·
|
|
118
|
+
`pjm_project_status` · `pjm_check_step` · `pjm_report` · `pjm_submit_feedback` · `pjm_write_project` ·
|
|
119
|
+
`pjm_close_project` · `pjm_next_action`
|
|
120
|
+
|
|
121
|
+
| 常见草案名 | 房主实际 | 说明 |
|
|
122
|
+
|---|---|---|
|
|
123
|
+
| `ask_user_choice`(emit 事件后返回“请等待”) | **宿主工具 `ask_user_question`**(preset 已挂载) | 草案那样“发完就返回”不会真正阻塞,模型会继续瞎编;真实阻塞在宿主 `userQuestions` |
|
|
124
|
+
| `write_prd_doc` | `pm_write_prd` | 只写 Session workspace 下 `docs/pm`,越界拒写;PRD 结构由 `pm_create_prd` 先落库 |
|
|
125
|
+
| `spawn_dev_environment` | `pjm_take_handoff` + `pjm_dispatch_step` | 无 `umbrella.derivedManager`;派发只走 `fangForker.spawn`,无 forker 即 fail-closed |
|
|
126
|
+
| `execute_project_step` | `pjm_dispatch_step` + `pjm_complete_step` | 房主不直接跑命令;执行发生在隔离执行体,结果由 `complete_step` 记账 |
|
|
127
|
+
| `check_task_status`(`vm_job_status`) | `pjm_project_status` + `pjm_check_step` | 没有 VM 作业队列;状态来自计划、节点汇报与派发记录 |
|
|
128
|
+
|
|
129
|
+
**契约测试**(`test/agent-tools.test.mjs`,6 条)保证:
|
|
130
|
+
|
|
131
|
+
- 每个工具都有合法命名、`type:'object'` 参数、`output.schema/render` 封套;
|
|
132
|
+
- `AGENT_CATALOG` 声称的工具**确实注册了**(防止文档漂移);
|
|
133
|
+
- 任何工具的描述/参数里**不出现** `vm_create/vm_exec/vm_job_submit/vm_job_status/vm_snapshot/vm_restore/umbrella.`;
|
|
134
|
+
- 未知 id 一律返回具名 code(`prd-not-found` / `plan-not-found` / `feedback-not-found` / `handoff-not-found` / `step-not-found`);
|
|
135
|
+
- PM→PJM 闭环全部经工具跑通,且派发在无 forker 时返回 `fang-forker-unavailable`;
|
|
136
|
+
- 不存在任何工具去“假装阻塞提问”。
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
### 4.2 编排策略:下一步做什么由状态机决定(3.3.5)
|
|
141
|
+
|
|
142
|
+
模型知道工具清单,但未必知道"现在该用哪个"。所以把闭环状态机写成确定性函数
|
|
143
|
+
(`src/dual/orchestrator.ts`),并通过 `pm_next_action` / `pjm_next_action` 暴露给模型:
|
|
144
|
+
|
|
145
|
+
| 状态 | PM 推荐动作 | PJM 推荐动作 |
|
|
146
|
+
|---|---|---|
|
|
147
|
+
| 还没有 PRD | `create_prd` | — |
|
|
148
|
+
| 有 PRD 无盘点 | `create_assessment` | — |
|
|
149
|
+
| 有未决问题 / 冲突 | `clarify`(**blocking**,必须问用户) | — |
|
|
150
|
+
| 验收齐备、无未决 | `handoff` | — |
|
|
151
|
+
| 交接包未就绪 | `blocked`(原因) | `return_to_pm`(**blocking**,不得硬开工) |
|
|
152
|
+
| 交接包 ready | 等 PJM(非阻塞) | `take_handoff` |
|
|
153
|
+
| 有交接无计划 | — | `create_plan`(WBS 由模型拆) |
|
|
154
|
+
| 有 pending 步骤 | — | `dispatch`(下一个 stepId) |
|
|
155
|
+
| 有 running 步骤 | — | `report`(progress,不重复派发) |
|
|
156
|
+
| 有 failed 步骤 | — | `report`(blocked,不得写成完成) |
|
|
157
|
+
| 节点报告为 blocked | — | `report`(blocking,等决策) |
|
|
158
|
+
| 全部 done 无反馈 | — | `complete_feedback` |
|
|
159
|
+
| 有反馈未复核 | `review` | `close` / `done` |
|
|
160
|
+
| 已通过 / 已退回 | `done` | `done` |
|
|
161
|
+
|
|
162
|
+
**测试保证**:每个推荐动作都必须映射到 preset 里真实注册的工具
|
|
163
|
+
(`test/orchestrator.test.mjs` 交叉比对 `AGENT_CATALOG`),防止文档与工具漂移;
|
|
164
|
+
反向也检查工具清单没有"孤儿工具"。编排只给建议,不产生任何副作用。
|
|
165
|
+
|
|
166
|
+
### 4.1 「自动交接」到底自动了什么
|
|
167
|
+
|
|
168
|
+
| 自动(代码确定性完成) | 不自动(需要模型判断) |
|
|
169
|
+
|---|---|
|
|
170
|
+
| 门禁判定:验收标准 ≥1、未决问题/冲突清零 → `ready` | 把验收标准拆成 WBS 步骤,以及每步的 prompt/owner/场景/估时 |
|
|
171
|
+
| 接单绑定:`ready` 才能接,接单后写 `status=accepted`、`planId`、`plan.handoffId` | 判断某条验收是否真的满足(PM 复核时逐条 `met`) |
|
|
172
|
+
| 状态流转:`drafted → handoff-ready / needs-clarification → planning → feedback → accepted / changes-requested` | 写 PRD 的需求内容、六类盘点、复盘结论 |
|
|
173
|
+
| 防重:重复接单、未完成就提交反馈、结论与验收不一致,全部拒收 | 决定是否退回重做、退回理由是否成立 |
|
|
174
|
+
|
|
175
|
+
**为什么不做「自动拆 WBS」**:交接包只带验收标准与约束,**不含 `steps` 字段**;带 `handoffId` 调
|
|
176
|
+
`pjm_create_plan` 仍必须由 PJM 给步骤,否则返回 `steps-required`。这是刻意的——把验收翻译成步骤需要
|
|
177
|
+
理解语义,代码里硬编码只会产出看起来合理、实际不可执行的假 WBS(`test/pm-pjm-loop.test.mjs` 有断言)。
|
|
94
178
|
|
|
95
179
|
## 5. 如何启用
|
|
96
180
|
|