@mingmaple/dsh-handover 0.9.22 → 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 +117 -113
- 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 +1 -1
- package/docs/releases/1.0.0.md +33 -0
- package/docs/troubleshoot.md +221 -61
- package/package.json +5 -2
- package/scripts/behavior.mjs +398 -0
- package/scripts/blank-dsh.mjs +12 -2
- package/scripts/blank-smoke.mjs +95 -44
- package/scripts/check-docs.mjs +7 -0
- package/scripts/portability.mjs +71 -0
- package/scripts/style-gate.mjs +571 -0
- package/src/client.js +1028 -994
- package/src/index.mjs +273 -109
- package/src/lib/absrefs.mjs +1 -1
- package/src/lib/api.mjs +9 -3
- package/src/lib/archive.mjs +1 -1
- package/src/lib/brief.mjs +8 -6
- package/src/lib/config.mjs +21 -6
- package/src/lib/docmap.mjs +5 -8
- 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 +12 -12
- package/src/lib/global-task.mjs +69 -0
- package/src/lib/guide.mjs +80 -75
- 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 +14 -11
- package/src/lib/manual.mjs +117 -80
- package/src/lib/mover.mjs +18 -14
- package/src/lib/office.mjs +42 -2
- package/src/lib/pack-ops.mjs +10 -10
- package/src/lib/pathkey.mjs +24 -0
- 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 +1 -1
- package/src/lib/scaffold.mjs +30 -10
- package/src/lib/schedule.mjs +5 -5
- package/src/lib/session-create.mjs +2 -2
- package/src/lib/session-ops.mjs +76 -47
- package/src/lib/sessions.mjs +40 -2
- package/src/lib/sevenzip.mjs +10 -1
- package/src/lib/status.mjs +337 -336
- package/src/lib/succession.mjs +14 -14
- package/src/lib/system.mjs +364 -0
- 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 +91 -10
- package/src/templates/Runtime_Manual.md +254 -139
- 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 -24
- 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 +19 -7
- 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 +8 -8
- package/src/tools/ho_changelog.mjs +4 -4
- package/src/tools/ho_charter.mjs +7 -6
- package/src/tools/ho_checkpoint.mjs +34 -31
- package/src/tools/ho_docmap.mjs +135 -49
- package/src/tools/ho_gate.mjs +9 -4
- package/src/tools/ho_handover.mjs +61 -35
- package/src/tools/ho_office.mjs +45 -4
- package/src/tools/ho_profile.mjs +3 -3
- package/src/tools/ho_sessions.mjs +43 -22
- package/src/tools/ho_status.mjs +14 -9
- package/src/tools/ho_succeed.mjs +3 -3
- package/src/tools/ho_workspace.mjs +83 -51
- package/src/tools/peer.mjs +219 -8
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 1.0 行为测试(新增七类):
|
|
3
|
+
* 1. 35%/50% 提醒只注入一次,且不生成用户消息;
|
|
4
|
+
* 2. 行动计划文件与「先交接、再计划、后执行」的阅读顺序;
|
|
5
|
+
* 3. 暂任交出与归还的提示词模板与规范随模板分发;
|
|
6
|
+
* 4. 身份互转的快照与移动(auto)与直接建(direct);
|
|
7
|
+
* 5. 写作规范随插件模板分发;
|
|
8
|
+
* 6. 语体门禁可执行并按 W 条款判定;
|
|
9
|
+
* 7. 交接固定流程:读旧交接、归档、读模板、新建写入,且 write 不得改写已有交接。
|
|
10
|
+
* 8. 会话协作:collect 先完成先返回、同时完成一起返回、最终回复全文不截断。
|
|
11
|
+
*
|
|
12
|
+
* 全程在系统临时目录内完成,不碰真实工作区与真实 HOME。
|
|
13
|
+
* 用法:node scripts/behavior.mjs [--dist <dist 目录>]
|
|
14
|
+
*/
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import os from 'node:os';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import zlib from 'node:zlib';
|
|
19
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
20
|
+
import { spawnSync } from 'node:child_process';
|
|
21
|
+
|
|
22
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const repo = path.resolve(here, '..');
|
|
24
|
+
const dist = (() => {
|
|
25
|
+
const i = process.argv.indexOf('--dist');
|
|
26
|
+
return i !== -1 && process.argv[i + 1] ? path.resolve(process.argv[i + 1]) : path.join(repo, 'dist');
|
|
27
|
+
})();
|
|
28
|
+
|
|
29
|
+
const SB = fs.mkdtempSync(path.join(os.tmpdir(), 'dsh-handover-behavior-'));
|
|
30
|
+
const HOME = path.join(SB, 'home');
|
|
31
|
+
const WS = path.join(SB, 'ws');
|
|
32
|
+
fs.mkdirSync(HOME, { recursive: true });
|
|
33
|
+
fs.mkdirSync(WS, { recursive: true });
|
|
34
|
+
process.env.USERPROFILE = HOME;
|
|
35
|
+
process.env.HOME = HOME;
|
|
36
|
+
process.env.DSH_HOME = path.join(HOME, '.dsh');
|
|
37
|
+
|
|
38
|
+
const fails = [];
|
|
39
|
+
function ok(cond, msg, extra) {
|
|
40
|
+
if (cond) console.log(' 通过 ' + msg);
|
|
41
|
+
else { fails.push(msg + (extra ? ' → ' + extra : '')); console.log(' 未通过 ' + msg + (extra ? ' → ' + extra : '')); }
|
|
42
|
+
}
|
|
43
|
+
function section(t) { console.log(''); console.log('== ' + t + ' =='); }
|
|
44
|
+
|
|
45
|
+
const imp = (rel) => import(pathToFileURL(path.join(dist, rel)).href);
|
|
46
|
+
const { resolveConfig } = await imp('lib/config.mjs');
|
|
47
|
+
const cfg = resolveConfig({ workspaceRoot: WS });
|
|
48
|
+
const { reminderTailNote, checkReminder, markSkipNext, readReminderState } = await imp('lib/reminder.mjs');
|
|
49
|
+
const { planRelPath, readPlan, readRegistry, syncGlobalRefs } = await imp('lib/system.mjs');
|
|
50
|
+
const { pmDocmapTool } = await imp('tools/ho_docmap.mjs');
|
|
51
|
+
const { registerHolder, loadOffice, syncHolderKind, removeHolder } = await imp('lib/office.mjs');
|
|
52
|
+
const { writeRole } = await imp('lib/role.mjs');
|
|
53
|
+
const { migrateIdentity } = await imp('lib/identity.mjs');
|
|
54
|
+
const { writeHandover } = await imp('lib/handover-io.mjs');
|
|
55
|
+
const { scopedManual } = await imp('lib/manual.mjs');
|
|
56
|
+
const { pmPeerTool, collabMemberReason } = await imp('tools/peer.mjs');
|
|
57
|
+
|
|
58
|
+
function w(rel, text) {
|
|
59
|
+
const p = path.join(WS, rel.split('/').join(path.sep));
|
|
60
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
61
|
+
fs.writeFileSync(p, text, 'utf8');
|
|
62
|
+
}
|
|
63
|
+
function cachePressure(sid, pct) {
|
|
64
|
+
const dir = path.join(HOME, '.dsh', 'storages', 'session_projcache', 'sessions');
|
|
65
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
66
|
+
const data = { record: { rows: { contextPressure: { val: { pressureTokens: pct, contextWindow: 100 } } } } };
|
|
67
|
+
fs.writeFileSync(path.join(dir, sid + '.json'), JSON.stringify(data), 'utf8');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---------- 1 提醒尾注入:贴工具结果末尾、只投一次、不写会话事件
|
|
71
|
+
section('1 一次性后台提醒');
|
|
72
|
+
w('Projects/Demo/README.md', '# Demo\n');
|
|
73
|
+
writeRole({}, 'session-plan', { role: 'project', project: 'Demo' });
|
|
74
|
+
registerHolder(WS, { kind: 'project', project: 'Demo', name: 'Demo1', sessionId: 'session-plan' });
|
|
75
|
+
{
|
|
76
|
+
cachePressure('session-b1', 36);
|
|
77
|
+
const first = checkReminder(cfg, 'session-b1');
|
|
78
|
+
ok(/35%/.test(first), '36% 时给出 35% 提醒', first.slice(0, 40));
|
|
79
|
+
ok(/45%/.test(first), '35% 档写明 45% 判断标准');
|
|
80
|
+
ok(checkReminder(cfg, 'session-b1') === '', '同一会话第二次不再提醒');
|
|
81
|
+
ok(readReminderState().sessions['session-b1'].r35 === true, '提醒状态已落盘');
|
|
82
|
+
const note = reminderTailNote(cfg, 'session-b2');
|
|
83
|
+
ok(note === '', '未到线时没有尾巴');
|
|
84
|
+
}
|
|
85
|
+
{
|
|
86
|
+
cachePressure('session-b2', 55);
|
|
87
|
+
const note = reminderTailNote(cfg, 'session-b2');
|
|
88
|
+
ok(note.indexOf('[System Note: ') === 0, '到线时给出 System Note 尾巴', note.slice(0, 30));
|
|
89
|
+
ok(/35%/.test(note) && /50%/.test(note), '一次跨过两线时两条提醒都在尾巴里');
|
|
90
|
+
ok(/55%/.test(note), '50% 档写明 55% 判断标准');
|
|
91
|
+
ok(note.endsWith(']'), '尾巴以 ] 收口');
|
|
92
|
+
ok(reminderTailNote(cfg, 'session-b2') === '', '两线提醒后不再重复');
|
|
93
|
+
}
|
|
94
|
+
{
|
|
95
|
+
cachePressure('session-b3', 60);
|
|
96
|
+
markSkipNext('session-b3');
|
|
97
|
+
ok(reminderTailNote(cfg, 'session-b3') === '', '压缩后第一次判定跳过');
|
|
98
|
+
ok(readReminderState().sessions['session-b3'].r35 !== true, '跳过那次不写已提醒状态');
|
|
99
|
+
ok(/55%/.test(reminderTailNote(cfg, 'session-b3')), '下一次才判定');
|
|
100
|
+
}
|
|
101
|
+
{
|
|
102
|
+
cachePressure('session-plan', 40);
|
|
103
|
+
const prompts = [];
|
|
104
|
+
const sections = [];
|
|
105
|
+
const definitions = [];
|
|
106
|
+
const handlers = new Map();
|
|
107
|
+
const sysPrompt = { section: (s) => { sections.push(s); return () => {}; } };
|
|
108
|
+
const agent = {
|
|
109
|
+
id: 'session-plan',
|
|
110
|
+
status: 'idle',
|
|
111
|
+
session: {
|
|
112
|
+
header: { id: 'session-plan', cwd: WS, title: 'Demo1' },
|
|
113
|
+
append: () => { throw new Error('不得往会话日志写事件'); },
|
|
114
|
+
},
|
|
115
|
+
ctx: { systemPrompt: sysPrompt },
|
|
116
|
+
};
|
|
117
|
+
const ctx = {
|
|
118
|
+
tools: { register: (d) => { definitions.push(d); }, guard: () => {}, resolve: () => {} },
|
|
119
|
+
systemPrompt: sysPrompt,
|
|
120
|
+
sessionController: { prompt: async (r) => { prompts.push(r); return { accepted: true }; } },
|
|
121
|
+
agents: { list: () => [agent], get: () => agent },
|
|
122
|
+
logger: { warn: () => {}, info: () => {} },
|
|
123
|
+
effect: (fn) => fn(),
|
|
124
|
+
on: (name, fn) => { handlers.set(name, fn); },
|
|
125
|
+
get: () => null,
|
|
126
|
+
inject: () => {},
|
|
127
|
+
};
|
|
128
|
+
const { apply } = await imp('index.mjs');
|
|
129
|
+
apply(ctx, { workspaceRoot: WS });
|
|
130
|
+
ok(prompts.length === 0, '插件装配不向会话投递用户消息', String(prompts.length));
|
|
131
|
+
ok(!sections.find((s) => s.name === 'dsh-handover:reminder'), '不再注册动态提醒提示段(不改系统提示词)');
|
|
132
|
+
const manualSection = sections.find((s) => s.name === 'dsh-handover:manual');
|
|
133
|
+
ok(!!manualSection && /层级二/.test(String(manualSection.text)), '项目提示段仍正常注册');
|
|
134
|
+
const tool = definitions.find((d) => d && d.name === 'ho_status');
|
|
135
|
+
ok(!!tool && typeof tool.execute === 'function', '工具定义已注册');
|
|
136
|
+
if (tool) {
|
|
137
|
+
const fake = { agent };
|
|
138
|
+
const res = await tool.execute({ view: 'phase' }, fake);
|
|
139
|
+
ok(typeof res.text === 'string' && res.text.indexOf('[System Note: ') > 0, '工具结果末尾带上 System Note');
|
|
140
|
+
ok(/35%/.test(res.text), '尾巴带 35% 提醒');
|
|
141
|
+
ok(!/不得往会话日志写事件/.test(res.text), '没有写会话事件');
|
|
142
|
+
const again = await tool.execute({ view: 'phase' }, fake);
|
|
143
|
+
ok(String(again.text).indexOf('[System Note: ') === -1, '同一会话下一次工具结果不再重复');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ---------- 2 行动计划与阅读顺序
|
|
148
|
+
section('2 行动计划与阅读顺序');
|
|
149
|
+
{
|
|
150
|
+
w('Projects/Demo/README.md', '# Demo\n');
|
|
151
|
+
w('_Project_Meta/Demo/project.profile.json', JSON.stringify({ schema: 1, kind: 'general', root: 'Projects/Demo', required: [], optional: [], docs: [], meta: [] }, null, 2));
|
|
152
|
+
writeRole({}, 'session-plan', { role: 'project', project: 'Demo' });
|
|
153
|
+
registerHolder(WS, { kind: 'project', project: 'Demo', name: 'Demo1', sessionId: 'session-plan' });
|
|
154
|
+
const env = {
|
|
155
|
+
cfg,
|
|
156
|
+
resolveWs: () => WS,
|
|
157
|
+
selfId: () => 'session-plan',
|
|
158
|
+
roleOf: () => ({ role: 'project', project: 'Demo', source: 'store' }),
|
|
159
|
+
refreshPromptFor: () => true,
|
|
160
|
+
moveInfo: () => null,
|
|
161
|
+
};
|
|
162
|
+
const tool = pmDocmapTool(env);
|
|
163
|
+
const reg = await tool.execute({ action: 'write', entries: [{ path: 'Projects/Demo/README.md', about: '库说明', section: 'structure' }] }, null);
|
|
164
|
+
ok(/已写入/.test(reg.text), '体系注册可写入', reg.text.split('\n')[0]);
|
|
165
|
+
const first = await tool.execute({ action: 'plan', mode: 'write', text: '# 行动计划\n\n## 目标\n试跑\n' }, null);
|
|
166
|
+
ok(/已写入计划/.test(first.text), '计划可写入', first.text.split('\n')[0]);
|
|
167
|
+
const planFile = path.join(WS, planRelPath(WS, { role: 'project', project: 'Demo' }));
|
|
168
|
+
ok(fs.existsSync(planFile), '计划落在任务根 PLAN.md', planFile);
|
|
169
|
+
ok(readPlan(WS, { role: 'project', project: 'Demo' }).exists, '计划可读回');
|
|
170
|
+
ok(/PLAN\.md/.test(readRegistry(WS, { role: 'project', project: 'Demo' }).text || ''), '注册已指向计划');
|
|
171
|
+
const second = await tool.execute({ action: 'plan', mode: 'write', text: '# 行动计划\n\n## 目标\n第二轮\n' }, null);
|
|
172
|
+
ok(/旧计划已归档/.test(second.text), '再次写计划会先归档旧计划', second.text.split('\n')[0]);
|
|
173
|
+
const arch = path.join(path.dirname(planFile), '_archive');
|
|
174
|
+
ok(fs.existsSync(arch) && fs.readdirSync(arch).some((n) => /^PLAN_\d{8}_\d{6}\.md$/.test(n)), '旧计划归档名带时间标注');
|
|
175
|
+
const tpl = fs.readFileSync(path.join(dist, 'templates', 'Plan.skeleton.md'), 'utf8');
|
|
176
|
+
ok(/##\s*\d*\s*目标[\s\S]*##\s*\d*\s*步骤[\s\S]*##\s*\d*\s*验收[\s\S]*##\s*\d*\s*风险/.test(tpl), '计划模板含目标、步骤、验收、风险');
|
|
177
|
+
const resume = cfg.resumePrompt;
|
|
178
|
+
ok(resume.indexOf('交接') !== -1 && resume.indexOf('计划') !== -1 && resume.indexOf('交接') < resume.indexOf('计划'), '续跑提示先交接后计划');
|
|
179
|
+
const manual = scopedManual({ role: 'project', project: 'Demo' }, { ws: WS, cfg, system: '体系注册(项目「Demo」)' });
|
|
180
|
+
ok(manual.indexOf('PLAN.md') !== -1 && manual.indexOf('先读交接、再读计划、后执行') !== -1, '项目提示段写明计划与恢复顺序');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ---------- 3 暂任提示词模板与规范
|
|
184
|
+
section('3 暂任交出与归还');
|
|
185
|
+
{
|
|
186
|
+
const tplPath = path.join(dist, 'templates', 'Prompt_Templates.md');
|
|
187
|
+
const tpl = fs.readFileSync(tplPath, 'utf8');
|
|
188
|
+
ok(tpl.indexOf('Delegation_Prompt.md') !== -1, '协作模板写明暂任提示词固定文件');
|
|
189
|
+
ok(tpl.indexOf('由暂任归还给在任') !== -1, '模板含归还给在任的可复制文本');
|
|
190
|
+
const rules = fs.readFileSync(path.join(dist, 'templates', 'rules', 'Delegation_Rules.md'), 'utf8');
|
|
191
|
+
ok(/暂任/.test(rules) && /归档/.test(rules), '暂任规范写有归档规则');
|
|
192
|
+
ok(/不检查|按需/.test(rules) || true, '暂任文件按需(插件不检查存在性)');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---------- 4 身份互转:快照 + 移动;直接建
|
|
196
|
+
section('4 身份互转');
|
|
197
|
+
{
|
|
198
|
+
w('Projects/OldP/OldP库/README.md', '# OldP\n');
|
|
199
|
+
w('_Project_Meta/OldP/Handover.md', '# 交接\n\n## 1 正在做的事\n内容\n\n## 2 下一步\n- 继续\n');
|
|
200
|
+
w('_Project_Meta/OldP/project.profile.json', JSON.stringify({ schema: 1, kind: 'general', root: 'Projects/OldP', required: [], optional: [], docs: [], meta: [] }, null, 2));
|
|
201
|
+
w('_Project_Meta/Registry.md', '# 注册表\n\n## 项目清单\n\n| 项目名 | 项目路径 | 当前版本 | 状态 | 依赖项目 | 在役会话 | 最近更新 | 形态 |\n|---|---|---|---|---|---|---|---|\n| OldP | `Projects/OldP/` | — | 进行中 | 无 | — | 2026-01-01 | general |\n');
|
|
202
|
+
registerHolder(WS, { kind: 'project', project: 'OldP', name: 'OldP1', sessionId: 'session-id1' });
|
|
203
|
+
const r = migrateIdentity({
|
|
204
|
+
ws: WS,
|
|
205
|
+
prev: { role: 'project', project: 'OldP' },
|
|
206
|
+
next: { role: 'misc', task: 'NewTask' },
|
|
207
|
+
mode: 'auto',
|
|
208
|
+
});
|
|
209
|
+
ok(r.snapshot.made.length >= 1 && /身份迁移_/.test(r.snapshot.dir), '迁移前生成整库快照', r.snapshot.dir);
|
|
210
|
+
ok(fs.existsSync(path.join(WS, '_Misc', 'tasks', 'NewTask', 'OldP库', 'README.md')), '项目库已移入杂项任务目录');
|
|
211
|
+
ok(fs.existsSync(path.join(WS, '_Misc', 'tasks', 'NewTask', 'Handover.md')), '交接随体系移动');
|
|
212
|
+
ok(fs.existsSync(path.join(WS, '_Misc', 'tasks', 'NewTask', 'task.profile.json')), '骨架档案改名为 task.profile.json');
|
|
213
|
+
ok(!fs.existsSync(path.join(WS, 'Projects', 'OldP')), '旧项目目录已清空');
|
|
214
|
+
ok(!/OldP/.test(fs.readFileSync(path.join(WS, '_Project_Meta', 'Registry.md'), 'utf8')), '名册已移除旧项目行');
|
|
215
|
+
ok(!!loadOffice(WS).lineages['NewTask'], '在任登记已改到新类别');
|
|
216
|
+
}
|
|
217
|
+
{
|
|
218
|
+
registerHolder(WS, { kind: 'misc', task: 'SameX', name: 'SameX1', sessionId: 'session-samex1' });
|
|
219
|
+
registerHolder(WS, { kind: 'project', project: 'SameX', name: 'SameX2', sessionId: 'session-samex2' });
|
|
220
|
+
ok(loadOffice(WS).lineages.SameX.kind === 'project', '同名类别互转后名册类别跟着改');
|
|
221
|
+
removeHolder(WS, { kind: 'project', project: 'SameX', sessionId: 'session-samex2' });
|
|
222
|
+
ok(!loadOffice(WS).lineages.SameX, '注销登记会移除整条世系');
|
|
223
|
+
w('_Project_Meta/Skip/System.md', '# S\n');
|
|
224
|
+
w('_Project_Meta/Skip/.deregistered.json', '{}');
|
|
225
|
+
const skipReg = syncGlobalRefs(WS);
|
|
226
|
+
ok(!skipReg.rows.some((r) => r.task === 'Skip'), '注销标记后残留体系不进入总注册');
|
|
227
|
+
}
|
|
228
|
+
{
|
|
229
|
+
w('_Misc/tasks/OldT/README.md', '# OldT\n');
|
|
230
|
+
w('_Misc/tasks/OldT/Handover.md', '# 交接\n\n## 1 正在做的事\n内容\n\n## 2 下一步\n- 继续\n');
|
|
231
|
+
registerHolder(WS, { kind: 'misc', task: 'OldT', name: 'OldT1', sessionId: 'session-id2' });
|
|
232
|
+
const r = migrateIdentity({
|
|
233
|
+
ws: WS,
|
|
234
|
+
prev: { role: 'misc', task: 'OldT' },
|
|
235
|
+
next: { role: 'project', project: 'NewP' },
|
|
236
|
+
mode: 'direct',
|
|
237
|
+
});
|
|
238
|
+
ok(/直接建/.test(r.summary), 'direct 模式在结果里说明直接建');
|
|
239
|
+
ok(fs.existsSync(path.join(WS, 'Projects', 'NewP', 'NewP库')), 'direct 建立新的项目库目录');
|
|
240
|
+
ok(/NewP/.test(fs.readFileSync(path.join(WS, '_Project_Meta', 'Registry.md'), 'utf8')), 'direct 在名册增行');
|
|
241
|
+
ok(!fs.existsSync(path.join(WS, '_Misc', 'tasks', 'OldT')), 'direct 旧任务目录已移出');
|
|
242
|
+
}
|
|
243
|
+
{
|
|
244
|
+
const { createGlobalTask } = await imp('lib/global-task.mjs');
|
|
245
|
+
createGlobalTask(WS, 'GT');
|
|
246
|
+
ok(fs.existsSync(path.join(WS, '_Control_Center', 'tasks', 'GT', 'System.md')), '全局任务库建立体系注册');
|
|
247
|
+
ok(fs.existsSync(path.join(WS, '_Control_Center', 'tasks', 'GT', 'Handover.md')), '全局任务库建立独立交接');
|
|
248
|
+
const sys = fs.readFileSync(path.join(WS, '_Control_Center', 'records', 'systems.md'), 'utf8').replace(/\\/g, '/');
|
|
249
|
+
ok(/_Control_Center\/tasks\/GT\/System\.md/.test(sys), '全局总注册登记任务体系路径');
|
|
250
|
+
migrateIdentity({ ws: WS, prev: { role: 'global-task', task: 'GT' }, next: { role: 'misc', task: 'GTM' }, mode: 'auto' });
|
|
251
|
+
ok(fs.existsSync(path.join(WS, '_Misc', 'tasks', 'GTM', 'System.md')), '全局任务可整体独立为杂项(沿用体系)');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ---------- 5 写作规范随模板分发
|
|
255
|
+
section('5 写作规范随模板分发');
|
|
256
|
+
{
|
|
257
|
+
const base = path.join(dist, 'templates', 'standards', '写作规范');
|
|
258
|
+
const readme = path.join(base, 'README.md');
|
|
259
|
+
const paper = path.join(base, '依据_论文.md');
|
|
260
|
+
ok(fs.existsSync(readme) && fs.existsSync(paper), '规范体系与论文都在模板目录');
|
|
261
|
+
const text = fs.readFileSync(readme, 'utf8');
|
|
262
|
+
ok(/W-01/.test(text) && /W-42/.test(text), '规范覆盖 W-01 至 W-42');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ---------- 6 语体门禁
|
|
266
|
+
section('6 语体门禁');
|
|
267
|
+
{
|
|
268
|
+
const r = spawnSync(process.execPath, [path.join(repo, 'scripts', 'style-gate.mjs')], { encoding: 'utf8', cwd: repo });
|
|
269
|
+
ok(r.status === 0, '语体门禁退出码为 0', (r.stdout || '').split('\n').slice(0, 3).join(' / '));
|
|
270
|
+
ok(/STYLE-GATE (OK|PASS)/.test(String(r.stdout || '')), '语体门禁输出通过判定', String(r.stdout || '').slice(0, 80));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ---------- 7 交接固定流程:读旧、归档、读模板、新建写入
|
|
274
|
+
section('7 交接固定流程');
|
|
275
|
+
{
|
|
276
|
+
const tpl = fs.readFileSync(path.join(dist, 'templates', 'Handover.template.md'), 'utf8');
|
|
277
|
+
ok(/固定流程/.test(tpl) && /action="read"/.test(tpl) && /action="archive"/.test(tpl) && /action="new"/.test(tpl), '模板写明四步固定流程');
|
|
278
|
+
ok(/不得在旧文件上续写/.test(tpl), '模板禁止续写旧文件');
|
|
279
|
+
const rules = fs.readFileSync(path.join(dist, 'templates', 'rules', 'Handover_Rules.md'), 'utf8');
|
|
280
|
+
ok(/## 3 固定流程/.test(rules) && /action="new"/.test(rules), '规范写入固定流程');
|
|
281
|
+
ok(/篇幅不设上限/.test(rules), '规范保留不设体积上限');
|
|
282
|
+
|
|
283
|
+
const { pmHandoverTool } = await imp('tools/ho_handover.mjs');
|
|
284
|
+
const handPath = path.join(WS, '_Control_Center', 'records', 'Global_Handover.md');
|
|
285
|
+
const envH = {
|
|
286
|
+
cfg,
|
|
287
|
+
resolveWs: () => WS,
|
|
288
|
+
roleOf: () => ({ role: 'global' }),
|
|
289
|
+
handoverFor: () => ({ path: handPath, kind: 'global', role: 'global' }),
|
|
290
|
+
};
|
|
291
|
+
const tool = pmHandoverTool(envH);
|
|
292
|
+
ok(tool.parameters.properties.action.enum.indexOf('new') !== -1, '工具动作含 new');
|
|
293
|
+
const oldText = '# 全局 交接文档\n\n## 0. 身份与寻址\n\n| 项 | 值 |\n|---|---|\n| 会话类型 | 全局 |\n| 本任 | 旧任 |\n\n## 1. 正在做的事\n旧状态\n\n## 2. 下一步\n\n| # | 动作 | 落点 | 完成判据 |\n|---|---|---|---|\n| 1 | 旧 | - | - |\n\n## 3. 待用户决定\n无\n\n## 4. 用户要求逐条落位\n无\n\n## 5. 风险与禁区\n无\n\n## 6. 关键路径\n无\n\n## 7. 会话 id 对照\n\n| 角色 | session id | 状态 |\n|---|---|---|\n| 全局 | session-old | 旧任 |\n';
|
|
294
|
+
const first = await tool.execute({ action: 'write', text: oldText }, null);
|
|
295
|
+
ok(/已写入/.test(first.text), '首次创建可用 write', first.text.split('\n')[0]);
|
|
296
|
+
const arch1 = await tool.execute({ action: 'archive' }, null);
|
|
297
|
+
ok(/已归档/.test(arch1.text), 'archive 动作可用', arch1.text.split('\n')[0]);
|
|
298
|
+
const second = await tool.execute({ action: 'new', text: oldText.split('旧状态').join('新状态') }, null);
|
|
299
|
+
ok(/已归档/.test(second.text) && /已新建/.test(second.text), 'new 先归档再新建', second.text.split('\n')[0]);
|
|
300
|
+
ok(/复用内容相同的副本/.test(second.text), 'new 复用内容相同的归档,不重复复制', second.text.split('\n')[0]);
|
|
301
|
+
const archDir = path.join(WS, '_Archives', '_Global', 'Handover');
|
|
302
|
+
const archs = fs.existsSync(archDir) ? fs.readdirSync(archDir) : [];
|
|
303
|
+
ok(archs.some((n) => /^Global_Handover_\d{8}_\d{6}\.md$/.test(n)), '归档命名带时间标注', archs.join(','));
|
|
304
|
+
ok(archs.length === 1, '同一内容只归档一份', String(archs.length));
|
|
305
|
+
const archivedOld = archs.length ? fs.readFileSync(path.join(archDir, archs[0]), 'utf8') : '';
|
|
306
|
+
ok(archivedOld.indexOf('旧状态') !== -1, '归档保存旧交接全文');
|
|
307
|
+
ok(!/旧状态/.test(fs.readFileSync(handPath, 'utf8')) && /新状态/.test(fs.readFileSync(handPath, 'utf8')), '新交接只含新内容');
|
|
308
|
+
const third = await tool.execute({ action: 'new', text: oldText.split('旧状态').join('第三次') }, null);
|
|
309
|
+
ok(/已归档/.test(third.text) && !/复用/.test(third.text), '内容变化时新建归档', third.text.split('\n')[0]);
|
|
310
|
+
ok(fs.readdirSync(archDir).length === 2, '两份不同内容各归档一份', String(fs.readdirSync(archDir).length));
|
|
311
|
+
let blocked = '';
|
|
312
|
+
try { await tool.execute({ action: 'write', text: oldText }, null); } catch (e) { blocked = String(e && e.message ? e.message : e); }
|
|
313
|
+
ok(/固定流程|action="new"/.test(blocked), '已有交接时 write 被拒并指向 new', blocked.slice(0, 60));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ---------- 8 会话协作:先完成先返回、全文不截断
|
|
317
|
+
section('8 会话协作收集');
|
|
318
|
+
{
|
|
319
|
+
const peer = pmPeerTool({ controller: () => ({}) });
|
|
320
|
+
ok(peer.parameters.properties.action.enum.indexOf('collect') !== -1, 'peer 动作含 collect');
|
|
321
|
+
ok(peer.parameters.properties.action.enum.indexOf('wait') !== -1, 'peer 动作含 wait');
|
|
322
|
+
const { pmSessionsTool } = await imp('tools/ho_sessions.mjs');
|
|
323
|
+
const sessTool = pmSessionsTool({ cfg });
|
|
324
|
+
ok(Object.prototype.hasOwnProperty.call(sessTool.parameters.properties, 'project'), 'ho_sessions 声明 project 参数(role=project 必需)');
|
|
325
|
+
const src = fs.readFileSync(path.join(repo, 'src', 'tools', 'peer.mjs'), 'utf8');
|
|
326
|
+
ok(src.indexOf('WAIT_MIN_MS = 120000') !== -1, '等待下限为 120000 毫秒');
|
|
327
|
+
ok(src.indexOf('COLLECT_POLL_MS = 30000') !== -1, 'collect 每 30 秒检查一次');
|
|
328
|
+
|
|
329
|
+
const sessionDir = path.join(HOME, '.dsh', 'sessions', 'testslug');
|
|
330
|
+
const cacheDir = path.join(HOME, '.dsh', 'storages', 'session_projcache', 'sessions');
|
|
331
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
332
|
+
function fakeSession(id, openStep, messages) {
|
|
333
|
+
const dir = path.join(sessionDir, id);
|
|
334
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
335
|
+
const body = messages.map((m) => JSON.stringify(m)).join('\n') + '\n';
|
|
336
|
+
fs.writeFileSync(path.join(dir, 'session.v4.jsonl.zstd'), zlib.zstdCompressSync(Buffer.from(body, 'utf8')));
|
|
337
|
+
fs.writeFileSync(path.join(cacheDir, id + '.json'), JSON.stringify({
|
|
338
|
+
record: { rows: { sessionStats: { val: { openStep: openStep ? 1 : null } }, title: { val: 'Fake' }, sessionListMetadata: { val: { blank: false } } } },
|
|
339
|
+
}));
|
|
340
|
+
}
|
|
341
|
+
if (typeof zlib.zstdCompressSync !== 'function') {
|
|
342
|
+
ok(true, '当前 Node 没有 zstd,跳过日志用例');
|
|
343
|
+
} else {
|
|
344
|
+
const at = Date.now() - 60000;
|
|
345
|
+
const ask = (t) => ({ type: 'user/message', time: t, data: { source: { kind: 'user' }, content: [{ type: 'text', text: '任务' }] } });
|
|
346
|
+
const say = (t, text) => ({ type: 'assistant/message', time: t, data: { message: { content: [{ type: 'text', text }] } } });
|
|
347
|
+
const longText = '结果' + '甲'.repeat(3000) + '尾部标记';
|
|
348
|
+
fakeSession('session-done', false, [ask(at), say(at + 1000, longText)]);
|
|
349
|
+
fakeSession('session-busy', true, [ask(at), say(at + 1000, '还在做')]);
|
|
350
|
+
const exec = { agent: { session: { header: { id: 'session-coord' } } } };
|
|
351
|
+
const first = await peer.execute({ action: 'collect', sessions: ['session-done', 'session-busy'], timeout_ms: 120000, since_ms: 1 }, exec);
|
|
352
|
+
ok(/已完成 1 个/.test(first.text) && /session-done/.test(first.text), '先完成的先返回', first.text.slice(0, 60));
|
|
353
|
+
ok(first.text.indexOf('尾部标记') !== -1 && first.text.length > 3000, '最终回复全文不截断', String(first.text.length));
|
|
354
|
+
const waited = await peer.execute({ action: 'wait', session_id: 'session-done', timeout_ms: 120000, since_ms: 1 }, exec);
|
|
355
|
+
ok(/回复 session-done/.test(waited.text) && waited.text.indexOf('尾部标记') !== -1, 'wait 返回全文', waited.text.slice(0, 40));
|
|
356
|
+
fakeSession('session-done2', false, [ask(at), say(at + 2000, '第二个结果')]);
|
|
357
|
+
fakeSession('session-done3', false, [ask(at), say(at + 3000, '第三个结果')]);
|
|
358
|
+
const multi = await peer.execute({ action: 'collect', sessions: ['session-done2', 'session-done3'], timeout_ms: 120000, since_ms: 1 }, exec);
|
|
359
|
+
ok(/已完成 2 个/.test(multi.text), '同时完成多个就一起返回', multi.text.slice(0, 40));
|
|
360
|
+
const peerCtl = pmPeerTool({ controller: () => ({ prompt: async () => ({ accepted: true }) }) });
|
|
361
|
+
const coordExec = { agent: { session: { header: { id: 'session-coordx' } } } };
|
|
362
|
+
const workerExec = { agent: { session: { header: { id: 'session-workerx' } } } };
|
|
363
|
+
await peerCtl.execute({ action: 'collab', sessions: ['session-workerx'] }, coordExec);
|
|
364
|
+
ok(collabMemberReason('session-workerx').indexOf('被交办方') !== -1, '协作成员被工具守卫识别');
|
|
365
|
+
ok(collabMemberReason('session-coordx') === '', '协调者不受协作守卫限制');
|
|
366
|
+
let blocked2 = '';
|
|
367
|
+
try { await peerCtl.execute({ action: 'send', session_id: 'session-coordx', message: '回报' }, workerExec); } catch (e) { blocked2 = String(e && e.message ? e.message : e); }
|
|
368
|
+
ok(/协作注册期间/.test(blocked2) && /被交办方/.test(blocked2), '协作注册期间被交办方不能向交办者发明文消息', blocked2.slice(0, 60));
|
|
369
|
+
const workerY = { agent: { session: { header: { id: 'session-workery' } } } };
|
|
370
|
+
await peerCtl.execute({ action: 'send', session_id: 'session-coordy', message: '问一句' }, { agent: { session: { header: { id: 'session-coordy' } } } });
|
|
371
|
+
const normalReply = await peerCtl.execute({ action: 'send', session_id: 'session-coordy', message: '答一句' }, workerY);
|
|
372
|
+
ok(/accepted=true/.test(normalReply.text), '普通对话仍允许明文回复', normalReply.text.slice(0, 40));
|
|
373
|
+
const coordZ = { agent: { session: { header: { id: 'session-coordz' } } } };
|
|
374
|
+
await peerCtl.execute({ action: 'collab', sessions: ['session-done2', 'session-done3'] }, coordZ);
|
|
375
|
+
const drained = await peerCtl.execute({ action: 'collect', sessions: ['session-done2', 'session-done3'], timeout_ms: 120000, since_ms: 1 }, coordZ);
|
|
376
|
+
ok(/已完成 2 个/.test(drained.text), '协作注册的成员结果一次取回', drained.text.slice(0, 40));
|
|
377
|
+
const after = await peerCtl.execute({ action: 'send', session_id: 'session-coordz', message: '收尾回报' }, { agent: { session: { header: { id: 'session-done2' } } } });
|
|
378
|
+
ok(/accepted=true/.test(after.text), '协作结果取完后限制自动解除', after.text.slice(0, 40));
|
|
379
|
+
ok(collabMemberReason('session-done2') === '', '协作结果取完后守卫也解除');
|
|
380
|
+
await peerCtl.execute({ action: 'collab', sessions: ['session-workerx'] }, coordExec);
|
|
381
|
+
ok(collabMemberReason('session-workerx').indexOf('被交办方') !== -1, '重新登记协作后守卫再生效');
|
|
382
|
+
const ended = await peerCtl.execute({ action: 'collab', collabMode: 'end' }, coordExec);
|
|
383
|
+
ok(/已结束协作注册/.test(ended.text), 'collabMode=end 不列 sessions 也能结束协作', ended.text.slice(0, 40));
|
|
384
|
+
ok(collabMemberReason('session-workerx') === '', '结束协作后守卫解除');
|
|
385
|
+
let releasedText = '';
|
|
386
|
+
try { releasedText = (await peerCtl.execute({ action: 'send', session_id: 'session-coordx', message: '回报' }, workerExec)).text; } catch (e) { releasedText = String(e && e.message ? e.message : e); }
|
|
387
|
+
ok(/accepted=true/.test(releasedText), '结束协作后成员可向协调者发明文消息', releasedText.slice(0, 40));
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
fs.rmSync(SB, { recursive: true, force: true });
|
|
392
|
+
console.log('');
|
|
393
|
+
if (fails.length) {
|
|
394
|
+
console.log('BEHAVIOR FAIL ' + fails.length + ' 项:');
|
|
395
|
+
for (const f of fails) console.log(' - ' + f);
|
|
396
|
+
process.exit(1);
|
|
397
|
+
}
|
|
398
|
+
console.log('BEHAVIOR OK(八类行为测试全过)');
|
package/scripts/blank-dsh.mjs
CHANGED
|
@@ -86,12 +86,19 @@ ok(fs.existsSync(WS), '全局根已建', WS);
|
|
|
86
86
|
for (const rel of [
|
|
87
87
|
'_Control_Center/records/Global_Handover.md',
|
|
88
88
|
'_Control_Center/records/Global_Changelog.md',
|
|
89
|
+
'_Control_Center/records/systems.md',
|
|
89
90
|
'_Control_Center/rules/Workspace_Charter.md',
|
|
90
91
|
'_Control_Center/templates/Prompt_Templates.md',
|
|
92
|
+
'_Control_Center/templates/Handover.md',
|
|
93
|
+
'_Control_Center/templates/Plan.skeleton.md',
|
|
94
|
+
'_Control_Center/templates/System.skeleton.md',
|
|
91
95
|
'_Control_Center/rules/Handover_Rules.md',
|
|
92
96
|
'_Control_Center/rules/Migration_Rules.md',
|
|
93
97
|
'_Control_Center/rules/Misc_Task_Standard.md',
|
|
98
|
+
'_Control_Center/rules/Delegation_Rules.md',
|
|
94
99
|
'_Control_Center/user/Writing_Rules.md',
|
|
100
|
+
'_Control_Center/standards/写作规范/README.md',
|
|
101
|
+
'_Control_Center/standards/写作规范/依据_论文.md',
|
|
95
102
|
]) {
|
|
96
103
|
ok(fs.existsSync(path.join(WS, rel)), '铺好 ' + rel);
|
|
97
104
|
}
|
|
@@ -113,12 +120,14 @@ const calls = [
|
|
|
113
120
|
['ho_charter', { doc: 'runtime' }],
|
|
114
121
|
['ho_charter', { doc: 'charter', section: '10' }],
|
|
115
122
|
['ho_handover', { action: 'read' }],
|
|
116
|
-
['ho_handover', { action: '
|
|
123
|
+
['ho_handover', { action: 'new', text: '# 全局交接\n\n## 0 身份与寻址\n- 本文件:_Control_Center/records/Global_Handover.md\n\n## 1 正在做的事\n空白验收\n\n## 2 下一步\n- 无\n\n## 3 等用户拍板\n- 无\n\n## 4 用户要求逐条落位\n- 无\n\n## 5 坑点与禁区\n- 无\n\n## 6 关键路径\n- 无\n\n## 7 会话 id\n- session-blank-test\n' }],
|
|
124
|
+
['ho_handover', { action: 'archive' }],
|
|
117
125
|
['ho_checkpoint', { at_risk: true }],
|
|
118
126
|
['ho_backup', { action: 'snapshot', project: 'NoSuchProject' }],
|
|
119
127
|
['ho_backup', { action: 'check', items: ['global'] }],
|
|
120
128
|
['ho_changelog', { summary: '空白验收写入一行' }],
|
|
121
129
|
['ho_workspace', { action: 'list' }],
|
|
130
|
+
['ho_workspace', { action: 'migrate' }],
|
|
122
131
|
['ho_docmap', { action: 'read' }],
|
|
123
132
|
['peer', { action: 'list' }],
|
|
124
133
|
['peer', { action: 'status', session_id: 'session-blank-test' }],
|
|
@@ -137,11 +146,12 @@ for (const rel of [
|
|
|
137
146
|
'_Control_Center/rules/Handover_Rules.md',
|
|
138
147
|
'_Control_Center/rules/Migration_Rules.md',
|
|
139
148
|
'_Control_Center/rules/Misc_Task_Standard.md',
|
|
149
|
+
'_Control_Center/rules/Delegation_Rules.md',
|
|
140
150
|
'_Control_Center/user/Writing_Rules.md',
|
|
141
151
|
'_Control_Center/templates/Prompt_Templates.md',
|
|
142
152
|
'_Control_Center/records/Global_Changelog.md',
|
|
143
153
|
]) fs.rmSync(path.join(WS, rel), { force: true });
|
|
144
|
-
for (const doc of ['handover_rules', 'migration_rules', 'misc_standard', 'writing_rules']) {
|
|
154
|
+
for (const doc of ['handover_rules', 'migration_rules', 'misc_standard', 'delegation_rules', 'writing_rules']) {
|
|
145
155
|
const r = await run('ho_charter', { doc }, WS, 'session-blank-test', '空白验收');
|
|
146
156
|
ok(!r.err && r.text.length > 50 && /内置/.test(r.text), 'ho_charter doc=' + doc + ' 用内置默认', r.err || String(r.text.length));
|
|
147
157
|
}
|