@mingmaple/dsh-handover 0.9.14 → 1.0.0
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 +123 -92
- package/docs/migrate.md +52 -41
- package/docs/releases/0.9.12.md +2 -3
- package/docs/releases/0.9.14.md +1 -5
- package/docs/releases/0.9.22.md +34 -0
- package/docs/releases/1.0.0.md +33 -0
- package/docs/troubleshoot.md +230 -46
- package/package.json +74 -71
- package/scripts/behavior.mjs +398 -0
- package/scripts/blank-dsh.mjs +41 -25
- package/scripts/blank-smoke.mjs +278 -109
- package/scripts/check-docs.mjs +7 -0
- package/scripts/e2e-migration.mjs +2 -2
- package/scripts/office.mjs +3 -3
- package/scripts/portability.mjs +71 -0
- package/scripts/style-gate.mjs +571 -0
- package/src/client.js +1028 -955
- package/src/index.mjs +313 -142
- package/src/lib/absrefs.mjs +1 -1
- package/src/lib/api.mjs +18 -10
- package/src/lib/archive.mjs +1 -1
- package/src/lib/brief.mjs +8 -6
- package/src/lib/config.mjs +23 -8
- package/src/lib/consent.mjs +54 -1
- package/src/lib/docmap.mjs +140 -0
- package/src/lib/drift.mjs +1 -1
- package/src/lib/events.mjs +1 -1
- package/src/lib/facts.mjs +19 -12
- package/src/lib/gates.mjs +30 -6
- package/src/lib/global-task.mjs +69 -0
- package/src/lib/guide.mjs +81 -85
- package/src/lib/handover-io.mjs +69 -6
- package/src/lib/identity.mjs +330 -0
- package/src/lib/legacy.mjs +89 -0
- package/src/lib/lint.mjs +17 -13
- package/src/lib/manual.mjs +135 -59
- package/src/lib/mover.mjs +23 -19
- package/src/lib/names.mjs +14 -19
- package/src/lib/office.mjs +63 -15
- package/src/lib/pack-ops.mjs +11 -11
- package/src/lib/pathkey.mjs +24 -0
- package/src/lib/peer.mjs +4 -4
- package/src/lib/phase.mjs +8 -13
- package/src/lib/profile.mjs +1 -1
- package/src/lib/reminder.mjs +110 -0
- package/src/lib/role.mjs +9 -6
- package/src/lib/scaffold.mjs +68 -26
- package/src/lib/schedule.mjs +5 -6
- package/src/lib/session-create.mjs +3 -3
- package/src/lib/session-ops.mjs +80 -51
- package/src/lib/sessions.mjs +41 -3
- package/src/lib/sevenzip.mjs +10 -1
- package/src/lib/status.mjs +337 -176
- package/src/lib/succession.mjs +16 -16
- package/src/lib/system.mjs +364 -0
- package/src/lib/zstd-log.mjs +1 -1
- package/src/selftest.mjs +6 -6
- package/src/templates/Changelog.skeleton.md +3 -3
- package/src/templates/Charter.skeleton.md +13 -9
- package/src/templates/Handover.global.skeleton.md +20 -13
- package/src/templates/Handover.template.md +89 -0
- package/src/templates/Plan.skeleton.md +34 -0
- package/src/templates/Prompt_Templates.md +101 -20
- package/src/templates/Runtime_Manual.md +263 -122
- package/src/templates/System.skeleton.md +31 -0
- package/src/templates/Systems.global.skeleton.md +13 -0
- package/src/templates/rules/Delegation_Rules.md +45 -0
- package/src/templates/rules/Handover_Rules.md +46 -23
- package/src/templates/rules/Migration_Rules.md +7 -7
- package/src/templates/rules/Misc_Task_Standard.md +17 -10
- package/src/templates/rules/Writing_Rules.md +33 -16
- package/src/templates/standards/README.md +37 -0
- package/src/templates/standards//345/206/231/344/275/234/350/247/204/350/214/203/README.md +120 -0
- package/src/templates/standards//345/206/231/344/275/234/350/247/204/350/214/203//344/276/235/346/215/256_/350/256/272/346/226/207.md +389 -0
- package/src/tools/ho_backup.mjs +135 -0
- package/src/tools/ho_changelog.mjs +4 -4
- package/src/tools/ho_charter.mjs +7 -6
- package/src/tools/ho_checkpoint.mjs +36 -32
- package/src/tools/ho_docmap.mjs +239 -0
- package/src/tools/ho_gate.mjs +11 -5
- package/src/tools/ho_handover.mjs +61 -35
- package/src/tools/ho_office.mjs +46 -5
- package/src/tools/ho_profile.mjs +3 -3
- package/src/tools/ho_sessions.mjs +46 -26
- package/src/tools/ho_status.mjs +251 -0
- package/src/tools/ho_succeed.mjs +3 -3
- package/src/tools/ho_workspace.mjs +146 -42
- package/src/tools/peer.mjs +416 -0
- package/src/tools/ho_brief.mjs +0 -57
- package/src/tools/ho_diag.mjs +0 -87
- package/src/tools/ho_drift.mjs +0 -41
- package/src/tools/ho_init.mjs +0 -82
- package/src/tools/ho_pack.mjs +0 -38
- package/src/tools/ho_phase.mjs +0 -37
- package/src/tools/ho_snapshot.mjs +0 -104
- package/src/tools/ho_tokens.mjs +0 -57
- package/src/tools/peer_list.mjs +0 -37
- package/src/tools/peer_new.mjs +0 -75
- package/src/tools/peer_peek.mjs +0 -41
- package/src/tools/peer_rename.mjs +0 -31
- package/src/tools/peer_send.mjs +0 -52
- package/src/tools/peer_status.mjs +0 -46
package/scripts/blank-smoke.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 空白环境冒烟:不依赖任何真实工作区,验「装上就能跑」。
|
|
3
|
-
* 1) 宿主半在假 ctx 上 apply:
|
|
3
|
+
* 1) 宿主半在假 ctx 上 apply:14 个工具、手册段、状态路由(200/404/405/HEAD)、只读工具不抛
|
|
4
4
|
* 2) 浏览器半在假 window/React 上装载:bundle 外形、settings.section 注册、成体视图能渲染
|
|
5
5
|
* 用法:node scripts/blank-smoke.mjs [--dist <dist 目录>]
|
|
6
6
|
*/
|
|
@@ -26,7 +26,23 @@ process.env.HOME = BLANK;
|
|
|
26
26
|
const { apply, schedDepsFor } = await import(pathToFileURL(path.join(dist, 'index.mjs')).href);
|
|
27
27
|
const reg = { tools: [], sections: [], logs: [], listeners: {} };
|
|
28
28
|
const ctx = {
|
|
29
|
-
tools: {
|
|
29
|
+
tools: {
|
|
30
|
+
register: (t, o) => { reg.tools.push(t); (reg.toolOpts = reg.toolOpts || []).push(o); },
|
|
31
|
+
restrict() { },
|
|
32
|
+
resolve: () => ({}),
|
|
33
|
+
// 宿主允许注册多个守卫:按注册顺序依次判,任意一个拒绝即拒绝。
|
|
34
|
+
guard: (fn) => {
|
|
35
|
+
reg.guardFns = (reg.guardFns || []).concat([fn]);
|
|
36
|
+
reg.guardFn = (exec) => {
|
|
37
|
+
for (const g of reg.guardFns) {
|
|
38
|
+
let r;
|
|
39
|
+
try { r = g(exec); } catch (e) { return String(e && e.message ? e.message : e); }
|
|
40
|
+
if (r) return r;
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
},
|
|
30
46
|
systemPrompt: { section: (s) => { reg.sections.push(s); return () => { reg.disposed = (reg.disposed || 0) + 1; }; } },
|
|
31
47
|
sessionController: {
|
|
32
48
|
create: async () => ({ sessionId: 'session-deploy-test' }),
|
|
@@ -42,7 +58,7 @@ const ctx = {
|
|
|
42
58
|
};
|
|
43
59
|
apply(ctx, {});
|
|
44
60
|
|
|
45
|
-
ok(reg.tools.length ===
|
|
61
|
+
ok(reg.tools.length === 14, '工具数应为 14', String(reg.tools.length));
|
|
46
62
|
ok(reg.sections.length === 0, '未开启全局前置提示词时不得注册提示段', String(reg.sections.length));
|
|
47
63
|
ok(reg.tools.filter((t) => !t || !t.name || !t.description || !t.execute).length === 0, '每个工具都要有 name/description/execute');
|
|
48
64
|
// 上下文成本实测:工具描述是每次请求都要付的固定开销。参考值只用于展示,不作为通过条件。
|
|
@@ -52,11 +68,11 @@ const budget = await import(pathToFileURL(path.join(dist, 'lib', 'facts.mjs')).h
|
|
|
52
68
|
const widest = rows.slice().sort((a, b) => b.n - a.n)[0];
|
|
53
69
|
console.log('工具描述 token 实测:最宽 ' + widest.name + ' ' + widest.n + '(参考值 ' + budget.TOKEN_BUDGET.toolDescTokens + ',不判失败)');
|
|
54
70
|
}
|
|
55
|
-
ok(reg.tools.filter((t) => t.name.startsWith('ho_')).length ===
|
|
56
|
-
ok(reg.tools.filter((t) => t.name
|
|
71
|
+
ok(reg.tools.filter((t) => t.name.startsWith('ho_')).length === 13, 'ho_* 应为 13 个', String(reg.tools.filter((t) => t.name.startsWith('ho_')).length));
|
|
72
|
+
ok(reg.tools.filter((t) => t.name === 'peer').length === 1, '平级会话工具应合并为一个 peer');
|
|
57
73
|
// 每步条件启用:内核 0.2.2 起工具按「步」解析,子会话在解析工具表这一步就要被排除,
|
|
58
|
-
// 否则它的首轮提示词会带着
|
|
59
|
-
ok((reg.toolOpts || []).length ===
|
|
74
|
+
// 否则它的首轮提示词会带着 14 个用不了的 schema(每步白付约 1.5k token)。
|
|
75
|
+
ok((reg.toolOpts || []).length === 14 && (reg.toolOpts || []).every((o) => o && typeof o.when === 'function'), '14 个工具都应带每步启用条件', String((reg.toolOpts || []).length));
|
|
60
76
|
|
|
61
77
|
/** 取某个工具的每步启用条件(0.2.2 起工具按步解析)。 */
|
|
62
78
|
const gateFor = (name) => {
|
|
@@ -66,7 +82,7 @@ const gateFor = (name) => {
|
|
|
66
82
|
|
|
67
83
|
{
|
|
68
84
|
// 每步条件同时承担子会话排除与角色过滤:调用方每步重算工具表,换角色下一轮即生效。
|
|
69
|
-
const gate = gateFor('
|
|
85
|
+
const gate = gateFor('ho_status');
|
|
70
86
|
ok(typeof gate === 'function', '每步启用条件应可调用');
|
|
71
87
|
if (typeof gate === 'function') {
|
|
72
88
|
ok(gate({}) === true, '没有 agent 的调用应放行(命令行与演练路径)');
|
|
@@ -75,10 +91,10 @@ const gateFor = (name) => {
|
|
|
75
91
|
ok(gate({ agent: { session: { header: { id: 'session-plain', cwd: path.join(BLANK, 'nowhere') } } } }) === false, '未登记角色的会话按普通处理(0 个插件工具)');
|
|
76
92
|
const roleMod = await import(pathToFileURL(path.join(dist, 'lib', 'role.mjs')).href);
|
|
77
93
|
roleMod.writeRole({}, 'session-boss', { role: 'global' });
|
|
78
|
-
ok(gate({ agent: { session: { header: { id: 'session-boss', cwd: path.join(BLANK, 'nowhere') } } } }) === true, '全局角色应可见
|
|
79
|
-
const gatePlain = gateFor('
|
|
80
|
-
ok(typeof gatePlain === 'function' && gatePlain({ agent: { session: { header: { id: 'session-boss', cwd: path.join(BLANK, 'nowhere') } } } }) === true, '全局角色应可见
|
|
81
|
-
ok(gatePlain({ agent: { session: { header: { id: 'session-plain', cwd: path.join(BLANK, 'nowhere') } } } }) === false, '普通角色不可见
|
|
94
|
+
ok(gate({ agent: { session: { header: { id: 'session-boss', cwd: path.join(BLANK, 'nowhere') } } } }) === true, '全局角色应可见 ho_status');
|
|
95
|
+
const gatePlain = gateFor('ho_office');
|
|
96
|
+
ok(typeof gatePlain === 'function' && gatePlain({ agent: { session: { header: { id: 'session-boss', cwd: path.join(BLANK, 'nowhere') } } } }) === true, '全局角色应可见 ho_office');
|
|
97
|
+
ok(gatePlain({ agent: { session: { header: { id: 'session-plain', cwd: path.join(BLANK, 'nowhere') } } } }) === false, '普通角色不可见 ho_office');
|
|
82
98
|
}
|
|
83
99
|
}
|
|
84
100
|
// 可选注入多了 connection(/api 载体)。两个都必须是**可选**的,否则旧宿主整插件不激活。
|
|
@@ -149,11 +165,11 @@ let payload = null;
|
|
|
149
165
|
try { payload = JSON.parse(r1.body); } catch (e) { fails.push('返回不是 JSON:' + e.message); }
|
|
150
166
|
if (payload) {
|
|
151
167
|
ok(payload.plugin.version && payload.plugin.version !== '?', '应报出版本号', String(payload.plugin.version));
|
|
152
|
-
ok(payload.plugin.tools ===
|
|
168
|
+
ok(payload.plugin.tools === 14, 'payload 工具数应为 14', String(payload.plugin.tools));
|
|
153
169
|
ok(payload.workspace && payload.workspace.found === false, '空白 HOME 下应判定为没找到工作区', JSON.stringify(payload.workspace));
|
|
154
170
|
ok(Array.isArray(payload.notes) && payload.notes.length > 0, '没找到工作区时应给中文提示');
|
|
155
171
|
ok(payload.guide && Array.isArray(payload.guide.situations) && payload.guide.situations.length >= 8, 'payload 应带「怎么用」的时机表');
|
|
156
|
-
ok(payload.guide && payload.guide.groups.reduce((a, g) => a + g.tools.length, 0) ===
|
|
172
|
+
ok(payload.guide && payload.guide.groups.reduce((a, g) => a + g.tools.length, 0) === 14, '工具明细里应列全 14 个工具', payload.guide && String(payload.guide.groups.reduce((a, g) => a + g.tools.length, 0)));
|
|
157
173
|
ok(payload.plugin.initErrors && payload.plugin.initErrors.length === 0, '初始化错误应为空', JSON.stringify(payload.plugin.initErrors));
|
|
158
174
|
}
|
|
159
175
|
ok((await call('GET', '/dsh-handover/nope.json')).code === 404, '未知路径应 404');
|
|
@@ -198,6 +214,23 @@ ok(fs.existsSync(path.join(root, '_Control_Center', 'rules', 'Workspace_Charter.
|
|
|
198
214
|
ok(fs.existsSync(path.join(root, '_Control_Center', 'templates', 'Prompt_Templates.md')), '建全局根:提示词模板应在');
|
|
199
215
|
ok(fs.existsSync(path.join(root, 'Projects')) && fs.existsSync(path.join(root, '_Project_Meta')), '建全局根:项目目录应在');
|
|
200
216
|
|
|
217
|
+
// 通用规范库:init 要铺出目录与说明;既有工作区靠 scaffold 补齐,且不得覆盖已有内容。
|
|
218
|
+
if (root) {
|
|
219
|
+
const standardsDir = path.join(root, '_Control_Center', 'standards');
|
|
220
|
+
const standardsReadme = path.join(standardsDir, 'README.md');
|
|
221
|
+
ok(fs.existsSync(standardsReadme), '建全局根:通用规范库的说明应在', standardsReadme);
|
|
222
|
+
const scafMod = await import(pathToFileURL(path.join(dist, 'lib', 'scaffold.mjs')).href);
|
|
223
|
+
const keep = '# 本库自己的说明' + '\n';
|
|
224
|
+
fs.writeFileSync(standardsReadme, keep, 'utf8');
|
|
225
|
+
fs.rmSync(path.join(root, '_Control_Center', 'tools'), { recursive: true, force: true });
|
|
226
|
+
const sc1 = scafMod.ensureScaffold(root);
|
|
227
|
+
ok(sc1.ok && sc1.created.indexOf('_Control_Center/tools') !== -1, 'scaffold 应补齐缺失的目录', JSON.stringify(sc1.created));
|
|
228
|
+
ok(fs.readFileSync(standardsReadme, 'utf8') === keep, 'scaffold 不得覆盖已有文件');
|
|
229
|
+
const sc2 = scafMod.ensureScaffold(root);
|
|
230
|
+
ok(sc2.ok && sc2.created.length === 0 && sc2.written.length === 0, 'scaffold 二次执行应无改动', JSON.stringify([sc2.created.length, sc2.written.length]));
|
|
231
|
+
ok(scafMod.ensureScaffold(path.join(BLANK, 'nowhere-at-all')).ok === false, 'scaffold 对不存在的根应报错');
|
|
232
|
+
}
|
|
233
|
+
|
|
201
234
|
// cwd 已失效(旧路径被删)时,根解析必须退回登记表,不能在幽灵路径上重建 _Control_Center
|
|
202
235
|
if (root) {
|
|
203
236
|
const regDir0 = path.join(BLANK, '.dsh', 'storages');
|
|
@@ -252,18 +285,18 @@ ok(e3.code === 200 && e3.json && e3.json.ok && e3.json.chars === 0 && e3.json.en
|
|
|
252
285
|
const s4 = await postJson('/dsh-handover/register', { path: root || PARENT });
|
|
253
286
|
ok(s4.code === 501 && s4.json && s4.json.ok === false && String(s4.json.hint || '').indexOf(root || PARENT) !== -1, '无 workspaceRegistry 时应回退给「手工粘贴路径」的提示', JSON.stringify(s4.json));
|
|
254
287
|
|
|
255
|
-
const initTool = reg.tools.find((t) => t.name === '
|
|
256
|
-
ok(!!initTool, '应注册
|
|
288
|
+
const initTool = reg.tools.find((t) => t.name === 'ho_workspace');
|
|
289
|
+
ok(!!initTool, '应注册 ho_workspace(含首次安装 action=init)');
|
|
257
290
|
if (initTool) {
|
|
258
291
|
const PARENT2 = path.join(BLANK, 'mother2');
|
|
259
292
|
fs.mkdirSync(PARENT2, { recursive: true });
|
|
260
|
-
const ri = await initTool.execute({ parent: PARENT2 }, null);
|
|
261
|
-
ok(/已建立全局文件体系/.test(ri.text), '
|
|
262
|
-
const ri2 = await initTool.execute({ parent: PARENT2 }, null);
|
|
263
|
-
ok(/未建立/.test(ri2.text), '
|
|
293
|
+
const ri = await initTool.execute({ action: 'init', parent: PARENT2 }, null);
|
|
294
|
+
ok(/已建立全局文件体系/.test(ri.text), 'ho_workspace(action=init) 应建树并报出路径', ri.text.split('\n')[0]);
|
|
295
|
+
const ri2 = await initTool.execute({ action: 'init', parent: PARENT2 }, null);
|
|
296
|
+
ok(/未建立/.test(ri2.text), 'ho_workspace(action=init) 对非空目标应拒绝且不改动', ri2.text.split('\n')[0]);
|
|
264
297
|
}
|
|
265
298
|
|
|
266
|
-
const readonly = ['
|
|
299
|
+
const readonly = ['ho_status', 'ho_sessions', 'ho_gate', 'ho_charter', 'ho_workspace'];
|
|
267
300
|
for (const t of reg.tools) {
|
|
268
301
|
if (readonly.indexOf(t.name) === -1) continue;
|
|
269
302
|
try {
|
|
@@ -285,7 +318,7 @@ const { resolveConfig } = await import(DIST('lib/config.mjs'));
|
|
|
285
318
|
const WS = path.join(BLANK, 'ws');
|
|
286
319
|
fs.mkdirSync(path.join(WS, '_Control_Center'), { recursive: true });
|
|
287
320
|
fs.mkdirSync(path.join(WS, 'Projects', 'Demo', 'Demo库'), { recursive: true });
|
|
288
|
-
fs.mkdirSync(path.join(WS, '_Misc', 'tasks', '
|
|
321
|
+
fs.mkdirSync(path.join(WS, '_Misc', 'tasks', '示例任务'), { recursive: true });
|
|
289
322
|
|
|
290
323
|
// ---------- 在任复用:工具层前置校验(任何写入之前返回) ----------
|
|
291
324
|
{
|
|
@@ -305,7 +338,7 @@ fs.mkdirSync(path.join(WS, '_Misc', 'tasks', '抢名额'), { recursive: true });
|
|
|
305
338
|
} catch (e) { return String(e && e.message ? e.message : e); }
|
|
306
339
|
};
|
|
307
340
|
const lowErr = await newErr({});
|
|
308
|
-
ok(/已有在任会话 Demo1/.test(lowErr) && /session-hold/.test(lowErr) && /
|
|
341
|
+
ok(/已有在任会话 Demo1/.test(lowErr) && /session-hold/.test(lowErr) && /peer/.test(lowErr) && /force=true/.test(lowErr), '项目类别有低水位在任时应拒绝新建并给出复用建议', lowErr);
|
|
309
342
|
const forcedErr = await newErr({ force: true });
|
|
310
343
|
ok(forcedErr.indexOf('已有在任会话') === -1, 'force=true 应越过复用判据', forcedErr);
|
|
311
344
|
|
|
@@ -337,11 +370,11 @@ const rTitle = resolveRole({ sessionId: 'session-t1', title: '全局9' });
|
|
|
337
370
|
ok(rTitle.role === 'global' && rTitle.source === 'title', '角色判定:标题含「全局」→ global', JSON.stringify(rTitle));
|
|
338
371
|
const rCwd = resolveRole({ sessionId: 'session-t2', title: 'Demo1', cwd: path.join(WS, 'Projects', 'Demo', 'Demo库') });
|
|
339
372
|
ok(rCwd.role === 'project' && rCwd.project === 'Demo', '角色判定:cwd 在 Projects/<名>/ → project', JSON.stringify(rCwd));
|
|
340
|
-
const rMisc = resolveRole({ sessionId: 'session-t3', title: '杂项1', cwd: path.join(WS, '_Misc', 'tasks', '
|
|
341
|
-
ok(rMisc.role === 'misc' && rMisc.task === '
|
|
342
|
-
const rPrompt = resolveRole({ sessionId: 'session-t4', title: '', firstPrompt: '先看 ' + path.join(WS, 'Projects', '
|
|
343
|
-
ok(rPrompt.role === 'project' && rPrompt.project === '
|
|
344
|
-
const rPlain = resolveRole({ sessionId: 'session-t5', title: '
|
|
373
|
+
const rMisc = resolveRole({ sessionId: 'session-t3', title: '杂项1', cwd: path.join(WS, '_Misc', 'tasks', '示例任务') });
|
|
374
|
+
ok(rMisc.role === 'misc' && rMisc.task === '示例任务', '角色判定:cwd 在 _Misc/tasks/<任务>/ → misc', JSON.stringify(rMisc));
|
|
375
|
+
const rPrompt = resolveRole({ sessionId: 'session-t4', title: '', firstPrompt: '先看 ' + path.join(WS, 'Projects', 'Demo2', 'Demo2库') + ' 里的内容' });
|
|
376
|
+
ok(rPrompt.role === 'project' && rPrompt.project === 'Demo2', '角色判定:首条提示里的 Projects/<名> → project', JSON.stringify(rPrompt));
|
|
377
|
+
const rPlain = resolveRole({ sessionId: 'session-t5', title: '未命名', cwd: '/tmp' });
|
|
345
378
|
ok(rPlain.role === 'plain', '角色判定:都不中 → plain(普通会话)', JSON.stringify(rPlain));
|
|
346
379
|
|
|
347
380
|
writeRole({}, 'session-t6', { role: 'misc', task: 'X', source: 'user' });
|
|
@@ -351,7 +384,7 @@ ok(readRoles({}).sessions['session-t6'] && readRoles({}).sessions['session-t6'].
|
|
|
351
384
|
|
|
352
385
|
// 回归锁:首条提示里的 `Projects/**` 通配曾被当成项目名 `**、_Project_Meta`,
|
|
353
386
|
// 于是交接路径、备份目录、归档目录全落在这个 Windows 非法名字上(实测四类报错)。
|
|
354
|
-
const rGlob = resolveRole({ sessionId: 'session-glob-prompt', title: '
|
|
387
|
+
const rGlob = resolveRole({ sessionId: 'session-glob-prompt', title: '普通会话', cwd: 'C:/Users/x/.dsh/profiles/desktop', firstPrompt: '- 动 _Control_Center/**、Projects/**、_Project_Meta/** 前先问用户。' });
|
|
355
388
|
ok(rGlob.role === 'plain', '首条提示里的 Projects/** 通配不得被当成项目名', JSON.stringify(rGlob));
|
|
356
389
|
const hpBad = handoverPathFor(WS, { role: 'project', project: '**、_Project_Meta' });
|
|
357
390
|
ok(hpBad.path === null && /不合法/.test(String(hpBad.reason || '')), '非法项目名不得拼出交接路径', JSON.stringify(hpBad));
|
|
@@ -366,25 +399,84 @@ const hpG = handoverPathFor(WS, { role: 'global' });
|
|
|
366
399
|
ok(/Global_Handover\.md$/.test(hpG.path), '全局交接路径', hpG.path);
|
|
367
400
|
const hpP = handoverPathFor(WS, { role: 'project', project: 'Demo' });
|
|
368
401
|
ok(/_Project_Meta/.test(hpP.path) && /Demo/.test(hpP.path) && /Handover\.md$/.test(hpP.path), '项目交接路径应落 _Project_Meta/<项目>/Handover.md', hpP.path);
|
|
369
|
-
const hpM = handoverPathFor(WS, { role: 'misc', task: '
|
|
370
|
-
ok(/_Misc/.test(hpM.path) &&
|
|
402
|
+
const hpM = handoverPathFor(WS, { role: 'misc', task: '示例任务' });
|
|
403
|
+
ok(/_Misc/.test(hpM.path) && /示例任务/.test(hpM.path) && /Handover\.md$/.test(hpM.path), '杂项交接路径应落 _Misc/tasks/<任务>/Handover.md', hpM.path);
|
|
371
404
|
const hpU = handoverPathFor(WS, { role: 'plain' });
|
|
372
405
|
ok(hpU.path === null && !!hpU.reason, '普通会话不得给出交接路径(不退回全局)', JSON.stringify(hpU));
|
|
373
406
|
|
|
374
407
|
const tsG = toolsetFor('global');
|
|
375
|
-
ok(tsG.allow.length ===
|
|
408
|
+
ok(tsG.allow.length === 14 && tsG.deny.length === 0, '全局应可见全部 14 个工具', JSON.stringify([tsG.allow.length, tsG.deny.length]));
|
|
376
409
|
const tsP = toolsetFor('project');
|
|
377
410
|
ok(tsP.allow.length === 11, '项目角色应可见 11 个工具', String(tsP.allow.length));
|
|
378
411
|
ok(tsP.allow.indexOf('ho_handover') !== -1 && tsP.allow.indexOf('ho_checkpoint') !== -1, '项目角色必须能写自己的交接与压缩');
|
|
379
|
-
ok(tsP.allow.indexOf('
|
|
412
|
+
ok(tsP.allow.indexOf('ho_sessions') === -1 && tsP.allow.indexOf('ho_changelog') === -1, '项目角色不该看见全局管理工具');
|
|
380
413
|
ok(tsP.deny.indexOf('ho_handover') === -1 && tsP.deny.indexOf('run_code') === -1, 'deny 名单不得含保留名或本角色已允许的工具');
|
|
381
414
|
const tsM = toolsetFor('misc');
|
|
382
|
-
ok(tsM.allow.length ===
|
|
415
|
+
ok(tsM.allow.length === 10 && tsM.allow.indexOf('peer') !== -1 && tsM.allow.indexOf('ho_gate') === -1, '杂项角色:10 个,含 peer、不含 ho_gate', String(tsM.allow.length));
|
|
383
416
|
const tsU = toolsetFor('plain');
|
|
384
|
-
ok(tsU.allow.length === 0 && tsU.deny.length ===
|
|
417
|
+
ok(tsU.allow.length === 0 && tsU.deny.length === 14, '普通会话不载入任何插件工具', JSON.stringify([tsU.allow.length, tsU.deny.length]));
|
|
385
418
|
const rSub = resolveRole({ sessionId: 'session-sub', title: 'Demo1', cwd: path.join(WS, 'Projects', 'Demo', 'Demo库'), subagent: true });
|
|
386
419
|
ok(rSub.role === 'plain' && rSub.source === 'subagent', '子会话默认按普通处理,不按 cwd 判项目', JSON.stringify(rSub));
|
|
387
420
|
|
|
421
|
+
// ---------- 体系注册与授权提示:写入权限、路径校验、空注册、注入、计划 ----------
|
|
422
|
+
{
|
|
423
|
+
const { pmDocmapTool } = await import(DIST('tools/ho_docmap.mjs'));
|
|
424
|
+
const { registerHolder } = await import(DIST('lib/office.mjs'));
|
|
425
|
+
const { systemBlock, registryRelPath, readRegistry, planRelPath } = await import(DIST('lib/system.mjs'));
|
|
426
|
+
const manDm = await import(DIST('lib/manual.mjs'));
|
|
427
|
+
const { grantConsent, consumedConsent, activeConsentsFor, consentPromptLines } = await import(DIST('lib/consent.mjs'));
|
|
428
|
+
|
|
429
|
+
const dmInfo = { role: 'project', project: 'Demo', task: '' };
|
|
430
|
+
const dmEnv = {
|
|
431
|
+
cfg: resolveConfig({ workspaceRoot: WS }),
|
|
432
|
+
resolveWs: () => WS,
|
|
433
|
+
selfId: (exec) => (exec && exec.agent && exec.agent.session ? exec.agent.session.header.id : null),
|
|
434
|
+
roleOf: () => dmInfo,
|
|
435
|
+
refreshPromptFor: () => true,
|
|
436
|
+
};
|
|
437
|
+
const dmExec = (sid) => ({ agent: { session: { header: { id: sid, cwd: path.join(WS, 'Projects', 'Demo', 'Demo库') } } } });
|
|
438
|
+
fs.writeFileSync(path.join(WS, 'Projects', 'Demo', 'README.md'), 'x', 'utf8');
|
|
439
|
+
registerHolder(WS, { kind: 'project', project: 'Demo', name: 'Demo1', sessionId: 'session-dm', workspace: WS });
|
|
440
|
+
const dm = pmDocmapTool(dmEnv);
|
|
441
|
+
const wroteDm = await dm.execute({ action: 'write', entries: [{ path: 'Projects/Demo/README.md', about: '项目简介', when: '动手前', section: 'files' }] }, dmExec('session-dm'));
|
|
442
|
+
ok(/已写入/.test(wroteDm.text), '体系注册:在任者写自己的注册应成功', wroteDm.text.split('\n')[0]);
|
|
443
|
+
const readBack = readRegistry(WS, dmInfo);
|
|
444
|
+
ok(readBack.exists && readBack.data.files.length === 1 && readBack.data.files[0].path === 'Projects/Demo/README.md', '体系注册:写盘后可解析回一条', JSON.stringify(readBack.data.files));
|
|
445
|
+
const sysBlock = systemBlock(WS, dmInfo);
|
|
446
|
+
ok(/体系注册/.test(sysBlock) && /Projects\/Demo\/README\.md/.test(sysBlock) && /动手前/.test(sysBlock), '体系注册:注入块含路径与时机', sysBlock);
|
|
447
|
+
const injectedDm = manDm.scopedManual(dmInfo, { ws: WS, system: sysBlock });
|
|
448
|
+
ok(injectedDm.indexOf('体系注册') !== -1 && injectedDm.split('体系注册').length === 2, '体系注册:注入段只出现一次', String(injectedDm.split('体系注册').length - 1));
|
|
449
|
+
let missErr = '';
|
|
450
|
+
try { await dm.execute({ action: 'write', entries: [{ path: 'Projects/Demo/nope.md', about: 'x' }] }, dmExec('session-dm')); } catch (e) { missErr = String(e && e.message ? e.message : e); }
|
|
451
|
+
ok(/未写入/.test(missErr) && /nope\.md/.test(missErr), '体系注册:路径不存在应整条拒绝并点名', missErr);
|
|
452
|
+
ok(readRegistry(WS, dmInfo).data.files.length === 1, '体系注册:拒绝后原注册不得被改');
|
|
453
|
+
const planWrote = await dm.execute({ action: 'plan', mode: 'write', text: '# 行动计划\n\n## 1 目标\n试跑\n' }, dmExec('session-dm'));
|
|
454
|
+
ok(/已写入计划/.test(planWrote.text), '行动计划:可写入任务根', planWrote.text.split('\n')[0]);
|
|
455
|
+
ok(fs.existsSync(path.join(WS, planRelPath(WS, dmInfo))), '行动计划:落在任务根 PLAN.md');
|
|
456
|
+
const planRead = await dm.execute({ action: 'plan', mode: 'read' }, dmExec('session-dm'));
|
|
457
|
+
ok(/行动计划/.test(planRead.text), '行动计划:可读回');
|
|
458
|
+
registerHolder(WS, { kind: 'project', project: 'Demo', name: 'Demo2', sessionId: 'session-dm2', workspace: WS });
|
|
459
|
+
let oldErr = '';
|
|
460
|
+
try { await dm.execute({ action: 'write', entries: [{ path: 'Projects/Demo/README.md', about: 'x' }] }, dmExec('session-dm')); } catch (e) { oldErr = String(e && e.message ? e.message : e); }
|
|
461
|
+
ok(/只有在任会话可以写/.test(oldErr), '体系注册:已交出的旧会话只读', oldErr);
|
|
462
|
+
const emptyDm = await dm.execute({ action: 'write', entries: [] }, dmExec('session-dm2'));
|
|
463
|
+
ok(/已写入/.test(emptyDm.text) && /目录 0 条、规范 0 条/.test(emptyDm.text), '体系注册:空 entries 写入空注册', emptyDm.text.split('\n')[0]);
|
|
464
|
+
ok(systemBlock(WS, dmInfo).indexOf('体系注册(项目「Demo」)') !== -1, '体系注册:空注册只注入标题一行');
|
|
465
|
+
ok(registryRelPath(dmInfo).replace(/\\/g, '/') === '_Project_Meta/Demo/System.md', '体系注册:文件按任务独立存放', registryRelPath(dmInfo));
|
|
466
|
+
|
|
467
|
+
grantConsent('session-dm2', 'blank-smoke', 'compact');
|
|
468
|
+
const actDm = activeConsentsFor('session-dm2');
|
|
469
|
+
const consentLines = consentPromptLines(actDm);
|
|
470
|
+
ok(actDm.length === 1 && consentLines.length === 1 && /用户已授权压缩/.test(consentLines[0]), '授权:有效期内应生成一行提示', JSON.stringify(consentLines));
|
|
471
|
+
ok(activeConsentsFor('session-other').length === 0, '授权:别的会话看不到', JSON.stringify(activeConsentsFor('session-other')));
|
|
472
|
+
const withConsent = manDm.scopedManual(dmInfo, { ws: WS, system: '', consents: actDm });
|
|
473
|
+
ok(withConsent.indexOf('用户已授权压缩') !== -1, '授权:提示段应带那一行', withConsent.slice(-160));
|
|
474
|
+
consumedConsent('session-dm2', 'compact');
|
|
475
|
+
ok(consentPromptLines(activeConsentsFor('session-dm2')).length === 0, '授权:用掉后提示消失(不占位)');
|
|
476
|
+
const afterUse = manDm.scopedManual(dmInfo, { ws: WS, system: '', consents: activeConsentsFor('session-dm2') });
|
|
477
|
+
ok(afterUse.indexOf('已授权') === -1, '授权:用掉后提示段不再有该行');
|
|
478
|
+
}
|
|
479
|
+
|
|
388
480
|
// ---------- 骨架档案:预设、默认骨架、注册、校验 ----------
|
|
389
481
|
const prof = await import(DIST('lib/profile.mjs'));
|
|
390
482
|
const pjs = prof.presets();
|
|
@@ -467,8 +559,8 @@ ok(tg.indexOf('收尾并停止') !== -1, '续跑文案应允许交接无待办
|
|
|
467
559
|
const globName = io.archiveFileName('E:/ws/_Control_Center/records/Global_Handover.md', stamp);
|
|
468
560
|
ok(globName === 'Global_Handover_' + stamp + '.md', '全局归档名应为 Global_Handover_<时间>.md', globName);
|
|
469
561
|
const ad = await import(pathToFileURL(path.join(dist, 'lib', 'role.mjs')).href);
|
|
470
|
-
const miscDir = ad.archiveDirFor(WS, { role: 'misc', task: '
|
|
471
|
-
ok(miscDir.endsWith('_Misc/tasks
|
|
562
|
+
const miscDir = ad.archiveDirFor(WS, { role: 'misc', task: '示例任务' }).replace(/\\/g, '/');
|
|
563
|
+
ok(miscDir.endsWith('_Misc/tasks/示例任务/_archive'), '杂项归档目录应在任务目录内(_archive)', miscDir);
|
|
472
564
|
const handTool = reg.tools.find((t) => t.name === 'ho_handover');
|
|
473
565
|
ok(!!handTool, '应注册 ho_handover');
|
|
474
566
|
if (handTool) {
|
|
@@ -499,21 +591,29 @@ const projAgent = {
|
|
|
499
591
|
};
|
|
500
592
|
for (const cb of listeners) cb({ agent: projAgent, source: 'fresh' });
|
|
501
593
|
ok(calls.restrict.length === 0, '按步路径不用 restrict 收窄工具', String(calls.restrict.length));
|
|
502
|
-
const projGatePack = gateFor('
|
|
594
|
+
const projGatePack = gateFor('ho_sessions');
|
|
503
595
|
const projGateHand = gateFor('ho_handover');
|
|
504
596
|
const projExec = { agent: projAgent };
|
|
505
|
-
ok(typeof projGatePack === 'function' && projGatePack(projExec) === false, '项目角色不可见
|
|
597
|
+
ok(typeof projGatePack === 'function' && projGatePack(projExec) === false, '项目角色不可见 ho_sessions(每步条件)');
|
|
506
598
|
ok(typeof projGateHand === 'function' && projGateHand(projExec) === true, '项目角色可见 ho_handover(每步条件)');
|
|
507
|
-
ok(gateFor('
|
|
508
|
-
ok(calls.sections.length
|
|
509
|
-
const
|
|
599
|
+
ok(gateFor('ho_changelog')(projExec) === false, '项目角色不可见 ho_changelog');
|
|
600
|
+
ok(calls.sections.length >= 1 && calls.sections.some((s) => s.name === 'dsh-handover:manual'), '应注册同名提示段遮蔽兜底段', JSON.stringify(calls.sections.map((s) => s.name)));
|
|
601
|
+
const projSec = calls.sections.find((s) => s.name === 'dsh-handover:manual');
|
|
602
|
+
const projText = String(projSec && projSec.text || '');
|
|
603
|
+
ok(!calls.sections.some((s) => s.name === 'dsh-handover:reminder'), '不再注册动态提醒段(提醒改走工具结果尾注入)');
|
|
604
|
+
const secNorm = projText.replace(/\\/g, '/');
|
|
510
605
|
ok(secNorm.indexOf('Projects/Demo') !== -1 && secNorm.indexOf('_Project_Meta/Demo/Handover.md') !== -1, '项目提示段应含自己的路径', secNorm.slice(0, 200));
|
|
511
606
|
ok(secNorm.indexOf('任务完成不报全局') !== -1, '项目提示段应写明任务完成不向全局报告', secNorm.slice(0, 400));
|
|
512
607
|
ok(secNorm.indexOf('任务达成') === -1, '项目提示段不得再出现任务达成');
|
|
513
|
-
ok(secNorm.indexOf('可直接
|
|
608
|
+
ok(secNorm.indexOf('可直接 peer') !== -1, '项目提示段应允许会话之间直接对话', secNorm.slice(0, 400));
|
|
514
609
|
ok(secNorm.indexOf('resume=wait') !== -1, '项目提示段应说明收尾压缩不唤醒');
|
|
515
|
-
|
|
516
|
-
ok(
|
|
610
|
+
ok(secNorm.indexOf('_Control_Center/standards/') !== -1, '项目提示段应给出通用规范的查找范围', secNorm.slice(-240));
|
|
611
|
+
ok(secNorm.indexOf('把某某写成规范') !== -1 && secNorm.indexOf('本会话所在的工作区') !== -1 && secNorm.indexOf('写入通用规范库') !== -1 && secNorm.indexOf('报全局') !== -1, '项目提示段应写明未指明写本库、通用规范库报全局', secNorm.slice(-400));
|
|
612
|
+
console.log('项目提示段 token 实测:' + budget.estimateTokens(projText) + '(参考值 ' + budget.TOKEN_BUDGET.projectTokens + ',不判失败)');
|
|
613
|
+
ok(projText.indexOf('纯窗口是否可能超过 40%') !== -1, '项目段应携带 1.0 子智能体判定线(纯窗口 40%)');
|
|
614
|
+
ok(secNorm.indexOf('写计划就把拆子智能体安排进去') !== -1, '项目段应写明超过 40% 时计划里引入子智能体', secNorm.slice(0, 400));
|
|
615
|
+
ok(secNorm.indexOf('超过时,写计划就把拆子智能体安排进去') < secNorm.indexOf('写行动计划 PLAN.md(任务根)'), '项目段应是先判纯窗口、再写一次计划', secNorm.slice(0, 400));
|
|
616
|
+
ok(secNorm.indexOf('子智能体') !== -1 && secNorm.indexOf('并发 ≤3') !== -1 && secNorm.indexOf('交回原作者续用') !== -1, '项目段应携带子智能体硬线(并发与续用)', secNorm.slice(0, 500));
|
|
517
617
|
// 真实工作区路径 + 迁移提示是最坏情形,参考值另设一档
|
|
518
618
|
{
|
|
519
619
|
const man = await import(pathToFileURL(path.join(dist, 'lib', 'manual.mjs')).href);
|
|
@@ -542,11 +642,16 @@ const globAgent = {
|
|
|
542
642
|
};
|
|
543
643
|
for (const cb of listeners) cb({ agent: globAgent, source: 'fresh' });
|
|
544
644
|
ok(gCalls.restrict === 0, '全局角色不应被裁剪', String(gCalls.restrict));
|
|
545
|
-
ok(gCalls.sections.
|
|
645
|
+
ok(gCalls.sections.some((s) => s.name === 'dsh-handover:manual'), '全局也应遮蔽兜底段', JSON.stringify(gCalls.sections.map((s) => s.name)));
|
|
646
|
+
const globSec = gCalls.sections.find((s) => s.name === 'dsh-handover:manual');
|
|
647
|
+
const globText = String(globSec && globSec.text || '');
|
|
648
|
+
ok(!gCalls.sections.some((s) => s.name === 'dsh-handover:reminder'), '全局同样不注册动态提醒段');
|
|
546
649
|
// 常驻段只放每轮判定,一次性流程下沉手册;参考值按加权 token 计,只打印实测值。
|
|
547
|
-
console.log('全局提示段 token 实测:' + budget.estimateTokens(
|
|
548
|
-
ok(
|
|
549
|
-
ok(
|
|
650
|
+
console.log('全局提示段 token 实测:' + budget.estimateTokens(globText) + '(参考值 ' + budget.TOKEN_BUDGET.globalTokens + ',不判失败)');
|
|
651
|
+
ok(globText.indexOf('纯窗口是否可能超过 40%') !== -1, '全局段应携带 1.0 子智能体判定线(纯窗口 40%)');
|
|
652
|
+
ok(globText.indexOf('写计划就把拆子智能体安排进去') < globText.indexOf('写行动计划 PLAN.md(任务根)'), '全局段应是先判纯窗口、再写一次计划');
|
|
653
|
+
ok(globText.indexOf('并发 ≤3') !== -1 && globText.indexOf('交回原作者续用') !== -1, '全局段应携带子智能体硬线(并发与续用)');
|
|
654
|
+
ok(globText.indexOf('list_agents') === -1, '全局段不携带子智能体检索细节(细则在运行时手册)');
|
|
550
655
|
// 真机最坏情形:长路径 + 迁移提示行
|
|
551
656
|
{
|
|
552
657
|
const man2 = await import(pathToFileURL(path.join(dist, 'lib', 'manual.mjs')).href);
|
|
@@ -555,25 +660,27 @@ ok(gCalls.sections[0].text.indexOf('send_message 续用') === -1, '全局段不
|
|
|
555
660
|
const gWorst = man2.globalManual({ ws: longWs2, move: { phase: 'switched', dst: otherWs2, src: longWs2 } });
|
|
556
661
|
console.log('全局最坏情形提示段 token 实测:' + budget.estimateTokens(gWorst) + '(参考值 ' + budget.TOKEN_BUDGET.globalWorstTokens + ',不判失败)');
|
|
557
662
|
}
|
|
558
|
-
ok(
|
|
559
|
-
//
|
|
663
|
+
ok(globText.indexOf('前置提示词') === -1, '插件角色段不得再携带前置提示词(由用户全局前置提示词注入)');
|
|
664
|
+
// 结构:1.0 为六节(身份、权限、任务流程、上下文与压缩、交接与体系、按需取用)
|
|
560
665
|
{
|
|
561
|
-
const g =
|
|
666
|
+
const g = globText;
|
|
562
667
|
const cnt = (k) => g.split(k).length - 1;
|
|
563
|
-
const secs = ['一、身份', '二、权限', '
|
|
668
|
+
const secs = ['一、身份', '二、权限', '三、任务流程', '四、上下文与压缩', '五、交接与体系', '六、按需取用'];
|
|
564
669
|
const bad = secs.filter((k) => cnt(k) !== 1);
|
|
565
|
-
ok(bad.length === 0, '
|
|
566
|
-
const absent = ['
|
|
670
|
+
ok(bad.length === 0, '全局段六节骨架应各出现一次', bad.join(','));
|
|
671
|
+
const absent = ['反模式', '自检'];
|
|
567
672
|
const still = absent.filter((k) => g.indexOf(k) !== -1);
|
|
568
673
|
ok(still.length === 0, '插件角色段不得携带非会话约束', still.join(','));
|
|
569
|
-
ok(cnt('换任') <=
|
|
674
|
+
ok(cnt('换任') <= 4, '「换任」在全局段出现不应超过四处', String(cnt('换任')));
|
|
570
675
|
}
|
|
571
676
|
ok(gCalls.injected === 0, '全局不再重复注入工作流(提示段里已有)', String(gCalls.injected));
|
|
572
677
|
// 分层护栏——常驻段只留判定,工作流细节必须留在手册里
|
|
573
678
|
{
|
|
574
|
-
const g0 =
|
|
575
|
-
ok(g0.indexOf('
|
|
679
|
+
const g0 = globText;
|
|
680
|
+
ok(g0.indexOf('ho_backup') === -1 && g0.indexOf('move-probe') === -1, '常驻段不得回灌工作流细节(应放手册)');
|
|
576
681
|
ok(g0.indexOf('ho_charter(doc="runtime", section=…') !== -1, '常驻段应给出按节取用的指针');
|
|
682
|
+
ok(g0.indexOf('_Control_Center/standards/') !== -1, '全局段应给出通用规范的查找范围', g0.slice(-240));
|
|
683
|
+
ok(g0.indexOf('把某某写成规范') !== -1 && g0.indexOf('写入通用规范库') !== -1 && g0.indexOf('本会话所在的工作区') !== -1, '全局段应写明规范的写入范围', g0.slice(-400));
|
|
577
684
|
const rm = fs.readFileSync(path.join(dist, 'templates', 'Runtime_Manual.md'), 'utf8');
|
|
578
685
|
ok(rm.indexOf('## 11. 首次安装') !== -1 && rm.indexOf('## 12. 提示词预算') !== -1, '手册应含首次安装与提示词预算两节');
|
|
579
686
|
ok(rm.indexOf('续用优先') !== -1, '手册 §5 应含子智能体续用纪律');
|
|
@@ -594,24 +701,25 @@ ok(gCalls.injected === 0, '全局不再重复注入工作流(提示段里已
|
|
|
594
701
|
const uiFiles = listFiles(path.join(repo, 'src'), ['.mjs', '.js'], { skip: ['templates'] });
|
|
595
702
|
const docFiles = [path.join(repo, 'README.md')]
|
|
596
703
|
.concat(listFiles(path.join(repo, 'src', 'templates'), ['.md']))
|
|
597
|
-
|
|
704
|
+
// 写作规范正文与论文属规范性文本,条款本身会引用被禁词;Writing_Rules 是用户内容。
|
|
705
|
+
.filter((f) => f.indexOf('Writing_Rules.md') === -1 && f.indexOf(path.join('templates', 'standards')) === -1);
|
|
598
706
|
const uiHits = scanFiles(uiFiles, { ui: true });
|
|
599
707
|
const docHits = scanFiles(docFiles, { ui: false });
|
|
600
708
|
const fmt = (hs) => hs.slice(0, 6).map((h) => path.relative(repo, h.file) + ':' + h.line + ' ' + h.rules.join('、')).join(' | ');
|
|
601
709
|
ok(uiHits.length === 0, '界面文案应一律使用标准书面语(词表与符号扫描)', fmt(uiHits));
|
|
602
710
|
ok(docHits.length === 0, '文档文案应一律使用标准书面语(词表与符号扫描)', fmt(docHits));
|
|
603
711
|
}
|
|
604
|
-
ok(opsSrc.indexOf('不得自行开展作业') !== -1 && opsSrc.indexOf('
|
|
712
|
+
ok(opsSrc.indexOf('不得自行开展作业') !== -1 && opsSrc.indexOf('待当前用户指派') !== -1, '启动提示词应先报告再待指派');
|
|
605
713
|
}
|
|
606
714
|
|
|
607
715
|
// 按步路径(0.2.2 起):创建即按角色登记提示段;工具可见性由每步条件算,不再调用 restrict。
|
|
608
|
-
const unrCalls = { restrict: 0, section: null };
|
|
716
|
+
const unrCalls = { restrict: 0, section: null, sections: [] };
|
|
609
717
|
const unrAgent = {
|
|
610
718
|
id: 'session-unr',
|
|
611
|
-
session: { header: { id: 'session-unr', title: '
|
|
719
|
+
session: { header: { id: 'session-unr', title: '未命名', cwd: '/tmp/x' } },
|
|
612
720
|
ctx: {
|
|
613
721
|
tools: { restrict: () => { unrCalls.restrict++; return () => { }; } },
|
|
614
|
-
systemPrompt: { section: (o) => { unrCalls.section = o; return () => { }; } },
|
|
722
|
+
systemPrompt: { section: (o) => { unrCalls.section = o; unrCalls.sections.push(o); return () => { }; } },
|
|
615
723
|
},
|
|
616
724
|
inject: () => { throw new Error('未设角色的会话不应写历史'); },
|
|
617
725
|
};
|
|
@@ -623,9 +731,9 @@ ok(!unrCalls.section, '普通会话不注册角色提示段');
|
|
|
623
731
|
// 开轮前核对:休眠期间改了角色,下一轮开始时补装提示段(工具由每步条件即时生效)。
|
|
624
732
|
writeRole({}, 'session-unr', { role: 'project', project: 'Demo', source: 'user' });
|
|
625
733
|
for (const cb of ((reg.listeners && reg.listeners['agent/status']) || [])) cb({ agent: unrAgent, status: 'running' });
|
|
626
|
-
ok(
|
|
734
|
+
ok(unrCalls.sections.some((s) => s.name === 'dsh-handover:manual' && /项目/.test(String(s.text))), '开轮前核对应按新角色补装提示段', JSON.stringify(unrCalls.sections.map((s) => s.name)));
|
|
627
735
|
|
|
628
|
-
// 旧内核(没有 tools.resolve)才走 restrict:创建时按普通收窄
|
|
736
|
+
// 旧内核(没有 tools.resolve)才走 restrict:创建时按普通收窄 14 个,改角色后开轮前换成 3 个。
|
|
629
737
|
{
|
|
630
738
|
const legacyCalls = { restrict: 0, lastDeny: null, disposed: 0 };
|
|
631
739
|
const legacyListeners = {};
|
|
@@ -651,10 +759,10 @@ ok(!!unrCalls.section && /项目/.test(String(unrCalls.section.text)), '开轮
|
|
|
651
759
|
ctx: { tools: legacyTools, systemPrompt: legacyCtx.systemPrompt },
|
|
652
760
|
};
|
|
653
761
|
for (const cb of (legacyListeners['agent/created'] || [])) cb({ agent: legacyAgent, source: 'fresh' });
|
|
654
|
-
ok(legacyCalls.restrict === 1 && legacyCalls.lastDeny && legacyCalls.lastDeny.length ===
|
|
762
|
+
ok(legacyCalls.restrict === 1 && legacyCalls.lastDeny && legacyCalls.lastDeny.length === 14, '旧内核:创建时按普通收窄 14 个', JSON.stringify(legacyCalls.lastDeny && legacyCalls.lastDeny.length));
|
|
655
763
|
writeRole({}, 'session-legacy', { role: 'project', project: 'Demo', source: 'user' });
|
|
656
764
|
for (const cb of (legacyListeners['agent/status'] || [])) cb({ agent: legacyAgent, status: 'running' });
|
|
657
|
-
ok(legacyCalls.restrict === 2 && legacyCalls.lastDeny && legacyCalls.lastDeny.length ===
|
|
765
|
+
ok(legacyCalls.restrict === 2 && legacyCalls.lastDeny && legacyCalls.lastDeny.length === 3, '旧内核:开轮前换成项目角色的 3 个', JSON.stringify(legacyCalls.lastDeny && legacyCalls.lastDeny.length));
|
|
658
766
|
ok(legacyCalls.disposed === 1, '旧内核:换角色应撤销旧限制', String(legacyCalls.disposed));
|
|
659
767
|
}
|
|
660
768
|
// ---------- 角色路由 ----------
|
|
@@ -679,7 +787,7 @@ const oBad = await orderCall({ action: 'order', order: 'wave', session_id: 'sess
|
|
|
679
787
|
ok(/只能是 handover 或 compact/.test(oBad), 'order 取值应受限', oBad);
|
|
680
788
|
// 交接/压缩指令必须先问用户
|
|
681
789
|
const oNoConfirm = await orderCall({ action: 'order', order: 'handover', session_id: 'session-norole' });
|
|
682
|
-
ok(
|
|
790
|
+
ok(/需要当前用户确认/.test(oNoConfirm), 'order 未确认应拒绝', oNoConfirm);
|
|
683
791
|
const oUnr = await orderCall({ action: 'order', order: 'handover', session_id: 'session-norole', confirm: true });
|
|
684
792
|
ok(/没有交接路径/.test(oUnr), '未设角色的会话不能下交接指令(避免退回全局交接)', oUnr);
|
|
685
793
|
const oCold = await orderCall({ action: 'order', order: 'compact', session_id: 'session-r1', confirm: true });
|
|
@@ -697,15 +805,15 @@ const guardExec = (name, argsObj, sid, cwd, title) => ({
|
|
|
697
805
|
});
|
|
698
806
|
ok(typeof reg.guardFn === 'function', '应注册工具守卫', typeof reg.guardFn);
|
|
699
807
|
if (typeof reg.guardFn === 'function') {
|
|
700
|
-
const gGlobal = reg.guardFn(guardExec('
|
|
701
|
-
ok(gGlobal === undefined, '全局角色
|
|
702
|
-
const gReport = reg.guardFn(guardExec('
|
|
808
|
+
const gGlobal = reg.guardFn(guardExec('peer', { action: 'send', session_id: 'session-proj-x' }, 'session-glob', WS, '全局9'));
|
|
809
|
+
ok(gGlobal === undefined, '全局角色 peer(action=send) 应放行', String(gGlobal));
|
|
810
|
+
const gReport = reg.guardFn(guardExec('peer', { action: 'send', session_id: 'session-glob' }, 'session-proj', path.join(WS, 'Projects', 'Demo', 'Demo库'), 'Demo1'));
|
|
703
811
|
ok(gReport === undefined, '项目会话向全局报告应放行', String(gReport));
|
|
704
|
-
const gPeer = reg.guardFn(guardExec('
|
|
812
|
+
const gPeer = reg.guardFn(guardExec('peer', { action: 'send', session_id: 'session-proj' }, 'session-proj', path.join(WS, 'Projects', 'Demo', 'Demo库'), 'Demo1'));
|
|
705
813
|
ok(gPeer === undefined, '会话之间可直接对话,不必经全局转述', String(gPeer));
|
|
706
|
-
const gNew = reg.guardFn(guardExec('
|
|
814
|
+
const gNew = reg.guardFn(guardExec('peer', { action: 'new', workspace: 'Demo库' }, 'session-proj', path.join(WS, 'Projects', 'Demo', 'Demo库'), 'Demo1'));
|
|
707
815
|
ok(typeof gNew === 'string' && /仅属全局/.test(gNew), '项目会话不得建平级会话', String(gNew));
|
|
708
|
-
const gMisc = reg.guardFn(guardExec('
|
|
816
|
+
const gMisc = reg.guardFn(guardExec('peer', { action: 'new', workspace: 'Misc' }, 'session-misc', path.join(WS, '_Misc', 'tasks', '示例任务'), '杂项1'));
|
|
709
817
|
ok(gMisc === undefined, '杂项会话自建 Misc 换任应放行', String(gMisc));
|
|
710
818
|
}
|
|
711
819
|
|
|
@@ -732,7 +840,7 @@ const plainRows = [];
|
|
|
732
840
|
for (let i = 0; i < 10; i++) {
|
|
733
841
|
plainRows.push({
|
|
734
842
|
sid: 'session-p' + i, title: '旧会话' + i, role: 'plain', roleLabel: '普通', source: 'none',
|
|
735
|
-
project: '', task: '', pct: i + 1, state: 'idle', active: false, seen: '2026-10-21 20:19 +0800',
|
|
843
|
+
project: '', task: '', wsKey: '', wsTitle: '', wsPath: '', pct: i + 1, state: 'idle', active: false, onduty: false, seen: '2026-10-21 20:19 +0800',
|
|
736
844
|
});
|
|
737
845
|
}
|
|
738
846
|
|
|
@@ -745,22 +853,25 @@ const FIXTURE = {
|
|
|
745
853
|
guide: payload ? payload.guide : { intro: ['说明'], situations: [{ when: 'w', say: 's', what: 'x' }], groups: [{ title: 'g', tools: [['ho_x', 'y']] }], rules: ['r'] },
|
|
746
854
|
extraPrompt: '',
|
|
747
855
|
extraPromptEnabled: false,
|
|
748
|
-
gate: { ok: true, verdict: '全部落位。', total: 1, bodyLines: 24, libraries: [{ name: '
|
|
749
|
-
handover: { global: { verdict: '通过', reasons: [] }, projects: [{ name: '
|
|
856
|
+
gate: { ok: true, verdict: '全部落位。', total: 1, bodyLines: 24, libraries: [{ name: 'Demo1', ok: true, titleOk: true, waterOk: true, missing: 0, lines: 24 }] },
|
|
857
|
+
handover: { global: { verdict: '通过', reasons: [] }, projects: [{ name: 'Demo1', verdict: '通过', reasons: [] }, { name: 'Idle', verdict: '缺节', reasons: ['缺 ## 1. 正在做的事(2026-10-12 起的强制项,本项目还没补)'] }], misc: [{ name: '示例任务', verdict: '通过', reasons: [] }], fail: 0 },
|
|
750
858
|
sessions: {
|
|
859
|
+
workspaces: [{ key: 'w1', title: '全局', path: 'C:/ws' }],
|
|
751
860
|
rows: [
|
|
752
|
-
{ sid: 'session-y', project: '
|
|
753
|
-
{ sid: 'session-
|
|
861
|
+
{ sid: 'session-y', title: 'Demo1', project: 'Demo1', role: 'project', roleLabel: '项目', dutyKind: '项目', dutyKey: 'Demo1', pct: 34.6, state: 'idle', active: true, onduty: true, seen: '2026-10-23 23:19 +0800' },
|
|
862
|
+
{ sid: 'session-g1', title: '全局1', role: 'global', roleLabel: '全局', dutyKind: '全局', dutyKey: '全局1', pct: 40, state: 'idle', active: true, onduty: true, seen: '2026-11-07 21:00 +0800' },
|
|
863
|
+
{ sid: 'session-misc', title: '示例任务', role: 'misc', roleLabel: '杂项', dutyKind: '杂项', dutyKey: '示例任务', pct: 5, state: 'idle', active: false, onduty: true, seen: '2026-10-20 09:00 +0800' },
|
|
864
|
+
{ sid: 'session-l', project: 'Demo2', title: 'Demo2', role: 'project', roleLabel: '项目', dutyKind: '项目', dutyKey: 'Demo2', pct: 50, state: 'idle', active: false, onduty: false, seen: '2026-10-22 20:00 +0800' },
|
|
754
865
|
],
|
|
755
866
|
all: [
|
|
756
|
-
{ sid: 'session-g1', title: '全局1', role: 'global', roleLabel: '全局', source: 'title', project: '', task: '', pct: 40, state: 'idle', active: true, seen: '2026-11-07 21:00 +0800' },
|
|
757
|
-
{ sid: 'session-y', title: '
|
|
758
|
-
{ sid: 'session-l', title: '
|
|
867
|
+
{ sid: 'session-g1', title: '全局1', role: 'global', roleLabel: '全局', source: 'title', project: '', task: '', wsKey: 'w1', wsTitle: '全局', wsPath: 'C:/ws', pct: 40, state: 'idle', active: true, onduty: true, seen: '2026-11-07 21:00 +0800' },
|
|
868
|
+
{ sid: 'session-y', title: 'Demo1', role: 'project', roleLabel: '项目', source: 'store', project: 'Demo1', task: '', wsKey: 'w1', wsTitle: '全局', wsPath: 'C:/ws', pct: 34.6, state: 'idle', active: true, onduty: true, seen: '2026-10-23 23:19 +0800' },
|
|
869
|
+
{ sid: 'session-l', title: 'Demo2', role: 'project', roleLabel: '项目', source: 'cwd', project: 'Demo2', task: '', wsKey: 'w1', wsTitle: '全局', wsPath: 'C:/ws', pct: 50, state: 'idle', active: false, onduty: false, seen: '2026-10-22 20:00 +0800' },
|
|
759
870
|
].concat(plainRows),
|
|
760
|
-
notify: [{ sid: 'session-y',
|
|
761
|
-
verdict: '2
|
|
871
|
+
notify: [{ sid: 'session-y', key: 'Demo1', name: 'Demo1' }], hot: 2, unknown: 0, cutoff: '2026-11-07 09:25 +0800', silentHours: 12,
|
|
872
|
+
verdict: '2 个会话已过 35% 线,应先收齐交接再换会话',
|
|
762
873
|
},
|
|
763
|
-
consent: [{ sessionId: 'session-y', leftMs: 120000 }],
|
|
874
|
+
consent: [{ sessionId: 'session-y', leftMs: 120000, kind: 'compact', onduty: true }],
|
|
764
875
|
notes: [],
|
|
765
876
|
};
|
|
766
877
|
globalThis.fetch = async () => ({ ok: true, json: async () => FIXTURE });
|
|
@@ -817,29 +928,50 @@ function textOf(node, out = []) {
|
|
|
817
928
|
}
|
|
818
929
|
const text = textOf(tree).join(' ');
|
|
819
930
|
const treeJson = JSON.stringify(tree);
|
|
820
|
-
for (const want of ['dsh-handover', '刷新', '读数', '
|
|
931
|
+
for (const want of ['dsh-handover', '刷新', '读数', '授权', '会话角色', '全局前置提示词', '会话水位', '通用约束条闸门', '交接文档体检', '工具明细', '使用说明']) {
|
|
821
932
|
ok(text.indexOf(want) !== -1, '成体视图应出现「' + want + '」');
|
|
822
933
|
}
|
|
823
934
|
|
|
824
935
|
// 回到重构前的版式——常用区直接展开,只折叠低频项;授权可直接点。
|
|
825
936
|
const sections = tree.kids.filter((k) => k && k.type === 'section');
|
|
826
937
|
const cardTitles = sections.map((sec) => textOf(sec.kids[0]).join(''));
|
|
827
|
-
const wantOrder = ['
|
|
828
|
-
ok(JSON.stringify(cardTitles) === JSON.stringify(wantOrder), '
|
|
938
|
+
const wantOrder = ['授权', '移动全局根目录', '会话角色', '全局前置提示词', '会话水位', '通用约束条闸门', '交接文档体检', '插件', '最近一次会话间通信判定', '工具明细', '使用说明'];
|
|
939
|
+
ok(JSON.stringify(cardTitles) === JSON.stringify(wantOrder), '分区应自上而下为授权、迁移、角色、提示词、只读数据、插件、判定、说明', JSON.stringify(cardTitles));
|
|
829
940
|
|
|
830
941
|
const folds = treeJson.split('"details"').length - 1;
|
|
831
|
-
ok(folds ===
|
|
942
|
+
ok(folds === 6, '折叠项应为迁移、前置提示词、工具明细、使用说明与两个工作区组', String(folds));
|
|
832
943
|
|
|
833
|
-
//
|
|
834
|
-
const
|
|
944
|
+
// 会话角色:按 DSH 工作区分组、默认折叠,已归档会话不列出。
|
|
945
|
+
const roleCard = sections.find((sec) => textOf(sec.kids[0]).join('') === '会话角色');
|
|
946
|
+
const roleDetails = [];
|
|
835
947
|
(function walk(n) {
|
|
836
948
|
if (!n || typeof n !== 'object') return;
|
|
837
949
|
if (Array.isArray(n)) { n.forEach(walk); return; }
|
|
838
|
-
if (n.type === '
|
|
950
|
+
if (n.type === 'details') roleDetails.push(n);
|
|
951
|
+
(n.kids || []).forEach(walk);
|
|
952
|
+
})(roleCard);
|
|
953
|
+
ok(roleDetails.length === 2, '会话角色应按工作区分组(全局+未分组)', String(roleDetails.length));
|
|
954
|
+
ok(roleDetails.every((d) => d.props.open !== true), '工作区分组默认收起');
|
|
955
|
+
const roleText = textOf(roleCard).join(' ');
|
|
956
|
+
ok(roleText.indexOf('全局(3 个会话)') !== -1 && roleText.indexOf('未分组(10 个会话)') !== -1, '分组标题应带会话数,未登记会话归入未分组', roleText.slice(0, 200));
|
|
957
|
+
ok(roleText.indexOf('已归档会话不列出') === -1 && roleText.indexOf('点击「设置角色」当场生效') === -1, '会话角色卡不再堆说明文字,只留标题与分组');
|
|
958
|
+
|
|
959
|
+
// 授权:只列在任会话,两组各一列按钮,用户不必手填 id。
|
|
960
|
+
const grantBtns = [];
|
|
961
|
+
(function walk(n) {
|
|
962
|
+
if (!n || typeof n !== 'object') return;
|
|
963
|
+
if (Array.isArray(n)) { n.forEach(walk); return; }
|
|
964
|
+
if (typeof n.type === 'function') { walk(n.type(n.props || {})); return; }
|
|
965
|
+
if (n.type === 'button') {
|
|
966
|
+
const label = textOf(n).join('');
|
|
967
|
+
if (label === '授权压缩' || label === '授权换任') grantBtns.push(label);
|
|
968
|
+
}
|
|
839
969
|
(n.kids || []).forEach(walk);
|
|
840
970
|
})(tree);
|
|
841
|
-
ok(
|
|
842
|
-
ok(
|
|
971
|
+
ok(grantBtns.filter((x) => x === '授权压缩').length === 3, '授权压缩:两张在任会话按钮+输入行按钮', JSON.stringify(grantBtns));
|
|
972
|
+
ok(grantBtns.filter((x) => x === '授权换任').length === 3, '授权换任:两张在任会话按钮+输入行按钮', JSON.stringify(grantBtns));
|
|
973
|
+
ok(text.indexOf('有授权') !== -1, '有有效授权时应显示「有授权」');
|
|
974
|
+
ok(text.indexOf('当前无有效授权') === -1, '不再显示无授权占位');
|
|
843
975
|
|
|
844
976
|
// 角色下拉含普通,不再回退到空值。
|
|
845
977
|
ok(text.indexOf('普通') !== -1, '角色下拉应含普通选项');
|
|
@@ -860,23 +992,60 @@ ok(migrateDetail && migrateDetail.type === 'details' && migrateDetail.props.open
|
|
|
860
992
|
// 交接体检只写问题,不带括注。
|
|
861
993
|
ok(text.indexOf('缺 ## 1. 正在做的事') !== -1 && text.indexOf('强制项') === -1, '交接体检的问题列不应带括注说明');
|
|
862
994
|
|
|
863
|
-
//
|
|
864
|
-
|
|
995
|
+
// 会话水位只列在任会话,全局、项目、杂项都要有,并给出本轮需通知。
|
|
996
|
+
const waterCard = sections.find((sec) => textOf(sec.kids[0]).join('') === '会话水位');
|
|
997
|
+
const waterText = textOf(waterCard).join(' ');
|
|
998
|
+
ok(waterText.indexOf('Demo1') !== -1 && waterText.indexOf('34.6%') !== -1, '会话水位应列出项目在任会话与水位');
|
|
999
|
+
ok(waterText.indexOf('全局1') !== -1 && waterText.indexOf('示例任务') !== -1, '会话水位应含全局与杂项的在任会话', waterText.slice(0, 240));
|
|
1000
|
+
ok(waterText.indexOf('Demo2') === -1, '会话水位不列已交出的会话', waterText.slice(0, 240));
|
|
865
1001
|
ok(text.indexOf('本轮需通知') !== -1, '水位段应给出本轮需通知');
|
|
866
|
-
ok(text.indexOf('
|
|
1002
|
+
ok(text.indexOf('Demo2') !== -1, '角色表应列出平级会话');
|
|
1003
|
+
const hoCard = sections.find((sec) => textOf(sec.kids[0]).join('') === '交接文档体检');
|
|
1004
|
+
const hoText = textOf(hoCard).join(' ');
|
|
1005
|
+
ok(hoText.indexOf('杂项') !== -1 && hoText.indexOf('示例任务') !== -1, '交接体检应覆盖已登记的杂项任务', hoText.slice(0, 240));
|
|
867
1006
|
|
|
868
1007
|
// 版式约束:无进度条、无图形结论符号。
|
|
869
1008
|
ok(treeJson.indexOf('dshh-bar') === -1, '不得使用进度条');
|
|
870
1009
|
const clientSrc = fs.readFileSync(path.join(dist, 'client.js'), 'utf8');
|
|
871
1010
|
ok(!/[\u2705\u274C\u26A0\u2717]/.test(clientSrc), '界面不得使用图形结论符号');
|
|
872
|
-
ok(clientSrc.indexOf('delWord.trim() !== "删除"') !== -1, '
|
|
1011
|
+
ok(clientSrc.indexOf('delWord.trim() !== "删除"') !== -1, '删除旧备份须输入确认词');
|
|
1012
|
+
// 保留的旧备份不算待处理:迁移卡只在妨碍使用的情形下提示。
|
|
1013
|
+
ok(clientSrc.indexOf('(st.phase === "switched" && (st.olds || []).length)') === -1, '保留的旧备份不得计入待处理');
|
|
1014
|
+
ok(clientSrc.indexOf('旧备份保留,不影响使用') !== -1, '迁移完成后应注明旧备份不影响使用');
|
|
1015
|
+
// 角色下拉顺序保持全局、项目、杂项、普通;会话行本身按时间倒序,不再按角色排列。
|
|
1016
|
+
{
|
|
1017
|
+
const g = clientSrc.indexOf('value: "global"');
|
|
1018
|
+
const p = clientSrc.indexOf('value: "project"');
|
|
1019
|
+
const m = clientSrc.indexOf('value: "misc"');
|
|
1020
|
+
const n = clientSrc.indexOf('value: "plain"');
|
|
1021
|
+
ok(g !== -1 && p !== -1 && m !== -1 && n !== -1 && g < p && p < m && m < n, '角色下拉应为全局、项目、杂项、普通', JSON.stringify({ g, p, m, n }));
|
|
1022
|
+
ok(clientSrc.indexOf('ROLE_ORDER') === -1 && clientSrc.indexOf('byTime') !== -1, '会话角色组内应按时间倒序,不再按角色排列');
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// 组内顺序实测:夹具中全局组按时间应为 全局1、Demo1、Demo2。
|
|
1026
|
+
{
|
|
1027
|
+
const first = roleDetails[0];
|
|
1028
|
+
const rows = [];
|
|
1029
|
+
(function collect(n) {
|
|
1030
|
+
if (!n || typeof n !== 'object') return;
|
|
1031
|
+
if (Array.isArray(n)) { n.forEach(collect); return; }
|
|
1032
|
+
if (n.type === 'tr') {
|
|
1033
|
+
const cells = (n.kids || []).filter((k) => k && k.type === 'td');
|
|
1034
|
+
if (cells.length >= 2) rows.push(textOf(cells[0]).join(''));
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
if (typeof n.type === 'function') { collect(n.type(n.props || {})); return; }
|
|
1038
|
+
(n.kids || []).forEach(collect);
|
|
1039
|
+
})(first);
|
|
1040
|
+
ok(JSON.stringify(rows.slice(0, 3)) === JSON.stringify(['全局1', 'Demo1', 'Demo2']), '角色组内会话应按时间倒序排列', JSON.stringify(rows.slice(0, 3)));
|
|
1041
|
+
}
|
|
873
1042
|
|
|
874
1043
|
// 未接入工作区:一键部署卡片置顶。
|
|
875
1044
|
const NOT_READY = {
|
|
876
1045
|
generatedAt: '2026-11-07 21:25 +0800',
|
|
877
1046
|
plugin: { name: '@mingmaple/dsh-handover', version: 'test', tools: 22, inject: ['tools'], initErrors: [], scheduler: '就绪' },
|
|
878
1047
|
workspace: { root: '', found: false },
|
|
879
|
-
setup: { steps: ['准备一个长期存在的目录', '
|
|
1048
|
+
setup: { steps: ['准备一个长期存在的目录', '粘贴路径后点击「一键部署」'] },
|
|
880
1049
|
guide: payload ? payload.guide : null,
|
|
881
1050
|
notes: ['未找到 _Control_Center:这个实例还没接入工作区。'],
|
|
882
1051
|
};
|
|
@@ -890,7 +1059,7 @@ ok(titles3[0] === '首次安装', '未接入工作区时首次安装置顶', JSO
|
|
|
890
1059
|
const text3 = textOf(tree3).join(' ');
|
|
891
1060
|
ok(text3.indexOf('一键部署') !== -1, '未接入时应能一键部署(建体系+注册工作区+建全局会话)');
|
|
892
1061
|
ok(text3.indexOf('仅建立文件体系') !== -1, '未接入时仍可只建立文件体系');
|
|
893
|
-
ok(text3.indexOf('
|
|
1062
|
+
ok(text3.indexOf('未找到 _Control_Center') !== -1, '未接入工作区时应给出中文提示');
|
|
894
1063
|
|
|
895
1064
|
// 已接入但没有全局会话:给出补建入口,且排在常规卡片之前。
|
|
896
1065
|
const READY_NO_GLOBAL = Object.assign({}, FIXTURE, { globalSessions: 0 });
|
|
@@ -1020,7 +1189,7 @@ try {
|
|
|
1020
1189
|
ok(/工作区/.test(newErr) && /已注册的工作区/.test(newErr), '新建会话缺工作区时应报错并列出可选工作区', newErr.slice(0, 160));
|
|
1021
1190
|
|
|
1022
1191
|
// 回归锁(0.9.14):① 新建杂项会话的启动提示读本任务交接,不退回全局交接;
|
|
1023
|
-
// ② 杂项换任的接任者落回本会话所在的注册工作区;③ 接任提示里的「全局 id
|
|
1192
|
+
// ② 杂项换任的接任者落回本会话所在的注册工作区;③ 接任提示里的「全局 id」是现任全局,不是已交出的旧会话。
|
|
1024
1193
|
{
|
|
1025
1194
|
const { registerHolder } = await import(DIST('lib/office.mjs'));
|
|
1026
1195
|
const storages = path.join(BLANK, '.dsh', 'storages');
|
|
@@ -1045,26 +1214,26 @@ ok(/工作区/.test(newErr) && /已注册的工作区/.test(newErr), '新建会
|
|
|
1045
1214
|
{
|
|
1046
1215
|
let createdOut = '';
|
|
1047
1216
|
try {
|
|
1048
|
-
const r = await sessT.execute({ action: 'new', confirm: true, title: '杂项2', kind: 'misc', role: 'misc', task: '
|
|
1217
|
+
const r = await sessT.execute({ action: 'new', confirm: true, title: '杂项2', kind: 'misc', role: 'misc', task: '示例任务', workspace: 'ws-misc' }, makerExec2);
|
|
1049
1218
|
createdOut = String((r && r.text) || '');
|
|
1050
1219
|
} catch (e) { createdOut = 'ERR ' + String(e && e.message ? e.message : e); }
|
|
1051
1220
|
const p = seen.prompts[seen.prompts.length - 1] || '';
|
|
1052
1221
|
ok(/已建会话/.test(createdOut), '新建杂项会话应成功', createdOut.slice(0, 200));
|
|
1053
|
-
ok(/_Misc[\\/]tasks[\\/]
|
|
1222
|
+
ok(/_Misc[\\/]tasks[\\/]示例任务[\\/]Handover\.md/.test(p), '新建杂项会话的启动提示应读本任务交接', p.slice(0, 200));
|
|
1054
1223
|
ok(p.indexOf('Global_Handover') === -1, '新建杂项会话的启动提示不得指向全局交接', p.slice(0, 200));
|
|
1055
1224
|
}
|
|
1056
1225
|
{
|
|
1057
1226
|
const { runSuccession } = await import(DIST('lib/succession.mjs'));
|
|
1058
1227
|
const { grantConsent } = await import(DIST('lib/consent.mjs'));
|
|
1059
|
-
const miscHandover = path.join(miscWs, 'tasks', '
|
|
1228
|
+
const miscHandover = path.join(miscWs, 'tasks', '示例任务', 'Handover.md');
|
|
1060
1229
|
fs.mkdirSync(path.dirname(miscHandover), { recursive: true });
|
|
1061
|
-
fs.writeFileSync(miscHandover, '#
|
|
1230
|
+
fs.writeFileSync(miscHandover, '# 示例任务交接' + '\n\n## 下一步\n- 等用户指派下一步\n', 'utf8');
|
|
1062
1231
|
registerHolder(WS, { kind: 'global', name: '全局1', sessionId: 'session-global-cur', workspace: WS });
|
|
1063
|
-
registerHolder(WS, { kind: 'misc', task: '
|
|
1232
|
+
registerHolder(WS, { kind: 'misc', task: '示例任务', name: '杂项1', sessionId: 'session-misc-old', workspace: miscWs });
|
|
1064
1233
|
grantConsent('session-misc-old', 'blank-smoke', 'succeed');
|
|
1065
1234
|
const succOut = await runSuccession({
|
|
1066
1235
|
cfg: resolveConfig({ workspaceRoot: WS }),
|
|
1067
|
-
roleOf: () => ({ role: 'misc', task: '
|
|
1236
|
+
roleOf: () => ({ role: 'misc', task: '示例任务', project: '' }),
|
|
1068
1237
|
selfId: () => 'session-misc-old',
|
|
1069
1238
|
selfCwd: () => miscWs,
|
|
1070
1239
|
handoverFor: () => ({ path: miscHandover, reason: '' }),
|
|
@@ -1078,9 +1247,9 @@ ok(/工作区/.test(newErr) && /已注册的工作区/.test(newErr), '新建会
|
|
|
1078
1247
|
const madeArgs = seen.creates[seen.creates.length - 1] || {};
|
|
1079
1248
|
ok(madeArgs.workspaceId === 'ws-misc', '杂项接任者应落回本会话所在的工作区', JSON.stringify(madeArgs));
|
|
1080
1249
|
const p = seen.prompts[seen.prompts.length - 1] || '';
|
|
1081
|
-
ok(new RegExp('.{0,1}
|
|
1250
|
+
ok(new RegExp('.{0,1}示例任务[\\\\/]Handover\\.md').test(p), '接任提示末尾应带本任务交接路径', p.slice(-320));
|
|
1082
1251
|
ok(p.indexOf('session-global-cur') !== -1, '接任提示的「全局 id」应是现任全局', p.slice(-320));
|
|
1083
|
-
ok(p.indexOf('session-misc-old') === -1, '
|
|
1252
|
+
ok(p.indexOf('session-misc-old') === -1, '接任提示不得把旧会话当全局', p.slice(-320));
|
|
1084
1253
|
}
|
|
1085
1254
|
}
|
|
1086
1255
|
|