@joekytc/dsh-swarm 0.3.8 → 0.3.10
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/lib/config.d.ts +1 -1
- package/lib/dispatcher/agent-runner.js +37 -2
- package/lib/dispatcher/dispatcher.d.ts +8 -0
- package/lib/dispatcher/dispatcher.js +21 -0
- package/lib/dispatcher/merge-gate.d.ts +1 -0
- package/lib/dispatcher/merge-gate.js +28 -1
- package/lib/dispatcher/session-events.d.ts +9 -1
- package/lib/dispatcher/session-events.js +14 -2
- package/lib/dispatcher/v-orchestrator.js +13 -6
- package/lib/domain/delivery-contract.d.ts +2 -1
- package/lib/domain/delivery-contract.js +16 -3
- package/lib/domain/ocr-review.d.ts +5 -1
- package/lib/domain/ocr-review.js +10 -2
- package/lib/domain/planning-checklist.d.ts +33 -0
- package/lib/domain/planning-checklist.js +197 -0
- package/lib/domain/prefetch-manifest.js +15 -4
- package/lib/roles/preset-installer.d.ts +14 -0
- package/lib/roles/preset-installer.js +17 -0
- package/lib/roles/toolsets.d.ts +1 -1
- package/lib/roles/toolsets.js +7 -5
- package/lib/routes/planning-driver.d.ts +2 -1
- package/lib/routes/planning-driver.js +13 -10
- package/lib/services/ocr-cli.d.ts +5 -0
- package/lib/services/ocr-cli.js +10 -1
- package/lib/tools/kanban-tools.js +10 -2
- package/lib/tools/main-session-tools.d.ts +2 -1
- package/lib/tools/main-session-tools.js +101 -20
- package/lib/tools/ocr-review-tools.d.ts +3 -1
- package/lib/tools/ocr-review-tools.js +23 -5
- package/lib/tools/planning-tools.d.ts +3 -0
- package/lib/tools/planning-tools.js +248 -3
- package/lib/tools/spec-card-tools.js +8 -2
- package/lib/tools/wiki-tools.js +1 -1
- package/lib/wiki/page-path.d.ts +2 -1
- package/lib/wiki/page-path.js +6 -4
- package/package.json +2 -2
- package/personas/kanban-dt/agent.cordis.yml +4 -1
- package/personas/persona-dt.md +2 -1
- package/skills/grill-me/SKILL.md +7 -0
|
@@ -4,6 +4,8 @@ const STR_FIELDS = [
|
|
|
4
4
|
['problem', 'problem'], ['solution', 'solution'], ['testing', 'testing'], ['out_of_scope', 'out_of_scope'],
|
|
5
5
|
];
|
|
6
6
|
const ARR_FIELDS = [['user_stories', 'user_stories'], ['impl_decisions', 'impl_decisions']];
|
|
7
|
+
/** 闸2 禁词表:澄清答案中指代不明的缩写;宁窄后扩,避免误伤正常表述。 */
|
|
8
|
+
export const FORBIDDEN_SHORTHANDS = ['按推荐', '同上', '见上', '同前', '如前所述', 'same as above', 'ditto', 'see above'];
|
|
7
9
|
/** 需求澄清清单 schema 硬校验:返回错误列表(空数组=合法)。清单缺段即拒绝保存(硬闸,主 agent 会话内修正)。 */
|
|
8
10
|
export function validatePlanningChecklist(raw) {
|
|
9
11
|
const errors = [];
|
|
@@ -62,6 +64,13 @@ export function validatePlanningChecklist(raw) {
|
|
|
62
64
|
const got = typeof o === 'object' && o !== null ? Object.keys(o).join(',') : JSON.stringify(el);
|
|
63
65
|
errors.push(`checklist.clarifications[${i}] must be {"q": string, "a": string} with both non-empty (got keys: ${got}) — use keys "q" and "a", not "question"/"answer"`);
|
|
64
66
|
}
|
|
67
|
+
if (ok) {
|
|
68
|
+
const answer = o['a'].toLowerCase();
|
|
69
|
+
const hit = FORBIDDEN_SHORTHANDS.find((w) => answer.includes(w.toLowerCase()));
|
|
70
|
+
if (hit) {
|
|
71
|
+
errors.push(`checklist.clarifications[${i}].a contains forbidden shorthand "${hit}" — 每条问答必须含完整决策正文(字段名/接口路径/枚举值/组件名/位置规则),禁止指代不明缩写,展开写全`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
65
74
|
});
|
|
66
75
|
}
|
|
67
76
|
const dbs = c['doubts'];
|
|
@@ -101,6 +110,79 @@ export function validatePlanningChecklist(raw) {
|
|
|
101
110
|
});
|
|
102
111
|
}
|
|
103
112
|
}
|
|
113
|
+
// 闸1:需求来源必挂。恢复重建旧页无来源时按错误文案中的逃生口录「无来源+原因」条目
|
|
114
|
+
const sources = c['sources'];
|
|
115
|
+
if (!Array.isArray(sources) || sources.length === 0) {
|
|
116
|
+
errors.push('checklist.sources must be a non-empty array — record requirement source links (TAPD/Jira/其他). If genuinely no source exists, record exactly [{type:"其他", url:"", note:"无来源+原因"}]');
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
sources.forEach((el, i) => {
|
|
120
|
+
const o = el;
|
|
121
|
+
const typeOk = o !== null && typeof o === 'object' && (o['type'] === 'TAPD' || o['type'] === 'Jira' || o['type'] === '其他');
|
|
122
|
+
const urlOk = typeof o?.['url'] === 'string';
|
|
123
|
+
const noteOk = typeof o?.['note'] === 'string';
|
|
124
|
+
if (!typeOk || !urlOk || !noteOk) {
|
|
125
|
+
const got = typeof o === 'object' && o !== null ? Object.keys(o).join(',') : JSON.stringify(el);
|
|
126
|
+
errors.push(`checklist.sources[${i}] must be {"type": "TAPD"|"Jira"|"其他", "url": string, "note": string} (got keys: ${got})`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (o['url'].trim() === '' && o['note'].trim() === '') {
|
|
130
|
+
errors.push(`checklist.sources[${i}] url 为空(无来源)时 note 必须写明无来源原因`);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
// 闸3:PRD 采集凭证。登记链接必须落到 collected(含摘要/页路径)或 blocked(含原因);
|
|
135
|
+
// 其余状态(含漏填)= 登记了却没采 → 拒收
|
|
136
|
+
const prd = c['prdCollection'];
|
|
137
|
+
if (!Array.isArray(prd)) {
|
|
138
|
+
errors.push('checklist.prdCollection must be an array — register every PRD link the user provided (empty [] only when the user gave no PRD link)');
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
prd.forEach((el, i) => {
|
|
142
|
+
const o = el;
|
|
143
|
+
const urlOk = typeof o?.['url'] === 'string' && o['url'].trim().length > 0;
|
|
144
|
+
const status = o?.['status'];
|
|
145
|
+
if (typeof o !== 'object' || o === null || !urlOk) {
|
|
146
|
+
errors.push(`checklist.prdCollection[${i}] must be an object with non-empty "url"`);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (status !== 'collected' && status !== 'blocked') {
|
|
150
|
+
errors.push(`checklist.prdCollection[${i}].status must be "collected" or "blocked" (got: ${JSON.stringify(status)}) — 链接已登记就必须完成采集或记录阻塞原因,禁止悬空`);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (typeof o['summary'] !== 'string' || o['summary'].trim() === '') {
|
|
154
|
+
errors.push(`checklist.prdCollection[${i}].summary must be a non-empty string(采了什么+关键信息+缺口)`);
|
|
155
|
+
}
|
|
156
|
+
if (status === 'blocked' && (typeof o['blockedReason'] !== 'string' || o['blockedReason'].trim() === '')) {
|
|
157
|
+
errors.push(`checklist.prdCollection[${i}].blockedReason required when status="blocked"(登录态 | 缺技能 | 404 | 其他)`);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
// 占位策略(可选):存在则三要素非空
|
|
162
|
+
const phs = c['placeholders'];
|
|
163
|
+
if (phs !== undefined) {
|
|
164
|
+
if (!Array.isArray(phs)) {
|
|
165
|
+
errors.push('checklist.placeholders must be an array when present');
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
phs.forEach((el, i) => {
|
|
169
|
+
const o = el;
|
|
170
|
+
const ok = typeof o === 'object' && o !== null &&
|
|
171
|
+
typeof o['target'] === 'string' && o['target'].trim() !== '' &&
|
|
172
|
+
typeof o['value'] === 'string' &&
|
|
173
|
+
typeof o['replace'] === 'string' && o['replace'].trim() !== '';
|
|
174
|
+
if (!ok) {
|
|
175
|
+
const got = typeof o === 'object' && o !== null ? Object.keys(o).join(',') : JSON.stringify(el);
|
|
176
|
+
errors.push(`checklist.placeholders[${i}] must be {"target": string, "value": string, "replace": string} with target/replace non-empty (got keys: ${got})`);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// greenfield(可选):存在即须布尔。true=绿地(无 .git 全新项目),D 阶段建仓前置;false/缺省=既有仓库
|
|
182
|
+
const gf = c['greenfield'];
|
|
183
|
+
if (gf !== undefined && typeof gf !== 'boolean') {
|
|
184
|
+
errors.push(`checklist.greenfield must be a boolean when present (got: ${JSON.stringify(gf)})`);
|
|
185
|
+
}
|
|
104
186
|
return errors;
|
|
105
187
|
}
|
|
106
188
|
/** 需求澄清清单页标题:与任务卡 title 同源同逻辑(buildChainTitle),保证 KB 可检索。 */
|
|
@@ -126,9 +208,16 @@ export function formatChecklistBody(c) {
|
|
|
126
208
|
if (repo.branch)
|
|
127
209
|
lines.push('- 当前分支: ' + repo.branch);
|
|
128
210
|
lines.push('- 未提交改动: ' + (repo.dirtyFiles.length ? repo.dirtyFiles.map((f) => `\`${f}\``).join(', ') : '无'));
|
|
211
|
+
if (c.greenfield === true)
|
|
212
|
+
lines.push('- greenfield: true(全新项目,D 阶段 git init 建仓前置)');
|
|
129
213
|
lines.push('', '### 文件基线', '', '| 路径 | 期望 | 备注 |', '| --- | --- | --- |');
|
|
130
214
|
for (const f of files)
|
|
131
215
|
lines.push(`| ${f.path} | ${f.expected} | ${f.note ?? '-'} |`);
|
|
216
|
+
lines.push('## 需求来源', '');
|
|
217
|
+
for (const s of c.sources) {
|
|
218
|
+
lines.push(`- [${s.type}] ${s.url || '(无来源)'}${s.note ? ` — ${s.note}` : ''}`);
|
|
219
|
+
}
|
|
220
|
+
lines.push('');
|
|
132
221
|
lines.push('', '## 澄清问答', '');
|
|
133
222
|
clarifications.forEach((qa, i) => {
|
|
134
223
|
lines.push(`### Q${i + 1}. ${qa.q}`, `- **A**: ${qa.a}`, '');
|
|
@@ -151,5 +240,113 @@ export function formatChecklistBody(c) {
|
|
|
151
240
|
}
|
|
152
241
|
if (doubts.length === 0)
|
|
153
242
|
lines.push('(无)');
|
|
243
|
+
lines.push('', '## PRD 采集记录', '');
|
|
244
|
+
const prd = c.prdCollection ?? [];
|
|
245
|
+
if (prd.length === 0) {
|
|
246
|
+
lines.push('(用户未提供 PRD 链接)');
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
for (const p of prd) {
|
|
250
|
+
lines.push(`- ${p.url} — ${p.status}${p.platform ? `(${p.platform})` : ''}${p.degraded ? ' — degraded(截图超限或写入失败,已降级为文字描述)' : ''}`);
|
|
251
|
+
lines.push(` - 摘要: ${p.summary}`);
|
|
252
|
+
if (p.status === 'blocked' && p.blockedReason)
|
|
253
|
+
lines.push(` - 阻塞原因: ${p.blockedReason}`);
|
|
254
|
+
for (const pg of p.pages ?? [])
|
|
255
|
+
lines.push(` - 原文页: ${pg}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
lines.push('', '## 占位策略', '');
|
|
259
|
+
const phs = c.placeholders ?? [];
|
|
260
|
+
if (phs.length === 0) {
|
|
261
|
+
lines.push('(无后端未就绪项)');
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
lines.push('| 占位对象 | 占位值 | 替换方式 |', '| --- | --- | --- |');
|
|
265
|
+
for (const p of phs)
|
|
266
|
+
lines.push(`| ${p.target} | ${p.value} | ${p.replace} |`);
|
|
267
|
+
}
|
|
268
|
+
// 机读段(2026-09-21 恢复流程矫正):页正文是人读 markdown(有损),重启恢复若让 LLM 读页重建
|
|
269
|
+
// 再回存 = 有损再创作 + 双重编码风险(销服一体清单 7 轮失败即发生在恢复场景)。页尾内嵌无损
|
|
270
|
+
// JSON 单行段,/openspec: 路由2 直接提取灌内存建链,LLM 零参与;HTML 注释对人读零干扰。
|
|
271
|
+
const machine = JSON.stringify({ requirementName: c.requirementName ?? null, spec: c.spec, manifest: c.manifest, clarifications: c.clarifications, doubts: c.doubts, risks: c.risks ?? [], sources: c.sources, prdCollection: c.prdCollection ?? [], placeholders: c.placeholders ?? [], greenfield: c.greenfield ?? false });
|
|
272
|
+
lines.push('', `<!-- dsh-swarm:checklist-json ${machine} -->`);
|
|
154
273
|
return lines.join('\n').replace(/\n{3,}/g, '\n\n');
|
|
155
274
|
}
|
|
275
|
+
const CHECKLIST_JSON_MARK = '<!-- dsh-swarm:checklist-json ';
|
|
276
|
+
/** 从清单页原文提取机读 PlanningChecklist(无损恢复路径):无段/损坏/校验不过一律返回 null,
|
|
277
|
+
* 由调用方回退 legacy LLM 重建流程。domain 纯函数,无副作用。 */
|
|
278
|
+
export function extractChecklistJson(pageMd) {
|
|
279
|
+
const start = pageMd.indexOf(CHECKLIST_JSON_MARK);
|
|
280
|
+
if (start < 0)
|
|
281
|
+
return null;
|
|
282
|
+
const jsonStart = start + CHECKLIST_JSON_MARK.length;
|
|
283
|
+
const end = pageMd.indexOf('-->', jsonStart);
|
|
284
|
+
if (end < 0)
|
|
285
|
+
return null;
|
|
286
|
+
let raw;
|
|
287
|
+
try {
|
|
288
|
+
raw = JSON.parse(pageMd.slice(jsonStart, end).trim());
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
if (validatePlanningChecklist(raw).length > 0)
|
|
294
|
+
return null;
|
|
295
|
+
return raw;
|
|
296
|
+
}
|
|
297
|
+
/** PRD 链接域名路由:已知平台直接判定,其余返回 null 交 LLM 自判。 */
|
|
298
|
+
export function routePrdPlatform(url) {
|
|
299
|
+
let host;
|
|
300
|
+
try {
|
|
301
|
+
host = new URL(url).hostname.toLowerCase();
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
if (host === 'feishu.cn' || host.endsWith('.feishu.cn'))
|
|
307
|
+
return 'feishu';
|
|
308
|
+
if (host === 'doc.weixin.qq.com')
|
|
309
|
+
return 'wecom';
|
|
310
|
+
if (host === 'modao.cc' || host.endsWith('.modao.cc'))
|
|
311
|
+
return 'modao';
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
/** PRD 原文切片:优先按一级标题(# 开头)切章,单片超 maxBytes 按行硬切。
|
|
315
|
+
* 默认 45KB/片(wiki 大文档拆页惯例,低于服务端 body 上限留余量)。 */
|
|
316
|
+
export function slicePrdMarkdown(md, maxBytes = 45_000) {
|
|
317
|
+
const chapters = [];
|
|
318
|
+
let cur = [];
|
|
319
|
+
for (const line of md.split('\n')) {
|
|
320
|
+
if (/^#\s/.test(line) && cur.length > 0) {
|
|
321
|
+
chapters.push(cur.join('\n'));
|
|
322
|
+
cur = [line];
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
cur.push(line);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (cur.length > 0)
|
|
329
|
+
chapters.push(cur.join('\n'));
|
|
330
|
+
const parts = [];
|
|
331
|
+
for (const ch of chapters) {
|
|
332
|
+
if (Buffer.byteLength(ch, 'utf8') <= maxBytes) {
|
|
333
|
+
parts.push(ch);
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
let buf = [];
|
|
337
|
+
let size = 0;
|
|
338
|
+
for (const line of ch.split('\n')) {
|
|
339
|
+
const lb = Buffer.byteLength(line + '\n', 'utf8');
|
|
340
|
+
if (size + lb > maxBytes && buf.length > 0) {
|
|
341
|
+
parts.push(buf.join('\n'));
|
|
342
|
+
buf = [];
|
|
343
|
+
size = 0;
|
|
344
|
+
}
|
|
345
|
+
buf.push(line);
|
|
346
|
+
size += lb;
|
|
347
|
+
}
|
|
348
|
+
if (buf.length > 0)
|
|
349
|
+
parts.push(buf.join('\n'));
|
|
350
|
+
}
|
|
351
|
+
return parts.length > 0 ? parts : [''];
|
|
352
|
+
}
|
|
@@ -52,22 +52,33 @@ export function validatePrefetchManifest(raw) {
|
|
|
52
52
|
errors.push('manifest.files must be an array');
|
|
53
53
|
}
|
|
54
54
|
else {
|
|
55
|
+
// files[] 逐条错误先收集再聚合(2026-09-21:26 条同文错误墙稀释有效信息,模型只见截断前几条)。
|
|
56
|
+
// 完全相同的消息合并为 "<msg> ×N",不同消息保持原样与原序。
|
|
57
|
+
const fileErrors = [];
|
|
55
58
|
for (const f of m['files']) {
|
|
56
59
|
if (typeof f !== 'object' || f === null) {
|
|
57
|
-
|
|
60
|
+
fileErrors.push('manifest.files entry must be an object');
|
|
58
61
|
continue;
|
|
59
62
|
}
|
|
60
63
|
const e = f;
|
|
61
64
|
if (typeof e['path'] !== 'string' || e['path'].trim().length === 0) {
|
|
62
|
-
|
|
65
|
+
fileErrors.push(`manifest.files[].path (got: ${JSON.stringify(e['path'])})`);
|
|
63
66
|
}
|
|
64
67
|
if (typeof e['expected'] !== 'string' || !EXPECTED_VALUES.has(e['expected'])) {
|
|
65
|
-
|
|
68
|
+
fileErrors.push(`manifest.files[].expected (got: ${JSON.stringify(e['expected'])})`);
|
|
66
69
|
}
|
|
67
70
|
if (e['expected'] === 'content-hash' && (typeof e['note'] !== 'string' || e['note'].trim().length === 0)) {
|
|
68
|
-
|
|
71
|
+
fileErrors.push(`manifest.files[].note required for content-hash (got: ${JSON.stringify(e['note'])})`);
|
|
69
72
|
}
|
|
70
73
|
}
|
|
74
|
+
errors.push(...tallyIdentical(fileErrors));
|
|
71
75
|
}
|
|
72
76
|
return errors;
|
|
73
77
|
}
|
|
78
|
+
/** 相同消息聚合计数:["a","a","b"] → ["a ×2","b"]。domain 纯函数,无副作用。 */
|
|
79
|
+
function tallyIdentical(messages) {
|
|
80
|
+
const counts = new Map();
|
|
81
|
+
for (const msg of messages)
|
|
82
|
+
counts.set(msg, (counts.get(msg) ?? 0) + 1);
|
|
83
|
+
return [...counts.entries()].map(([msg, n]) => (n > 1 ? `${msg} ×${n}` : msg));
|
|
84
|
+
}
|
|
@@ -6,3 +6,17 @@ export declare function userPresetsRoot(): string;
|
|
|
6
6
|
* 尽力而为:单 preset 写失败(用户预设根不可写等)仅告警不抛出,
|
|
7
7
|
* 后续 agentPresets.mount('kanban-<role>') 失败由 runner 降级日志兜底。 */
|
|
8
8
|
export declare function installRolePresets(): string[];
|
|
9
|
+
/** 角色 preset 挂载的容器面(dsh-agent-presets roster 的最小结构类型)。 */
|
|
10
|
+
export interface PresetMountLike {
|
|
11
|
+
mount(ctx: unknown, id: string): Promise<unknown>;
|
|
12
|
+
/** 官方重链入口(0.1.2-rc.1 起提供):已绑定 → binding.rebind;未绑定 → bind。 */
|
|
13
|
+
recompose?(ctx: unknown, id: string): Promise<unknown>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* preset 挂载收敛入口(2026-09-21 事故修复):宿主 dsh-scope 的 scope key 绑定在进程
|
|
17
|
+
* 生命周期内一次性(bindScopeParent 重复调用抛 "already bound to a parent"),官方重链
|
|
18
|
+
* 入口是 recompose(已绑 → binding.rebind)。同 agentKey 的二次 setup(P/W/D 卡 blocked/
|
|
19
|
+
* unblock 多轮唤醒 resume 同一 session、live 复用 re-setup 补挂)此前直接二次 mount 必炸。
|
|
20
|
+
* 收敛语义:mount 失败且系 already-bound → recompose 重链;其余错误(组合不可用等)原样上抛。
|
|
21
|
+
*/
|
|
22
|
+
export declare function mountOrRecompose(presets: PresetMountLike, agentCtx: unknown, presetId: string): Promise<void>;
|
|
@@ -51,3 +51,20 @@ export function installRolePresets() {
|
|
|
51
51
|
}
|
|
52
52
|
return installed;
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* preset 挂载收敛入口(2026-09-21 事故修复):宿主 dsh-scope 的 scope key 绑定在进程
|
|
56
|
+
* 生命周期内一次性(bindScopeParent 重复调用抛 "already bound to a parent"),官方重链
|
|
57
|
+
* 入口是 recompose(已绑 → binding.rebind)。同 agentKey 的二次 setup(P/W/D 卡 blocked/
|
|
58
|
+
* unblock 多轮唤醒 resume 同一 session、live 复用 re-setup 补挂)此前直接二次 mount 必炸。
|
|
59
|
+
* 收敛语义:mount 失败且系 already-bound → recompose 重链;其余错误(组合不可用等)原样上抛。
|
|
60
|
+
*/
|
|
61
|
+
export async function mountOrRecompose(presets, agentCtx, presetId) {
|
|
62
|
+
try {
|
|
63
|
+
await presets.mount(agentCtx, presetId);
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
if (!String(err).includes('already bound to a parent') || typeof presets.recompose !== 'function')
|
|
67
|
+
throw err;
|
|
68
|
+
await presets.recompose(agentCtx, presetId);
|
|
69
|
+
}
|
|
70
|
+
}
|
package/lib/roles/toolsets.d.ts
CHANGED
|
@@ -110,7 +110,7 @@ export declare function buildSwarmSessionGuard(deps?: SubagentGuardDeps): (execu
|
|
|
110
110
|
/** wiki_write 会话级放行 preset 默认白名单:与 config.ts wikiWritePresets 默认值同源,
|
|
111
111
|
* 仅供 guard deps 未接线(测试/裸调用)时兜底;生产经 dispatcher 注入热读配置。
|
|
112
112
|
* 'ptc':普通(PTC 模式)会话亦可写 wiki 命名空间——写面本身由 wiki_write 工具内的
|
|
113
|
-
*
|
|
113
|
+
* 六类命名空间白名单硬约束;2026-09-17 起 preset 判定走统一读面(session-preset.ts),
|
|
114
114
|
* 'ptc' 不再是「蜂群会话被误读成 ptc」的兜底,而是显式放行普通会话。 */
|
|
115
115
|
export declare const DEFAULT_WIKI_WRITE_PRESETS: ReadonlyArray<string>;
|
|
116
116
|
export interface StandaloneDtGuardDeps {
|
package/lib/roles/toolsets.js
CHANGED
|
@@ -405,8 +405,10 @@ export async function installRoleTools(agentCtx, role, deps) {
|
|
|
405
405
|
registry.register(tool);
|
|
406
406
|
}
|
|
407
407
|
}
|
|
408
|
-
// ocr_review:ocr CLI 三子命令(preview/rule/managed);未安装/托管未配置在 execute
|
|
409
|
-
|
|
408
|
+
// ocr_review:ocr CLI 三子命令(preview/rule/managed);未安装/托管未配置在 execute 内降级引导,注册无条件。
|
|
409
|
+
// 目标仓库由 execute 内解析(repo 参数 → exec.agent.session.header.cwd)——不注入 cwd,
|
|
410
|
+
// 避免回退到插件进程目录(非仓库,ocr 必报 not a git repository)。
|
|
411
|
+
registry.register(buildOcrReviewTool({}));
|
|
410
412
|
}
|
|
411
413
|
else if (role === 'v') {
|
|
412
414
|
for (const tool of buildSpecCardTools(deps.kanban, caller)) {
|
|
@@ -595,7 +597,7 @@ function dualVerbGitDenyReason(verb, rest, deny) {
|
|
|
595
597
|
/** wiki_write 会话级放行 preset 默认白名单:与 config.ts wikiWritePresets 默认值同源,
|
|
596
598
|
* 仅供 guard deps 未接线(测试/裸调用)时兜底;生产经 dispatcher 注入热读配置。
|
|
597
599
|
* 'ptc':普通(PTC 模式)会话亦可写 wiki 命名空间——写面本身由 wiki_write 工具内的
|
|
598
|
-
*
|
|
600
|
+
* 六类命名空间白名单硬约束;2026-09-17 起 preset 判定走统一读面(session-preset.ts),
|
|
599
601
|
* 'ptc' 不再是「蜂群会话被误读成 ptc」的兜底,而是显式放行普通会话。 */
|
|
600
602
|
export const DEFAULT_WIKI_WRITE_PRESETS = ['swarm', 'kanban-w', 'ptc'];
|
|
601
603
|
export function buildStandaloneDtGuard(deps = {}) {
|
|
@@ -665,7 +667,7 @@ export function buildStandaloneDtGuard(deps = {}) {
|
|
|
665
667
|
if (isRoleComposed(execution?.agent) || isDtChainSubagent)
|
|
666
668
|
return undefined;
|
|
667
669
|
// 放宽(2026-09-16):swarm 主会话与 W preset 会话可写 wiki——can('wiki-write') 本就含 w
|
|
668
|
-
// (permissions.ts),写面由 wiki_write 工具内 assertAllowedWikiPagePath
|
|
670
|
+
// (permissions.ts),写面由 wiki_write 工具内 assertAllowedWikiPagePath 六类命名空间
|
|
669
671
|
// 白名单硬约束;local 模式 wiki_write 未全局注册,无工具面即无暴露面。
|
|
670
672
|
// 放行面配置化(硬规则 6):白名单经 deps 热读 config.wikiWritePresets,宿主 preset
|
|
671
673
|
// 体系演化/部署自定义模式 id 时改配置即可;config 缺字段回 [](fail-closed,仅内置默认兜底测试)。
|
|
@@ -694,7 +696,7 @@ export function registerStandaloneReviewerTools(ctx, configProvider) {
|
|
|
694
696
|
const registry = ctx.get('tools');
|
|
695
697
|
if (!registry)
|
|
696
698
|
return; // 测试裸 Context 无 tools 服务,跳过注册
|
|
697
|
-
registry.register(buildOcrReviewTool({
|
|
699
|
+
registry.register(buildOcrReviewTool({})); // 目标仓库同链上路径:repo 参数 → 会话工作目录(见 ocr-review-tools)
|
|
698
700
|
const kbMode = configProvider.mode;
|
|
699
701
|
const wiki = (kbMode === 'local'
|
|
700
702
|
? new LocalWikiClient(ensureLocalKbRoot())
|
|
@@ -3,7 +3,8 @@ import type { SpecCard, SpecCardAttachment } from '../domain/types.js';
|
|
|
3
3
|
import { type PrefixRoutes } from '../config.js';
|
|
4
4
|
/** 阶段 0 规划引导:命令串从 config 派生,/openspec: 改名时文案自动跟随。
|
|
5
5
|
* 提问节奏与 skills/grill-me/SKILL.md 对齐:官方 frontier 分轮制(整批问 + 推荐答案 + 答复后解锁下一轮);
|
|
6
|
-
*
|
|
6
|
+
* 需求澄清十规则:来源必挂 + PRD 采集 + 范围边界 + 人话 + 仓库事实/缺口暂停 + 收敛录入铁律(含决策可追溯);
|
|
7
|
+
* 收敛产物仍为 planning_checklist_save(KB 清单是建链数据源)。 */
|
|
7
8
|
export declare function buildPlanningGuidance(routes: PrefixRoutes, opts?: {
|
|
8
9
|
swarm?: boolean;
|
|
9
10
|
}): string;
|
|
@@ -3,19 +3,22 @@ import { parsePrefix } from './prefix-router.js';
|
|
|
3
3
|
import { DEFAULT_PREFIX_ROUTES } from '../config.js';
|
|
4
4
|
/** 阶段 0 规划引导:命令串从 config 派生,/openspec: 改名时文案自动跟随。
|
|
5
5
|
* 提问节奏与 skills/grill-me/SKILL.md 对齐:官方 frontier 分轮制(整批问 + 推荐答案 + 答复后解锁下一轮);
|
|
6
|
-
*
|
|
6
|
+
* 需求澄清十规则:来源必挂 + PRD 采集 + 范围边界 + 人话 + 仓库事实/缺口暂停 + 收敛录入铁律(含决策可追溯);
|
|
7
|
+
* 收敛产物仍为 planning_checklist_save(KB 清单是建链数据源)。 */
|
|
7
8
|
export function buildPlanningGuidance(routes, opts) {
|
|
8
|
-
// 第
|
|
9
|
+
// 第 8 条按形态分叉:前缀 = 教学旧命令收尾(收尾句不变);swarm = 确认闸语义(不提旧命令,与 KANBAN_HANDOFF_RULE swarm 确认闸对齐)。
|
|
9
10
|
const finish = opts?.swarm
|
|
10
|
-
? `
|
|
11
|
-
: `
|
|
11
|
+
? `8. 收尾(确认闸):清单落库后向用户征求确认;仅当用户回复含明确肯定语义(确认/开干/开跑/开始/go 等)才调 kanban_route{intent:'openspec'} 建链开跑;模糊回复视为未确认,继续澄清。`
|
|
12
|
+
: `8. 收尾:提醒用户以 ${routes.openspec} 确认执行结束规划阶段——${routes.openspec} 会从清单建链并自动串行执行。`;
|
|
12
13
|
return `
|
|
13
|
-
# 阶段 0 规划对话(
|
|
14
|
-
1.
|
|
15
|
-
2.
|
|
16
|
-
3.
|
|
17
|
-
4.
|
|
18
|
-
5.
|
|
14
|
+
# 阶段 0 规划对话(v5:grill-me frontier 分轮 + 需求澄清十规则)
|
|
15
|
+
1. 需求来源必挂:起手记录需求来源(TAPD/Jira/其他链接);确无来源也要显式记录「无来源+原因」,不许空。同时判定是否绿地(目标目录不存在或尚无 .git):是 → 清单声明 greenfield:true(D 阶段将 git init 建仓),否则不声明。
|
|
16
|
+
2. PRD 链接必采集:用户给了 PRD/需求文档链接,必须先调 planning_prd_collect 逐条采集(一条链接一次调用,多条可并行),采集完成前不得进入 grill-me 提问。采集失败(blocked)立即停下,把工具返回的 guidance 按类转告用户:登录态→请用户自行登录后重采;缺技能→引导安装(带登录态浏览器 huashu-chrome / CDP 兜底 web-access / 企微 wecom-docs / 飞书 lark);404→请用户提供有效链接。用户解决后继续未完成的采集。禁止盲猜页面内容、禁止跳过、禁止自行探索浪费时间。
|
|
17
|
+
3. 范围边界:起手明确「范围内/范围外」清单,尤其需求文档中标注多但用户未点名的部分,逐条确认纳入与否。
|
|
18
|
+
4. 需求澄清(grill-me,frontier 分轮):把计划映射为设计决策树——每轮只问当前前提已就绪的问题(frontier),整批问出:编号 + 每问附推荐答案;等用户全部答复后重算 frontier 再发下一轮。先澄清目的、约束、成功标准,逐支拷问直至用户"没有任何疑问"。
|
|
19
|
+
5. 人话硬规则:问题正文与选项必须通俗;术语避不开时首次出现括号一句话解释;发问前自检「不懂本仓库的人能直接回答吗」,不能就重写。
|
|
20
|
+
6. 仓库事实(planning_prefetch)与缺口暂停:事实自查是规划者的职责——涉筛选字段/列表字段/取数 hook/组件选型,先调只读子代理查证仓库(存在/可复用/复用+转化),推荐答案言出有据,不拿可自查的问题问用户;澄清中发现信息缺口 → 停止提问 → 先调 planning_prefetch 补齐 → 再继续,不边猜边问。每功能点先查仓库同类实现,可复用优先复用。
|
|
21
|
+
7. 收敛(planning_checklist_save):把结论写成结构化需求澄清清单存入 KB(KB 不可达自动兜底临时目录)。录入铁律:每条问答含完整决策正文(字段名/接口路径/枚举值/组件名/位置规则),禁止「按推荐」「同上」等指代不明缩写(代码硬闸会拒收);sources(需求来源)/ prdCollection(每条 PRD 链接的采集状态)必填;后端未就绪项统一登记 placeholders(占位对象/占位值/替换方式),集中声明避免逐点重复讨论。每条决策可追溯(问答编号/仓库源码路径/接口文档位置至少其一);原始文档值与确认决策冲突处必须标【已调整】/【已确认】,未确认值不录或显式标「待确认」——无来源记录的决策会被视为 agent 自行默认。
|
|
19
22
|
${finish}
|
|
20
23
|
护栏:规划期只读仓库,禁止任何 git/源码写入;只写 KB 与临时目录。
|
|
21
24
|
`;
|
|
@@ -23,6 +23,11 @@ export declare function runOcr(args: string[], opts: {
|
|
|
23
23
|
stderr: string;
|
|
24
24
|
error?: string;
|
|
25
25
|
}>;
|
|
26
|
+
/** 托管就绪探测:provider 为 dsh-managed 且模型非空。
|
|
27
|
+
* 踩坑:`ocr config set model <m>` 的落盘位置随当前 provider 类型分流——内置 provider 写顶层
|
|
28
|
+
* `model`,自定义 provider(本插件的 dsh-managed)写 `custom_providers.dsh-managed.model`
|
|
29
|
+
* (沙箱 HOME 实测)。只读顶层 model 会把已接好的托管配置判成未就绪,工具层随即返回
|
|
30
|
+
* 「托管模式未配置 LLM」而永不真正调用 `ocr review`(两处都算就绪)。 */
|
|
26
31
|
export declare function managedProviderReady(deps?: OcrCliDeps): boolean;
|
|
27
32
|
export declare function wireManagedProvider(a: {
|
|
28
33
|
baseUrl: string;
|
package/lib/services/ocr-cli.js
CHANGED
|
@@ -99,11 +99,20 @@ export async function runOcr(args, opts, deps = {}) {
|
|
|
99
99
|
};
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
|
+
/** 托管就绪探测:provider 为 dsh-managed 且模型非空。
|
|
103
|
+
* 踩坑:`ocr config set model <m>` 的落盘位置随当前 provider 类型分流——内置 provider 写顶层
|
|
104
|
+
* `model`,自定义 provider(本插件的 dsh-managed)写 `custom_providers.dsh-managed.model`
|
|
105
|
+
* (沙箱 HOME 实测)。只读顶层 model 会把已接好的托管配置判成未就绪,工具层随即返回
|
|
106
|
+
* 「托管模式未配置 LLM」而永不真正调用 `ocr review`(两处都算就绪)。 */
|
|
102
107
|
export function managedProviderReady(deps = {}) {
|
|
103
108
|
try {
|
|
104
109
|
const home = deps.homedirFn ? deps.homedirFn() : homedir();
|
|
105
110
|
const cfg = JSON.parse(readFileSync(join(home, '.opencodereview', 'config.json'), 'utf8'));
|
|
106
|
-
|
|
111
|
+
if (cfg.provider !== OCR_MANAGED_PROVIDER_NAME)
|
|
112
|
+
return false;
|
|
113
|
+
const top = typeof cfg.model === 'string' ? cfg.model : '';
|
|
114
|
+
const nested = cfg.custom_providers?.[OCR_MANAGED_PROVIDER_NAME]?.model;
|
|
115
|
+
return (top || (typeof nested === 'string' ? nested : '')).length > 0;
|
|
107
116
|
}
|
|
108
117
|
catch {
|
|
109
118
|
return false; // 文件不存在/解析失败/字段缺失一律视为未就绪
|
|
@@ -93,12 +93,20 @@ export function buildKanbanTools(service, getCaller) {
|
|
|
93
93
|
parameters: {
|
|
94
94
|
taskId: { type: 'string', required: true },
|
|
95
95
|
summary: { type: 'string', required: true, description: 'Human-readable completion summary' },
|
|
96
|
-
metadata
|
|
96
|
+
// metadata 用官方 object schema(2026-09-21:{type:'json'} 不施加约束,模型双重编码字符串静默
|
|
97
|
+
// 穿透到 delivery 闸才爆出误导性 "delivery required"——open object = 必须对象、键值任意,字符串
|
|
98
|
+
// 由运行时 ToolArgsError "must be an object" 单轮拦截。additionalProperties 必填(DSL 硬规则)。
|
|
99
|
+
metadata: { type: 'object', additionalProperties: true, description: 'Machine-readable handoff: changed_files/verification/kb_url... W3/kb may add report = { requirement, status, branch, tasks: [{ text, done }], acceptance, verification, leftovers?, todos? } (delivery-report fields; tasks mirrors the OpenSpec plan checklist openspec/changes/<id>/tasks.md verbatim, done = checked). Pass the object itself — never a JSON-encoded string.' },
|
|
97
100
|
},
|
|
98
101
|
output: { schema: { type: 'json' }, render: (_a, v) => [{ type: 'text', text: JSON.stringify(v) }] },
|
|
99
102
|
async execute(args) {
|
|
100
103
|
const caller = getCaller();
|
|
101
|
-
|
|
104
|
+
// 运行时诚实检查(纵深):schema 已强制 object,此处拦截直调/回归路径——绝不静默强转字符串
|
|
105
|
+
const meta = args.metadata;
|
|
106
|
+
if (meta !== undefined && (typeof meta !== 'object' || Array.isArray(meta))) {
|
|
107
|
+
throw new Error(`metadata must be a JSON object (got ${Array.isArray(meta) ? 'array' : typeof meta}) — pass the object itself, never a JSON-encoded string`);
|
|
108
|
+
}
|
|
109
|
+
const done = await service.completeTask(args.taskId, { summary: args.summary, metadata: (meta ?? {}), completedAt: Date.now() }, caller.actor, { boundTaskId: caller.boundTaskId });
|
|
102
110
|
return done;
|
|
103
111
|
},
|
|
104
112
|
}),
|
|
@@ -2,7 +2,7 @@ import type { Context } from '@deepseek-ai/cordis';
|
|
|
2
2
|
import type { PrefixRoutes } from '../config.js';
|
|
3
3
|
import type { ConfigProvider } from '../services/config-provider.js';
|
|
4
4
|
import { type PlanningToolDeps } from './planning-tools.js';
|
|
5
|
-
import type
|
|
5
|
+
import { type PlanningChecklist } from '../domain/planning-checklist.js';
|
|
6
6
|
/** v2 规划上下文(/plan: 捕获 → planning_checklist_save 回写 → /openspec: 建链)。模块级内存,随插件进程存活。 */
|
|
7
7
|
export interface PlanningContext {
|
|
8
8
|
workspaceDir: string | null;
|
|
@@ -33,6 +33,7 @@ export declare function buildOpenspecNarrationRule(r: {
|
|
|
33
33
|
};
|
|
34
34
|
}): string;
|
|
35
35
|
export declare function buildSpawnPrefetch(ctx: Context): PlanningToolDeps['spawnPrefetch'] | undefined;
|
|
36
|
+
export declare function buildSpawnPrdCollect(ctx: Context, listSkills?: () => string[]): PlanningToolDeps['spawnPrdCollect'] | undefined;
|
|
36
37
|
/** v2 主会话工具面:/plan: 捕获规划上下文(零副作用)→ planning_checklist_save 回写 → /openspec: 建链。
|
|
37
38
|
* 工具面 = kanban_route + 只读 kanban 子集 + spec_card_view + planning 工具;
|
|
38
39
|
* 无 spec_card_edit/approve、无 kanban_create/complete/block(主会话越权写由工具面裁剪 + prefetch 子代理只读护栏双保险)。 */
|