@mingmaple/dsh-handover 0.0.0-stage → 0.9.12

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 (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +149 -2
  3. package/docs/migrate.md +61 -0
  4. package/docs/troubleshoot.md +77 -0
  5. package/package.json +70 -5
  6. package/scripts/blank-dsh.mjs +203 -0
  7. package/scripts/blank-smoke.mjs +981 -0
  8. package/scripts/build.mjs +61 -0
  9. package/scripts/check-docs.mjs +74 -0
  10. package/scripts/e2e-migration.mjs +127 -0
  11. package/scripts/lib/register.mjs +103 -0
  12. package/src/client.js +945 -0
  13. package/src/cordis.patch.yml +16 -0
  14. package/src/index.mjs +726 -0
  15. package/src/lib/absrefs.mjs +112 -0
  16. package/src/lib/api.mjs +291 -0
  17. package/src/lib/archive.mjs +198 -0
  18. package/src/lib/brief.mjs +74 -0
  19. package/src/lib/charter.mjs +28 -0
  20. package/src/lib/config.mjs +37 -0
  21. package/src/lib/consent.mjs +84 -0
  22. package/src/lib/drift.mjs +65 -0
  23. package/src/lib/events.mjs +89 -0
  24. package/src/lib/extra-prompt.mjs +41 -0
  25. package/src/lib/facts.mjs +70 -0
  26. package/src/lib/gates.mjs +148 -0
  27. package/src/lib/guide.mjs +103 -0
  28. package/src/lib/handover-io.mjs +69 -0
  29. package/src/lib/lint.mjs +152 -0
  30. package/src/lib/manual.mjs +124 -0
  31. package/src/lib/mover.mjs +663 -0
  32. package/src/lib/names.mjs +33 -0
  33. package/src/lib/output.mjs +11 -0
  34. package/src/lib/pack-ops.mjs +332 -0
  35. package/src/lib/peer.mjs +260 -0
  36. package/src/lib/phase.mjs +36 -0
  37. package/src/lib/profile.mjs +204 -0
  38. package/src/lib/prompt.mjs +18 -0
  39. package/src/lib/registry.mjs +53 -0
  40. package/src/lib/retarget-helper.tmpl +46 -0
  41. package/src/lib/role.mjs +251 -0
  42. package/src/lib/rpc-carrier.mjs +65 -0
  43. package/src/lib/scaffold.mjs +118 -0
  44. package/src/lib/schedule.mjs +223 -0
  45. package/src/lib/session-create.mjs +42 -0
  46. package/src/lib/session-ops.mjs +408 -0
  47. package/src/lib/sessions.mjs +143 -0
  48. package/src/lib/sevenzip.mjs +48 -0
  49. package/src/lib/status.mjs +177 -0
  50. package/src/lib/text.mjs +101 -0
  51. package/src/lib/webroute.mjs +130 -0
  52. package/src/lib/workspace.mjs +23 -0
  53. package/src/lib/zstd-log.mjs +65 -0
  54. package/src/selftest.mjs +84 -0
  55. package/src/templates/Changelog.skeleton.md +10 -0
  56. package/src/templates/Charter.skeleton.md +53 -0
  57. package/src/templates/Handover.global.skeleton.md +56 -0
  58. package/src/templates/Prompt_Templates.md +96 -0
  59. package/src/templates/Runtime_Manual.md +166 -0
  60. package/src/templates/rules/Handover_Rules.md +44 -0
  61. package/src/templates/rules/Migration_Rules.md +32 -0
  62. package/src/templates/rules/Misc_Task_Standard.md +23 -0
  63. package/src/templates/rules/Writing_Rules.md +24 -0
  64. package/src/tools/ho_brief.mjs +57 -0
  65. package/src/tools/ho_changelog.mjs +59 -0
  66. package/src/tools/ho_charter.mjs +89 -0
  67. package/src/tools/ho_checkpoint.mjs +132 -0
  68. package/src/tools/ho_diag.mjs +87 -0
  69. package/src/tools/ho_drift.mjs +41 -0
  70. package/src/tools/ho_gate.mjs +69 -0
  71. package/src/tools/ho_handover.mjs +84 -0
  72. package/src/tools/ho_init.mjs +82 -0
  73. package/src/tools/ho_pack.mjs +38 -0
  74. package/src/tools/ho_phase.mjs +37 -0
  75. package/src/tools/ho_profile.mjs +105 -0
  76. package/src/tools/ho_sessions.mjs +115 -0
  77. package/src/tools/ho_snapshot.mjs +104 -0
  78. package/src/tools/ho_tokens.mjs +57 -0
  79. package/src/tools/ho_workspace.mjs +217 -0
  80. package/src/tools/peer_list.mjs +37 -0
  81. package/src/tools/peer_new.mjs +75 -0
  82. package/src/tools/peer_peek.mjs +41 -0
  83. package/src/tools/peer_rename.mjs +31 -0
  84. package/src/tools/peer_send.mjs +52 -0
  85. package/src/tools/peer_status.mjs +46 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MingMaple
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,150 @@
1
- # Temporary Holding Version
1
+ # dsh-handover
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ DeepSeek Harness 插件,用于管理长会话的上下文水位、交接文档与压缩流程。插件按会话角色装配工具与提示段,只使用宿主公开的扩展点,不改动 DSH 本体。
4
+
5
+ ## 为什么需要
6
+
7
+ 单个会话的上下文越用越多,用到一定程度之后,出错的可能也会随之上升;可若这时压缩上下文,或者写好交接换一个会话,又会丢掉已经积累的信息。面对这样的两难,有没有更好的取舍?
8
+
9
+ 本插件一定能很好地帮助你。来试试吧;不满意,随时关闭插件并删除即可。
10
+
11
+ ## 功能
12
+
13
+ - 起始简报:水位、闸门、活跃与沉默、阶段建议,一条命令返回。
14
+ - 交接体检:检查交接文档的结构、字段与占位符,逐条列出问题。
15
+ - 压缩与续跑:由水位与剩余压缩次数共同判定时机,由用户授权,回合结束后执行;压缩后的去向由助手在登记时选择,继续工作时续跑,任务已收尾时静默等待;状态切换与失败原因写入结果。
16
+ - 会话清点与角色:查看会话水位与状态,设定角色,建立下一代会话,归档与解归档。
17
+ - 会话间通信:会话之间可直接对话,不必经全局转述;默认沉默,任务完成不向全局报告。
18
+ - 工作区迁移:预检、复制并逐文件校验、切换、改写登记、重建链接、清理旧副本。
19
+ - 快照与迁移包:库快照、打包、校验与恢复,包内绝对路径单独体检。
20
+ - 设置页:常规卡片为压缩授权、移动全局根目录、会话角色、全局前置提示词、会话水位、通用约束条闸门、交接文档体检、插件与会话间通信守卫,末尾另有默认折叠的「使用说明」;尚未接入工作区时置顶显示「首次安装」,可一键部署文件体系、工作区与会话;已接入但还没有全局会话时显示「全局会话」卡片。
21
+
22
+ ## 安装
23
+
24
+ ### 从 GitHub 安装
25
+
26
+ 桌面版:侧栏「插件」→「添加插件」,粘贴仓库地址 `https://github.com/MingMaple/dsh-handover`,装完重启 DSH。
27
+
28
+ Web 或 CLI:
29
+
30
+ ```bash
31
+ dsh plugin --profile web add github:MingMaple/dsh-handover
32
+ ```
33
+
34
+ ### 从 npm 安装
35
+
36
+ 包名为 `@mingmaple/dsh-handover`,版本与仓库同步:
37
+
38
+ ```bash
39
+ dsh plugin --profile web add @mingmaple/dsh-handover
40
+ ```
41
+
42
+ ### 从本地源码安装
43
+
44
+ 1. 构建:`node scripts/build.mjs`,产物在 `dist/`。
45
+ 2. 用 DSH 的插件管理器安装本地目录 `dist/`,target 传绝对路径。
46
+ 3. 在插件列表里启用,重启 DSH。
47
+ 4. 首次使用按下一节「首次使用」的引导部署。
48
+
49
+ ### 首次使用
50
+
51
+ 首次安装后,工作区、文件体系与会话一次部署完成:
52
+
53
+ 1. 打开设置页中的 dsh-handover,在「首次安装」卡片粘贴一个长期存在目录的绝对路径。
54
+ 2. 点「一键部署」。插件在该目录内建立 `_Workspace` 与整套文件结构,注册为「全局」工作区,并新建一个「全局」会话。
55
+ 3. 在左侧「全局」工作区打开该会话即可开始。只需先铺文件时点「仅建立文件体系」,之后再点「注册为工作区」。
56
+ 4. 工作区已接入但还没有全局会话时,设置页会单独出现「全局会话」卡片,可随时补建。
57
+
58
+ 命令行等价操作:`ho_init(parent="<已存在目录>", register=true)`,再新建一个标题含「全局」的会话。
59
+
60
+ ## 使用
61
+
62
+ 安装后直接说明要做的事,不必记命令:
63
+
64
+ | 想做什么 | 可以这样说 |
65
+ |---|---|
66
+ | 看整体状态 | 先出一份起始简报 |
67
+ | 查规则 | 把 Charter 第 7.2.3 节取出来 |
68
+ | 交接并压缩 | 先把交接文档写好,再压缩并继续 |
69
+ | 换任 | 建立下一代全局会话 |
70
+ | 设置角色 | 把 Demo1 设成项目角色,并让它整理自己的文件 |
71
+ | 自检 | 检查插件当前状态 |
72
+
73
+ ### 四类会话
74
+
75
+ | 角色 | 提示段 | 工具数 | 说明 |
76
+ |---|---|---|---|
77
+ | 全局 | 有 | 22 | 协调会话、维护规则与交接 |
78
+ | 项目 | 有 | 9 | 管理单个项目库,自己写骨架档案 |
79
+ | 杂项 | 有 | 9 | 管理单个杂项任务 |
80
+ | 普通 | 无 | 0 | 不注入提示、不加载工具 |
81
+
82
+ 代理子会话不装配 `ho_` 与 `peer_` 工具。宿主逐步解析工具表,子会话在解析阶段即被排除,首轮即不可用。
83
+
84
+ ### 交接与压缩
85
+
86
+ | 水位 | 动作 |
87
+ |---|---|
88
+ | 低于 32% | 继续 |
89
+ | 32% 至 35% | 可写交接;处于关键步骤时可暂缓 |
90
+ | 35% 至 85%,已压少于 3 次 | 写交接、原地压缩、读交接续跑 |
91
+ | 85% 及以上,已压少于 3 次 | 强制压缩兜底 |
92
+ | 任意水位,已压满 3 次 | 写交接、建立新会话 |
93
+
94
+ 压缩次数用尽(默认 3 次)的判定先于水位:额度用尽后,无论水位高低均建议换任;`force` 不能越过额度,`ho_sessions` 的压缩指令(`action="order", order="compact"`)同样会被拒绝。
95
+
96
+ 压缩须由用户在设置页点「压缩授权」,单次授权 5 分钟内有效;`confirm=true` 只表示意图。压缩在回合结束后执行;登记时以 `resume` 选择去向,继续工作时续跑,任务已收尾时不唤醒。全局交接写在 `_Control_Center/records/Global_Handover.md`,项目与杂项交接写在各自目录,每次写入前备份。
97
+
98
+ 归档文件名为 `<原文件名>_<YYYYMMDD_HHMMSS>.md`,同一秒重复时追加 `-2`。全局与项目的归档位于 `_Archives/<范围>/Handover/`,杂项归档位于 `_Misc/tasks/<任务>/_archive/`。
99
+
100
+ ### 设置页
101
+
102
+ 设置页为单页卡片式,常用区直接展开,低频与长文段折叠。未接入工作区时,首次安装卡片置顶出现。删除旧副本须先输入「删除」两字才能解锁按钮。时间显示为 `YYYY-MM-DD HH:mm` 加本机时区偏移。
103
+
104
+ ## 迁移与恢复
105
+
106
+ 换盘或搬动全局根目录时,按预检、复制并校验、切换、改写登记、清理旧副本五步执行,每一步均可在设置页「移动全局根目录」卡片内完成。切换须在 DSH 完全退出后由后台助手执行,原路径随后成为链接。
107
+
108
+ 详见 [搬迁全局根目录](docs/migrate.md)。
109
+
110
+ ## 故障处理
111
+
112
+ 工具数量不符、工作区未接入、设置页刷新失败、压缩未生效、项目名或任务名不合法、新建会话找不到工作区、设置角色后工具未更新、迁移后原路径失效,各有对应处置。
113
+
114
+ 详见 [常见故障处理](docs/troubleshoot.md)。
115
+
116
+ ## 上下文开销
117
+
118
+ 固定开销为工具 schema 与提示段。按 0.9.6 实测,按角色载入后,全局提示段约 271 加权 token,项目约 247 至 256,杂项约 248 至 254,随项目名与路径长度小幅波动;单条工具描述不超过 70 token。
119
+
120
+ ## 边界与限制
121
+
122
+ - 依赖 DSH 的 `ctx.compaction` 服务;未挂载时压缩相关工具明确报错。
123
+ - `ptc` 呈现模式下,插件工具只能经程序调用。
124
+ - 会话压力读取依赖宿主的投影缓存布局;字段变化时用 `ho_diag` 排查。
125
+ - 迁移只搬文件与清单,不改写文件内容;写死的绝对路径以体检形式报出。
126
+ - 工具可见性按每一步计算:换角色在下一轮请求即生效,不必重启 DSH。插件重载后,已存在的会话沿用旧提示段文本,需要新文本时重启一次。
127
+ - 项目名与任务名按目录名校验:项目名不超过 24 字符,任务名不超过 40 字符;不含 `< > : " / \ | ? *` 与控制字符;不以点或空格结尾;不使用设备保留名。名字来自自由文本时同样校验,不合法则报错且不写入磁盘。
128
+ - 卸载后残留只限 `~/.dsh/storages/dsh-handover.*` 与 `~/.dsh/storages/dsh-handover/logs/`,删除即可清零。
129
+
130
+ ## 开发与测试
131
+
132
+ ```bash
133
+ npm test # 开发自测:逐个工具跑只读用例
134
+ npm run test:blank # 空白 DSH 验收:临时 HOME 与空目录跑 22 工具及迁移四件
135
+ npm run test:smoke # 空白环境冒烟:工具装配、状态路由、浏览器半渲染、语体扫描
136
+ npm run test:migrate # 迁移与恢复端到端
137
+ npm run test:docs # 文档校验:工具名、旧数、链接、版本、语体与长度上限
138
+ ```
139
+
140
+ 源码在 `src/`,`dist/` 为构建产物,不入版本库。服务端代码替换 `dist/` 后在插件列表停用再启用即生效,不必重启 DSH;已存在的会话继续使用旧的提示段文本,需要新文本时重启 DSH 或让会话重建。客户端 bundle 不热更新,改设置页必须重建并重启 DSH。
141
+
142
+ ## 版本
143
+
144
+ 当前版本 0.9.12,首次公开版本为 0.9.6。内部版本史不随仓库发布。
145
+
146
+ 支持的内核为 0.2.1-rc.2 至 0.3.0(不含 0.3.0 及其候选版),见 `package.json` 的 `engines.dsh`;0.2.2-rc.1 上的工具装配、角色裁剪与压缩调度已实测通过。
147
+
148
+ ## 许可
149
+
150
+ MIT,见 [LICENSE](LICENSE)。
@@ -0,0 +1,61 @@
1
+ # 搬迁全局根目录
2
+
3
+ 适用场景:把全局根目录换到另一块盘,例如从 D: 换到 E:。全程由 `ho_workspace` 的迁移动作完成,插件不改写文件内容。
4
+
5
+ ## 前提
6
+
7
+ - 先跑一次完整备份或 `ho_snapshot`。
8
+ - 目标父目录可以不存在,插件会自动创建;目标不得在源内部,也不得等于源。
9
+ - 切换必须在 DSH 完全退出后执行:源目录在运行期间被占用,目录改名与建链接都会失败。
10
+
11
+ ## 1. 预检
12
+
13
+ 设置页「移动全局根目录」卡片,或:
14
+
15
+ ```text
16
+ ho_workspace(action="move-probe", to="<目标父目录>")
17
+ ```
18
+
19
+ 核对源总大小、文件数、目标盘可用空间、目标是否为空、是否在源内部。`to` 是父目录,`_Workspace` 会整个搬进去。
20
+
21
+ ## 2. 复制并校验
22
+
23
+ ```text
24
+ ho_workspace(action="move")
25
+ ```
26
+
27
+ 复制到新位置,两边各算一遍逐文件 sha256 清单并比对。不一致时不生成切换脚本,报告差异文件。
28
+
29
+ ## 3. 切换
30
+
31
+ ```text
32
+ ho_workspace(action="move-switch")
33
+ ```
34
+
35
+ 生成并派出后台助手,等你完全退出 DSH 后自动完成:旧目录改名为 `<旧>.old-<时间戳>`,再在原位置建 junction 指向新位置,并校验指向。日志写在 `~/.dsh/storages/dsh-handover.move.log`。
36
+
37
+ 也可以双击生成的 `dsh-handover.move.cmd`。切换后原路径成为链接,工作区登记、profile 与会话工作目录都无需修改。
38
+
39
+ ## 4. 改登记(仅在需要时)
40
+
41
+ 切换助手完成后,若 DSH 的 `workspace.json` 或聊天集成登记仍写着旧路径,页面会提示。执行:
42
+
43
+ ```text
44
+ ho_workspace(action="move-retarget")
45
+ ```
46
+
47
+ 只替换 JSON 文本里的路径字符串,结构不动,每份先备份 `.bak-<时间>`。
48
+
49
+ ## 5. 清理旧副本
50
+
51
+ 确认新位置可正常工作、并已重启 DSH 后,在设置页「移动全局根目录」卡片里输入「删除」确认,再执行:
52
+
53
+ ```text
54
+ ho_workspace(action="move-cleanup", confirm=true)
55
+ ```
56
+
57
+ 只删匹配 `<旧>.old-` 前缀、且既不是源也不是目标的目录。此操作不可撤销。
58
+
59
+ ## 回退
60
+
61
+ 删除链接,把 `.old-*` 改回原名。若登记已改写,再执行一次改登记指回原路径。
@@ -0,0 +1,77 @@
1
+ # 常见故障处理
2
+
3
+ ## 工具缺失或数量不对
4
+
5
+ 1. 在全局会话执行 `ho_diag`,看「角色判定与工具集」一节。
6
+ 2. 全局应为 22 个,项目与杂项 9 个,普通 0 个。
7
+ 3. 新建会话时角色表中尚无该会话,插件会判为普通,并在 1.5 秒与 4.5 秒后复核。角色写入后应自动装配。
8
+ 4. 仍未装配时,在设置页「会话角色」卡片里重新设一次角色;仍无效则执行 `ho_diag` 查 `roleGating` 字段。
9
+ 5. 改过源码但未生效:执行 `node scripts/build.mjs`,然后在插件列表停用并启用一次,不必重启 DSH。提示段文本不随重载替换,仍显示旧文本时重启 DSH。
10
+ 6. 子会话不装配 `ho_` 与 `peer_` 工具,属正常:子会话在工具表解析阶段即被排除,与角色设置无关。
11
+
12
+ ## 工作区未接入
13
+
14
+ `ho_diag` 或设置页显示未接入时,走设置页「首次安装」卡片:粘贴绝对路径后点「一键部署」,文件体系、工作区与会话一次建好;也可以只点「仅建立文件体系」,再点「注册为工作区」。命令行等价操作:
15
+
16
+ ```text
17
+ ho_init(parent="<已存在目录的绝对路径>", register=true)
18
+ ```
19
+
20
+ 装配里没有写死 `workspaceRoot`。工具侧按配置项、会话工作目录向上查找的顺序定位,向上找不到时再查工作区登记;设置页没有会话上下文,按配置项、标题含「全局」且带 `_Control_Center` 的工作区、任一带 `_Control_Center` 的工作区定位。
21
+
22
+ ## 新建会话报「必须指明工作区」
23
+
24
+ `ho_sessions(action="new")` 必须落到明确的工作区:传 `workspace`(id、标题或路径),或传某个已注册工作区根的 `cwd`。两者都没有,或项目没有对应工作区时,工具会报错并列出已注册的工作区,不会静默落到调用者所在的工作区。
25
+
26
+ 处置:
27
+
28
+ 1. 先看清单:`ho_workspace(action="list")`,或直接读报错文本里的清单。
29
+ 2. 还没有工作区时先建:`ho_workspace(action="create", path="<目录>", title="<标题>", ensure=true)`。`ensure=true` 会在目录不存在时自动创建。
30
+ 3. 再建会话:`ho_sessions(action="new", confirm=true, workspace="<id 或标题>", title="<标题>")`。项目会话可同时传 `project`,插件会按同名工作区匹配。
31
+
32
+ ## 设置角色后工具没有变化
33
+
34
+ 会话休眠时设定角色,插件会在它下次开轮时装配工具与提示段。0.9.12 起这一装配由全局会话事件驱动,不必重启 DSH。若仍看不到应有的工具:
35
+
36
+ 1. 在设置页「会话角色」重新设定一次,注意「存活会话当场生效」一栏的提示。
37
+ 2. 执行 `ho_diag`,看「角色收窄」一栏的已装配会话数与最近时间。
38
+ 3. 插件刚从旧版本换上来时,已存在的会话沿用旧提示段文本;重启 DSH 一次即换成新文本,工具裁剪不受影响。
39
+
40
+ ## 设置页刷新失败
41
+
42
+ 页面保留上次读数,并显示其时间与失败原因。常见原因:
43
+
44
+ - 宿主没有 `connection` 服务:插件改用 HTTP 回退;纯 CLI 场合回退会被静默跳过。
45
+ - 桌面版 GUI 对非浏览器策略请求返回 403:不要用裸 `fetch` 取数,插件已走 `/api` 载体。
46
+ - 请求超时:页面加载 30 秒、状态 8 至 30 秒、写操作 15 至 900 秒;稍后重试。
47
+
48
+ ## 压缩未生效
49
+
50
+ 1. 压缩须由用户在设置页点「压缩授权」,单次授权 5 分钟内有效,或在聊天框发送 `/allow-compact`。
51
+ 2. `ho_checkpoint` 只登记,由调度器在回合结束后执行;目标必须空闲。
52
+ 3. 交接体检不通过时拒绝排压缩,先用 `ho_handover(action=read)` 检查结构。
53
+ 4. 已压满 3 次后不再压缩,应换任:`ho_sessions(action="new")`,须用户同意后传 `confirm=true`。额度用尽的判定先于水位;低水位与 `force` 均不能越过,`ho_sessions` 的压缩指令(`action="order", order="compact"`)同样会被拒绝。
54
+
55
+ ## 项目名或任务名不合法
56
+
57
+ 工具报出「项目名不合法」「任务名不合法」一类错误时,说明名字不能作为目录名使用;此时不写入磁盘,也不改动登记。按下列规则改名后重试:
58
+
59
+ - 项目名不超过 24 字符,任务名不超过 40 字符。
60
+ - 不含 `< > : " / \ | ? *` 与控制字符,不以点或空格结尾,不使用 `CON`、`NUL`、`COM1` 这类设备保留名。
61
+ - 名字通常从会话工作目录、首条提示或标题推断。若报错指出名字含 `*` 等字符,检查首条提示中是否将 `Projects/**` 这类通配写法写在与项目名相邻的位置。
62
+
63
+ 名字合法但被判为普通角色的会话,用设置页「会话角色」卡片或 `ho_sessions(action="role")` 明确设定。
64
+
65
+ ## 迁移后原路径失效
66
+
67
+ 若链接被误删,页面会显示「原路径不存在」与「重建链接」。执行:
68
+
69
+ ```text
70
+ ho_workspace(action="move-repair")
71
+ ```
72
+
73
+ 只重建链接,不改动文件。若新位置也不存在,先确认数据实际位置,再决定是否回退。
74
+
75
+ ## 交接体检报「缺节」
76
+
77
+ 项目交接至少要有「正在做的事」与「下一步」两节;全局交接需要 §0 至 §7 全套,§0 含七个字段,§7 至少一行 session id。缺节允许压缩,不合格必须先补齐。
package/package.json CHANGED
@@ -1,6 +1,71 @@
1
- {
2
- "name": "@mingmaple/dsh-handover",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
1
+ {
2
+ "name": "@mingmaple/dsh-handover",
3
+ "version": "0.9.12",
4
+ "type": "module",
5
+ "description": "DeepSeek Harness 插件:把「全局」的交接、闸门、会话清点与上下文压缩续跑流程做成工具(dsh-handover)",
6
+ "license": "MIT",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/MingMaple/dsh-handover.git"
13
+ },
14
+ "homepage": "https://github.com/MingMaple/dsh-handover#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/MingMaple/dsh-handover/issues"
17
+ },
18
+ "keywords": [
19
+ "dsh",
20
+ "deepseek-harness",
21
+ "plugin",
22
+ "handover",
23
+ "context-compaction"
24
+ ],
25
+ "engines": {
26
+ "node": ">=20",
27
+ "dsh": ">=0.2.1-rc.2 <0.3.0-0"
28
+ },
29
+ "exports": {
30
+ ".": "./src/index.mjs",
31
+ "./client": "./src/client.js"
32
+ },
33
+ "files": [
34
+ "src",
35
+ "scripts",
36
+ "docs",
37
+ "README.md",
38
+ "LICENSE"
39
+ ],
40
+ "scripts": {
41
+ "build": "node scripts/build.mjs",
42
+ "test": "node src/selftest.mjs --tool all",
43
+ "test:blank": "node scripts/blank-dsh.mjs",
44
+ "test:smoke": "node scripts/blank-smoke.mjs",
45
+ "test:migrate": "node scripts/e2e-migration.mjs",
46
+ "test:docs": "node scripts/check-docs.mjs"
47
+ },
48
+ "dsh": {
49
+ "bundle": {
50
+ "patch": "./src/cordis.patch.yml"
51
+ },
52
+ "client": {
53
+ "platform": "web",
54
+ "inject": [
55
+ "@deepseek-ai/dsh-client-ui-settings",
56
+ "@deepseek-ai/dsh-client-connection"
57
+ ]
58
+ },
59
+ "compatibility": {
60
+ "dsh": "0.2.2-rc.1",
61
+ "profiles": [
62
+ "desktop",
63
+ "web"
64
+ ],
65
+ "dshReleases": {
66
+ "0.2.1-rc.2": "compatible",
67
+ "0.2.2-rc.1": "compatible"
68
+ }
69
+ }
70
+ }
6
71
  }
@@ -0,0 +1,203 @@
1
+ /**
2
+ * 空白 DSH 验收:证明「移到一个没有任何旧全局文件的空 DSH 上,插件所有功能 100% 可用」。
3
+ * 做法:临时 HOME(无 ~/.dsh 状态)→ 只靠 dist/ 自带的东西跑完 22 个工具 + 迁移四件。
4
+ * 额外覆盖:7-Zip 不存在(强制 format=tar)、工作区规则文件被删光(必须退回内置模板)。
5
+ * 用法:node scripts/blank-dsh.mjs
6
+ */
7
+ import fs from 'node:fs';
8
+ import os from 'node:os';
9
+ import path from 'node:path';
10
+ import crypto from 'node:crypto';
11
+ import { pathToFileURL, fileURLToPath } from 'node:url';
12
+
13
+ const here = path.dirname(fileURLToPath(import.meta.url));
14
+ const repo = path.resolve(here, '..');
15
+ const dist = path.join(repo, 'dist');
16
+
17
+ const fails = [];
18
+ const lines = [];
19
+ function ok(cond, label, extra) {
20
+ if (cond) lines.push(' ok ' + label);
21
+ else { fails.push(label + (extra ? ' :: ' + extra : '')); lines.push(' FAIL ' + label + (extra ? ' :: ' + extra : '')); }
22
+ }
23
+ function section(t) { lines.push(''); lines.push('== ' + t + ' =='); }
24
+
25
+ const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), 'dsh-handover-blankdsh-'));
26
+ const HOME = path.join(ROOT, 'home');
27
+ const WORK = path.join(ROOT, 'work');
28
+ fs.mkdirSync(HOME, { recursive: true });
29
+ fs.mkdirSync(WORK, { recursive: true });
30
+ process.env.USERPROFILE = HOME;
31
+ process.env.HOME = HOME;
32
+
33
+ const { apply } = await import(pathToFileURL(path.join(dist, 'index.mjs')).href);
34
+ const { safeRel } = await import(pathToFileURL(path.join(dist, 'lib', 'archive.mjs')).href);
35
+
36
+ const reg = { tools: [], sections: [], listeners: {}, logs: [] };
37
+ const prompts = [];
38
+ const ctx = {
39
+ tools: { register: (t) => reg.tools.push(t), restrict() { }, guard() { } },
40
+ systemPrompt: { section: (s) => { reg.sections.push(s); return () => { }; } },
41
+ sessionController: {
42
+ prompt: async (r) => { prompts.push(r); return { accepted: true }; },
43
+ list: async () => ({ items: [] }),
44
+ create: async () => ({ sessionId: 'session-created' }),
45
+ rename: async () => ({}),
46
+ },
47
+ workspaceRegistry: {
48
+ archiveSession: async () => ({}),
49
+ unarchiveSession: async () => ({}),
50
+ get: () => null,
51
+ },
52
+ logger: { warn: (m) => reg.logs.push(String(m)), error: (m) => reg.logs.push(String(m)), info() { } },
53
+ effect: (fn) => fn(),
54
+ on: (n, cb) => { (reg.listeners[n] = reg.listeners[n] || []).push(cb); },
55
+ get: () => null,
56
+ inject: (names, cb) => { reg.injectCb = cb; },
57
+ };
58
+ apply(ctx, { packFormat: 'tar' }); // 强制内置归档器:模拟一台没有 7-Zip 的机器
59
+
60
+ section('装配');
61
+ ok(reg.tools.length === 22, '注册 22 个工具', String(reg.tools.length));
62
+ ok(reg.tools.every((t) => t && t.name && t.description && typeof t.execute === 'function'), '每个工具都有 name/description/execute');
63
+ const names = reg.tools.map((t) => t.name).sort();
64
+ ok(names.indexOf('ho_pack') !== -1 && names.indexOf('peer_send') !== -1, '关键工具都在');
65
+
66
+ const byName = new Map(reg.tools.map((t) => [t.name, t]));
67
+ function execFor(cwd, id, title) {
68
+ return { name: '', arguments: {}, agent: { session: { header: { id: id || 'session-blank-test', cwd, title: title || '' } } } };
69
+ }
70
+ async function run(tool, args, cwd, id, title) {
71
+ const t = byName.get(tool);
72
+ if (!t) return { err: 'no tool ' + tool };
73
+ try {
74
+ const out = await t.execute(args, execFor(cwd, id, title));
75
+ return { text: String(out && out.text !== undefined ? out.text : out) };
76
+ } catch (e) {
77
+ return { err: String(e && e.message ? e.message : e) };
78
+ }
79
+ }
80
+
81
+ section('首次安装(只用插件自带模板)');
82
+ const init = await run('ho_init', { parent: WORK, name: '_Workspace', register: false });
83
+ ok(!init.err, 'ho_init 不抛错', init.err);
84
+ const WS = path.join(WORK, '_Workspace');
85
+ ok(fs.existsSync(WS), '全局根已建', WS);
86
+ for (const rel of [
87
+ '_Control_Center/records/Global_Handover.md',
88
+ '_Control_Center/records/Global_Changelog.md',
89
+ '_Control_Center/rules/Workspace_Charter.md',
90
+ '_Control_Center/templates/Prompt_Templates.md',
91
+ '_Control_Center/rules/Handover_Rules.md',
92
+ '_Control_Center/rules/Migration_Rules.md',
93
+ '_Control_Center/rules/Misc_Task_Standard.md',
94
+ '_Control_Center/user/Writing_Rules.md',
95
+ ]) {
96
+ ok(fs.existsSync(path.join(WS, rel)), '铺好 ' + rel);
97
+ }
98
+
99
+ section('22 个工具在空白工作区逐个跑');
100
+ const calls = [
101
+ ['ho_diag', {}],
102
+ ['ho_brief', {}],
103
+ ['ho_gate', { scope: 'all' }],
104
+ ['ho_sessions', { action: 'list' }],
105
+ ['ho_phase', {}],
106
+ ['ho_charter', { doc: 'runtime' }],
107
+ ['ho_charter', { doc: 'charter', section: '10' }],
108
+ ['ho_handover', { action: 'read' }],
109
+ ['ho_handover', { action: 'write', 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' }],
110
+ ['ho_checkpoint', { at_risk: true }],
111
+ ['ho_snapshot', { project: 'NoSuchProject' }],
112
+ ['ho_drift', {}],
113
+ ['ho_changelog', { summary: '空白验收写入一行' }],
114
+ ['ho_tokens', { sessions: ['session-blank-test'] }],
115
+ ['ho_pack', { action: 'check', items: ['global'] }],
116
+ ['ho_workspace', { action: 'list' }],
117
+ ['peer_list', {}],
118
+ ['peer_status', { session_id: 'session-blank-test' }],
119
+ ['peer_peek', { session_id: 'session-blank-test', limit: 2 }],
120
+ ['peer_rename', { session_id: 'session-blank-test', title: '空白验收' }],
121
+ ];
122
+ for (const [tool, args] of calls) {
123
+ const r = await run(tool, args, WS, 'session-blank-test', '空白验收');
124
+ const acceptable = !r.err || /找不到项目|日志找不到|没有可读会话|not found/i.test(r.err);
125
+ ok(acceptable, tool + ' 可用', r.err);
126
+ }
127
+
128
+ section('删掉旧全局留下的规则文件:必须退回内置模板');
129
+ for (const rel of [
130
+ '_Control_Center/rules/Handover_Rules.md',
131
+ '_Control_Center/rules/Migration_Rules.md',
132
+ '_Control_Center/rules/Misc_Task_Standard.md',
133
+ '_Control_Center/user/Writing_Rules.md',
134
+ '_Control_Center/templates/Prompt_Templates.md',
135
+ '_Control_Center/records/Global_Changelog.md',
136
+ ]) fs.rmSync(path.join(WS, rel), { force: true });
137
+ for (const doc of ['handover_rules', 'migration_rules', 'misc_standard', 'writing_rules']) {
138
+ const r = await run('ho_charter', { doc }, WS, 'session-blank-test', '空白验收');
139
+ ok(!r.err && r.text.length > 50 && /内置/.test(r.text), 'ho_charter doc=' + doc + ' 用内置默认', r.err || String(r.text.length));
140
+ }
141
+ {
142
+ const r = await run('ho_sessions', { action: 'prompt', kind: 'project', project: 'Demo' }, WS, 'session-blank-test', '空白验收');
143
+ ok(!r.err && r.text.length > 50, 'ho_sessions(action=prompt) 缺模板也能渲染', r.err);
144
+ }
145
+ {
146
+ const r = await run('ho_changelog', { summary: '缺台账时自建' }, WS, 'session-blank-test', '空白验收');
147
+ ok(!r.err && fs.existsSync(path.join(WS, '_Control_Center/records/Global_Changelog.md')), 'ho_changelog 缺台账时自建', r.err);
148
+ }
149
+
150
+ section('有 _Control_Center 但没有 Charter:闸门仍要能跑(用内置约束条)');
151
+ fs.writeFileSync(path.join(WS, '_Control_Center', 'rules', 'Workspace_Charter.md'), '# empty\n', 'utf8');
152
+ {
153
+ const r = await run('ho_gate', { scope: 'all' }, WS, 'session-blank-test', '空白验收');
154
+ ok(!r.err && /内置口径|全部落位|缺/.test(r.text), 'Charter 空壳时闸门用内置约束条', r.err || String(r.text.slice(0, 80)));
155
+ }
156
+ fs.rmSync(path.join(WS, '_Control_Center', 'rules', 'Workspace_Charter.md'), { force: true });
157
+ {
158
+ const r = await run('ho_gate', { scope: 'all' }, WS, 'session-blank-test', '空白验收');
159
+ ok(!r.err && /内置/.test(r.text), 'Charter 完全缺失时闸门仍用内置约束条', r.err || String(r.text.slice(0, 80)));
160
+ }
161
+
162
+ section('迁移四件(无 7-Zip,走内置 tar.gz)');
163
+ fs.mkdirSync(path.join(WS, 'Projects', 'Demo', 'Demo库'), { recursive: true });
164
+ fs.writeFileSync(path.join(WS, 'Projects', 'Demo', 'Demo库', 'RULES.md'), '# Demo RULES\n', 'utf8');
165
+ fs.mkdirSync(path.join(WS, '_Project_Meta', 'Demo'), { recursive: true });
166
+ fs.writeFileSync(path.join(WS, '_Project_Meta', 'Demo', 'Handover.md'), '# Demo 交接\n\n## 1 正在做的事\n- 空\n\n## 2 下一步\n- 空\n\n## 3 坑点\n- 空\n', 'utf8');
167
+ const packOut = path.join(ROOT, 'packs');
168
+ const packed = await run('ho_pack', { action: 'pack', kind: 'project', name: 'Demo', out: packOut, format: 'tar' }, WS, 'session-blank-test', '空白验收');
169
+ ok(!packed.err, 'pack 打包成功', packed.err);
170
+ const pkg = fs.existsSync(packOut) ? fs.readdirSync(packOut).filter((f) => f.endsWith('.tar.gz')) : [];
171
+ ok(pkg.length === 1, '产出 1 个 .tar.gz', pkg.join(','));
172
+ if (pkg.length === 1) {
173
+ const packPath = path.join(packOut, pkg[0]);
174
+ const ver = await run('ho_pack', { action: 'verify', pack: packPath }, WS, 'session-blank-test', '空白验收');
175
+ ok(!ver.err && /一致/.test(ver.text), 'verify 校包可跑', ver.err);
176
+ const dry = await run('ho_pack', { action: 'restore', pack: packPath, target: path.join(ROOT, 'target'), dry_run: true }, WS, 'session-blank-test', '空白验收');
177
+ ok(!dry.err && /预演/.test(dry.text), 'restore 预演不写盘', dry.err);
178
+ const applied = await run('ho_pack', { action: 'restore', pack: packPath, target: path.join(ROOT, 'target'), apply: true }, WS, 'session-blank-test', '空白验收');
179
+ ok(!applied.err, 'restore 写入成功', applied.err);
180
+ const restored = path.join(ROOT, 'target', 'Projects', 'Demo', 'Demo库', 'RULES.md');
181
+ ok(fs.existsSync(restored), '恢复出来的文件在(相对路径正确)', restored);
182
+ if (fs.existsSync(restored)) {
183
+ const h = (f) => crypto.createHash('sha256').update(fs.readFileSync(f)).digest('hex');
184
+ ok(h(restored) === h(path.join(WS, 'Projects', 'Demo', 'Demo库', 'RULES.md')), '内容一致');
185
+ }
186
+ }
187
+
188
+ section('防越界');
189
+ ok(safeRel('../x') === null, 'safeRel 拒绝 ..');
190
+ ok(safeRel('/abs') === null, 'safeRel 拒绝绝对路径');
191
+ ok(safeRel('C:/x') === null, 'safeRel 拒绝带盘符');
192
+ ok(safeRel('a/b.md') === 'a/b.md', 'safeRel 放行正常相对路径');
193
+
194
+ fs.rmSync(ROOT, { recursive: true, force: true });
195
+ console.log(lines.join('\n'));
196
+ console.log('');
197
+ if (fails.length) {
198
+ console.log('BLANK-DSH FAIL ' + fails.length + ' 项:');
199
+ for (const f of fails) console.log(' - ' + f);
200
+ process.exit(1);
201
+ }
202
+ console.log('BLANK-DSH OK(空白 DSH 上 22 工具 + 迁移四件全部可用)');
203
+