@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
package/scripts/blank-smoke.mjs
CHANGED
|
@@ -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' }),
|
|
@@ -402,11 +418,11 @@ ok(tsU.allow.length === 0 && tsU.deny.length === 14, '普通会话不载入任
|
|
|
402
418
|
const rSub = resolveRole({ sessionId: 'session-sub', title: 'Demo1', cwd: path.join(WS, 'Projects', 'Demo', 'Demo库'), subagent: true });
|
|
403
419
|
ok(rSub.role === 'plain' && rSub.source === 'subagent', '子会话默认按普通处理,不按 cwd 判项目', JSON.stringify(rSub));
|
|
404
420
|
|
|
405
|
-
// ----------
|
|
421
|
+
// ---------- 体系注册与授权提示:写入权限、路径校验、空注册、注入、计划 ----------
|
|
406
422
|
{
|
|
407
423
|
const { pmDocmapTool } = await import(DIST('tools/ho_docmap.mjs'));
|
|
408
424
|
const { registerHolder } = await import(DIST('lib/office.mjs'));
|
|
409
|
-
const {
|
|
425
|
+
const { systemBlock, registryRelPath, readRegistry, planRelPath } = await import(DIST('lib/system.mjs'));
|
|
410
426
|
const manDm = await import(DIST('lib/manual.mjs'));
|
|
411
427
|
const { grantConsent, consumedConsent, activeConsentsFor, consentPromptLines } = await import(DIST('lib/consent.mjs'));
|
|
412
428
|
|
|
@@ -422,37 +438,42 @@ ok(rSub.role === 'plain' && rSub.source === 'subagent', '子会话默认按普
|
|
|
422
438
|
fs.writeFileSync(path.join(WS, 'Projects', 'Demo', 'README.md'), 'x', 'utf8');
|
|
423
439
|
registerHolder(WS, { kind: 'project', project: 'Demo', name: 'Demo1', sessionId: 'session-dm', workspace: WS });
|
|
424
440
|
const dm = pmDocmapTool(dmEnv);
|
|
425
|
-
const wroteDm = await dm.execute({ action: 'write', entries: [{ path: 'Projects/Demo/README.md', about: '项目简介', when: '动手前' }] }, dmExec('session-dm'));
|
|
426
|
-
ok(/已写入/.test(wroteDm.text), '
|
|
427
|
-
const readBack =
|
|
428
|
-
ok(readBack.exists && readBack.
|
|
429
|
-
const
|
|
430
|
-
ok(
|
|
431
|
-
const injectedDm = manDm.scopedManual(dmInfo, { ws: WS,
|
|
432
|
-
ok(injectedDm.indexOf('
|
|
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));
|
|
433
449
|
let missErr = '';
|
|
434
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); }
|
|
435
|
-
ok(/未写入/.test(missErr) && /nope\.md/.test(missErr), '
|
|
436
|
-
ok(
|
|
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), '行动计划:可读回');
|
|
437
458
|
registerHolder(WS, { kind: 'project', project: 'Demo', name: 'Demo2', sessionId: 'session-dm2', workspace: WS });
|
|
438
459
|
let oldErr = '';
|
|
439
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); }
|
|
440
|
-
ok(/只有在任会话可以写/.test(oldErr), '
|
|
461
|
+
ok(/只有在任会话可以写/.test(oldErr), '体系注册:已交出的旧会话只读', oldErr);
|
|
441
462
|
const emptyDm = await dm.execute({ action: 'write', entries: [] }, dmExec('session-dm2'));
|
|
442
|
-
ok(
|
|
443
|
-
ok(
|
|
444
|
-
ok(
|
|
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));
|
|
445
466
|
|
|
446
467
|
grantConsent('session-dm2', 'blank-smoke', 'compact');
|
|
447
468
|
const actDm = activeConsentsFor('session-dm2');
|
|
448
469
|
const consentLines = consentPromptLines(actDm);
|
|
449
470
|
ok(actDm.length === 1 && consentLines.length === 1 && /用户已授权压缩/.test(consentLines[0]), '授权:有效期内应生成一行提示', JSON.stringify(consentLines));
|
|
450
471
|
ok(activeConsentsFor('session-other').length === 0, '授权:别的会话看不到', JSON.stringify(activeConsentsFor('session-other')));
|
|
451
|
-
const withConsent = manDm.scopedManual(dmInfo, { ws: WS,
|
|
472
|
+
const withConsent = manDm.scopedManual(dmInfo, { ws: WS, system: '', consents: actDm });
|
|
452
473
|
ok(withConsent.indexOf('用户已授权压缩') !== -1, '授权:提示段应带那一行', withConsent.slice(-160));
|
|
453
474
|
consumedConsent('session-dm2', 'compact');
|
|
454
475
|
ok(consentPromptLines(activeConsentsFor('session-dm2')).length === 0, '授权:用掉后提示消失(不占位)');
|
|
455
|
-
const afterUse = manDm.scopedManual(dmInfo, { ws: WS,
|
|
476
|
+
const afterUse = manDm.scopedManual(dmInfo, { ws: WS, system: '', consents: activeConsentsFor('session-dm2') });
|
|
456
477
|
ok(afterUse.indexOf('已授权') === -1, '授权:用掉后提示段不再有该行');
|
|
457
478
|
}
|
|
458
479
|
|
|
@@ -576,8 +597,11 @@ const projExec = { agent: projAgent };
|
|
|
576
597
|
ok(typeof projGatePack === 'function' && projGatePack(projExec) === false, '项目角色不可见 ho_sessions(每步条件)');
|
|
577
598
|
ok(typeof projGateHand === 'function' && projGateHand(projExec) === true, '项目角色可见 ho_handover(每步条件)');
|
|
578
599
|
ok(gateFor('ho_changelog')(projExec) === false, '项目角色不可见 ho_changelog');
|
|
579
|
-
ok(calls.sections.length
|
|
580
|
-
const
|
|
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, '/');
|
|
581
605
|
ok(secNorm.indexOf('Projects/Demo') !== -1 && secNorm.indexOf('_Project_Meta/Demo/Handover.md') !== -1, '项目提示段应含自己的路径', secNorm.slice(0, 200));
|
|
582
606
|
ok(secNorm.indexOf('任务完成不报全局') !== -1, '项目提示段应写明任务完成不向全局报告', secNorm.slice(0, 400));
|
|
583
607
|
ok(secNorm.indexOf('任务达成') === -1, '项目提示段不得再出现任务达成');
|
|
@@ -585,8 +609,11 @@ ok(secNorm.indexOf('可直接 peer') !== -1, '项目提示段应允许会话之
|
|
|
585
609
|
ok(secNorm.indexOf('resume=wait') !== -1, '项目提示段应说明收尾压缩不唤醒');
|
|
586
610
|
ok(secNorm.indexOf('_Control_Center/standards/') !== -1, '项目提示段应给出通用规范的查找范围', secNorm.slice(-240));
|
|
587
611
|
ok(secNorm.indexOf('把某某写成规范') !== -1 && secNorm.indexOf('本会话所在的工作区') !== -1 && secNorm.indexOf('写入通用规范库') !== -1 && secNorm.indexOf('报全局') !== -1, '项目提示段应写明未指明写本库、通用规范库报全局', secNorm.slice(-400));
|
|
588
|
-
console.log('项目提示段 token 实测:' + budget.estimateTokens(
|
|
589
|
-
ok(
|
|
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));
|
|
590
617
|
// 真实工作区路径 + 迁移提示是最坏情形,参考值另设一档
|
|
591
618
|
{
|
|
592
619
|
const man = await import(pathToFileURL(path.join(dist, 'lib', 'manual.mjs')).href);
|
|
@@ -615,11 +642,16 @@ const globAgent = {
|
|
|
615
642
|
};
|
|
616
643
|
for (const cb of listeners) cb({ agent: globAgent, source: 'fresh' });
|
|
617
644
|
ok(gCalls.restrict === 0, '全局角色不应被裁剪', String(gCalls.restrict));
|
|
618
|
-
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'), '全局同样不注册动态提醒段');
|
|
619
649
|
// 常驻段只放每轮判定,一次性流程下沉手册;参考值按加权 token 计,只打印实测值。
|
|
620
|
-
console.log('全局提示段 token 实测:' + budget.estimateTokens(
|
|
621
|
-
ok(
|
|
622
|
-
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, '全局段不携带子智能体检索细节(细则在运行时手册)');
|
|
623
655
|
// 真机最坏情形:长路径 + 迁移提示行
|
|
624
656
|
{
|
|
625
657
|
const man2 = await import(pathToFileURL(path.join(dist, 'lib', 'manual.mjs')).href);
|
|
@@ -628,23 +660,23 @@ ok(gCalls.sections[0].text.indexOf('send_message 续用') === -1, '全局段不
|
|
|
628
660
|
const gWorst = man2.globalManual({ ws: longWs2, move: { phase: 'switched', dst: otherWs2, src: longWs2 } });
|
|
629
661
|
console.log('全局最坏情形提示段 token 实测:' + budget.estimateTokens(gWorst) + '(参考值 ' + budget.TOKEN_BUDGET.globalWorstTokens + ',不判失败)');
|
|
630
662
|
}
|
|
631
|
-
ok(
|
|
632
|
-
//
|
|
663
|
+
ok(globText.indexOf('前置提示词') === -1, '插件角色段不得再携带前置提示词(由用户全局前置提示词注入)');
|
|
664
|
+
// 结构:1.0 为六节(身份、权限、任务流程、上下文与压缩、交接与体系、按需取用)
|
|
633
665
|
{
|
|
634
|
-
const g =
|
|
666
|
+
const g = globText;
|
|
635
667
|
const cnt = (k) => g.split(k).length - 1;
|
|
636
|
-
const secs = ['一、身份', '二、权限', '
|
|
668
|
+
const secs = ['一、身份', '二、权限', '三、任务流程', '四、上下文与压缩', '五、交接与体系', '六、按需取用'];
|
|
637
669
|
const bad = secs.filter((k) => cnt(k) !== 1);
|
|
638
|
-
ok(bad.length === 0, '
|
|
639
|
-
const absent = ['
|
|
670
|
+
ok(bad.length === 0, '全局段六节骨架应各出现一次', bad.join(','));
|
|
671
|
+
const absent = ['反模式', '自检'];
|
|
640
672
|
const still = absent.filter((k) => g.indexOf(k) !== -1);
|
|
641
673
|
ok(still.length === 0, '插件角色段不得携带非会话约束', still.join(','));
|
|
642
|
-
ok(cnt('换任') <=
|
|
674
|
+
ok(cnt('换任') <= 4, '「换任」在全局段出现不应超过四处', String(cnt('换任')));
|
|
643
675
|
}
|
|
644
676
|
ok(gCalls.injected === 0, '全局不再重复注入工作流(提示段里已有)', String(gCalls.injected));
|
|
645
677
|
// 分层护栏——常驻段只留判定,工作流细节必须留在手册里
|
|
646
678
|
{
|
|
647
|
-
const g0 =
|
|
679
|
+
const g0 = globText;
|
|
648
680
|
ok(g0.indexOf('ho_backup') === -1 && g0.indexOf('move-probe') === -1, '常驻段不得回灌工作流细节(应放手册)');
|
|
649
681
|
ok(g0.indexOf('ho_charter(doc="runtime", section=…') !== -1, '常驻段应给出按节取用的指针');
|
|
650
682
|
ok(g0.indexOf('_Control_Center/standards/') !== -1, '全局段应给出通用规范的查找范围', g0.slice(-240));
|
|
@@ -669,7 +701,8 @@ ok(gCalls.injected === 0, '全局不再重复注入工作流(提示段里已
|
|
|
669
701
|
const uiFiles = listFiles(path.join(repo, 'src'), ['.mjs', '.js'], { skip: ['templates'] });
|
|
670
702
|
const docFiles = [path.join(repo, 'README.md')]
|
|
671
703
|
.concat(listFiles(path.join(repo, 'src', 'templates'), ['.md']))
|
|
672
|
-
|
|
704
|
+
// 写作规范正文与论文属规范性文本,条款本身会引用被禁词;Writing_Rules 是用户内容。
|
|
705
|
+
.filter((f) => f.indexOf('Writing_Rules.md') === -1 && f.indexOf(path.join('templates', 'standards')) === -1);
|
|
673
706
|
const uiHits = scanFiles(uiFiles, { ui: true });
|
|
674
707
|
const docHits = scanFiles(docFiles, { ui: false });
|
|
675
708
|
const fmt = (hs) => hs.slice(0, 6).map((h) => path.relative(repo, h.file) + ':' + h.line + ' ' + h.rules.join('、')).join(' | ');
|
|
@@ -680,13 +713,13 @@ ok(gCalls.injected === 0, '全局不再重复注入工作流(提示段里已
|
|
|
680
713
|
}
|
|
681
714
|
|
|
682
715
|
// 按步路径(0.2.2 起):创建即按角色登记提示段;工具可见性由每步条件算,不再调用 restrict。
|
|
683
|
-
const unrCalls = { restrict: 0, section: null };
|
|
716
|
+
const unrCalls = { restrict: 0, section: null, sections: [] };
|
|
684
717
|
const unrAgent = {
|
|
685
718
|
id: 'session-unr',
|
|
686
719
|
session: { header: { id: 'session-unr', title: '未命名', cwd: '/tmp/x' } },
|
|
687
720
|
ctx: {
|
|
688
721
|
tools: { restrict: () => { unrCalls.restrict++; return () => { }; } },
|
|
689
|
-
systemPrompt: { section: (o) => { unrCalls.section = o; return () => { }; } },
|
|
722
|
+
systemPrompt: { section: (o) => { unrCalls.section = o; unrCalls.sections.push(o); return () => { }; } },
|
|
690
723
|
},
|
|
691
724
|
inject: () => { throw new Error('未设角色的会话不应写历史'); },
|
|
692
725
|
};
|
|
@@ -698,7 +731,7 @@ ok(!unrCalls.section, '普通会话不注册角色提示段');
|
|
|
698
731
|
// 开轮前核对:休眠期间改了角色,下一轮开始时补装提示段(工具由每步条件即时生效)。
|
|
699
732
|
writeRole({}, 'session-unr', { role: 'project', project: 'Demo', source: 'user' });
|
|
700
733
|
for (const cb of ((reg.listeners && reg.listeners['agent/status']) || [])) cb({ agent: unrAgent, status: 'running' });
|
|
701
|
-
ok(
|
|
734
|
+
ok(unrCalls.sections.some((s) => s.name === 'dsh-handover:manual' && /项目/.test(String(s.text))), '开轮前核对应按新角色补装提示段', JSON.stringify(unrCalls.sections.map((s) => s.name)));
|
|
702
735
|
|
|
703
736
|
// 旧内核(没有 tools.resolve)才走 restrict:创建时按普通收窄 14 个,改角色后开轮前换成 3 个。
|
|
704
737
|
{
|
|
@@ -836,7 +869,7 @@ const FIXTURE = {
|
|
|
836
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' },
|
|
837
870
|
].concat(plainRows),
|
|
838
871
|
notify: [{ sid: 'session-y', key: 'Demo1', name: 'Demo1' }], hot: 2, unknown: 0, cutoff: '2026-11-07 09:25 +0800', silentHours: 12,
|
|
839
|
-
verdict: '2
|
|
872
|
+
verdict: '2 个会话已过 35% 线,应先收齐交接再换会话',
|
|
840
873
|
},
|
|
841
874
|
consent: [{ sessionId: 'session-y', leftMs: 120000, kind: 'compact', onduty: true }],
|
|
842
875
|
notes: [],
|
|
@@ -917,10 +950,10 @@ const roleDetails = [];
|
|
|
917
950
|
if (n.type === 'details') roleDetails.push(n);
|
|
918
951
|
(n.kids || []).forEach(walk);
|
|
919
952
|
})(roleCard);
|
|
920
|
-
ok(roleDetails.length === 2, '
|
|
953
|
+
ok(roleDetails.length === 2, '会话角色应按工作区分组(全局+未分组)', String(roleDetails.length));
|
|
921
954
|
ok(roleDetails.every((d) => d.props.open !== true), '工作区分组默认收起');
|
|
922
955
|
const roleText = textOf(roleCard).join(' ');
|
|
923
|
-
ok(roleText.indexOf('全局(3 个会话)') !== -1 && roleText.indexOf('
|
|
956
|
+
ok(roleText.indexOf('全局(3 个会话)') !== -1 && roleText.indexOf('未分组(10 个会话)') !== -1, '分组标题应带会话数,未登记会话归入未分组', roleText.slice(0, 200));
|
|
924
957
|
ok(roleText.indexOf('已归档会话不列出') === -1 && roleText.indexOf('点击「设置角色」当场生效') === -1, '会话角色卡不再堆说明文字,只留标题与分组');
|
|
925
958
|
|
|
926
959
|
// 授权:只列在任会话,两组各一列按钮,用户不必手填 id。
|
|
@@ -979,14 +1012,32 @@ ok(clientSrc.indexOf('delWord.trim() !== "删除"') !== -1, '删除旧备份须
|
|
|
979
1012
|
// 保留的旧备份不算待处理:迁移卡只在妨碍使用的情形下提示。
|
|
980
1013
|
ok(clientSrc.indexOf('(st.phase === "switched" && (st.olds || []).length)') === -1, '保留的旧备份不得计入待处理');
|
|
981
1014
|
ok(clientSrc.indexOf('旧备份保留,不影响使用') !== -1, '迁移完成后应注明旧备份不影响使用');
|
|
982
|
-
//
|
|
1015
|
+
// 角色下拉顺序保持全局、项目、杂项、普通;会话行本身按时间倒序,不再按角色排列。
|
|
983
1016
|
{
|
|
984
1017
|
const g = clientSrc.indexOf('value: "global"');
|
|
985
1018
|
const p = clientSrc.indexOf('value: "project"');
|
|
986
1019
|
const m = clientSrc.indexOf('value: "misc"');
|
|
987
1020
|
const n = clientSrc.indexOf('value: "plain"');
|
|
988
1021
|
ok(g !== -1 && p !== -1 && m !== -1 && n !== -1 && g < p && p < m && m < n, '角色下拉应为全局、项目、杂项、普通', JSON.stringify({ g, p, m, n }));
|
|
989
|
-
ok(clientSrc.indexOf('ROLE_ORDER') !== -1, '
|
|
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)));
|
|
990
1041
|
}
|
|
991
1042
|
|
|
992
1043
|
// 未接入工作区:一键部署卡片置顶。
|
package/scripts/check-docs.mjs
CHANGED
|
@@ -22,6 +22,13 @@ const problems = [];
|
|
|
22
22
|
const readmeLines = fs.readFileSync(readme, 'utf8').split(/\r?\n/).length;
|
|
23
23
|
if (readmeLines > 200) problems.push('README 应不超过 200 行(当前 ' + readmeLines + ' 行)');
|
|
24
24
|
|
|
25
|
+
// 1.1 README「为什么需要」一节为用户 2026-11-13 与 2026-11-15 两次指定的正文(例外 E-01):逐字比对,改动即失败
|
|
26
|
+
const WHY_PINNED = '单个会话的上下文越用越多,用到一定程度之后,出错的可能也会随之上升;可若这时压缩上下文,或者写好交接换一个会话,又会丢掉已经积累的信息。面对这样的两难,有没有更好的取舍?\n\n本插件一定能很好地帮助你。来试试吧;不满意,随时关闭插件并删除即可。';
|
|
27
|
+
{
|
|
28
|
+
const m = fs.readFileSync(readme, 'utf8').match(/## 为什么需要\s*\n+([\s\S]*?)\n+## /);
|
|
29
|
+
const got = m ? m[1].trim() : '';
|
|
30
|
+
if (got !== WHY_PINNED) problems.push('README「为什么需要」一节为用户指定正文(E-01),不得改写');
|
|
31
|
+
}
|
|
25
32
|
// 2. 旧数与旧术语
|
|
26
33
|
const FORBIDDEN = ['21 个工具', '27 个工具', '未分类', '追加系统提示词', '4,028', '4.6 KB', '每库 24 行', 'CHANGELOG'];
|
|
27
34
|
// 审阅语境与内部用语:这些词只应出现在桌面审阅材料里,不能进对外文档与版本说明。
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 跨平台可移植性测试(1.0):
|
|
3
|
+
* 1. 路径等价键按平台折叠大小写:Windows 与 macOS 折叠,Linux 保留;
|
|
4
|
+
* 2. 7-Zip 探测覆盖 Windows、macOS、Linux 的常见位置与 PATH;
|
|
5
|
+
* 3. 工作区相对路径一律用 path.sep 组装,不写死分隔符;
|
|
6
|
+
* 4. 迁移助手不依赖 PowerShell 或 bash,链接类型按平台选择;
|
|
7
|
+
* 5. 路径比较不再无条件折叠大小写。
|
|
8
|
+
*
|
|
9
|
+
* 全程只读源码与构建产物,不碰真实工作区与真实 HOME。
|
|
10
|
+
* 用法:node scripts/portability.mjs [--dist <dist 目录>]
|
|
11
|
+
*/
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
15
|
+
|
|
16
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const repo = path.resolve(here, '..');
|
|
18
|
+
const dist = (() => {
|
|
19
|
+
const i = process.argv.indexOf('--dist');
|
|
20
|
+
return i !== -1 && process.argv[i + 1] ? path.resolve(process.argv[i + 1]) : path.join(repo, 'dist');
|
|
21
|
+
})();
|
|
22
|
+
|
|
23
|
+
const fails = [];
|
|
24
|
+
function ok(cond, msg, extra) {
|
|
25
|
+
if (cond) console.log(' 通过 ' + msg);
|
|
26
|
+
else { fails.push(msg + (extra ? ' → ' + extra : '')); console.log(' 未通过 ' + msg + (extra ? ' → ' + extra : '')); }
|
|
27
|
+
}
|
|
28
|
+
function section(t) { console.log(''); console.log('== ' + t + ' =='); }
|
|
29
|
+
const imp = (rel) => import(pathToFileURL(path.join(dist, rel)).href);
|
|
30
|
+
const read = (rel) => fs.readFileSync(path.join(repo, rel), 'utf8');
|
|
31
|
+
|
|
32
|
+
const { pathKey, samePath, relToAbs } = await imp('lib/pathkey.mjs');
|
|
33
|
+
|
|
34
|
+
section('1 路径等价键');
|
|
35
|
+
ok(pathKey('C:\\Data\\WS\\', 'win32') === 'c:/data/ws', 'Windows 折叠大小写并去分隔符', pathKey('C:\\Data\\WS\\', 'win32'));
|
|
36
|
+
ok(pathKey('C:\\Data\\WS', 'darwin') === 'c:/data/ws', 'macOS 折叠大小写', pathKey('C:\\Data\\WS', 'darwin'));
|
|
37
|
+
ok(pathKey('/data/WS', 'linux') === '/data/WS', 'Linux 保留大小写', pathKey('/data/WS', 'linux'));
|
|
38
|
+
ok(samePath('C:\\A\\b', 'c:/a/B', 'win32') === true, 'Windows 两种写法视为同一路径');
|
|
39
|
+
ok(samePath('/data/WS', '/data/ws', 'linux') === false, 'Linux 大小写不同视为不同路径');
|
|
40
|
+
ok(relToAbs('/root', 'a/b') === path.join('/root', 'a', 'b'), '相对路径按平台分隔符组装');
|
|
41
|
+
|
|
42
|
+
section('2 7-Zip 探测');
|
|
43
|
+
const seven = read('src/lib/sevenzip.mjs');
|
|
44
|
+
for (const p of ['/usr/bin/7z', '/usr/local/bin/7z', '/opt/homebrew/bin/7z', '/usr/bin/7za', '/opt/homebrew/bin/7zz', '7z.exe']) {
|
|
45
|
+
ok(seven.indexOf(p) !== -1, '候选含 ' + p);
|
|
46
|
+
}
|
|
47
|
+
ok(seven.indexOf('BARE') !== -1, 'PATH 上的 7z、7zz、7za 也探测');
|
|
48
|
+
|
|
49
|
+
section('3 路径组装');
|
|
50
|
+
ok(read('src/lib/scaffold.mjs').indexOf(".split('/').join(path.sep)") !== -1, '脚手架用 path.sep 组装相对路径');
|
|
51
|
+
ok(read('src/lib/archive.mjs').indexOf(".split('/').join(path.sep)") !== -1, '归档恢复用 path.sep 组装相对路径');
|
|
52
|
+
|
|
53
|
+
section('4 迁移与链接');
|
|
54
|
+
const mover = read('src/lib/mover.mjs');
|
|
55
|
+
ok(mover.indexOf("process.platform === 'win32' ? 'junction' : 'dir'") !== -1, '链接类型按平台选择');
|
|
56
|
+
ok(mover.indexOf('不依赖 PowerShell') !== -1, '迁移助手不依赖外部 shell');
|
|
57
|
+
ok(mover.indexOf("process.platform === 'win32' ? t.toLowerCase() : t") !== -1, '链接目标比较按平台折叠大小写');
|
|
58
|
+
|
|
59
|
+
section('5 路径比较不再无条件折叠');
|
|
60
|
+
const files = ['src/lib/mover.mjs', 'src/lib/api.mjs', 'src/lib/session-ops.mjs', 'src/lib/status.mjs', 'src/lib/scaffold.mjs', 'src/tools/ho_workspace.mjs'];
|
|
61
|
+
const bad = files.filter((f) => read(f).indexOf(".replace(/\\/+$/, '').toLowerCase()") !== -1);
|
|
62
|
+
ok(bad.length === 0, '以上文件不再写死「转斜杠 + 去尾 + 小写」', bad.join(', '));
|
|
63
|
+
ok(read('src/lib/pathkey.mjs').indexOf('process.platform') !== -1, '大小写折叠集中在 pathkey.mjs');
|
|
64
|
+
|
|
65
|
+
console.log('');
|
|
66
|
+
if (fails.length) {
|
|
67
|
+
console.log('PORTABILITY FAIL ' + fails.length + ' 项:');
|
|
68
|
+
for (const f of fails) console.log(' - ' + f);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
console.log('PORTABILITY OK(跨平台检查全过)');
|