@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.
Files changed (81) hide show
  1. package/README.md +117 -113
  2. package/docs/migrate.md +52 -41
  3. package/docs/releases/0.9.12.md +2 -3
  4. package/docs/releases/0.9.14.md +1 -5
  5. package/docs/releases/0.9.22.md +1 -1
  6. package/docs/releases/1.0.0.md +33 -0
  7. package/docs/troubleshoot.md +221 -61
  8. package/package.json +5 -2
  9. package/scripts/behavior.mjs +398 -0
  10. package/scripts/blank-dsh.mjs +12 -2
  11. package/scripts/blank-smoke.mjs +95 -44
  12. package/scripts/check-docs.mjs +7 -0
  13. package/scripts/portability.mjs +71 -0
  14. package/scripts/style-gate.mjs +571 -0
  15. package/src/client.js +1028 -994
  16. package/src/index.mjs +273 -109
  17. package/src/lib/absrefs.mjs +1 -1
  18. package/src/lib/api.mjs +9 -3
  19. package/src/lib/archive.mjs +1 -1
  20. package/src/lib/brief.mjs +8 -6
  21. package/src/lib/config.mjs +21 -6
  22. package/src/lib/docmap.mjs +5 -8
  23. package/src/lib/drift.mjs +1 -1
  24. package/src/lib/events.mjs +1 -1
  25. package/src/lib/facts.mjs +19 -12
  26. package/src/lib/gates.mjs +12 -12
  27. package/src/lib/global-task.mjs +69 -0
  28. package/src/lib/guide.mjs +80 -75
  29. package/src/lib/handover-io.mjs +69 -6
  30. package/src/lib/identity.mjs +330 -0
  31. package/src/lib/legacy.mjs +89 -0
  32. package/src/lib/lint.mjs +14 -11
  33. package/src/lib/manual.mjs +117 -80
  34. package/src/lib/mover.mjs +18 -14
  35. package/src/lib/office.mjs +42 -2
  36. package/src/lib/pack-ops.mjs +10 -10
  37. package/src/lib/pathkey.mjs +24 -0
  38. package/src/lib/phase.mjs +8 -13
  39. package/src/lib/profile.mjs +1 -1
  40. package/src/lib/reminder.mjs +110 -0
  41. package/src/lib/role.mjs +1 -1
  42. package/src/lib/scaffold.mjs +30 -10
  43. package/src/lib/schedule.mjs +5 -5
  44. package/src/lib/session-create.mjs +2 -2
  45. package/src/lib/session-ops.mjs +76 -47
  46. package/src/lib/sessions.mjs +40 -2
  47. package/src/lib/sevenzip.mjs +10 -1
  48. package/src/lib/status.mjs +337 -336
  49. package/src/lib/succession.mjs +14 -14
  50. package/src/lib/system.mjs +364 -0
  51. package/src/templates/Changelog.skeleton.md +3 -3
  52. package/src/templates/Charter.skeleton.md +13 -9
  53. package/src/templates/Handover.global.skeleton.md +20 -13
  54. package/src/templates/Handover.template.md +89 -0
  55. package/src/templates/Plan.skeleton.md +34 -0
  56. package/src/templates/Prompt_Templates.md +91 -10
  57. package/src/templates/Runtime_Manual.md +254 -139
  58. package/src/templates/System.skeleton.md +31 -0
  59. package/src/templates/Systems.global.skeleton.md +13 -0
  60. package/src/templates/rules/Delegation_Rules.md +45 -0
  61. package/src/templates/rules/Handover_Rules.md +46 -24
  62. package/src/templates/rules/Migration_Rules.md +7 -7
  63. package/src/templates/rules/Misc_Task_Standard.md +17 -10
  64. package/src/templates/rules/Writing_Rules.md +33 -16
  65. package/src/templates/standards/README.md +19 -7
  66. package/src/templates/standards//345/206/231/344/275/234/350/247/204/350/214/203/README.md +120 -0
  67. 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
  68. package/src/tools/ho_backup.mjs +8 -8
  69. package/src/tools/ho_changelog.mjs +4 -4
  70. package/src/tools/ho_charter.mjs +7 -6
  71. package/src/tools/ho_checkpoint.mjs +34 -31
  72. package/src/tools/ho_docmap.mjs +135 -49
  73. package/src/tools/ho_gate.mjs +9 -4
  74. package/src/tools/ho_handover.mjs +61 -35
  75. package/src/tools/ho_office.mjs +45 -4
  76. package/src/tools/ho_profile.mjs +3 -3
  77. package/src/tools/ho_sessions.mjs +43 -22
  78. package/src/tools/ho_status.mjs +14 -9
  79. package/src/tools/ho_succeed.mjs +3 -3
  80. package/src/tools/ho_workspace.mjs +83 -51
  81. package/src/tools/peer.mjs +219 -8
@@ -26,7 +26,7 @@ function chooseFormat(args, cfg) {
26
26
  const want = String((args && args.format) || (cfg && cfg.packFormat) || 'auto').toLowerCase();
27
27
  if (want === '7z') {
28
28
  const bin = sevenZipBin(cfg && cfg.sevenZipPath);
29
- if (!bin) throw new Error('指定了 format=7z,但本机没有 7-Zip。去掉 format 用 auto,或写 format="tar" 用内置归档器。');
29
+ if (!bin) throw new Error('指定 format=7z,但本机无 7-Zip。可省略 format 改用 auto,或写 format="tar" 使用内置归档器。');
30
30
  return { fmt: '7z', bin };
31
31
  }
32
32
  if (want === 'tar' || want === 'targz' || want === 'tar.gz') return { fmt: 'tar', bin: '' };
@@ -48,11 +48,11 @@ function unpack(packPath, dir) {
48
48
  }
49
49
  if (fmt === '7z') {
50
50
  const bin = sevenZipBin('');
51
- if (!bin) return { code: -1, out: '这个包是 7z 格式,但本机没有 7-Zip;装一个 7-Zip,或改用 tar.gz 包。' };
51
+ if (!bin) return { code: -1, out: '该包为 7z 格式,但本机无 7-Zip;请安装 7-Zip,或改用 tar.gz 包。' };
52
52
  fs.mkdirSync(dir, { recursive: true });
53
53
  return run7zSafe(bin, packPath, dir);
54
54
  }
55
- return { code: -1, out: '认不出包格式(既不是 7z 魔数也不是 gzip):' + packPath };
55
+ return { code: -1, out: '无法识别包格式(既非 7z 魔数,也非 gzip):' + packPath };
56
56
  }
57
57
 
58
58
  /** 包内路径必须相对且不越界:拒绝绝对路径、盘符、..(防止恢复时写到目标目录之外)。 */
@@ -121,7 +121,7 @@ export function packCheck(env, args, exec) {
121
121
  const live = scan.rows.filter((r) => r.project === item);
122
122
  if (live.length) {
123
123
  const best = live.slice().sort((a, b) => b.mtime - a.mtime)[0];
124
- if (best.state === 'running') why.push('会话 ' + best.sid + ' 正在跑');
124
+ if (best.state === 'running') why.push('会话 ' + best.sid + ' 正在运行');
125
125
  if (now - best.mtime < minAge) why.push('最后活动距今 ' + Math.round((now - best.mtime) / 60000) + ' 分钟 < ' + Math.round(minAge / 60000));
126
126
  }
127
127
  const hand = path.join(ws, '_Project_Meta', item, 'Handover.md');
@@ -170,7 +170,7 @@ export function packNow(env, args, exec) {
170
170
  if (!entries.length) throw new Error('没有可打包的文件:' + roots.map((r) => r[0]).join('、'));
171
171
  const total = entries.reduce((a, b) => a + b.bytes, 0);
172
172
  const out = [];
173
- out.push('目标 ' + kind + (name ? ':' + name : '') + ' → ' + entries.length + ' 个文件 / ' + (total / 1048576).toFixed(2) + ' MB');
173
+ out.push('目标 ' + kind + (name ? ':' + name : '') + ',' + entries.length + ' 个文件 / ' + (total / 1048576).toFixed(2) + ' MB');
174
174
  if (args.dry_run) {
175
175
  for (const e of entries.slice(0, 20)) out.push(' ' + e.rel + ' ' + e.bytes + ' B');
176
176
  const pre = scanAbsoluteRefs(ws, entries.map((e) => e.rel));
@@ -205,7 +205,7 @@ export function packNow(env, args, exec) {
205
205
  fs.writeFileSync(path.join(stagingRoot, 'manifest.json'), JSON.stringify(manifest, null, 2), 'utf8');
206
206
  fs.writeFileSync(path.join(stagingRoot, 'CHECKSUMS.sha256'), files.map((f) => f.sha256 + ' ' + f.path).join(NL) + NL, 'utf8');
207
207
  const absLines = renderAbsoluteRefs(abs, { title: '绝对路径体检(包内)', max: 200 });
208
- const md = ['# 迁移包(最简版)', '', '- 目标: ' + kind + (name ? ':' + name : ''), '- 生成: ' + manifest.createdAt, '- 文件: ' + files.length + ' 个 / ' + total + ' B', '- 说明: ' + (args.note || '(无)'), '', ...absLines, '', '## 跨平台注意', '', '- **库内引用一律相对路径**:本包里有 ' + abs.totalHits + ' 处写死的绝对路径,插件**只报告、不改写**文件内容——迁移后需由人或项目会话自己改。', '- 源平台 ' + manifest.sourcePlatform + ';换行 CRLF ' + abs.crlf + ' 个 / LF ' + abs.lf + ' 个' + (abs.nonUtf8.length ? ';非 UTF-8 ' + abs.nonUtf8.length + ' 个' : '') + '。', '- 目标机若是别的系统(Windows ↔ macOS/Linux):盘符、路径分隔符、大小写敏感性、换行、编码都要项目会话自己核一遍。', '', '## 目标环境前置(先做这两件,再做恢复)', '', '1. 安装 dsh-handover 插件(否则本工具不存在);', '2. 打开本包随附的交接文件(_Project_Meta/<项目名>/Handover.md 或全局 _Control_Center/records/Global_Handover.md),按它恢复状态。', '', '## 恢复', '', 'ho_backup(action="restore", pack="<本包绝对路径>", dry_run=true) # 先预演,再加 apply=true', '', '> 会话本身**不迁移**:连续性只靠交接文档。'].join(NL);
208
+ const md = ['# 迁移包(最简版)', '', '- 目标: ' + kind + (name ? ':' + name : ''), '- 生成: ' + manifest.createdAt, '- 文件: ' + files.length + ' 个 / ' + total + ' B', '- 说明: ' + (args.note || '(无)'), '', ...absLines, '', '## 跨平台注意', '', '- **库内引用一律相对路径**:本包内有 ' + abs.totalHits + ' 处写死的绝对路径,插件只报告、不改写文件内容,迁移后须由人工或项目会话修改。', '- 源平台 ' + manifest.sourcePlatform + ';换行 CRLF ' + abs.crlf + ' 个 / LF ' + abs.lf + ' 个' + (abs.nonUtf8.length ? ';非 UTF-8 ' + abs.nonUtf8.length + ' 个' : '') + '。', '- 目标机若为其他系统(Windows ↔ macOS/Linux):盘符、路径分隔符、大小写敏感性、换行与编码均须项目会话自行核对。', '', '## 目标环境前置(先完成这两项,再执行恢复)', '', '1. 安装 dsh-handover 插件(否则本工具不存在);', '2. 打开本包随附的交接文件(_Project_Meta/<项目名>/Handover.md 或全局 _Control_Center/records/Global_Handover.md),按其中内容恢复状态。', '', '## 恢复', '', 'ho_backup(action="restore", pack="<本包绝对路径>", dry_run=true) # 先预演,再加 apply=true', '', '> 会话本身不迁移:连续性只依托交接文档。'].join(NL);
209
209
  fs.writeFileSync(path.join(stagingRoot, 'MIGRATION.md'), md, 'utf8');
210
210
  const outDir = args.out || path.join(ws, '_Archives', '_Global', 'Backups');
211
211
  fs.mkdirSync(outDir, { recursive: true });
@@ -263,7 +263,7 @@ export function packVerify(env, args, exec) {
263
263
  out.push('文件 ' + manifest.files.length + ' 个;一致 ' + same.length + ' / 漂移 ' + drift.length + ' / 缺失 ' + missing.length);
264
264
  for (const p of drift.slice(0, 10)) out.push(' 漂移:' + p);
265
265
  for (const p of missing.slice(0, 10)) out.push(' 缺失:' + p);
266
- out.push(drift.length === 0 && missing.length === 0 ? '判定:包内即现版,可直接用' : '判定:**包已作废**(盘上已变),必须重打');
266
+ out.push(drift.length === 0 && missing.length === 0 ? '判定:包内即现版,可直接使用' : '判定:包已作废(磁盘内容已变更),须重新打包');
267
267
  return out.join(NL);
268
268
  }
269
269
 
@@ -323,10 +323,10 @@ export function packRestore(env, args, exec) {
323
323
  out.push('包 ' + packPath + ',目标 ' + target);
324
324
  out.push('文件 ' + manifest.files.length + ' 个;新增 ' + adds.length + ' / 覆盖 ' + overwrites.length);
325
325
  for (const p of overwrites.slice(0, 10)) out.push(' 覆盖:' + p);
326
- if (crossPlatform) out.push('注意:源平台 ' + manifest.sourcePlatform + ' ≠ 本机 ' + process.platform + ':路径分隔符、盘符、大小写、换行、编码都需要项目会话自己核一遍。');
326
+ if (crossPlatform) out.push('注意:源平台 ' + manifest.sourcePlatform + ' ≠ 本机 ' + process.platform + ';路径分隔符、盘符、大小写、换行与编码均须项目会话自行核对。');
327
327
  out.push(...renderAbsoluteRefs(abs, { title: '绝对路径体检(恢复前)', max: 20 }));
328
- if (abs.totalHits) out.push(' ⇒ 这些位置插件**不会**改;迁移后由人或项目会话按新机器改,库内引用统一用相对路径。');
329
- if (!apply) out.push('**预演**:未写入任何文件。确认无误后用 apply=true 再调一次。');
328
+ if (abs.totalHits) out.push(' 上述位置插件不作改动;迁移后由人工或项目会话按新机器修改,库内引用统一使用相对路径。');
329
+ if (!apply) out.push('预演:未写入任何文件。确认无误后以 apply=true 再次调用。');
330
330
  else out.push('已写入;被覆盖的旧文件备份在 ' + backupDir);
331
331
  return out.join(NL);
332
332
  }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * 路径等价键:跨平台比较同一路径时用。
3
+ * Windows 与 macOS 的默认文件系统不区分大小写,Linux 区分;因此只在 win32 与 darwin 上折叠大小写。
4
+ * 反斜杠一律折成正斜杠,去掉末尾分隔符,便于比较用户输入与登记表里的路径。
5
+ * `platform` 可显式传入,便于按平台写单元测试。
6
+ */
7
+ import path from 'node:path';
8
+
9
+ /** 返回用于比较的路径键。 */
10
+ export function pathKey(p, platform) {
11
+ const plat = platform || process.platform;
12
+ const norm = String(p || '').replace(/\\/g, '/').replace(/\/+$/, '');
13
+ return (plat === 'win32' || plat === 'darwin') ? norm.toLowerCase() : norm;
14
+ }
15
+
16
+ /** 两个路径是否指向同一位置(按当前或指定平台判定)。 */
17
+ export function samePath(a, b, platform) {
18
+ return pathKey(a, platform) === pathKey(b, platform);
19
+ }
20
+
21
+ /** 把工作区相对路径('/' 分隔)转成本机路径。 */
22
+ export function relToAbs(root, rel) {
23
+ return path.join(root, String(rel).split('/').join(path.sep));
24
+ }
package/src/lib/phase.mjs CHANGED
@@ -3,34 +3,29 @@
3
3
  /**
4
4
  * @param {{ratio:number|null, compactionsUsed:number, cfg:object, lockState:string}} input
5
5
  * @returns {{action:string, reason:string}}
6
- * action ∈ continue | pause | checkpoint | new_session | force_compact | blocked | unknown
6
+ * action ∈ continue | checkpoint | force_compact | new_session | blocked | unknown
7
7
  */
8
8
  export function decide(input) {
9
9
  const cfg = input.cfg || {};
10
- const warn = cfg.warnRatio === undefined ? 0.32 : cfg.warnRatio;
11
10
  const handover = cfg.handoverRatio === undefined ? 0.35 : cfg.handoverRatio;
12
11
  const hard = cfg.hardRatio === undefined ? 0.85 : cfg.hardRatio;
13
12
  const maxComp = cfg.maxCompactions === undefined ? 3 : cfg.maxCompactions;
14
13
  const used = typeof input.compactionsUsed === 'number' ? input.compactionsUsed : 0;
15
14
  if (input.lockState === 'orphaned') {
16
- return { action: 'blocked', reason: '检测到未闭合的压缩锁(compaction/start 无配对 end):先处理再排新压缩' };
15
+ return { action: 'blocked', reason: '检测到未闭合的压缩锁(compaction/start 无配对 end):先处理压缩锁,再排新压缩' };
17
16
  }
18
17
  // 额度用尽优先于水位判定:3 次压满后再压不是「水位没到」,而是该换任了。
19
- // 放在水位判定之前,force 也就绕不过去(force 只在水位分支里放行)。
20
18
  if (used >= maxComp) {
21
- return { action: 'new_session', reason: '已压缩 ' + used + '/' + maxComp + ' 次:压缩额度用尽,应写交接并建新会话' };
19
+ return { action: 'new_session', reason: '已压缩 ' + used + '/' + maxComp + ' 次:压缩额度用尽,应写交接并新建会话' };
22
20
  }
23
21
  if (input.ratio === null || input.ratio === undefined) {
24
- return { action: 'unknown', reason: '水位读不到:人工判断,不要盲目压缩' };
22
+ return { action: 'unknown', reason: '水位读不到:需人工判断,不得盲目压缩' };
25
23
  }
26
24
  if (input.ratio >= hard) {
27
- return { action: 'force_compact', reason: '已到硬顶(' + Math.round(hard * 100) + '%):强制压缩,哪怕任务中途' };
25
+ return { action: 'force_compact', reason: '已到硬顶(' + Math.round(hard * 100) + '%):应强制压缩,任务中途亦同' };
28
26
  }
29
27
  if (input.ratio >= handover) {
30
- return { action: 'checkpoint', reason: '过交接线(' + Math.round(handover * 100) + '%):写交接,随后压缩,再读交接续跑' };
28
+ return { action: 'checkpoint', reason: '已过可压缩线(' + Math.round(handover * 100) + '%):先刷新交接,再登记压缩' };
31
29
  }
32
- if (input.ratio >= warn) {
33
- return { action: 'pause', reason: '过提醒线(' + Math.round(warn * 100) + '%):建议在阶段完成处调 ho_checkpoint(关键步骤返回 pause)' };
34
- }
35
- return { action: 'continue', reason: '水位正常' };
36
- }
30
+ return { action: 'continue', reason: '水位可继续;提前压缩仅在助手建议,或当前用户主动要求并授权时进行' };
31
+ }
@@ -88,7 +88,7 @@ function defaultProfile(info) {
88
88
 
89
89
  function cleanList(v, field, errors) {
90
90
  if (v === undefined) return [];
91
- if (!Array.isArray(v)) { errors.push(field + ' 必须是数组'); return []; }
91
+ if (!Array.isArray(v)) { errors.push(field + ' 须为数组'); return []; }
92
92
  const out = [];
93
93
  for (const x of v) {
94
94
  const s = String(x == null ? '' : x).trim().replace(/\\/g, '/');
@@ -0,0 +1,110 @@
1
+ /**
2
+ * 一次性后台提醒(1.0):35% 与 50% 各投一次。
3
+ *
4
+ * 投递方式(用户定):尾注入(Tail-end Injection)。插件工具返回结果时,把提醒贴在结果的末尾,
5
+ * 模型在下一步立刻读到;不改系统提示词(避免改写前缀缓存)、不拆回合、不另起消息、不打断工具。
6
+ * 不往会话日志写任何事件:宿主对 developer/message 有「必须匹配已打开的轮与步」的强校验,
7
+ * 写事件会把会话写成 SessionFormatError(已实测发生过)。
8
+ *
9
+ * 两处风险:
10
+ * 1) 压缩刚完成时投影缓存还是压缩前的旧水位,跳过恢复后的第一次判定。
11
+ * 2) 模型整轮不调用插件工具时没有注入点;此时它可自行用 ho_status 读水位。
12
+ */
13
+ import fs from 'node:fs';
14
+ import os from 'node:os';
15
+ import path from 'node:path';
16
+ import { readSessionPressurePct } from './sessions.mjs';
17
+
18
+ export function remindersPath() {
19
+ return path.join(os.homedir(), '.dsh', 'storages', 'dsh-handover.reminders.json');
20
+ }
21
+
22
+ export function readReminderState() {
23
+ try {
24
+ const v = JSON.parse(fs.readFileSync(remindersPath(), 'utf8').replace(/^\uFEFF/, ''));
25
+ const sessions = v && typeof v.sessions === 'object' && v.sessions ? v.sessions : {};
26
+ return { version: 1, sessions };
27
+ } catch {
28
+ return { version: 1, sessions: {} };
29
+ }
30
+ }
31
+
32
+ function writeReminderState(state) {
33
+ const file = remindersPath();
34
+ fs.mkdirSync(path.dirname(file), { recursive: true });
35
+ const tmp = file + '.tmp-' + process.pid;
36
+ fs.writeFileSync(tmp, JSON.stringify(state, null, 2) + String.fromCharCode(10), 'utf8');
37
+ fs.renameSync(tmp, file);
38
+ }
39
+
40
+ /** 35% 提醒文本。 */
41
+ export function remindText35() {
42
+ return '当前上下文约占窗口 35%。先把正在做的任务收束到可暂停的位置。'
43
+ + '随后判断剩余流程总量:较少则继续完成;较多则做完当前小步骤后停止,'
44
+ + '向当前用户确认是否继续,或写出剩余流程的行动计划、刷新交接,再压缩或换任。'
45
+ + '判定标准:完成全部剩余流程后,上下文占用不超过 45%。本条为一次性后台提醒。';
46
+ }
47
+
48
+ /** 50% 提醒文本。 */
49
+ export function remindText50() {
50
+ return '当前上下文约占窗口 50%。先把正在做的任务收束到可暂停的位置。'
51
+ + '随后判断剩余流程总量:较少则继续完成;较多则做完当前小步骤后停止,'
52
+ + '向当前用户确认是否继续,或写出剩余流程的行动计划、刷新交接,再压缩或换任。'
53
+ + '判定标准:完成全部剩余流程后,上下文占用不超过 55%。本条为一次性后台提醒。';
54
+ }
55
+
56
+ /**
57
+ * 判定一次:水位到线且该线未提醒过时写状态并返回提醒正文。
58
+ * 只应由「工具返回结果」这条路径调用:那时模型正好在等下一次装配。
59
+ * @param {{ remindRatio?: number, remind2Ratio?: number }} cfg
60
+ * @param {string} sid 会话 id
61
+ * @returns {string} 提醒正文;不需要投时为空串
62
+ */
63
+ export function checkReminder(cfg, sid) {
64
+ const id = String(sid || '');
65
+ if (!id) return '';
66
+ const state = readReminderState();
67
+ const rec = state.sessions[id] || {};
68
+ // 压缩刚完成后的那次判定:投影缓存可能还是压缩前的旧水位,跳过。
69
+ if (rec.skipNext) {
70
+ rec.skipNext = false;
71
+ state.sessions[id] = rec;
72
+ try { writeReminderState(state); } catch { /* 写盘失败不阻断工具 */ }
73
+ return '';
74
+ }
75
+ const pct = readSessionPressurePct(id);
76
+ if (pct === null || pct === undefined) return '';
77
+ const r1 = (cfg && cfg.remindRatio !== undefined ? cfg.remindRatio : 0.35) * 100;
78
+ const r2 = (cfg && cfg.remind2Ratio !== undefined ? cfg.remind2Ratio : 0.5) * 100;
79
+ const parts = [];
80
+ let changed = false;
81
+ if (pct >= r1 && !rec.r35) { rec.r35 = true; changed = true; parts.push(remindText35()); }
82
+ if (pct >= r2 && !rec.r50) { rec.r50 = true; changed = true; parts.push(remindText50()); }
83
+ if (!changed) return '';
84
+ state.sessions[id] = rec;
85
+ try { writeReminderState(state); } catch { /* 写盘失败不阻断工具 */ }
86
+ return parts.join(String.fromCharCode(10, 10));
87
+ }
88
+
89
+ /**
90
+ * 工具结果末尾的提醒尾巴:命中阈值时返回要 append 的文本,否则空串。
91
+ * 形如 `[System Note: …]`,紧贴工具结果,模型下一步立刻读到。
92
+ * @param {{ remindRatio?: number, remind2Ratio?: number }} cfg
93
+ * @param {string} sid 会话 id
94
+ */
95
+ export function reminderTailNote(cfg, sid) {
96
+ const text = checkReminder(cfg, sid);
97
+ if (!text) return '';
98
+ return '[System Note: ' + text + ']';
99
+ }
100
+
101
+ /** 记一次「压缩后跳过下一轮判定」;由 ho_checkpoint 在登记压缩时写入。 */
102
+ export function markSkipNext(sid) {
103
+ const id = String(sid || '');
104
+ if (!id) return;
105
+ const state = readReminderState();
106
+ const rec = state.sessions[id] || {};
107
+ rec.skipNext = true;
108
+ state.sessions[id] = rec;
109
+ try { writeReminderState(state); } catch { /* 写盘失败只影响一次判定 */ }
110
+ }
package/src/lib/role.mjs CHANGED
@@ -64,7 +64,7 @@ function writeRolesFile(cfg, data) {
64
64
  /** 写入/更新一条角色;返回写入后的条目。 */
65
65
  export function writeRole(cfg, sessionId, patch) {
66
66
  const sid = String(sessionId || '');
67
- if (!sid) throw new Error('缺会话 id');
67
+ if (!sid) throw new Error('缺少会话 id');
68
68
  const data = readRoles(cfg);
69
69
  const prev = data.sessions[sid] || {};
70
70
  const next = Object.assign({}, prev, patch || {});
@@ -6,6 +6,7 @@
6
6
  import fs from 'node:fs';
7
7
  import os from 'node:os';
8
8
  import path from 'node:path';
9
+ import { pathKey } from './pathkey.mjs';
9
10
 
10
11
  /** 插件自带的模板目录(dist/templates 或 src/templates)。 */
11
12
  const TPL = new URL('../templates/', import.meta.url);
@@ -23,6 +24,7 @@ const DIRS = [
23
24
  '_Control_Center/standards',
24
25
  '_Control_Center/templates',
25
26
  '_Control_Center/tools',
27
+ '_Control_Center/tasks',
26
28
  '_Control_Center/user',
27
29
  'Projects',
28
30
  '_Project_Meta',
@@ -34,33 +36,41 @@ const DIRS = [
34
36
  const FILES = [
35
37
  ['_Control_Center/records/Global_Handover.md', 'Handover.global.skeleton.md'],
36
38
  ['_Control_Center/records/Global_Changelog.md', 'Changelog.skeleton.md'],
39
+ // 全局总注册:只列各任务体系文件的路径;由 ho_docmap 或 scaffold 维护。
40
+ ['_Control_Center/records/systems.md', 'Systems.global.skeleton.md'],
37
41
  ['_Control_Center/rules/Workspace_Charter.md', 'Charter.skeleton.md'],
38
42
  ['_Control_Center/templates/Prompt_Templates.md', 'Prompt_Templates.md'],
39
- // 通用规范:一条规范一个子目录,各角色会话都可读,只有全局写。这里先铺说明,规范正文由会话按需建立。
43
+ ['_Control_Center/templates/Handover.md', 'Handover.template.md'],
44
+ ['_Control_Center/templates/Plan.skeleton.md', 'Plan.skeleton.md'],
45
+ ['_Control_Center/templates/System.skeleton.md', 'System.skeleton.md'],
46
+ // 通用规范:一条规范一个子目录,各角色会话都可读,只有全局写。
40
47
  ['_Control_Center/standards/README.md', 'standards/README.md'],
48
+ ['_Control_Center/standards/写作规范/README.md', 'standards/写作规范/README.md'],
49
+ ['_Control_Center/standards/写作规范/依据_论文.md', 'standards/写作规范/依据_论文.md'],
41
50
  // 规则类:工作区没有这份时,ho_charter 会退回同名内置版本;这里先铺一份可改的默认口径。
42
51
  ['_Control_Center/rules/Handover_Rules.md', 'rules/Handover_Rules.md'],
43
52
  ['_Control_Center/rules/Migration_Rules.md', 'rules/Migration_Rules.md'],
44
53
  ['_Control_Center/rules/Misc_Task_Standard.md', 'rules/Misc_Task_Standard.md'],
54
+ ['_Control_Center/rules/Delegation_Rules.md', 'rules/Delegation_Rules.md'],
45
55
  ['_Control_Center/user/Writing_Rules.md', 'rules/Writing_Rules.md'],
46
56
  ];
47
57
 
48
58
  /** 规整用户粘贴的路径:去掉引号/尾分隔符、展开 ~、解析成绝对路径。 */
49
59
  function normalizeParent(input) {
50
60
  let s = String(input || '').trim();
51
- if (!s) return { error: '先粘贴一个目录(父目录,全局根会建在它里面)' };
61
+ if (!s) return { error: '请先填写目录(父目录,全局根建于其中)' };
52
62
  s = s.replace(/^["']+|["']+$/g, '').trim();
53
- if (!s) return { error: '先粘贴一个目录' };
63
+ if (!s) return { error: '请先填写目录' };
54
64
  if (s === '~') s = os.homedir();
55
65
  else if (s.startsWith('~/') || s.startsWith('~\\')) s = path.join(os.homedir(), s.slice(2));
56
66
  const abs = path.resolve(s);
57
67
  let st = null;
58
68
  try { st = fs.statSync(abs); } catch { st = null; }
59
- if (st && !st.isDirectory()) return { error: '这不是一个目录:' + abs, path: abs };
69
+ if (st && !st.isDirectory()) return { error: '该路径不是目录:' + abs, path: abs };
60
70
  // 目标父目录不存在就建:允许直接指定尚不存在的路径。
61
71
  if (!st) {
62
72
  try { fs.mkdirSync(abs, { recursive: true }); }
63
- catch (e) { return { error: '建不出这个目录:' + abs + '(' + String(e && e.message ? e.message : e) + ')', path: abs }; }
73
+ catch (e) { return { error: '无法创建该目录:' + abs + '(' + String(e && e.message ? e.message : e) + ')', path: abs }; }
64
74
  }
65
75
  return { parent: abs, createdParent: !st };
66
76
  }
@@ -78,6 +88,7 @@ export function ensureScaffold(root) {
78
88
  const created = [];
79
89
  const written = [];
80
90
  const skipped = [];
91
+ const missingTemplates = [];
81
92
  for (const d of DIRS) {
82
93
  const p = path.join(abs, d.split('/').join(path.sep));
83
94
  if (!fs.existsSync(p)) created.push(d);
@@ -90,14 +101,23 @@ export function ensureScaffold(root) {
90
101
  for (const [rel, tpl] of FILES) {
91
102
  const dst = path.join(abs, rel.split('/').join(path.sep));
92
103
  if (fs.existsSync(dst)) { skipped.push(rel); continue; }
104
+ let body = '';
93
105
  try {
94
- fs.writeFileSync(dst, template(tpl), 'utf8');
106
+ body = template(tpl);
107
+ } catch {
108
+ // 内置模板缺失时不写空文件,登记后由调用方报出,避免静默留下半套体系。
109
+ missingTemplates.push(rel);
110
+ continue;
111
+ }
112
+ try {
113
+ fs.mkdirSync(path.dirname(dst), { recursive: true });
114
+ fs.writeFileSync(dst, body, 'utf8');
95
115
  written.push(rel);
96
116
  } catch (e) {
97
- return { ok: false, error: '写文件失败(' + rel + '):' + (e && e.message ? e.message : e), created, written, skipped };
117
+ return { ok: false, error: '写文件失败(' + rel + '):' + (e && e.message ? e.message : e), created, written, skipped, missingTemplates };
98
118
  }
99
119
  }
100
- return { ok: true, created, written, skipped };
120
+ return { ok: true, created, written, skipped, missingTemplates };
101
121
  }
102
122
 
103
123
  /**
@@ -112,9 +132,9 @@ export function createGlobalWorkspace(parentInput, opts) {
112
132
 
113
133
  const name = (opts && typeof opts.name === 'string' && opts.name.trim()) || DEFAULT_ROOT_NAME;
114
134
  if (name.indexOf('/') !== -1 || name.indexOf('\\') !== -1 || name === '.' || name === '..') {
115
- return { ok: false, error: '根目录名不能带路径分隔符:' + name };
135
+ return { ok: false, error: '根目录名不得含路径分隔符:' + name };
116
136
  }
117
- const sameName = path.basename(t.parent).toLowerCase() === name.toLowerCase();
137
+ const sameName = pathKey(path.basename(t.parent)) === pathKey(name);
118
138
  const root = sameName ? t.parent : path.join(t.parent, name);
119
139
 
120
140
  let existed = false;
@@ -71,9 +71,9 @@ export function createScheduler(deps) {
71
71
 
72
72
  async function compactOnce(sessionId) {
73
73
  const agent = agentOf(sessionId);
74
- if (!agent) throw new Error("agent 不在内存中(会话可能已休眠)");
74
+ if (!agent) throw new Error("会话不在内存中(可能已休眠)");
75
75
  const compaction = compactionOf(agent);
76
- if (!compaction || typeof compaction.compactNow !== "function") throw new Error("compaction.compactNow 不可用(profile 级与会话作用域都没取到)");
76
+ if (!compaction || typeof compaction.compactNow !== "function") throw new Error("compaction.compactNow 不可用(profile 级与会话作用域均未取到)");
77
77
  const ac = new AbortController();
78
78
  const timer = later(() => ac.abort(new Error("timeout")), cfg.compactTimeoutMs || 120000);
79
79
  try {
@@ -128,7 +128,7 @@ export function createScheduler(deps) {
128
128
  if (!agent) { fail(sessionId, "会话不在内存,无法压缩"); return; }
129
129
  if (agent.status === "running") {
130
130
  const idle = await waitIdle(agent, cfg.waitIdleMs || 180000);
131
- if (!idle) { fail(sessionId, "等了 " + Math.round((cfg.waitIdleMs || 180000) / 1000) + " 秒会话仍未空闲"); return; }
131
+ if (!idle) { fail(sessionId, "等待 " + Math.round((cfg.waitIdleMs || 180000) / 1000) + " 秒后,会话仍未空闲"); return; }
132
132
  }
133
133
  try {
134
134
  const result = await compactOnce(sessionId);
@@ -157,7 +157,7 @@ export function createScheduler(deps) {
157
157
  } catch (e) {
158
158
  const code = e && e.code;
159
159
  if (code === "busy") {
160
- if (Date.now() - job.startedAt > (cfg.waitIdleMs || 180000)) { fail(sessionId, "压缩一直 busy(有别的压缩或排队工作)"); return; }
160
+ if (Date.now() - job.startedAt > (cfg.waitIdleMs || 180000)) { fail(sessionId, "压缩持续处于 busy 状态(存在其他压缩或排队工作)"); return; }
161
161
  retryLater(sessionId, 800);
162
162
  return;
163
163
  }
@@ -181,7 +181,7 @@ export function createScheduler(deps) {
181
181
  pending.delete(sessionId);
182
182
  note("checkpoint failed " + sessionId + ": " + msg);
183
183
  try {
184
- promptSelf(sessionId, "【dsh-handover】压缩未执行:" + msg + "。历史没有被压缩,交接已就绪;请把这条消息原样告诉当前用户,不要自行重试压缩。").catch(() => {});
184
+ promptSelf(sessionId, "【dsh-handover】压缩未执行:" + msg + "。历史未被压缩,交接已就绪。请将本条消息原样告知当前用户,不得自行重试压缩。").catch(() => {});
185
185
  } catch { /* ignore */ }
186
186
  }
187
187
 
@@ -16,7 +16,7 @@ export async function newSessionIn(env, opts) {
16
16
  }
17
17
  const workspaceId = String(o.workspaceId || '');
18
18
  const cwd = String(o.cwd || '');
19
- if (!workspaceId && !cwd) return { error: '建会话要么给 workspaceId,要么给 cwd' };
19
+ if (!workspaceId && !cwd) return { error: '建会话须给出 workspaceId 或 cwd' };
20
20
  let created = null;
21
21
  try {
22
22
  created = workspaceId ? await controller.create({ workspaceId }) : await controller.create({ cwd });
@@ -24,7 +24,7 @@ export async function newSessionIn(env, opts) {
24
24
  return { error: '建会话失败:' + String(e && e.message ? e.message : e) };
25
25
  }
26
26
  const sessionId = String((created && created.sessionId) || '');
27
- if (!sessionId) return { error: 'create 没有返回 sessionId' };
27
+ if (!sessionId) return { error: 'create 未返回 sessionId' };
28
28
  const title = String(o.title || '');
29
29
  if (title) {
30
30
  try { if (typeof controller.rename === 'function') await controller.rename({ sessionId, title }); } catch { /* 改名失败可事后用 peer(action="rename") */ }