@mingmaple/dsh-handover 0.9.14 → 0.9.22

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 (64) hide show
  1. package/README.md +46 -19
  2. package/docs/migrate.md +6 -6
  3. package/docs/releases/0.9.22.md +34 -0
  4. package/docs/troubleshoot.md +34 -10
  5. package/package.json +71 -71
  6. package/scripts/blank-dsh.mjs +29 -23
  7. package/scripts/blank-smoke.mjs +205 -87
  8. package/scripts/e2e-migration.mjs +2 -2
  9. package/scripts/office.mjs +3 -3
  10. package/src/client.js +159 -120
  11. package/src/index.mjs +65 -58
  12. package/src/lib/api.mjs +9 -7
  13. package/src/lib/brief.mjs +2 -2
  14. package/src/lib/config.mjs +3 -3
  15. package/src/lib/consent.mjs +54 -1
  16. package/src/lib/docmap.mjs +143 -0
  17. package/src/lib/gates.mjs +25 -1
  18. package/src/lib/guide.mjs +35 -44
  19. package/src/lib/lint.mjs +3 -2
  20. package/src/lib/manual.mjs +48 -9
  21. package/src/lib/mover.mjs +10 -10
  22. package/src/lib/names.mjs +14 -19
  23. package/src/lib/office.mjs +22 -14
  24. package/src/lib/pack-ops.mjs +2 -2
  25. package/src/lib/peer.mjs +4 -4
  26. package/src/lib/role.mjs +8 -5
  27. package/src/lib/scaffold.mjs +42 -20
  28. package/src/lib/schedule.mjs +1 -2
  29. package/src/lib/session-create.mjs +1 -1
  30. package/src/lib/session-ops.mjs +16 -16
  31. package/src/lib/sessions.mjs +1 -1
  32. package/src/lib/status.mjs +205 -45
  33. package/src/lib/succession.mjs +7 -7
  34. package/src/lib/zstd-log.mjs +1 -1
  35. package/src/selftest.mjs +6 -6
  36. package/src/templates/Prompt_Templates.md +13 -13
  37. package/src/templates/Runtime_Manual.md +56 -30
  38. package/src/templates/rules/Handover_Rules.md +2 -1
  39. package/src/templates/rules/Migration_Rules.md +1 -1
  40. package/src/templates/standards/README.md +25 -0
  41. package/src/tools/ho_backup.mjs +135 -0
  42. package/src/tools/ho_checkpoint.mjs +12 -11
  43. package/src/tools/ho_docmap.mjs +153 -0
  44. package/src/tools/ho_gate.mjs +2 -1
  45. package/src/tools/ho_office.mjs +1 -1
  46. package/src/tools/ho_sessions.mjs +3 -4
  47. package/src/tools/ho_status.mjs +246 -0
  48. package/src/tools/ho_succeed.mjs +1 -1
  49. package/src/tools/ho_workspace.mjs +86 -14
  50. package/src/tools/peer.mjs +205 -0
  51. package/src/tools/ho_brief.mjs +0 -57
  52. package/src/tools/ho_diag.mjs +0 -87
  53. package/src/tools/ho_drift.mjs +0 -41
  54. package/src/tools/ho_init.mjs +0 -82
  55. package/src/tools/ho_pack.mjs +0 -38
  56. package/src/tools/ho_phase.mjs +0 -37
  57. package/src/tools/ho_snapshot.mjs +0 -104
  58. package/src/tools/ho_tokens.mjs +0 -57
  59. package/src/tools/peer_list.mjs +0 -37
  60. package/src/tools/peer_new.mjs +0 -75
  61. package/src/tools/peer_peek.mjs +0 -41
  62. package/src/tools/peer_rename.mjs +0 -31
  63. package/src/tools/peer_send.mjs +0 -52
  64. package/src/tools/peer_status.mjs +0 -46
package/src/lib/guide.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 设置页「使用说明」的内容:面向人,不面向模型。
2
+ * 设置页「使用说明」与「工具明细」的内容:面向人,不面向模型。
3
3
  * 数据放宿主侧,客户端只负责渲染——改文案不用动客户端 bundle。
4
4
  * 行文用标准书面语:不用口语化的示例句,示例一律写成可以直接提的要求。
5
5
  * 水位与工具数由 facts.mjs、names.mjs 推导,不在这里重复写死。
@@ -10,8 +10,8 @@ import { ALL_TOOL_NAMES } from './names.mjs';
10
10
  /** 首次安装、尚未接入工作区时的下一步(设置页与工具输出共用同一份)。 */
11
11
  export const SETUP_STEPS = [
12
12
  '准备一个长期存在的目录作为全局根的上级目录,不要选临时目录或下载目录。',
13
- '在本页下方的输入框中粘贴该目录的绝对路径,点「一键部署」:插件会在其中建立 _Workspace 与整套结构,注册为「全局」工作区,并新建一个「全局」会话。',
14
- '在左侧「全局」工作区中打开新会话即可开始。若只想先铺文件,可点「仅建立文件体系」,之后再注册工作区。',
13
+ '在本页下方的输入框中填写该目录的绝对路径,点击「一键部署」:插件会在其中建立 _Workspace 与整套结构,注册为「全局」工作区,并新建一个「全局」会话。',
14
+ '在左侧「全局」工作区中打开新会话即可开始。若只想先铺文件,可点击「仅建立文件体系」,之后再注册工作区。',
15
15
  '工作区已接入但还没有全局会话时,本页会单独出现「全局会话」卡片,可随时补建。',
16
16
  '新建项目或杂项会话时,应当指明目标工作区;找不到工作区时,插件会把已注册的工作区列出来,不会静默落到别处。',
17
17
  ];
@@ -22,76 +22,67 @@ export function buildGuide(cfg) {
22
22
  const toolCount = ALL_TOOL_NAMES.length;
23
23
  return {
24
24
  intro: [
25
- '本插件的全部功能都在对话中调用:不必记命令,也不必操作菜单,用中文说明要做的事,全局会话会选择相应的工具执行。',
26
- '工具与提示按会话角色载入:全局会话可见全部 ' + toolCount + ' 个工具,项目与杂项会话只载入自己需要的几个,普通会话不载入插件工具,也不注入提示段。角色在下方「会话角色」中指定。',
25
+ '插件功能主要在会话中调用:用中文说明要做的事,全局会话会选择相应工具执行,不必记命令;设置页另提供授权、迁移与会话角色等操作入口。',
26
+ '工具与提示按会话角色载入:全局会话可见全部 ' + toolCount + ' 个工具,项目与杂项会话只载入所需的几个,普通会话不载入插件工具,也不注入提示段。角色在下方「会话角色」中指定。',
27
27
  '本页除首次安装、全局前置提示词与会话角色外均为只读;需要最新读数时,点击右上角的「刷新」。',
28
28
  ],
29
29
  situations: [
30
- { when: '首次安装', say: '部署全局文件体系', what: '本页「一键部署」可一次完成三件事:建文件体系、注册「全局」工作区、建「全局」会话' },
31
- { when: '作业开始与结束', say: '先出一份起始简报', what: '汇总上下文水位、通用约束条闸门、会话清点与阶段建议(ho_brief)' },
32
- { when: '查阅规则', say: '把 Charter 第 7.2.3 节取出来', what: '按节返回规则正文,避免整篇读取(ho_charter;完整流程用 doc="runtime")' },
33
- { when: '交接与压缩续跑', say: '先把交接文档写好,再压缩并继续', what: '写自己角色的交接、体检、登记压缩,并由助手选择压缩后去向:继续工作时用 resume=continue,任务已收尾时用 resume=wait(压缩后不唤醒)(ho_handover、ho_checkpoint)' },
34
- { when: '接近上下文上限', say: '查一下本会话的上下文水位', what: compact + '(ho_phase)' },
35
- { when: '换任', say: '让在任会话自己完成换任', what: '用户先发送 /allow-succeed 授权,在任会话随后校验交接、建接任会话并登记在任(ho_succeed);全局仍可用 ho_sessions action=new 手动新建会话' },
36
- { when: '会话角色', say: '把 Demo1 设成项目角色,并让它整理自己的文件', what: '写角色表、当场收窄该会话的工具与提示、投递一次性整理指令(ho_sessions action=role / 本页会话角色)' },
37
- { when: '通知项目会话', say: '通知 Demo 更新交接文档', what: '投递一条消息;会话之间可直接对话,不必经全局转述;会消耗目标会话一整轮 token,应尽量少用(peer_send)' },
38
- { when: '全局前置提示词', say: '把这段设为全局前置提示词', what: '保存到本机;仅在你开启且内容非空时注入全部会话,关闭或留空时不注册提示段' },
39
- { when: '项目骨架', say: '注册 Demo 的骨架', what: '读取、注册、校验项目或杂项的骨架档案;由该项目会话自己写文件并注册(ho_profile)' },
40
- { when: '查看会话', say: '列出当前活跃的会话', what: '水位、运行状态、活跃或沉默、在任或前任(ho_sessions)' },
41
- { when: '打包迁移', say: '把 Demo 打包成迁移包', what: '收集文件、逐文件 sha256、7z、自检、绝对路径体检(ho_pack action=pack)' },
42
- { when: '在新机器恢复', say: '预演这次恢复', what: '校验、列出覆盖清单、备份、写入;默认仅预演(ho_pack action=restore)' },
43
- { when: '删除会话', say: '把这个会话归档', what: '官方仅提供归档;需要再次唤醒时须先解归档(ho_sessions action=archive、unarchive)' },
44
- { when: '建立工作区', say: '把这个目录注册为工作区', what: '注册已存在的目录;删除只删注册,目录与会话日志均保留(ho_workspace)' },
45
- { when: '首次安装', say: '把某个目录建成全局根目录', what: '在该目录下建立整套全局文件体系,并注册为工作区(ho_init)' },
46
- { when: '自检', say: '检查插件当前状态', what: '服务可解析性、压缩调度器、压缩计数、本会话角色与工具集(ho_diag)' },
47
- { when: '查在任', say: '现在 Mingent 是谁在任', what: '按全局、项目、杂项查现任会话的名字与会话 id(ho_office);不起会话也可用命令行 node scripts/office.mjs 同名查询' },
30
+ { when: '首次安装', say: '部署全局文件体系', what: '本页「一键部署」一次完成三步:建立文件体系、注册「全局」工作区、建立「全局」会话' },
31
+ { when: '作业开始与结束', say: '先出一份起始简报', what: '汇总上下文水位、通用约束条闸门、会话清点、交接漂移与阶段建议(ho_status view=brief)' },
32
+ { when: '查阅规则', say: '取出 Charter 第 7.2.3 节', what: '按节返回规则正文,避免整篇读取;完整流程用 doc="runtime"(ho_charter)' },
33
+ { when: '交接与压缩续跑', say: '写好交接文档后压缩并继续', what: '写本角色的交接、体检并登记压缩;压缩后的去向在登记时选定:未完成用 resume=continue 续跑,已收尾用 resume=wait(压缩后不唤醒)(ho_handover、ho_checkpoint)' },
34
+ { when: '接近上下文上限', say: '查询本会话的上下文水位', what: compact + '(ho_status view=phase;同一行还会列出当前授权及其剩余分钟)' },
35
+ { when: '换任', say: '让在任会话自己完成换任', what: '先由当前用户授权:聊天框发送 /allow-succeed,或在设置页「授权」卡点击「授权换任」;在任会话随后校验交接、建立接任会话并登记在任(ho_succeed)' },
36
+ { when: '会话角色', say: '把 Demo1 设为项目角色', what: '写角色表、当场收窄该会话的工具与提示;需要时再投递一次性整理指令(ho_sessions action=role,或本页「会话角色」)' },
37
+ { when: '通知会话', say: '通知 Demo 更新交接文档', what: '投递一条消息;会话之间可直接通信,不必经全局转述;会消耗目标会话一整轮 token,应尽量少用(peer action=send)' },
38
+ { when: '全局前置提示词', say: '将这段设为全局前置提示词', what: '在本页保存到本机;仅在开启且内容非空时注入全部会话,关闭或留空时不注册提示段' },
39
+ { when: '项目骨架', say: '注册 Demo 的骨架', what: '读取、注册、校验项目或杂项的骨架档案;档案文件由该项目会话自己写(ho_profile)' },
40
+ { when: '文档定位', say: '把本项目的文档定位写进提示段', what: '每个会话一份 map,列为路径、一句话介绍、什么时候该读;只指路,是否读正文由会话判断;空 map 不注入,只有该会话的在任者可写(ho_docmap)' },
41
+ { when: '查看会话', say: '列出当前会话', what: '水位、运行状态、活跃或沉默、角色与最后活动,并给出本轮通知名单(ho_sessions)' },
42
+ { when: '打包迁移', say: '把 Demo 打包成迁移包', what: '收集文件、逐文件 sha256、7z(无 7-Zip 时用内置 tar.gz)、自检与绝对路径体检(ho_backup action=pack)' },
43
+ { when: '在新机器恢复', say: '预演这次恢复', what: '校验、列出覆盖清单、备份后写入;默认仅预演(ho_backup action=restore)' },
44
+ { when: '归档会话', say: '把这个会话归档', what: 'DSH 只提供归档;再次唤醒须先解归档(ho_sessions action=archive、unarchive)' },
45
+ { when: '建立工作区', say: '把这个目录注册为工作区', what: 'init 在指定目录下建立整套全局文件体系;list、create、delete 管理注册,删除只删注册(ho_workspace)' },
46
+ { when: '自检', say: '检查插件当前状态', what: '服务可用性、压缩调度器与压缩计数、本会话角色与工具集、水位与迁移状态(ho_status view=diag)' },
47
+ { when: '查在任', say: '现在 Demo1 由谁在任', what: '按全局、项目、杂项查询现任会话的名字与会话 id(ho_office);也可用命令行 node scripts/office.mjs 查询' },
48
48
  ],
49
49
  groups: [
50
50
  {
51
51
  title: '只读工具(不消耗任何会话的 token)',
52
52
  tools: [
53
- ['ho_brief', '起始与收束的单条命令:水位、闸门、会话清点与阶段建议'],
54
- ['ho_gate', '通用约束条闸门(按库型判定)与交接文档体检'],
55
- ['ho_sessions', '会话清点;也用于设定角色、建会话、出提示词、归档'],
56
- ['ho_phase', '本会话阶段:水位、已压缩次数、锁状态、建议动作'],
53
+ ['ho_status', '会话状态:view=phase 水位与建议(默认)、brief 起始简报、diag 插件自检、drift 交接漂移、tokens token 体检'],
54
+ ['ho_gate', '通用约束条闸门(按库型判定)+交接文档体检+文档定位 map 的路径检查'],
55
+ ['ho_sessions', '会话清点与通知名单;也用于设定角色、建会话、出提示词、归档'],
57
56
  ['ho_charter', '按节返回规则正文;doc="runtime" 取完整流程手册'],
58
- ['ho_drift', '交接漂移体检:Δ 为当前轮与交接轮之差,大于 3 需刷新'],
59
- ['ho_diag', '插件自检:服务、调度器、压缩计数、角色判定与工具集'],
60
- ['ho_tokens', '会话 token 体检:轮数、步数、每步 token、最贵的单条返回'],
61
57
  ['ho_office', '在任登记表:查全局、项目、杂项现在由哪个会话在任(名字与会话 id)'],
58
+ ['ho_docmap', '本会话的文档定位:读、写、查;只指路,不规定读多少'],
62
59
  ],
63
60
  },
64
61
  {
65
62
  title: '写工具(会改动文件;均带备份或预演)',
66
63
  tools: [
67
64
  ['ho_handover', '读写、追加、归档本会话自己的交接(写前备份、写后按角色体检)'],
68
- ['ho_checkpoint', '写交接、登记压缩,回合结束后自动压缩并自动续跑'],
69
- ['ho_snapshot', '库快照:逐字节复制、sha256 清单、来源说明'],
65
+ ['ho_checkpoint', '写交接、登记压缩;回合结束后压缩,并按 resume 决定是否续跑'],
70
66
  ['ho_changelog', '按固定表格追加一行台账'],
71
- ['ho_pack', 'action:check 前置闸门、pack 打包、verify 校验、restore 恢复(默认预演)'],
72
- ['ho_workspace', '工作区列表、创建、删除;删除只删注册'],
73
- ['ho_init', '首次安装:在指定目录下建立整套全局文件体系,可顺带注册工作区'],
67
+ ['ho_backup', 'action:snapshot 库快照、check 前置闸门、pack 打包、verify 校验、restore 恢复(默认预演)'],
68
+ ['ho_workspace', 'init 首次建体系;list、create、delete 工作区;move-* 迁移全局根目录'],
74
69
  ['ho_profile', '项目或杂项骨架:读取、注册、校验、取预设'],
75
- ['ho_succeed', '换任自服务:在任会话经用户授权后自行校验交接、建接任会话并登记在任'],
70
+ ['ho_succeed', '换任自服务:在任会话经当前用户授权后自行校验交接、建接任会话并登记在任'],
76
71
  ],
77
72
  },
78
73
  {
79
74
  title: '平级会话工具',
80
75
  tools: [
81
- ['peer_list', '会话清点,只读投影缓存,不激活任何会话'],
82
- ['peer_status', '单会话详情:水位、运行状态、轮数、步数、工作目录'],
83
- ['peer_send', '向指定会话投递一条消息,是唯一会消耗对方 token 的动作'],
84
- ['peer_new', '新建平级会话,可指定工作区与标题'],
85
- ['peer_rename', '只改显示标题,不唤醒目标会话'],
86
- ['peer_peek', '查看指定会话近期的对话,会消耗本会话 token'],
76
+ ['peer', 'action:list 会话一览、status 单会话详情、send 投消息(耗对方 token)、peek 读近期会话历史、rename 改标题、new 新建'],
87
77
  ],
88
78
  },
89
79
  ],
90
80
  rules: [
91
81
  '交接线:上下文水位达到 ' + w.warn + ' 时提醒,达到 ' + w.handover + ' 时写交接并压缩,' + w.hard + ' 为硬上限;同一会话最多压缩 ' + w.max + ' 次,第四段改用新建会话。',
92
82
  '压缩不可逆:历史会被摘要替换,因此必须先写出合格的交接文档;体检未通过时,插件拒绝安排压缩。',
93
- '会话之间可直接 peer_send 对话,不必经全局转述;默认沉默仍适用:只在到交接线且已写交接、需用户决定、发现全局口径有误时发一条,任务完成不向全局报告。',
94
- '压缩后的去向由助手在登记时选择:任务未完成用 continue 续跑,任务已收尾用 wait 静默收尾,等用户派新任务。',
83
+ '会话之间可直接用 peer(action=send)通信,不必经全局转述;默认沉默仍适用:仅在到交接线且已写交接、需当前用户决定、发现全局口径有误时发送一条;任务完成不向全局报告。',
84
+ '当前用户授权(压缩或换任)写入后,目标会话的提示段会立刻多一行提示与到期时刻;授权过期或用掉后自动消失,不留占位。',
85
+ '压缩后的去向由助手在登记时选择:任务未完成用 continue 续跑,任务已收尾用 wait 静默收尾,等当前用户派新任务。',
95
86
  '交接文件按角色分离:全局写 _Control_Center/records/Global_Handover.md,项目写 _Project_Meta/<项目>/Handover.md,杂项写 _Misc/tasks/<任务>/Handover.md,互不串用。',
96
87
  '会话本身不迁移:跨机器、跨平台的连续性依托交接文档。',
97
88
  '全局前置提示词属于用户内容,不是插件约束;关闭或留空时不占用注入点位,可与其它提示词插件并存。',
package/src/lib/lint.mjs CHANGED
@@ -57,8 +57,9 @@ function tableRows(lines) {
57
57
  return lines.filter((l) => l.trim().startsWith('|') && l.indexOf('---') === -1);
58
58
  }
59
59
 
60
+ /** 列出条目:无序列表(`-`、`*`、`+`)与有序列表(`1.`、`1)`、`1、`)都算。 */
60
61
  function bulletRows(lines) {
61
- return lines.filter((l) => l.trim().startsWith('-') || l.trim().startsWith('*'));
62
+ return lines.filter((l) => /^\s*(?:[-*+]|\d+[.)、])\s+/.test(l));
62
63
  }
63
64
 
64
65
  /** 全局交接体检:{ verdict, reasons }。 */
@@ -111,8 +112,8 @@ export function lintProject(file) {
111
112
  if (!body) gap.push("缺 ## 1. 正在做的事 一节(2026-10-12 起的强制项,本项目还没补)");
112
113
  else {
113
114
  const core = body.filter((l) => l.trim() !== '' && !l.trim().startsWith('>'));
115
+ // 只判「有没有实质内容」;不设字数下限(2026-11-11 口径:交接类文档不设体积上限)。
114
116
  if (core.length === 0) bad.push("正在做的事 是空节(或只有引用块说明),没写实质内容");
115
- else if (core.join('').trim().length < 30) bad.push('正在做的事 内容过短,像是应付');
116
117
  }
117
118
  const nxt = sectionLines(text, '下一步') || sectionLines(text, '待办');
118
119
  if (!nxt) bad.push("缺「下一步待办」一节");
@@ -6,19 +6,35 @@
6
6
  */
7
7
  import { handoverPathFor, scopeLabel } from './role.mjs';
8
8
  import { handoverRuleText, watermark } from './facts.mjs';
9
+ import { consentPromptLines } from './consent.mjs';
9
10
 
10
11
  const NL = String.fromCharCode(10);
11
12
 
12
13
  /** 完整流程的指针句(三档角色共用)。 */
13
14
  const RUNTIME_POINTER = '细节按节取:ho_charter(doc="runtime", section=…)。';
14
15
 
16
+ /**
17
+ * 通用规范的位置与查找范围(三档角色共用,必须同一句)。
18
+ * 规范按名字被引用,本库(会话所在的工作区)与全局规范库都在范围内;细节写在规范库的 README 里。
19
+ */
20
+ const STANDARDS_POINTER = '通用规范在 _Control_Center/standards/(每条一个子目录);当前用户提到「按某某规范进行」时,本库(本会话所在的工作区)与该目录都算查找范围。';
21
+
22
+ /**
23
+ * 写入范围(三档角色共用前一句):未指明写本库;明确「写入通用规范库」一类要求才写规范库。
24
+ * 后一句按角色分叉:全局直接写,项目与杂项报全局。
25
+ */
26
+ const STANDARDS_WRITE_PREFIX = '当前用户提到「把某某写成规范」时:未指明范围就写本库;';
27
+ const STANDARDS_WRITE_TARGET = '说「写入通用规范库」「写成通用规范」「写成共用规范」的';
28
+ const STANDARDS_WRITE_GLOBAL = STANDARDS_WRITE_PREFIX + STANDARDS_WRITE_TARGET + ',写 _Control_Center/standards/。';
29
+ const STANDARDS_WRITE_SCOPED = STANDARDS_WRITE_PREFIX + STANDARDS_WRITE_TARGET + ',不得写 _Control_Center,报全局。';
30
+
15
31
  /**
16
32
  * 复用准则:全局、项目、杂项三处必须同一句。
17
33
  * 派活与新建会话前先查在任,低水位在任直接复用;确需另建才新建。
18
34
  */
19
35
  function reuseRule(cfg) {
20
36
  return '指派任务或新建会话前,先用 ho_office(空参数)查一次在任;同一类别已有在任且其水位低于 '
21
- + watermark(cfg).warn + ' 时,直接用 peer_send 复用,不新建;确需另建(原会话不可用、需并行、用户明确要求)时才新建。';
37
+ + watermark(cfg).warn + ' 时,直接用 peer(action="send")复用,不新建;确需另建(原会话不可用、需并行、当前用户明确要求)时才新建。';
22
38
  }
23
39
 
24
40
  /** 全局会话的常驻段:五节骨架(身份、权限、节奏、交接、按需取用)。 */
@@ -32,19 +48,25 @@ export function globalManual(opts) {
32
48
  '',
33
49
  '二、权限',
34
50
  '可写 _Control_Center、_Project_Meta 与项目结构;Projects 及各项目库内文件只读,不得代写;项目内作业不代办。',
35
- '新建会话与设定角色仅属全局;会话间可直接 peer_send;换任须用户授权(/allow-succeed 或设置页)后由在任会话调用 ho_succeed,不再请求全局。',
51
+ '新建会话与设定角色仅属全局;会话间可直接 peer(action="send");换任须当前用户授权(/allow-succeed 或设置页)后由在任会话调用 ho_succeed,不再请求全局。',
36
52
  reuseRule(o.cfg),
37
53
  '',
38
54
  '三、节奏',
39
55
  handoverRuleText(o.cfg),
40
- '压缩须用户点「压缩授权」,再写交接并 ho_checkpoint(confirm=true);任务未完成传 resume=continue,已收尾传 resume=wait,不再唤醒。confirm 仅表意图,授权以点击为准。关键步骤传 at_risk=true 暂缓。',
56
+ '压缩须当前用户点击「授权压缩」,再写交接并 ho_checkpoint(confirm=true);任务未完成传 resume=continue,已收尾传 resume=wait,不再唤醒。confirm 仅表意图,授权以点击为准。关键步骤传 at_risk=true 暂缓。',
41
57
  '',
42
58
  '四、交接',
43
59
  '交接:_Control_Center/records/Global_Handover.md。',
44
60
  '',
45
61
  '五、按需取用',
46
62
  RUNTIME_POINTER,
63
+ STANDARDS_POINTER,
64
+ STANDARDS_WRITE_GLOBAL,
47
65
  ];
66
+ const dm = docmapBlock(o);
67
+ if (dm.length) lines.push('', ...dm);
68
+ const cs = consentBlock(o);
69
+ if (cs.length) lines.push('', ...cs);
48
70
  const mv = moveLine(o);
49
71
  if (mv) lines.push(mv);
50
72
  return lines.join(NL);
@@ -72,11 +94,17 @@ export function scopedManual(info, opts) {
72
94
  hp.path
73
95
  ? '交接:' + hp.path + '(ho_handover 默认)。'
74
96
  : '交接路径未解析(缺角色或项目名),请先在设置页设定角色。',
75
- handoverRuleText(o.cfg) + '压缩须用户点「压缩授权」,再由本会话执行 ho_checkpoint(confirm=true);收尾时传 resume=wait,不再唤醒。',
76
- '会话间可直接 peer_send;默认沉默:仅在水位 ' + watermark(o.cfg).warn + ' 且已写交接、需用户决定、全局口径有误时向全局报告;任务完成不报全局。',
97
+ handoverRuleText(o.cfg) + '压缩须当前用户点击「授权压缩」,再由本会话执行 ho_checkpoint(confirm=true);收尾时传 resume=wait,不再唤醒。',
98
+ '会话间可直接 peer(action="send");默认沉默:仅在水位 ' + watermark(o.cfg).warn + ' 且已写交接、需当前用户决定、全局口径有误时向全局报告;任务完成不报全局。',
77
99
  reuseRule(o.cfg),
78
100
  RUNTIME_POINTER,
101
+ STANDARDS_POINTER,
102
+ STANDARDS_WRITE_SCOPED,
79
103
  ];
104
+ const dm = docmapBlock(o);
105
+ if (dm.length) lines.push('', ...dm);
106
+ const cs = consentBlock(o);
107
+ if (cs.length) lines.push('', ...cs);
80
108
  const mv = moveLine(o);
81
109
  if (mv) lines.push(mv);
82
110
  return lines.join(NL);
@@ -89,6 +117,17 @@ function moveLine(opts) {
89
117
  return '已迁移至 ' + (mv.dst || '?') + ';' + (mv.src || '?') + ' 现为链接,勿再手工迁移。';
90
118
  }
91
119
 
120
+ /** 文档定位(docmap)注入:空 map 不占位。 */
121
+ function docmapBlock(opts) {
122
+ const m = opts && opts.docmap;
123
+ return m ? [m] : [];
124
+ }
125
+
126
+ /** 用户已授权的提示行:每种有效授权一行,无授权不占位。 */
127
+ function consentBlock(opts) {
128
+ try { return consentPromptLines(opts && opts.consents); } catch { return []; }
129
+ }
130
+
92
131
  export function manualFor(info, opts) {
93
132
  const role = info && info.role ? info.role : 'plain';
94
133
  if (role === 'global') return globalManual(opts);
@@ -109,8 +148,8 @@ export function organizeMessageFor(info, opts) {
109
148
  '先通读本库,确定本项目的骨架:代码与文档主体、必需文件、临时与归档位置。构建型、动画型等项目不必套用插件类项目的文档集。',
110
149
  '把结论写入 _Project_Meta/' + p + '/project.profile.json;字段格式见 ho_profile(action=presets)。',
111
150
  '写入后调用 ho_profile(action=register) 校验并登记;返回缺失项时按清单补齐。',
112
- '约束:仅可改动本库与 _Project_Meta/' + p + '/;不得改动 _Control_Center 及其他项目;移动较大文件前先执行 ho_snapshot。可用工具:ho_snapshot、ho_handover、ho_phase、ho_checkpoint、ho_profile、peer_send。',
113
- '完成后以一句话向用户说明「注册了哪些骨架项、仍缺哪些」,无需每轮回执。',
151
+ '约束:仅可改动本库与 _Project_Meta/' + p + '/;不得改动 _Control_Center 及其他项目;移动较大文件前先执行 ho_backup(action="snapshot")。可用工具:ho_backup、ho_handover、ho_status、ho_checkpoint、ho_profile、peer(action="send")。',
152
+ '完成后以一句话向当前用户说明「注册了哪些骨架项、仍缺哪些」,无需每轮回执。',
114
153
  ].join(NL);
115
154
  }
116
155
  if (role === 'misc') {
@@ -120,13 +159,13 @@ export function organizeMessageFor(info, opts) {
120
159
  '先通读本任务目录,确定本任务的骨架:工作文件、规则文件、交接、快照与归档位置。',
121
160
  '把结论写入 _Misc/tasks/' + t + '/task.profile.json;字段格式见 ho_profile(action=presets)。',
122
161
  '写入后调用 ho_profile(action=register) 校验并登记。',
123
- '约束:仅可改动 _Misc/tasks/' + t + '/、_Temp/_Misc/' + t + '/、_Archives/_Misc/' + t + '/;_Control_Center、Projects 与其他任务不得写入。工具:ho_snapshot、ho_handover、ho_phase、ho_checkpoint、ho_profile、peer_send。',
162
+ '约束:仅可改动 _Misc/tasks/' + t + '/、_Temp/_Misc/' + t + '/、_Archives/_Misc/' + t + '/;_Control_Center、Projects 与其他任务不得写入。工具:ho_backup、ho_handover、ho_status、ho_checkpoint、ho_profile、peer(action="send")。',
124
163
  '完成后以一句话说明改动即可。',
125
164
  ].join(NL);
126
165
  }
127
166
  return [
128
167
  head,
129
- '- _Control_Center/{records,rules,user,templates,tools} — 交接、规则、写作规范、模板、脚本。',
168
+ '- _Control_Center/{records,rules,standards,user,templates,tools} — 交接、规则、通用规范、写作规范、模板、脚本。',
130
169
  '- _Project_Meta/Registry.md 与各项目元数据目录。',
131
170
  '- Projects/、_Misc/tasks/、_Archives/、_Temp/ — 只维护结构,不代写项目库内文件。',
132
171
  '约束:项目库内文件由该项目会话自行整理;骨架由项目自己注册,缺则派发,不代写。',
package/src/lib/mover.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 全局根目录搬迁(跨平台):探针 → 复制 → 全量校验 → 派后台助手在 DSH 退出后切换 → 状态 → 清旧副本。
2
+ * 全局根目录迁移(跨平台):探针 → 复制 → 全量校验 → 派后台助手在 DSH 退出后切换 → 状态 → 清旧备份。
3
3
  *
4
4
  * 平台无关的做法(Windows / Linux / macOS / 安卓 PRoot Ubuntu 同一套):
5
5
  * - **链接**:Windows 用 junction,POSIX 用目录 symlink;都用 node:fs.symlinkSync,不需要管理员。
@@ -313,7 +313,7 @@ function stop(m) { log('[中止] ' + m); process.exit(1); }
313
313
  let target = '';
314
314
  try { target = fs.readlinkSync(src); } catch { target = ''; }
315
315
  if (!target || stamp(target) !== stamp(dst)) {
316
- log('链接指向不对:' + target + '(旧副本在 ' + old + ')');
316
+ log('链接指向不对:' + target + '(旧备份在 ' + old + ')');
317
317
  stop('切换校验失败');
318
318
  }
319
319
  m.phase = 'switched';
@@ -327,10 +327,10 @@ function stop(m) { log('[中止] ' + m); process.exit(1); }
327
327
  spawn(process.execPath, [RETARGET_HELPER, '0'], { detached: true, stdio: 'ignore', windowsHide: true, env: Object.assign({}, process.env, { ELECTRON_RUN_AS_NODE: '1' }) }).unref();
328
328
  log('③ 已派登记修改登记助手(把 DSH 的登记改到新位置)');
329
329
  } catch (e) {
330
- log('修改登记助手未派成功:' + e.message + '(可在设置页点「把登记迁到新位置」)');
330
+ log('修改登记助手未派成功:' + e.message + '(可在设置页点击「把登记迁到新位置」)');
331
331
  }
332
332
  }
333
- log('切换完成。旧副本:' + old);
333
+ log('切换完成。旧备份:' + old);
334
334
  if (RELAUNCH && EXE) {
335
335
  try {
336
336
  spawn(EXE, [], { detached: true, stdio: 'ignore', env: Object.assign({}, process.env, { ELECTRON_RUN_AS_NODE: undefined }) }).unref();
@@ -352,7 +352,7 @@ function stop(m) { log('[中止] ' + m); process.exit(1); }
352
352
  /** 派后台助手:等本进程(DSH)退出后自动切换。 */
353
353
  export function stageSwitch({ relaunch, waitPid } = {}) {
354
354
  const m = readMarker();
355
- if (!m || !m.src || !m.dst) throw new Error('没有可用搬迁标记,先跑一次 move');
355
+ if (!m || !m.src || !m.dst) throw new Error('没有可用迁移标记,先跑一次 move');
356
356
  const st = moveStatus({ src: m.src });
357
357
  if (st.phase === 'switched' || st.phase === 'switched+cleaned') return { ok: true, already: true, phase: st.phase, log: switchLogPath() };
358
358
  if (st.phase !== 'verified') throw new Error('当前不是「已复制并校验通过」(phase=' + st.phase + '),不能切换');
@@ -394,7 +394,7 @@ export function stageSwitch({ relaunch, waitPid } = {}) {
394
394
  */
395
395
  export function repairLink() {
396
396
  const m = readMarker();
397
- if (!m || !m.src || !m.dst) throw new Error('没有搬迁记录,无法重建链接');
397
+ if (!m || !m.src || !m.dst) throw new Error('没有迁移记录,无法重建链接');
398
398
  if (!fs.existsSync(m.dst)) throw new Error('目标不存在(' + m.dst + '),先确认数据在哪里,本工具不动文件');
399
399
  const kind = linkKind();
400
400
  let st = null;
@@ -638,8 +638,8 @@ export async function rmTree(dir) {
638
638
  export async function cleanupOld({ src, confirm } = {}) {
639
639
  const st = moveStatus({ src });
640
640
  if (st.phase !== 'switched') throw new Error('当前不是已切换状态(phase=' + st.phase + '),拒绝删除任何目录');
641
- if (confirm !== true) throw new Error('删除旧副本需要明确确认(confirm=true)');
642
- if (!st.olds.length) return { ok: true, removed: [], bytes: 0, note: '没有找到旧副本目录' };
641
+ if (confirm !== true) throw new Error('删除旧备份需要明确确认(confirm=true)');
642
+ if (!st.olds.length) return { ok: true, removed: [], bytes: 0, note: '没有找到旧备份目录' };
643
643
  const removed = []; const failed = []; let bytes = 0;
644
644
  const base = path.basename(st.src) + '.old-';
645
645
  for (const o of st.olds) {
@@ -657,7 +657,7 @@ export async function cleanupOld({ src, confirm } = {}) {
657
657
  return {
658
658
  ok: failed.length === 0,
659
659
  removed, failed, bytes,
660
- note: failed.length ? ('有 ' + failed.length + ' 个旧副本没删干净,可重试')
661
- : (removed.length ? '已删除旧副本' : '没有可删的旧副本'),
660
+ note: failed.length ? ('有 ' + failed.length + ' 个旧备份未删净,可重试')
661
+ : (removed.length ? '已删除旧备份' : '没有可删的旧备份'),
662
662
  };
663
663
  }
package/src/lib/names.mjs CHANGED
@@ -1,33 +1,28 @@
1
1
  /**
2
- * 工具名集中管理:ho_* 由 PREFIX 与 KEYS 生成,peer_* 由 PEER_KEYS 生成。
3
- * 工具文件一律用 nameOf、peerNameOf 取名,index.mjs 启动时校验位置表,改名只需改这里。
2
+ * 工具名集中管理:ho_* 由 PREFIX 与 KEYS 生成,平级会话工具固定为 peer。
3
+ * 工具文件一律用 nameOf、PEER_NAME 取名,index.mjs 启动时校验位置表,改名只需改这里。
4
+ *
5
+ * 0.9.15 起按「会话一次要做的一件事」合并(见 docs/releases/0.9.15.md):
6
+ * ho_status —— 阶段、起始简报、自检、交接漂移、token 体检(原 ho_phase、ho_brief、ho_diag、ho_drift、ho_tokens)
7
+ * ho_backup —— 快照与迁移包(原 ho_snapshot、ho_pack)
8
+ * ho_workspace —— 工作区动作,含首次建立的 init(原 ho_init)
9
+ * peer —— 平级会话的六个动作(原 peer_list、peer_status、peer_send、peer_new、peer_rename、peer_peek)
4
10
  */
5
11
  const PREFIX = 'ho';
6
12
 
7
13
  export const nameOf = (key) => PREFIX + '_' + key;
8
14
 
9
- /**
10
- * ho_* 18 个。合并过的家族(刻意保持旧名,参数里用 action 分流):
11
- * ho_pack —— 打包 / 校验 / 预演 / 恢复(原 ho_pack_verify、ho_pack_restore、ho_pack_precheck)
12
- * ho_sessions —— 清点 / 设定角色 / 建会话 / 出提示词 / 归档 / 解归档(原 ho_session_new、ho_prompt、ho_session_archive、ho_session_unarchive)
13
- * ho_profile —— 骨架读取 / 注册 / 校验 / 预设
14
- */
15
15
  const KEYS = [
16
- 'brief', 'gate', 'sessions', 'phase', 'charter',
17
- 'handover', 'checkpoint', 'snapshot', 'diag',
18
- 'drift', 'changelog', 'tokens',
19
- 'pack', 'workspace', 'init', 'profile', 'office', 'succeed',
16
+ 'status', 'gate', 'sessions', 'charter',
17
+ 'handover', 'checkpoint', 'changelog',
18
+ 'backup', 'workspace', 'profile', 'office', 'succeed', 'docmap',
20
19
  ];
21
20
 
22
21
  export const NAMES = KEYS.reduce((acc, k) => { acc[k] = nameOf(k); return acc; }, {});
23
22
 
24
23
  export const TOOL_NAMES = KEYS.map((k) => NAMES[k]);
25
24
 
26
- /** 平级会话工具:名字固定为 peer_*,不走 PREFIX。 */
27
- export const peerNameOf = (key) => 'peer_' + key;
28
-
29
- const PEER_KEYS = ['list', 'status', 'send', 'new', 'rename', 'peek'];
30
-
31
- export const PEER_NAMES = PEER_KEYS.map((k) => peerNameOf(k));
25
+ /** 平级会话工具:一个入口,动作用 action 分流。 */
26
+ export const PEER_NAME = 'peer';
32
27
 
33
- export const ALL_TOOL_NAMES = TOOL_NAMES.concat(PEER_NAMES);
28
+ export const ALL_TOOL_NAMES = TOOL_NAMES.concat([PEER_NAME]);
@@ -85,7 +85,7 @@ export function nextName(store, key) {
85
85
  }
86
86
 
87
87
  /**
88
- * 名字 → 类别。用户给的多半是不确定的名字(全局3、全局、Mingent、赢了两次的旧叫法),
88
+ * 名字 → 类别。用户给的多半是不确定的名字(全局3、全局、Demo1、换了说法的旧叫法),
89
89
  * 按「类别名精确 → 去尾部编号 → 现任名 → 唯一子串」四步收敛;收敛不了就报候选,不瞎猜。
90
90
  */
91
91
  export function resolveOffice(store, query) {
@@ -185,20 +185,28 @@ export function registerHolder(ws, opts) {
185
185
  return { key, previous, current: lin.current, registry };
186
186
  }
187
187
 
188
- /** 类别当前空缺(会话退役、项目暂停)。 */
189
- export function retireHolder(ws, key, note) {
190
- const store = loadOffice(ws);
191
- const lin = store.lineages[String(key || '')];
192
- if (!lin) throw new Error('没有这条类别:' + key);
193
- const previous = lin.current && lin.current.name ? { ...lin.current } : null;
194
- if (previous) {
195
- lin.history.push({ name: previous.name, sessionId: previous.sessionId, from: previous.since || '', to: stampNow(), note: note || previous.note || '' });
188
+ /**
189
+ * 读 Registry.md「项目清单」里的项目名(按表内顺序)。
190
+ * 只认那一张表:表头、分隔行与空行一律跳过;读不到文件时返回空数组。
191
+ * 设置页的交接体检以它为准,避免把未登记的旧目录当成项目。
192
+ */
193
+ export function readRegistryProjects(ws) {
194
+ const file = path.join(String(ws || ''), '_Project_Meta', 'Registry.md');
195
+ let text = '';
196
+ try { text = readUtf8(file); } catch { return []; }
197
+ text = String(text).replace(/^\uFEFF/, '');
198
+ const names = [];
199
+ let inTable = false;
200
+ for (const line of text.split(NL)) {
201
+ if (line.startsWith('## ')) { inTable = line.indexOf('项目清单') !== -1; continue; }
202
+ if (!inTable || !line.startsWith('|')) continue;
203
+ const cells = splitCells(line);
204
+ if (cells.length < 4) continue;
205
+ const name = String(cells[1] || '').trim();
206
+ if (!name || name === '项目名' || /^[-:\s]+$/.test(name)) continue;
207
+ names.push(name);
196
208
  }
197
- lin.current = null;
198
- saveOffice(ws, store);
199
- let registry = { changed: [], error: '' };
200
- try { registry = syncRegistry(ws, store); } catch (e) { registry = { changed: [], error: String(e && e.message ? e.message : e) }; }
201
- return { key, previous, registry };
209
+ return names;
202
210
  }
203
211
 
204
212
  // ---------------------------------------------------------------- Registry.md 定点同步
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * 打包、校验、预演、恢复的实现(原 ho_pack / ho_pack_verify / ho_pack_restore / ho_pack_precheck 的代码搬到这里)。
3
- * 对外只经 ho_pack(action=…) 暴露,少 3 份 schema 的固定开销。
3
+ * 对外只经 ho_backup(action=…) 暴露:四份旧入口合为一份 schema,省下三份的固定开销。
4
4
  * 共同口径:只报告绝对路径、不改写文件内容;恢复默认预演、覆盖前备份、从不删除目标文件。
5
5
  */
6
6
  import fs from 'node:fs';
@@ -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_pack(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 });
package/src/lib/peer.mjs CHANGED
@@ -3,7 +3,7 @@
3
3
  * 数据源与 peer-link 完全一致,不引入任何第三方依赖:
4
4
  * ~/.dsh/storages/session_projcache/sessions/<session id>.json 水位 / 运行状态 / 项目名
5
5
  * ~/.dsh/storages/workspace.json 工作区 id / 标题 / 路径
6
- * ~/.dsh/sessions/<slug>/<session id>/session.v4.jsonl.zstd 会话日志(peer_peek 用)
6
+ * ~/.dsh/sessions/<slug>/<session id>/session.v4.jsonl.zstd 会话日志(peer 的 peek 动作用)
7
7
  * 平级会话的判据:投影缓存里 **没有** record.rows.subagent.identity(子会话才有)。
8
8
  */
9
9
  import fs from 'node:fs';
@@ -159,7 +159,7 @@ export function stamp(ms) {
159
159
  return p(d.getMonth() + 1) + '-' + p(d.getDate()) + ' ' + p(d.getHours()) + ':' + p(d.getMinutes());
160
160
  }
161
161
 
162
- /** 一行式摘要(给 peer_list 用)。 */
162
+ /** 一行式摘要(给 peer 的 list 动作用)。 */
163
163
  export function rowLine(sessionId, cache, fallback) {
164
164
  const pct = cache === null ? null : pressurePct(cache);
165
165
  const state = isBusy(cache) === true ? 'running' : fallback === undefined ? 'idle' : fallback;
@@ -171,14 +171,14 @@ export function rowLine(sessionId, cache, fallback) {
171
171
 
172
172
  /**
173
173
  * 会话清单:controller.list 的结果按 origin 过滤。
174
- * 冷会话在这一步就会被服务端从持久化里恢复成可寻址的行,所以 peer_list 本身不花 token。
174
+ * 冷会话在这一步就会被服务端从持久化里恢复成可寻址的行,所以 peer(action="list")本身不花 token。
175
175
  */
176
176
  export async function sessionRows(controller, includeSubagents) {
177
177
  const value = await controller.list({}, new AbortController().signal);
178
178
  return value.items.filter((row) => includeSubagents === true || row.origin !== 'subagent');
179
179
  }
180
180
 
181
- // ---------------------------------------------------------------- 会话日志(peer_peek)
181
+ // ---------------------------------------------------------------- 会话日志(peer 的 peek 动作)
182
182
 
183
183
  /** 解 zstd **多帧**日志并按行解析成事件数组;本 Node 构建没有 zstd 时返回 null。 */
184
184
  export function decodeSessionLog(file) {
package/src/lib/role.mjs CHANGED
@@ -8,19 +8,22 @@ import path from 'node:path';
8
8
  import { readJsonDir, pick, isSafeName, assertSafeName } from './text.mjs';
9
9
  import { projCacheDir, isSubagent } from './sessions.mjs';
10
10
  import { globalWorkspaceSessionIds } from './registry.mjs';
11
- import { TOOL_NAMES, PEER_NAMES, nameOf, peerNameOf } from './names.mjs';
11
+ import { TOOL_NAMES, PEER_NAME, nameOf } from './names.mjs';
12
12
  import { stampTz } from './brief.mjs';
13
13
 
14
14
  export const ROLES = ['global', 'project', 'misc', 'plain'];
15
15
  export const ROLE_LABEL = { global: '全局', project: '项目', misc: '杂项', plain: '普通' };
16
16
 
17
- export const ALL_TOOL_NAMES = TOOL_NAMES.concat(PEER_NAMES);
17
+ export const ALL_TOOL_NAMES = TOOL_NAMES.concat([PEER_NAME]);
18
18
 
19
- /** 角色 → 可见工具(其余一律 deny)。改这张表就是改成本结构。 */
19
+ /**
20
+ * 角色 → 可见工具(其余一律 deny)。改这张表就是改成本结构。
21
+ * 合并后一个工具内含多个动作,动作级权限在工具内部再判一次(见 tools/peer.mjs)。
22
+ */
20
23
  const ROLE_TOOLS = {
21
24
  global: ALL_TOOL_NAMES.slice(),
22
- project: [nameOf('phase'), nameOf('handover'), nameOf('checkpoint'), nameOf('charter'), nameOf('gate'), nameOf('snapshot'), nameOf('profile'), nameOf('office'), nameOf('succeed'), peerNameOf('send'), peerNameOf('status')],
23
- misc: [nameOf('phase'), nameOf('handover'), nameOf('checkpoint'), nameOf('charter'), nameOf('snapshot'), nameOf('profile'), nameOf('office'), nameOf('succeed'), peerNameOf('send'), peerNameOf('status'), peerNameOf('new')],
25
+ project: [nameOf('status'), nameOf('handover'), nameOf('checkpoint'), nameOf('charter'), nameOf('gate'), nameOf('backup'), nameOf('profile'), nameOf('office'), nameOf('succeed'), nameOf('docmap'), PEER_NAME],
26
+ misc: [nameOf('status'), nameOf('handover'), nameOf('checkpoint'), nameOf('charter'), nameOf('backup'), nameOf('profile'), nameOf('office'), nameOf('succeed'), nameOf('docmap'), PEER_NAME],
24
27
  plain: [],
25
28
  };
26
29