@twinsearth/dsh-master 3.3.3 → 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 +16 -0
- 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 +27 -1
- 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 +2 -0
- package/docs/CHANGELOG.md +16 -0
- package/docs/REVIEW-PM-PJM-3.3.4.md +153 -0
- 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 +28 -1
- package/src/plugins/cost-optimizer.ts +3 -3
- package/src/plugins/dual-manager.ts +134 -78
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 dsh-Master contributors — see LICENSE-CORE.
|
|
3
|
+
// @ts-nocheck — host JS→TS 迁移阶段:运行时行为优先,类型注解逐步补齐。
|
|
4
|
+
/** PM/PJM 共享的结构化契约。纯函数,不读写文件、不调用模型。 */
|
|
5
|
+
export const ASSESSMENT_CATEGORIES = Object.freeze(['completed', 'unfinished', 'blocked', 'planned', 'conflicts', 'ambiguities']);
|
|
6
|
+
export const PROJECT_SECTIONS = Object.freeze(['projectReview', 'steps', 'requirements', 'schedule', 'team', 'quality', 'closure']);
|
|
7
|
+
const text = (value, fallback = '') => String(value ?? fallback).trim();
|
|
8
|
+
const list = (value) => Array.isArray(value) ? value.map((item) => text(item)).filter(Boolean) : (text(value) ? [text(value)] : []);
|
|
9
|
+
const records = (value) => Array.isArray(value) ? value.filter((item) => item && typeof item === 'object') : [];
|
|
10
|
+
const error = (code, message, detail) => ({ ok: false, code, error: message, ...(detail === undefined ? {} : { detail }) });
|
|
11
|
+
const positiveMinutes = (value, fallback = 30) => {
|
|
12
|
+
const n = Number(value);
|
|
13
|
+
return Number.isFinite(n) && n > 0 ? Math.min(10_080, Math.round(n)) : fallback;
|
|
14
|
+
};
|
|
15
|
+
function normalizeModules(value) {
|
|
16
|
+
return records(value).map((item, index) => ({
|
|
17
|
+
id: text(item.id, `module-${index + 1}`),
|
|
18
|
+
name: text(item.name, `模块 ${index + 1}`),
|
|
19
|
+
result: text(item.result ?? item.outcome),
|
|
20
|
+
architecture: text(item.architecture),
|
|
21
|
+
status: text(item.status, 'planned'),
|
|
22
|
+
})).filter((item) => item.name);
|
|
23
|
+
}
|
|
24
|
+
export function normalizePrd(input = {}, { id, now = Date.now() } = {}) {
|
|
25
|
+
const title = text(input.title);
|
|
26
|
+
const goal = text(input.goal);
|
|
27
|
+
if (!title)
|
|
28
|
+
return error('title-required', 'title is required');
|
|
29
|
+
if (!goal)
|
|
30
|
+
return error('goal-required', 'goal is required');
|
|
31
|
+
const prd = Object.freeze({
|
|
32
|
+
id: String(id),
|
|
33
|
+
title,
|
|
34
|
+
goal,
|
|
35
|
+
why: text(input.why, goal),
|
|
36
|
+
what: list(input.what),
|
|
37
|
+
architecture: text(input.architecture),
|
|
38
|
+
outcomes: list(input.outcomes),
|
|
39
|
+
modules: Object.freeze(normalizeModules(input.modules)),
|
|
40
|
+
priority: ['low', 'normal', 'high', 'critical'].includes(text(input.priority, 'normal')) ? text(input.priority, 'normal') : 'normal',
|
|
41
|
+
constraints: Object.freeze(list(input.constraints)),
|
|
42
|
+
acceptance: Object.freeze(list(input.acceptance)),
|
|
43
|
+
nonGoals: Object.freeze(list(input.nonGoals)),
|
|
44
|
+
assumptions: Object.freeze(list(input.assumptions)),
|
|
45
|
+
openQuestions: Object.freeze(list(input.openQuestions)),
|
|
46
|
+
requester: text(input.requester, 'user'),
|
|
47
|
+
status: 'draft',
|
|
48
|
+
createdAt: now,
|
|
49
|
+
});
|
|
50
|
+
return { ok: true, prd };
|
|
51
|
+
}
|
|
52
|
+
function normalizeAssessmentItems(category, value) {
|
|
53
|
+
const items = [];
|
|
54
|
+
for (const [index, raw] of records(value).entries()) {
|
|
55
|
+
const title = text(raw.title ?? raw.name);
|
|
56
|
+
if (!title)
|
|
57
|
+
return error(`${category}[${index}].title-required`, `${category}[${index}].title is required`);
|
|
58
|
+
const base = { id: text(raw.id, `${category}-${index + 1}`), title, detail: text(raw.detail ?? raw.description) };
|
|
59
|
+
if (category === 'completed') {
|
|
60
|
+
const evidence = list(raw.evidence);
|
|
61
|
+
// “已完成”必须有可复核证据;否则只能放 planned/unfinished,不能写 100%。
|
|
62
|
+
if (evidence.length === 0)
|
|
63
|
+
return error(`completed[${index}].evidence-required`, `completed item "${title}" requires file/test/report evidence`);
|
|
64
|
+
const progress = Number(raw.progress ?? 100);
|
|
65
|
+
items.push({ ...base, progress: Number.isFinite(progress) ? Math.max(0, Math.min(100, progress)) : 100, evidence });
|
|
66
|
+
}
|
|
67
|
+
else if (category === 'unfinished') {
|
|
68
|
+
items.push({ ...base, reason: text(raw.reason, '未说明'), nextStep: text(raw.nextStep ?? raw.plan), progress: Math.max(0, Math.min(99, Number(raw.progress ?? 0) || 0)) });
|
|
69
|
+
}
|
|
70
|
+
else if (category === 'blocked') {
|
|
71
|
+
items.push({ ...base, reason: text(raw.reason, '未说明'), blocker: text(raw.blocker ?? raw.reason, '未说明'), nextStep: text(raw.nextStep) });
|
|
72
|
+
}
|
|
73
|
+
else if (category === 'planned') {
|
|
74
|
+
items.push({ ...base, plan: text(raw.plan ?? raw.nextStep), estimateMinutes: raw.estimateMinutes === undefined ? null : positiveMinutes(raw.estimateMinutes), priority: text(raw.priority, 'normal') });
|
|
75
|
+
}
|
|
76
|
+
else if (category === 'conflicts') {
|
|
77
|
+
const options = records(raw.options).map((option) => ({ value: text(option.value), label: text(option.label), consequence: text(option.consequence) })).filter((option) => option.value && option.label);
|
|
78
|
+
items.push({ ...base, question: text(raw.question, title), options, currentInterpretation: text(raw.currentInterpretation) });
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
items.push({ ...base, question: text(raw.question, title), currentInterpretation: text(raw.currentInterpretation), needsConfirmation: raw.needsConfirmation !== false });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return items;
|
|
85
|
+
}
|
|
86
|
+
export function normalizeAssessment(input = {}, { id, now = Date.now() } = {}) {
|
|
87
|
+
const title = text(input.title);
|
|
88
|
+
if (!title)
|
|
89
|
+
return error('title-required', 'assessment title is required');
|
|
90
|
+
const assessment = { id: String(id), prdId: text(input.prdId) || null, title, why: text(input.why), what: list(input.what), createdAt: now };
|
|
91
|
+
for (const category of ASSESSMENT_CATEGORIES) {
|
|
92
|
+
const normalized = normalizeAssessmentItems(category, input[category]);
|
|
93
|
+
if (!Array.isArray(normalized))
|
|
94
|
+
return normalized;
|
|
95
|
+
assessment[category] = Object.freeze(normalized.map((item) => Object.freeze(item)));
|
|
96
|
+
}
|
|
97
|
+
return { ok: true, assessment: Object.freeze(assessment) };
|
|
98
|
+
}
|
|
99
|
+
function normalizeTeam(value) {
|
|
100
|
+
return records(value).map((item, index) => ({
|
|
101
|
+
id: text(item.id, `owner-${index + 1}`),
|
|
102
|
+
role: text(item.role, 'developer'),
|
|
103
|
+
owner: text(item.owner, 'unassigned'),
|
|
104
|
+
responsibility: text(item.responsibility ?? item.scope),
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
function normalizeSteps(value) {
|
|
108
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
109
|
+
return error('steps-required', 'steps is required');
|
|
110
|
+
const steps = [];
|
|
111
|
+
for (const [index, raw] of value.entries()) {
|
|
112
|
+
const title = text(raw?.title);
|
|
113
|
+
const prompt = text(raw?.prompt);
|
|
114
|
+
if (!title)
|
|
115
|
+
return error(`steps[${index}].title-required`, `steps[${index}].title is required`);
|
|
116
|
+
if (!prompt)
|
|
117
|
+
return error(`steps[${index}].prompt-required`, `steps[${index}].prompt is required`);
|
|
118
|
+
steps.push({
|
|
119
|
+
id: text(raw.id, `step-${index + 1}`),
|
|
120
|
+
title,
|
|
121
|
+
prompt,
|
|
122
|
+
kind: text(raw.kind, 'development'),
|
|
123
|
+
owner: text(raw.owner, 'unassigned'),
|
|
124
|
+
where: text(raw.where, 'fork'),
|
|
125
|
+
estimateMinutes: positiveMinutes(raw.estimateMinutes),
|
|
126
|
+
dependsOn: list(raw.dependsOn),
|
|
127
|
+
acceptance: list(raw.acceptance),
|
|
128
|
+
status: 'pending',
|
|
129
|
+
fork: null,
|
|
130
|
+
summary: '',
|
|
131
|
+
evidence: [],
|
|
132
|
+
actualMinutes: null,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return steps;
|
|
136
|
+
}
|
|
137
|
+
export function normalizePlan(prd, input = {}, { id, now = Date.now() } = {}) {
|
|
138
|
+
if (!prd)
|
|
139
|
+
return error('prd-not-found', 'PRD not found');
|
|
140
|
+
const steps = normalizeSteps(input.steps);
|
|
141
|
+
if (!Array.isArray(steps))
|
|
142
|
+
return steps;
|
|
143
|
+
const totalEstimateMinutes = steps.reduce((sum, step) => sum + step.estimateMinutes, 0);
|
|
144
|
+
const plan = {
|
|
145
|
+
id: String(id),
|
|
146
|
+
prdId: prd.id,
|
|
147
|
+
title: text(input.title, `${prd.title} 项目计划`),
|
|
148
|
+
status: 'planned',
|
|
149
|
+
how: text(input.how),
|
|
150
|
+
where: text(input.where, 'fang fork / DSH Web / workspace'),
|
|
151
|
+
projectReview: list(input.projectReview ?? input.review),
|
|
152
|
+
requirements: list(input.requirements),
|
|
153
|
+
steps,
|
|
154
|
+
schedule: {
|
|
155
|
+
startAt: input.schedule?.startAt ? text(input.schedule.startAt) : null,
|
|
156
|
+
totalEstimateMinutes,
|
|
157
|
+
milestones: records(input.schedule?.milestones ?? input.milestones).map((item, index) => ({
|
|
158
|
+
id: text(item.id, `milestone-${index + 1}`),
|
|
159
|
+
title: text(item.title, `里程碑 ${index + 1}`),
|
|
160
|
+
owner: text(item.owner, 'unassigned'),
|
|
161
|
+
estimateMinutes: positiveMinutes(item.estimateMinutes),
|
|
162
|
+
dependsOn: list(item.dependsOn),
|
|
163
|
+
})),
|
|
164
|
+
},
|
|
165
|
+
team: normalizeTeam(input.team ?? input.who),
|
|
166
|
+
quality: {
|
|
167
|
+
tests: list(input.quality?.tests ?? input.tests),
|
|
168
|
+
deployment: list(input.quality?.deployment ?? input.deployment),
|
|
169
|
+
acceptance: list(input.quality?.acceptance ?? input.acceptance),
|
|
170
|
+
},
|
|
171
|
+
closure: { status: 'pending', summary: '', reviewReport: '', artifacts: [], acceptance: [], closedAt: null },
|
|
172
|
+
createdAt: now,
|
|
173
|
+
updatedAt: now,
|
|
174
|
+
};
|
|
175
|
+
return { ok: true, plan };
|
|
176
|
+
}
|
|
177
|
+
export function planProgress(plan) {
|
|
178
|
+
const steps = Array.isArray(plan?.steps) ? plan.steps : [];
|
|
179
|
+
const count = (status) => steps.filter((step) => step.status === status).length;
|
|
180
|
+
const total = steps.length;
|
|
181
|
+
const done = count('done');
|
|
182
|
+
const estimatedMinutes = steps.reduce((sum, step) => sum + (Number(step.estimateMinutes) || 0), 0);
|
|
183
|
+
const actualMinutes = steps.reduce((sum, step) => sum + (Number(step.actualMinutes) || 0), 0);
|
|
184
|
+
return {
|
|
185
|
+
total,
|
|
186
|
+
done,
|
|
187
|
+
running: count('running'),
|
|
188
|
+
failed: count('failed'),
|
|
189
|
+
pending: count('pending') + count('dispatching'),
|
|
190
|
+
percent: total === 0 ? 0 : Math.round((done / total) * 100),
|
|
191
|
+
estimatedMinutes,
|
|
192
|
+
actualMinutes,
|
|
193
|
+
canClose: total > 0 && done === total,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
export function closePlan(plan, input = {}, now = Date.now()) {
|
|
197
|
+
const progress = planProgress(plan);
|
|
198
|
+
if (!progress.canClose)
|
|
199
|
+
return error('plan-not-ready', 'all steps must be done before project closure', { progress });
|
|
200
|
+
const summary = text(input.summary);
|
|
201
|
+
if (!summary)
|
|
202
|
+
return error('summary-required', 'closure summary is required');
|
|
203
|
+
plan.status = 'closed';
|
|
204
|
+
plan.updatedAt = now;
|
|
205
|
+
plan.closure = {
|
|
206
|
+
status: 'closed',
|
|
207
|
+
summary,
|
|
208
|
+
reviewReport: text(input.reviewReport),
|
|
209
|
+
artifacts: list(input.artifacts),
|
|
210
|
+
acceptance: list(input.acceptance),
|
|
211
|
+
closedAt: now,
|
|
212
|
+
};
|
|
213
|
+
return { ok: true, plan, closure: plan.closure, progress };
|
|
214
|
+
}
|
package/dist-host/index.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* 协作只走服务键(见 kernel.mjs FANG_SERVICE):房主不 import 瓮城代码;瓮城服务
|
|
18
18
|
* 不可用时子插件必须 fail-closed,绝不本地降级执行。
|
|
19
19
|
*/
|
|
20
|
-
import { FANG_CONTRACT_MIN, FANG_SERVICE, MASTER_VERSION, ROLE_MATRIX, ROW, SERVICE, SUBPLUGIN_CATALOG, bearerOf, detectRole, enabledOf, jsonResponse, masterError, readJsonBody, roleAllowed, safeGet, } from "./kernel.js";
|
|
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
21
|
import { BRIDGE_PATH, filterEnv } from "./superagent/dispatch.js";
|
|
22
22
|
import { createBridgeTokenManager } from "./superagent/bridge-tokens.js";
|
|
23
23
|
export const name = 'dsh-master';
|
|
@@ -88,6 +88,22 @@ export function apply(ctx, config = {}) {
|
|
|
88
88
|
blockedReason: active || allowed ? null : `role-not-allowed:${role}`,
|
|
89
89
|
};
|
|
90
90
|
});
|
|
91
|
+
/**
|
|
92
|
+
* 3.3.4:两个智能体的**可选择性**(不是“已激活”)。
|
|
93
|
+
* preset 的激活由 agentPresets 服务在会话创建时决定,这里只如实报告宿主是否有该服务。
|
|
94
|
+
*/
|
|
95
|
+
const agentCards = () => {
|
|
96
|
+
const presets = safeGet(ctx, 'agentPresets');
|
|
97
|
+
const available = Boolean(presets) && typeof presets?.list === 'function';
|
|
98
|
+
return AGENT_CATALOG.map((entry) => ({
|
|
99
|
+
...entry,
|
|
100
|
+
presetServiceAvailable: available,
|
|
101
|
+
selectable: available,
|
|
102
|
+
hint: available
|
|
103
|
+
? '新会话创建时选择该 preset;已有会话不会被改变。'
|
|
104
|
+
: '宿主缺少 agentPresets 服务(DSH web bundle 才有),本 Host 无法选择该 preset。',
|
|
105
|
+
}));
|
|
106
|
+
};
|
|
91
107
|
const service = {
|
|
92
108
|
id: SERVICE.master,
|
|
93
109
|
name: 'dsh-master',
|
|
@@ -135,6 +151,29 @@ export function apply(ctx, config = {}) {
|
|
|
135
151
|
uptimeMs: Date.now() - startedAt,
|
|
136
152
|
cooperation: cooperation(),
|
|
137
153
|
subplugins: catalog(),
|
|
154
|
+
/** 3.3.4:PM / PJM 智能体(Agent preset)清单;与子插件行分开报告。 */
|
|
155
|
+
agents: agentCards(),
|
|
156
|
+
/**
|
|
157
|
+
* 3.3.4:智囊团只读摘要。dual-manager 未装配时给 null —— 不构造空壳假装成功。
|
|
158
|
+
*/
|
|
159
|
+
dual: (() => {
|
|
160
|
+
const dual = safeGet(ctx, SERVICE.dual);
|
|
161
|
+
if (!dual || typeof dual.status !== 'function')
|
|
162
|
+
return null;
|
|
163
|
+
try {
|
|
164
|
+
const snapshot = dual.status();
|
|
165
|
+
return {
|
|
166
|
+
ok: snapshot.ok === true,
|
|
167
|
+
agents: snapshot.agents ?? null,
|
|
168
|
+
pm: snapshot.pm ?? null,
|
|
169
|
+
pjm: snapshot.pjm ?? null,
|
|
170
|
+
cooperation: snapshot.cooperation ?? null,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
})(),
|
|
138
177
|
roleMatrix: ROLE_MATRIX,
|
|
139
178
|
bridge: {
|
|
140
179
|
enabled: Boolean(String(config.bridgeToken ?? '').trim()) || bridgeTokens.describe().tokens.length > 0,
|
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.4';
|
|
11
11
|
export const FANG_CONTRACT_MIN = '3.1.14';
|
|
12
12
|
export const SERVICE = Object.freeze({
|
|
13
13
|
master: 'dshMaster',
|
|
@@ -57,6 +57,32 @@ export const ROW = Object.freeze({
|
|
|
57
57
|
cost: 'master-cost-optimizer',
|
|
58
58
|
dual: 'master-dual-manager',
|
|
59
59
|
});
|
|
60
|
+
/**
|
|
61
|
+
* 3.3.4:两个可被新会话选择的智能体(Agent preset,不是子插件行)。
|
|
62
|
+
*
|
|
63
|
+
* 与 ROLE_MATRIX 的区别:preset 决定“这个会话是谁”,角色矩阵决定“这一行在哪个角色里装配”。
|
|
64
|
+
* 两者都不改变对方;智能体自身不授予任何执行权限,工具边界写在 cordis.patch.yml 的 preset 里。
|
|
65
|
+
*/
|
|
66
|
+
export const AGENT_CATALOG = Object.freeze([
|
|
67
|
+
Object.freeze({
|
|
68
|
+
id: 'master-pm',
|
|
69
|
+
preset: 'master-pm',
|
|
70
|
+
name: '产品经理(PM)',
|
|
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/盘点;不执行、不派发、不发布。',
|
|
75
|
+
}),
|
|
76
|
+
Object.freeze({
|
|
77
|
+
id: 'master-pjm',
|
|
78
|
+
preset: 'master-pjm',
|
|
79
|
+
name: '项目经理(PJM)',
|
|
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: '只读代码 + 计划/派发/汇报;派发必须经 fangForker.spawn,无 forker 即 fail-closed。',
|
|
84
|
+
}),
|
|
85
|
+
]);
|
|
60
86
|
export const ROLE = Object.freeze({ mother: 'mother', essence: 'essence', fork: 'fork' });
|
|
61
87
|
/**
|
|
62
88
|
* 角色矩阵:
|
|
@@ -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.4',
|
|
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.4',
|
|
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.4:系统层记账/预算/灰度/审计 + A1 工具输出紧凑化(默认 shadow)+ 瓮城回流出口(contract 1.1.1 真实投递,回执计数修复);工具面 A2–A3 与决策复用 B1–B4 已接入(shadow 默认);3.3.3 起角色矩阵母体全开、分身挂双主管;3.3.4 起内置 PM/PJM 两个智能体 preset。',
|
|
400
400
|
};
|
|
401
401
|
},
|
|
402
402
|
};
|