@twinsearth/w2m-dsh-plugin 2.0.4 → 2.0.6

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 (70) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +1795 -1779
  4. package/bin/w2m-joint.mjs +69 -7
  5. package/codex/mcp-servers.toml +1 -1
  6. package/docs/acceptance/2.0.5/RELEASE-205.json +21 -0
  7. package/docs/acceptance/2.0.5/RELEASE-2050.json +22 -0
  8. package/docs/acceptance/2.0.6/RELEASE-2060.json +22 -0
  9. package/docs/adr/0001-collaboration-crdt.md +59 -0
  10. package/lib/client.js +215 -19
  11. package/package.json +2 -2
  12. package/plugin.json +1 -1
  13. package/src/agent/agent.mjs +97 -36
  14. package/src/agent/artifact-delivery.mjs +89 -0
  15. package/src/agent/caps.mjs +16 -2
  16. package/src/agent/sinks/git-sink.mjs +78 -0
  17. package/src/agent/sinks/ssh-sink.mjs +83 -0
  18. package/src/agent/sse-cursor.mjs +16 -0
  19. package/src/agent/sse-watchdog.mjs +71 -0
  20. package/src/plugin/dataplane-sink-route.mjs +118 -0
  21. package/src/plugin/fleet-chat-route.mjs +11 -0
  22. package/src/plugin/joint-dev/dispatch.mjs +5 -1
  23. package/src/plugin/joint-dev/merge-approval.mjs +115 -0
  24. package/src/plugin/joint-dev/progress.mjs +107 -0
  25. package/src/plugin/joint-dev/relay-dispatch.mjs +52 -22
  26. package/src/plugin/project-stream-route.mjs +58 -0
  27. package/src/plugin/tools-joint.mjs +6 -1
  28. package/src/plugin/tools.mjs +25 -5
  29. package/src/relay/fleet-messages.mjs +48 -24
  30. package/src/relay/project-room.mjs +47 -2
  31. package/src/relay/server.mjs +11 -5
  32. package/src/relay/state.mjs +63 -3
  33. package/src/util/chat-crypto.mjs +70 -1
  34. package/docs/COLLAB/COLLABORATION-MODEL.md +0 -240
  35. package/docs/COLLAB/EXECUTION-AUTH-MODEL.md +0 -361
  36. package/docs/COLLAB/M1-ACCEPTANCE-RUNBOOK.md +0 -70
  37. package/docs/COLLAB/M1-MAINTENANCE.md +0 -313
  38. package/docs/COLLAB/M2-TASK-GRANT.md +0 -231
  39. package/docs/COLLAB/M3-GIT-NATIVE-TASKS.md +0 -291
  40. package/docs/COLLAB/M4-PROJECT-ROOM.md +0 -260
  41. package/docs/COLLAB/M5-MULTI-CLIENT-PRIMITIVES.md +0 -263
  42. package/docs/COLLAB/M6-CROSS-NETWORK-RELIABILITY.md +0 -231
  43. package/docs/COLLAB/M7-METRICS-ACCEPTANCE.md +0 -263
  44. package/docs/COLLAB/TASK-MODEL.md +0 -281
  45. package/docs/acceptance/1.9.10/RELEASE-1910.json +0 -21
  46. package/docs/acceptance/1.9.11/RELEASE-1911.json +0 -20
  47. package/docs/acceptance/1.9.12/RELEASE-1912.json +0 -20
  48. package/docs/acceptance/1.9.13/RELEASE-1913.json +0 -20
  49. package/docs/acceptance/1.9.14/RELEASE-1914.json +0 -20
  50. package/docs/acceptance/1.9.15/RELEASE-1915.json +0 -22
  51. package/docs/acceptance/1.9.16/RELEASE-1916.json +0 -21
  52. package/docs/acceptance/1.9.17/RELEASE-1917.json +0 -20
  53. package/docs/acceptance/1.9.19/RELEASE-1919.json +0 -21
  54. package/docs/acceptance/1.9.20/RELEASE-1920.json +0 -22
  55. package/docs/acceptance/1.9.4/RELEASE-194.json +0 -23
  56. package/docs/acceptance/1.9.5/RELEASE-195.json +0 -23
  57. package/docs/acceptance/1.9.6/RELEASE-196.json +0 -22
  58. package/docs/acceptance/1.9.7/RELEASE-197.json +0 -21
  59. package/docs/acceptance/1.9.8/RELEASE-198.json +0 -21
  60. package/docs/acceptance/1.9.9/RELEASE-199.json +0 -21
  61. package/lib/client.js.bak2 +0 -640
  62. package/src/plugin/dataflow.mjs +0 -72
  63. package/src/plugin/fleet-target.mjs.bak-arch2 +0 -812
  64. package/src/plugin/fleet-target.mjs.bak-archive +0 -812
  65. package/src/plugin/tofuture/report.mjs +0 -44
  66. package/src/plugin/tools.mjs.bak-c7 +0 -7029
  67. package/src/relay/tofuture-whitelist.mjs +0 -46
  68. package/src/util/data-plane.mjs +0 -180
  69. package/src/util/sealed-object.mjs +0 -141
  70. package/src/util/tofuture-hmac.mjs +0 -30
package/CHANGELOG.md CHANGED
@@ -7,229 +7,245 @@
7
7
 
8
8
  All notable changes to this project are documented here.
9
9
 
10
- ## [2.0.4] - 2026-10-28
11
-
12
- - GPW 双策略模式:小白模式(默认)只保留黑名单强制拒绝,其余工具调用放行;高级模式需手动切换,保持四色策略。
13
- - 小白模式自带默认保护文件夹(C:\Windows、C:\Program Files、D:\Program Files、Program Files (x86) 两盘),
14
- 与用户黑名单合并后强制拒绝;路径类调用每 3 秒最多自动放行一次,限制突发损失。
15
- - 高级模式挂起超时自动放行由 100 秒调整为 10 秒;黑白名单编辑与四色统计保持不变。
16
- - GPW 设置页新增模式选择;小白模式下隐藏白名单区块、只显示默认保护文件夹与黑名单。
17
- - 主 README 重写(六大核心功能、安装、Verified/Known gaps),并修正为连续三次点击到顶等与实现一致的描述。
18
-
19
- ## [2.0.3] - 2026-10-28
20
-
21
- - 滚动按钮移到会话正文列右上角(黑框位置):按官方浮动圆钮规格 34px、无边框、elevation 阴影、
22
- label-primary 图标色(亮色显示为黑色,暗色自动反白)、hover/active/focus-visible 状态。
23
- - 位置由会话滚动区实时计算(正文列右缘 + 顶部内缩 8px),窗口缩放与侧栏变化时重新对齐。
24
- - 关键修复:自动完成 DSH 官方“加载更早”——会话是虚拟列表,不点“加载更早”时 scrollTop 到顶即止,
25
- 这是之前“点击无效/无法到顶”的真正原因。现在滚动前若已在顶部会先点一次“加载更早”等待 350ms,
26
- “连续点击 3 次到顶”会循环点击“加载更早”直到它消失,再把滚动区钉在顶部。
27
- - 交互:点一次向上滚动约 3 轮;1.2 秒内连续点击 3 次回到第一轮。
28
-
29
- ## [2.0.2] - 2026-10-28
30
-
31
- - 滚动按钮移到右下角,与 DSH 官方浮动滚动控件对齐(同右边缘、位于其上方;官方控件出现前后都会重新对齐)。
32
- - 修复点击无反应:滚动容器改为“试滚探针”(scrollTop ±1 实际是否被浏览器接受)寻找,
33
- 不再依赖 overflow 样式;命中后直接写 scrollTop。
34
- - 交互改为:点一次向上滚动约 3 轮;1.2 秒内连续点击 3 次回到第一轮;不再使用长按。
35
-
36
- ## [2.0.1] - 2026-10-28
37
-
38
- - 修复右上角滚动按钮无效:滚动容器识别改为“命中测试 + 递归 shadow root + 允许 overflow:hidden 的程序化滚动容器”,
39
- 并直接写 scrollTop/scrollTo;找不到容器时回退到 window。旧实现只找 overflow:auto,虚拟列表/隐藏溢出容器下点击是空操作。
40
- - 只保留一个滚动图标(SVG 向上箭头,样式与右上角官方图标对齐:32px、8px 圆角、1px 边框、right:16px/top:64px),
41
- 删除原来的第二个“一键到顶”按钮与会话头部的重复“⇧3”按钮。
42
- - 交互:点一次向上滚动约 3 轮;长按 3 秒直接回到第一轮(长按后松手的 click 不会重复触发)。
43
-
44
- ## [2.0.0] - 2026-10-28
45
-
46
- 主插件与既有 Relay 的对接按《W2M Relay × ToFuture 外部对接 API v1.2》收敛;ToFuture 子插件按该文档
47
- §6/§9 保持完全独立(主插件不内嵌、不调用、不代理、不实现其运维/白名单/令牌接口)。
48
-
49
- - 能力协商:/healthz 读取 features、operator_token_required、signing.required;缺失
50
- target-machines/result-projection 时明确拒绝联合派发;signing.required=true 拒绝(v1 外部客户端不支持强签);
51
- operator_token_required=false 拒绝(生产必须为 true)。
52
- - 派发:控制消息显式携带 target_machines(API §4.6:发到哪台机器只由它决定),保留
53
- executor_machine_id 兼容旧 relay;同 id 派发捕获 409 后读回任务而不是当成成功;
54
- 200 但无 task_id(依赖被拒)按失败处理;被点名机器租约 refused/expired/cancelled 立即失败。
55
- - 移除主插件内所有 ToFuture 运维面模块(MAC/IP 白名单 HMAC 上报、relay 侧识别路由),
56
- 遵守 API v1.2 §9:主插件不得实现、调用或探测运维/白名单/令牌接口。
57
- - 联合开发(plan/approve/dispatch/merge)仍为实验性:数据面投递与合并接线未随本版交付;
58
- relay 部署/升级独立于插件发布,线上 relay 版本以 /healthz 为准。
59
-
60
- ## [1.9.21] - 2026-10-28
61
-
62
- - 发布边界:本版只发主插件;ToFuture 运维子插件由独立私有仓库另行发布,本包不包含安装/下载能力。
63
- - 保留与 Relay 服务器配套的隐藏上报模块:HMAC 网络标识上报(/v1/tofuture/identify,relay 侧静默比对、只存 HMAC)。
64
- 该能力默认关闭、不注册工具、不显示 UI;relay 需自行升级后才生效。
65
- - 删除本机隐藏能力(命令触发、隐藏加密存储、自助下载/安装、票据模型),避免在公共 npm 包中携带未交付的运维面。
66
- - 沿用 1.9.20 的浮动滚动浮标(⇧3 单击 3 轮 / 长按 3 秒到顶 + ⤒ 一键到顶)与搜索绿字黄底高亮。
67
-
68
- ## [1.9.20] - 2026-10-28
69
-
70
- - 新增浮动滚动浮标(固定在主工作区右上角,不依赖 slot 布局):
71
- · 「⇧3」:单击向上滚动约 3 轮对话;长按 3 秒直接回到第一轮;
72
- · 「⤒」:一键到顶。
73
- - 浮标挂载在插件生命周期内(ctx.effect 卸载时移除),失败只记录、不影响页面启动。
74
-
75
- ## [1.9.19] - 2026-10-28
76
-
77
- - 对话右上角新增「⇧3」按钮:单击向上滚动约 3 轮对话,长按 3 秒直接回到第一轮。
78
- - 全文搜索高亮修复:改为直接插入本地 mark 元素(黄色背景 rgba(255,235,59,.85) + 绿色文字 rgba(11,122,51,1)),并递归遍历 shadow DOM;不再依赖 CSS Custom Highlight API,命中项一定可见。
79
- - 同版带上 v1.9.18 的聊天收紧:relay 只接收文字与 /微笑 /火箭 /烟花 指令(图片字段、data:image、Markdown 图片、base64 图片魔数一律 400 拒绝;加密消息按设计豁免);表情按钮改为指令按钮;长提示词镜像按 3500 B 截断,避免被 4096 B 上限静默丢弃。
80
-
81
- ## [1.9.17] - 2026-10-28
82
-
83
- - 修复左侧栏 GPW 入口只剩小图标:恢复 🌐 GPW 文字(侧栏 label 仍为空,不会重复)。
84
- - 对话全文搜索高亮加强:命中项绿色文字 + 黄色背景(::highlight(w2m-conversation-search) 与 .w2mSearchMark 双样式),主工作区正文中直接可见;显示 当前/总数,Enter/Shift+Enter 或 ↑/↓ 跳转,Esc 清除。
85
-
86
- ### [2.0.0-beta.1](pre-release) - 2026-10-28
87
-
88
- 首个联合开发 beta(S0–S1)。
89
-
90
- - S0 事实基座:operator token 安全存储(0600 文件 + 指纹状态)、relay /healthz 广告 relay_version/features、客户端能力协商 fail-closed;版本偏斜策略按“三机同版本才允许联合开发、enforce 不开启”执行。
91
- - S1 选择真实生效:派发前按当前设备名册重校验被选机器(离线 → TARGET_UNAVAILABLE,不换机);派发后按 relay leases 校验,未被点名的机器也执行 → 取消任务并 SELECTION_NOT_HONOURED。
92
- - 联合开发操作员入口:w2m-joint(plan create/list/get、approve、status、dispatch --dry-run,能力协商接入);联合开发默认关闭,属实验性 beta。
93
- - 运维工具:w2m-scrub(dry-run/apply,递归清点 blobs/<aa>/<sha>,备份必须外置,二次幂等),用于旧 relay 清淤。
94
- - beta 发布:仅 GitHub prerelease;npm 不发布本 beta。
95
-
96
- ## [1.9.16] - 2026-10-28
97
-
98
- - G0-2:relay /healthz 新增 relay_version 与 features(target-machines / result-projection / guard-v1 / relay-version-v1);w2m_status 显示这两项,便于核对三台 relay 的实际版本与能力。
99
- - 新增 relay-capabilities 模块与 w2m-joint dispatch 能力协商:目标 relay 缺少必需 feature(或旧 relay 完全不广告)时,联合派发 fail-closed 返回 RELAY_CAPABILITIES_MISSING,不靠“先发再发现”。
100
- - 新增 test/relay-health-capabilities.test.mjs(5 项)。
101
-
102
- ## [1.9.15] - 2026-10-28
103
-
104
- - G0-1:operator token 安全存储。新增 `~/.w2m/operator-token`(0600,可用 W2M_OPERATOR_TOKEN_FILE 覆盖)与 `w2m-operator` CLI(set 从 stdin/文件读取、status 只显示指纹、clear);w2m_status 新增 operatorToken_fingerprint/operatorToken_source,token 值不进日志/状态。
105
- - 解析顺序:profile config(兼容)→ 安全文件(推荐)→ 环境变量(兼容)。
106
- - 新增 test/operator-token.test.mjs(6 项)。
107
-
108
- ## [1.9.14] - 2026-10-28
109
-
110
- - 修复「立即更新」失败(fetch failed):GitHub Release 下载被网络阻断时,自动回退到 npm registry 的同一版本 tarball;下载前校验 registry 的 sha512 integrity(缺失时 sha1 shasum),再由现有安装器做 sha256 校验、备份与回滚。
111
- - 安装器仍走运行时 pnpm(Windows 不再尝试 dsh.cmd)。
112
- - 更新相关测试 66/66 通过。
113
-
114
- ## [1.9.13] - 2026-10-28
115
-
116
- - 新增操作员联合开发 CLI `w2m-joint`:plan create/list/get、approve(人签、authority≥70、TTL 默认 10 分钟)、status、dispatch --dry-run(打印控制信封,真实发送未接线并返回 DISPATCH_NOT_WIRED)。
117
- - 该 CLI 是 SOP 的操作员入口:计划先落盘、人工审批、再允许派发;dispatch 真发仍要求 operator token 与 relay 能力广告(下一片)。
118
- - 端到端本机冒烟:create → approve → status(dispatch_allowed=true) → dispatch --dry-run(2 envelopes)。
119
-
120
- ## [1.9.12] - 2026-10-28
121
-
122
- - 联合开发安全门(S1/S2 预置):plan-store 保存与读取都重算 plan_sha256(篡改即 PLAN_TAMPERED)、planSha 强制 64 位十六进制(拒绝路径穿越)、审批绑定 plan_sha/approver/authority/TTL 并重算 approval_sha256(篡改/过期/低权限拒绝)、原子写。
123
- - 全部机器选中真实性(S1):w2m_run 在派发前用当前设备名册重校验被选机器(离线即 TARGET_UNAVAILABLE,不换机);派发后按 relay 的 leases 校验,发现未被点名的机器也拿到租约即取消任务并返回 SELECTION_NOT_HONOURED。
124
- - 新增 test/joint-dev-plan-store.test.mjs(11 项)。
125
-
126
- ## [1.9.11] - 2026-10-28
127
-
128
- - 修复左侧栏只剩图标:GPW 名称恢复到入口内(🌐 GPW),侧栏 label 仍为空,避免再次重复。
129
- - 对话全文搜索:匹配项以黄色高亮(CSS Custom Highlight API ::highlight(w2m-conversation-search)),显示 当前/总数 计数;Enter/Shift+Enter 或 ↑/↓ 在命中项间滚动,Esc 清除。
130
-
131
- ## [1.9.10] - 2026-10-28
132
-
133
- - 新增 `bin/w2m-self-update.mjs`:可被编队任务直接下发的自更新入口,读取 GitHub Release、校验 SHA256SUMS、调用已验证安装器,输出一行 JSON,不重启 DSH。
134
- - 固化升级策略:优先下发自更新指令;失败才使用 Node 远程执行回退(macOS 用 mac-self-update,Windows 用 node 替换+备份)。
135
- - 参数:--profile、--dry-run、--allow-prerelease;环境变量 W2M_PROFILE_DIR / W2M_UPDATE_REPO。
136
-
137
- ## [1.9.9] - 2026-10-28
138
-
139
- - 对话主工作区新增全文搜索:会话头部(置顶按钮同排)🔍 图标,点击展开搜索框,Enter/Shift+Enter 或 ↑/↓ 在已渲染对话中查找并高亮(Electron/Chromium find)。
140
-
141
- - 修复 Windows「立即更新」失败:dsh 以 dsh.cmd 安装时,child_process.spawn(shell:false) 会报 EINVAL。现在 Windows 下不再把 .cmd/.bat 当作可直接执行计划,自动改用运行时 pnpm;若 dsh 计划出现 EINVAL,也会回退 pnpm 重试一次。
142
- - 新增回归测试:.cmd 计划被拒绝并走 pnpm;spawn 失败仍回滚。
143
-
144
- ## [1.9.8] - 2026-10-28
145
-
146
- - 左栏 GPW 入口改为纯图标(🌐),去掉重复的文字条目。
147
- - 设置页 GPW 标签名称改为「GPW」,并使用与管理台一致的新页面(不再复用弹窗面板)。
148
- - 编队群组视图内补齐常用聊天表情行(此前只在浮窗全屏时出现)。
149
-
150
- ## [1.9.7] - 2026-10-28
151
-
152
- - 侧边栏与设置页入口统一命名为「GPW」,左对齐并沿用插件标签的字体、字号与间距。
153
- - GPW 主工作区面板与设置页面板改为管理台布局:标题 + 查询/操作卡片 + 标签页 + 数据表格(路径/类型/状态/移除),参考系统黑白名单界面。
154
- - 右下角浮标继续复用原弹窗面板,Composer 入口不变。
155
-
156
- ## [1.9.6] - 2026-10-28
157
- - 修复:左侧栏 GPW 点击无效。侧栏入口改用 `main` 槽位(key 与 panellist id 一致),点击切换到主工作区的完整 GPW 页面。
158
- - GPW 页面改为主工作区整页布局(960px 内容、独立滚动、卡片化分区),不再是右下角弹窗形态。
159
- - 修复:设置页 GPW 标签沿用设置页样式(与通用设置页一致的卡片/分区)。
160
- - 新增:「编队群组」对话视图标签(conversation.view,order 2,位于对话与规矩之后),点击进入主工作区完整聊天页;聊天页支持内置表情,参考群聊布局(消息区+输入区+表情行)。
161
- - 底部状态栏保持:本编队在线/总数、全部机器在线/总数。
162
- 本次为 v1.9.5 五方发布(1.9.4 未发布,内容并入本版本)。
163
- - 顶部标签栏新增「编队聊天」入口:点击进入完整分页面,支持内置表情。
164
- - 底部状态栏常驻显示:本编队在线/总数、全部机器在线/总数,无需展开 Composer 的机器列表。
165
- - 左侧导航栏新增 GPW 入口(🌐 图标),位于插件与自动化任务之间;点击进入完整分页面(960px 工作区),右下角浮标与 Composer 入口保持不变。
166
- - 设置页新增 GPW 标签栏,位于「模型」与「内置插件」之间,内嵌完整 GPW 管理面板。
167
- - 插件详情页(与卸载按钮同页)新增「W2M 插件更新」:检测更新、立即更新、自动更新开关;安装完成后自动刷新页面,无需卸载、无需重启应用。
168
- - 更新检测打开即执行;旧版本直接显示新版本号与安装按钮;npm 镜像失败回退 registry.npmjs.org。
169
-
170
- ## [1.9.5] - 2026-10-27
171
-
172
- ## [1.9.3] - 2026-10-27
173
- - 左侧边栏新增常驻 GPW 入口(默认显示、无关闭按钮),Composer 与右下角浮窗保留。
174
- - 插件设置页新增“W2M 插件更新”分区:检测更新、发现新版本后立即安装、自动更新开关(开启后每次启动自动安装)。
175
- - 更新检测修复:设置页打开即检查;本机为旧版时直接显示新版本号与安装按钮;npm 镜像不可用时回退 registry.npmjs.org。
176
- - 自动更新不再只是偏好:开关打开后会实际调用校验与安装流程,并提示重启。
177
-
178
- ## [1.9.2-beta.1] - 2026-10-27
179
- - P0 止血:relay 数据面端点退役(blob/blob-sessions/latest → 403),结果信封校验后投影后存储(不再落 argv/head/tail/路径原文)。
180
- - B1 兼容窗口:默认接收 v1 信封并投影,设置 W2M_DATA_PLANE_GUARD=enforce 才硬拒。
181
- - guard 递归扫描 + 16 KiB 语义上限 + 协议错误形状。
182
- - beta:仅 GitHub prerelease,不含 npm/packages 发布。
183
- **V1.9.1 更新**
184
-
185
- ## [1.9.1] — 2026-10-27
186
-
187
- ## [1.9.0] — 2026-10-27
188
-
189
- **V1.9.0 大版本更新**
190
-
191
- ## [1.8.2] — 2026-10-26
192
-
193
- **GPW 超时策略:黄类(名单外)100 秒无人应答自动同意;黑白名单规则不变;已覆盖父目录的子文件夹拦截记录折叠。**
194
-
195
- - **只读工具绿名单扩展**:`read/read_file/search/grep/glob/list/…`、`job_list/job_output/job_status/job_wait`、`w2m_status/w2m_devices/w2m_tasks/w2m_history/w2m_report/w2m_memory/w2m_stats` 判为绿色(无打扰放行);`job_kill/job_start/w2m_run/w2m_cancel/w2m_intent` 仍为黄色(可变更,需人工)。
196
- - 超时默认值 120s → **100s**;超时结果由「拒绝」改为「自动同意」(仅黄类 / 名单外路径调用)。
197
- - 红线(危险命令、敏感/系统路径、显式 deny)仍然立即拒绝,不进入待审批队列;白名单命中仍然立即放行。
198
- 因此超时规则不会扩大任何名单的授权范围。
199
- - 自动同意的调用会记录为会话授权(指纹),同一操作的后续出现以蓝色(已授权常态)直接放行。
200
- - 「最近被拦截」折叠逻辑确认覆盖父目录下的子文件夹:名单条目是根目录,其任意深度子文件夹的拦截记录都会进入
201
- 「已在名单中 N 条」折叠组(`folder_in` 双向包含匹配)。
202
- - 新增 `test/gpw-auto-approve.test.mjs`(4 项:超时自动同意→下次变蓝;红线仍拒;白名单立即放行;100s 默认值)。
203
- - 证据:`docs/acceptance/1.8.2/GPW-AUTO-APPROVE.json`。
204
- ## [1.8.1] — 2026-10-26
205
-
206
- **GPW「最近被拦截」去噪:已在名单中的条目折叠显示;加入名单后条目立即消失。**
207
-
208
- - 状态接口为每条 recent 增加 `folder_in`(whitelist / blacklist / null):文件夹已被名单覆盖的拦截记录不再占用主列表。
209
- - 面板把已覆盖条目折叠为「已在名单中 N 条 · 展开/收起」,展开后只读展示(不重复给加入按钮)。
210
- - 从最近拦截一键加入白/黑名单时,服务端同步裁剪该条目(以及被同一父目录覆盖的条目),下一次轮询不再出现。
211
- - 新增 `test/gpw-recent-fold.test.mjs`(4 项:覆盖归属、嵌套覆盖、客户端折叠静态断言、状态接口字段)。
212
- - 证据:`docs/acceptance/1.8.1/GPW-RECENT-FOLD.json`。
213
- ## [1.8.0] — 2026-10-26
214
-
215
- **GPW 增加四色审核 / 标记 / 提醒系统:绿(通过)、蓝(正常)、黄(警报)、红(风险)。**
216
-
217
- - 新增 `src/plugin/gpw-colors.mjs`:把 DSH 各插件的审核行为归位到四色(只读/L1-L2/白名单命中/allow-list = 绿;
218
- 本会话已批准过(指纹授权)= 蓝;越界写、非白名单命令、中风险 git/npm/网络操作、未知工具 = 黄;
219
- 危险命令黑名单、敏感路径、系统路径、显式 deny 目录 = 红)。
220
- - 门控改为颜色决策:绿/蓝直接放行(蓝来自会话内批准后的操作指纹);黄进入 GPW 待审批队列;红直接拒绝
221
- (`tools.guard` + `tools/pre-execute` 双保险,拒绝理由带颜色语义)。
222
- - 标记与提醒:`/api/w2m/gpw/state` 新增四色计数(colors)、待审批项与最近拦截项的颜色字段;
223
- 面板显示四色图例与计数,待审批/最近条目带颜色标记;「允许本次会话」动作(allow-session)批准后,
224
- 同一操作后续以蓝色(已授权常态)放行。
225
- - 一句话语义:绿=不用管,蓝=管过了,黄=现在管,红=管也不让过(或必须人工硬确认)。
226
- - 证据:`docs/acceptance/1.8.0/GPW-COLORS.json`。
10
+ ## [2.0.6] - 2026-10-29
11
+
12
+ - 修复 CI 跨平台失败:sink 授权 route 先把 stateDir 解析为 realpath(Windows RUNNER~1 / macOS
13
+ /var→/private/var),避免“stateDir 内部目录”被误放行;相关测试按 canonical 路径断言。
14
+ - 会话搜索:纯数字 ≤100 不触发搜索(无高亮/无跳转);只高亮当前视口,每屏最多 6 条,
15
+ 滚动时按新视口刷新(150ms 防抖,不抢滚动位置);输入仍保持 3 秒防抖。
16
+ - 一键到顶:等待“加载中…”结束后再点“加载更早”,连续两页无增长即停止。
17
+
18
+ ## [2.0.5] - 2026-10-29
19
+
20
+ - Relay 支持 AEAD v2 加密消息信封(A7 收尾):`enc.v=2` 携带 `aad:{session_id,machine_id,message_id}`,
21
+ relay 校验明文 AAD 与消息的 session/machine 一致后原样存储与回放;v1 信封保持兼容。
22
+ - 新增能力标记 `chat-enc-v2`(`/healthz.features`),主插件据此启用 encrypted mirror 发送。
23
+ - 新增错误码:`CHAT_AAD_MISSING` / `CHAT_AAD_MISMATCH` / `CHAT_AAD_INVALID`。
24
+ - 测试:fleet-messages AEAD v2 用例、能力协商断言(v1 兼容与未知版本拒绝保持)。
25
+
26
+ ## [2.0.4] - 2026-10-28
27
+
28
+ - GPW 双策略模式:小白模式(默认)只保留黑名单强制拒绝,其余工具调用放行;高级模式需手动切换,保持四色策略。
29
+ - 小白模式自带默认保护文件夹(C:\Windows、C:\Program Files、D:\Program Files、Program Files (x86) 两盘),
30
+ 与用户黑名单合并后强制拒绝;路径类调用每 3 秒最多自动放行一次,限制突发损失。
31
+ - 高级模式挂起超时自动放行由 100 秒调整为 10 秒;黑白名单编辑与四色统计保持不变。
32
+ - GPW 设置页新增模式选择;小白模式下隐藏白名单区块、只显示默认保护文件夹与黑名单。
33
+ - 主 README 重写(六大核心功能、安装、Verified/Known gaps),并修正为连续三次点击到顶等与实现一致的描述。
34
+
35
+ ## [2.0.3] - 2026-10-28
36
+
37
+ - 滚动按钮移到会话正文列右上角(黑框位置):按官方浮动圆钮规格 34px、无边框、elevation 阴影、
38
+ label-primary 图标色(亮色显示为黑色,暗色自动反白)、hover/active/focus-visible 状态。
39
+ - 位置由会话滚动区实时计算(正文列右缘 + 顶部内缩 8px),窗口缩放与侧栏变化时重新对齐。
40
+ - 关键修复:自动完成 DSH 官方“加载更早”——会话是虚拟列表,不点“加载更早”时 scrollTop 到顶即止,
41
+ 这是之前“点击无效/无法到顶”的真正原因。现在滚动前若已在顶部会先点一次“加载更早”等待 350ms,
42
+ “连续点击 3 次到顶”会循环点击“加载更早”直到它消失,再把滚动区钉在顶部。
43
+ - 交互:点一次向上滚动约 3 轮;1.2 秒内连续点击 3 次回到第一轮。
44
+
45
+ ## [2.0.2] - 2026-10-28
46
+
47
+ - 滚动按钮移到右下角,与 DSH 官方浮动滚动控件对齐(同右边缘、位于其上方;官方控件出现前后都会重新对齐)。
48
+ - 修复点击无反应:滚动容器改为“试滚探针”(scrollTop ±1 实际是否被浏览器接受)寻找,
49
+ 不再依赖 overflow 样式;命中后直接写 scrollTop。
50
+ - 交互改为:点一次向上滚动约 3 轮;1.2 秒内连续点击 3 次回到第一轮;不再使用长按。
51
+
52
+ ## [2.0.1] - 2026-10-28
53
+
54
+ - 修复右上角滚动按钮无效:滚动容器识别改为“命中测试 + 递归 shadow root + 允许 overflow:hidden 的程序化滚动容器”,
55
+ 并直接写 scrollTop/scrollTo;找不到容器时回退到 window。旧实现只找 overflow:auto,虚拟列表/隐藏溢出容器下点击是空操作。
56
+ - 只保留一个滚动图标(SVG 向上箭头,样式与右上角官方图标对齐:32px、8px 圆角、1px 边框、right:16px/top:64px),
57
+ 删除原来的第二个“一键到顶”按钮与会话头部的重复“⇧3”按钮。
58
+ - 交互:点一次向上滚动约 3 轮;长按 3 秒直接回到第一轮(长按后松手的 click 不会重复触发)。
59
+
60
+ ## [2.0.0] - 2026-10-28
61
+
62
+ 主插件与既有 Relay 的对接按《W2M Relay × ToFuture 外部对接 API v1.2》收敛;ToFuture 子插件按该文档
63
+ §6/§9 保持完全独立(主插件不内嵌、不调用、不代理、不实现其运维/白名单/令牌接口)。
64
+
65
+ - 能力协商:/healthz 读取 features、operator_token_required、signing.required;缺失
66
+ target-machines/result-projection 时明确拒绝联合派发;signing.required=true 拒绝(v1 外部客户端不支持强签);
67
+ operator_token_required=false 拒绝(生产必须为 true)。
68
+ - 派发:控制消息显式携带 target_machines(API §4.6:发到哪台机器只由它决定),保留
69
+ executor_machine_id 兼容旧 relay;同 id 派发捕获 409 后读回任务而不是当成成功;
70
+ 200 但无 task_id(依赖被拒)按失败处理;被点名机器租约 refused/expired/cancelled 立即失败。
71
+ - 移除主插件内所有 ToFuture 运维面模块(MAC/IP 白名单 HMAC 上报、relay 侧识别路由),
72
+ 遵守 API v1.2 §9:主插件不得实现、调用或探测运维/白名单/令牌接口。
73
+ - 联合开发(plan/approve/dispatch/merge)仍为实验性:数据面投递与合并接线未随本版交付;
74
+ relay 部署/升级独立于插件发布,线上 relay 版本以 /healthz 为准。
75
+
76
+ ## [1.9.21] - 2026-10-28
77
+
78
+ - 发布边界:本版只发主插件;ToFuture 运维子插件由独立私有仓库另行发布,本包不包含安装/下载能力。
79
+ - 保留与 Relay 服务器配套的隐藏上报模块:HMAC 网络标识上报(/v1/tofuture/identify,relay 侧静默比对、只存 HMAC)。
80
+ 该能力默认关闭、不注册工具、不显示 UI;relay 需自行升级后才生效。
81
+ - 删除本机隐藏能力(命令触发、隐藏加密存储、自助下载/安装、票据模型),避免在公共 npm 包中携带未交付的运维面。
82
+ - 沿用 1.9.20 的浮动滚动浮标(⇧3 单击 3 轮 / 长按 3 秒到顶 + ⤒ 一键到顶)与搜索绿字黄底高亮。
83
+
84
+ ## [1.9.20] - 2026-10-28
85
+
86
+ - 新增浮动滚动浮标(固定在主工作区右上角,不依赖 slot 布局):
87
+ · 「⇧3」:单击向上滚动约 3 轮对话;长按 3 秒直接回到第一轮;
88
+ · 「⤒」:一键到顶。
89
+ - 浮标挂载在插件生命周期内(ctx.effect 卸载时移除),失败只记录、不影响页面启动。
90
+
91
+ ## [1.9.19] - 2026-10-28
92
+
93
+ - 对话右上角新增「⇧3」按钮:单击向上滚动约 3 轮对话,长按 3 秒直接回到第一轮。
94
+ - 全文搜索高亮修复:改为直接插入本地 mark 元素(黄色背景 rgba(255,235,59,.85) + 绿色文字 rgba(11,122,51,1)),并递归遍历 shadow DOM;不再依赖 CSS Custom Highlight API,命中项一定可见。
95
+ - 同版带上 v1.9.18 的聊天收紧:relay 只接收文字与 /微笑 /火箭 /烟花 指令(图片字段、data:image、Markdown 图片、base64 图片魔数一律 400 拒绝;加密消息按设计豁免);表情按钮改为指令按钮;长提示词镜像按 3500 B 截断,避免被 4096 B 上限静默丢弃。
96
+
97
+ ## [1.9.17] - 2026-10-28
98
+
99
+ - 修复左侧栏 GPW 入口只剩小图标:恢复 🌐 GPW 文字(侧栏 label 仍为空,不会重复)。
100
+ - 对话全文搜索高亮加强:命中项绿色文字 + 黄色背景(::highlight(w2m-conversation-search) 与 .w2mSearchMark 双样式),主工作区正文中直接可见;显示 当前/总数,Enter/Shift+Enter 或 ↑/↓ 跳转,Esc 清除。
101
+
102
+ ### [2.0.0-beta.1](pre-release) - 2026-10-28
103
+
104
+ 首个联合开发 beta(S0–S1)。
105
+
106
+ - S0 事实基座:operator token 安全存储(0600 文件 + 指纹状态)、relay /healthz 广告 relay_version/features、客户端能力协商 fail-closed;版本偏斜策略按“三机同版本才允许联合开发、enforce 不开启”执行。
107
+ - S1 选择真实生效:派发前按当前设备名册重校验被选机器(离线 → TARGET_UNAVAILABLE,不换机);派发后按 relay leases 校验,未被点名的机器也执行 → 取消任务并 SELECTION_NOT_HONOURED。
108
+ - 联合开发操作员入口:w2m-joint(plan create/list/get、approve、status、dispatch --dry-run,能力协商接入);联合开发默认关闭,属实验性 beta。
109
+ - 运维工具:w2m-scrub(dry-run/apply,递归清点 blobs/<aa>/<sha>,备份必须外置,二次幂等),用于旧 relay 清淤。
110
+ - beta 发布:仅 GitHub prerelease;npm 不发布本 beta。
111
+
112
+ ## [1.9.16] - 2026-10-28
113
+
114
+ - G0-2:relay /healthz 新增 relay_version 与 features(target-machines / result-projection / guard-v1 / relay-version-v1);w2m_status 显示这两项,便于核对三台 relay 的实际版本与能力。
115
+ - 新增 relay-capabilities 模块与 w2m-joint dispatch 能力协商:目标 relay 缺少必需 feature(或旧 relay 完全不广告)时,联合派发 fail-closed 返回 RELAY_CAPABILITIES_MISSING,不靠“先发再发现”。
116
+ - 新增 test/relay-health-capabilities.test.mjs(5 项)。
117
+
118
+ ## [1.9.15] - 2026-10-28
119
+
120
+ - G0-1:operator token 安全存储。新增 `~/.w2m/operator-token`(0600,可用 W2M_OPERATOR_TOKEN_FILE 覆盖)与 `w2m-operator` CLI(set 从 stdin/文件读取、status 只显示指纹、clear);w2m_status 新增 operatorToken_fingerprint/operatorToken_source,token 值不进日志/状态。
121
+ - 解析顺序:profile config(兼容)→ 安全文件(推荐)→ 环境变量(兼容)。
122
+ - 新增 test/operator-token.test.mjs(6 项)。
123
+
124
+ ## [1.9.14] - 2026-10-28
125
+
126
+ - 修复「立即更新」失败(fetch failed):GitHub Release 下载被网络阻断时,自动回退到 npm registry 的同一版本 tarball;下载前校验 registry 的 sha512 integrity(缺失时 sha1 shasum),再由现有安装器做 sha256 校验、备份与回滚。
127
+ - 安装器仍走运行时 pnpm(Windows 不再尝试 dsh.cmd)。
128
+ - 更新相关测试 66/66 通过。
129
+
130
+ ## [1.9.13] - 2026-10-28
131
+
132
+ - 新增操作员联合开发 CLI `w2m-joint`:plan create/list/get、approve(人签、authority≥70、TTL 默认 10 分钟)、status、dispatch --dry-run(打印控制信封,真实发送未接线并返回 DISPATCH_NOT_WIRED)。
133
+ - 该 CLI 是 SOP 的操作员入口:计划先落盘、人工审批、再允许派发;dispatch 真发仍要求 operator token 与 relay 能力广告(下一片)。
134
+ - 端到端本机冒烟:create → approve → status(dispatch_allowed=true) → dispatch --dry-run(2 envelopes)。
135
+
136
+ ## [1.9.12] - 2026-10-28
137
+
138
+ - 联合开发安全门(S1/S2 预置):plan-store 保存与读取都重算 plan_sha256(篡改即 PLAN_TAMPERED)、planSha 强制 64 位十六进制(拒绝路径穿越)、审批绑定 plan_sha/approver/authority/TTL 并重算 approval_sha256(篡改/过期/低权限拒绝)、原子写。
139
+ - 全部机器选中真实性(S1):w2m_run 在派发前用当前设备名册重校验被选机器(离线即 TARGET_UNAVAILABLE,不换机);派发后按 relay 的 leases 校验,发现未被点名的机器也拿到租约即取消任务并返回 SELECTION_NOT_HONOURED。
140
+ - 新增 test/joint-dev-plan-store.test.mjs(11 项)。
141
+
142
+ ## [1.9.11] - 2026-10-28
143
+
144
+ - 修复左侧栏只剩图标:GPW 名称恢复到入口内(🌐 GPW),侧栏 label 仍为空,避免再次重复。
145
+ - 对话全文搜索:匹配项以黄色高亮(CSS Custom Highlight API ::highlight(w2m-conversation-search)),显示 当前/总数 计数;Enter/Shift+Enter 或 ↑/↓ 在命中项间滚动,Esc 清除。
146
+
147
+ ## [1.9.10] - 2026-10-28
148
+
149
+ - 新增 `bin/w2m-self-update.mjs`:可被编队任务直接下发的自更新入口,读取 GitHub Release、校验 SHA256SUMS、调用已验证安装器,输出一行 JSON,不重启 DSH。
150
+ - 固化升级策略:优先下发自更新指令;失败才使用 Node 远程执行回退(macOS 用 mac-self-update,Windows 用 node 替换+备份)。
151
+ - 参数:--profile、--dry-run、--allow-prerelease;环境变量 W2M_PROFILE_DIR / W2M_UPDATE_REPO。
152
+
153
+ ## [1.9.9] - 2026-10-28
154
+
155
+ - 对话主工作区新增全文搜索:会话头部(置顶按钮同排)🔍 图标,点击展开搜索框,Enter/Shift+Enter 或 ↑/↓ 在已渲染对话中查找并高亮(Electron/Chromium find)。
156
+
157
+ - 修复 Windows「立即更新」失败:dsh 以 dsh.cmd 安装时,child_process.spawn(shell:false) 会报 EINVAL。现在 Windows 下不再把 .cmd/.bat 当作可直接执行计划,自动改用运行时 pnpm;若 dsh 计划出现 EINVAL,也会回退 pnpm 重试一次。
158
+ - 新增回归测试:.cmd 计划被拒绝并走 pnpm;spawn 失败仍回滚。
159
+
160
+ ## [1.9.8] - 2026-10-28
161
+
162
+ - 左栏 GPW 入口改为纯图标(🌐),去掉重复的文字条目。
163
+ - 设置页 GPW 标签名称改为「GPW」,并使用与管理台一致的新页面(不再复用弹窗面板)。
164
+ - 编队群组视图内补齐常用聊天表情行(此前只在浮窗全屏时出现)。
165
+
166
+ ## [1.9.7] - 2026-10-28
167
+
168
+ - 侧边栏与设置页入口统一命名为「GPW」,左对齐并沿用插件标签的字体、字号与间距。
169
+ - GPW 主工作区面板与设置页面板改为管理台布局:标题 + 查询/操作卡片 + 标签页 + 数据表格(路径/类型/状态/移除),参考系统黑白名单界面。
170
+ - 右下角浮标继续复用原弹窗面板,Composer 入口不变。
171
+
172
+ ## [1.9.6] - 2026-10-28
173
+ - 修复:左侧栏 GPW 点击无效。侧栏入口改用 `main` 槽位(key 与 panellist id 一致),点击切换到主工作区的完整 GPW 页面。
174
+ - GPW 页面改为主工作区整页布局(960px 内容、独立滚动、卡片化分区),不再是右下角弹窗形态。
175
+ - 修复:设置页 GPW 标签沿用设置页样式(与通用设置页一致的卡片/分区)。
176
+ - 新增:「编队群组」对话视图标签(conversation.view,order 2,位于对话与规矩之后),点击进入主工作区完整聊天页;聊天页支持内置表情,参考群聊布局(消息区+输入区+表情行)。
177
+ - 底部状态栏保持:本编队在线/总数、全部机器在线/总数。
178
+ 本次为 v1.9.5 五方发布(1.9.4 未发布,内容并入本版本)。
179
+ - 顶部标签栏新增「编队聊天」入口:点击进入完整分页面,支持内置表情。
180
+ - 底部状态栏常驻显示:本编队在线/总数、全部机器在线/总数,无需展开 Composer 的机器列表。
181
+ - 左侧导航栏新增 GPW 入口(🌐 图标),位于插件与自动化任务之间;点击进入完整分页面(960px 工作区),右下角浮标与 Composer 入口保持不变。
182
+ - 设置页新增 GPW 标签栏,位于「模型」与「内置插件」之间,内嵌完整 GPW 管理面板。
183
+ - 插件详情页(与卸载按钮同页)新增「W2M 插件更新」:检测更新、立即更新、自动更新开关;安装完成后自动刷新页面,无需卸载、无需重启应用。
184
+ - 更新检测打开即执行;旧版本直接显示新版本号与安装按钮;npm 镜像失败回退 registry.npmjs.org。
185
+
186
+ ## [1.9.5] - 2026-10-27
187
+
188
+ ## [1.9.3] - 2026-10-27
189
+ - 左侧边栏新增常驻 GPW 入口(默认显示、无关闭按钮),Composer 与右下角浮窗保留。
190
+ - 插件设置页新增“W2M 插件更新”分区:检测更新、发现新版本后立即安装、自动更新开关(开启后每次启动自动安装)。
191
+ - 更新检测修复:设置页打开即检查;本机为旧版时直接显示新版本号与安装按钮;npm 镜像不可用时回退 registry.npmjs.org。
192
+ - 自动更新不再只是偏好:开关打开后会实际调用校验与安装流程,并提示重启。
193
+
194
+ ## [1.9.2-beta.1] - 2026-10-27
195
+ - P0 止血:relay 数据面端点退役(blob/blob-sessions/latest → 403),结果信封校验后投影后存储(不再落 argv/head/tail/路径原文)。
196
+ - B1 兼容窗口:默认接收 v1 信封并投影,设置 W2M_DATA_PLANE_GUARD=enforce 才硬拒。
197
+ - guard 递归扫描 + 16 KiB 语义上限 + 协议错误形状。
198
+ - beta:仅 GitHub prerelease,不含 npm/packages 发布。
199
+ **V1.9.1 更新**
200
+
201
+ ## [1.9.1] — 2026-10-27
202
+
203
+ ## [1.9.0] — 2026-10-27
204
+
205
+ **V1.9.0 大版本更新**
206
+
207
+ ## [1.8.2] — 2026-10-26
208
+
209
+ **GPW 超时策略:黄类(名单外)100 秒无人应答自动同意;黑白名单规则不变;已覆盖父目录的子文件夹拦截记录折叠。**
210
+
211
+ - **只读工具绿名单扩展**:`read/read_file/search/grep/glob/list/…`、`job_list/job_output/job_status/job_wait`、`w2m_status/w2m_devices/w2m_tasks/w2m_history/w2m_report/w2m_memory/w2m_stats` 判为绿色(无打扰放行);`job_kill/job_start/w2m_run/w2m_cancel/w2m_intent` 仍为黄色(可变更,需人工)。
212
+ - 超时默认值 120s → **100s**;超时结果由「拒绝」改为「自动同意」(仅黄类 / 名单外路径调用)。
213
+ - 红线(危险命令、敏感/系统路径、显式 deny)仍然立即拒绝,不进入待审批队列;白名单命中仍然立即放行。
214
+ 因此超时规则不会扩大任何名单的授权范围。
215
+ - 自动同意的调用会记录为会话授权(指纹),同一操作的后续出现以蓝色(已授权常态)直接放行。
216
+ - 「最近被拦截」折叠逻辑确认覆盖父目录下的子文件夹:名单条目是根目录,其任意深度子文件夹的拦截记录都会进入
217
+ 「已在名单中 N 条」折叠组(`folder_in` 双向包含匹配)。
218
+ - 新增 `test/gpw-auto-approve.test.mjs`(4 项:超时自动同意→下次变蓝;红线仍拒;白名单立即放行;100s 默认值)。
219
+ - 证据:`docs/acceptance/1.8.2/GPW-AUTO-APPROVE.json`。
220
+ ## [1.8.1] — 2026-10-26
221
+
222
+ **GPW「最近被拦截」去噪:已在名单中的条目折叠显示;加入名单后条目立即消失。**
223
+
224
+ - 状态接口为每条 recent 增加 `folder_in`(whitelist / blacklist / null):文件夹已被名单覆盖的拦截记录不再占用主列表。
225
+ - 面板把已覆盖条目折叠为「已在名单中 N 条 · 展开/收起」,展开后只读展示(不重复给加入按钮)。
226
+ - 从最近拦截一键加入白/黑名单时,服务端同步裁剪该条目(以及被同一父目录覆盖的条目),下一次轮询不再出现。
227
+ - 新增 `test/gpw-recent-fold.test.mjs`(4 项:覆盖归属、嵌套覆盖、客户端折叠静态断言、状态接口字段)。
228
+ - 证据:`docs/acceptance/1.8.1/GPW-RECENT-FOLD.json`。
229
+ ## [1.8.0] — 2026-10-26
230
+
231
+ **GPW 增加四色审核 / 标记 / 提醒系统:绿(通过)、蓝(正常)、黄(警报)、红(风险)。**
232
+
233
+ - 新增 `src/plugin/gpw-colors.mjs`:把 DSH 各插件的审核行为归位到四色(只读/L1-L2/白名单命中/allow-list = 绿;
234
+ 本会话已批准过(指纹授权)= 蓝;越界写、非白名单命令、中风险 git/npm/网络操作、未知工具 = 黄;
235
+ 危险命令黑名单、敏感路径、系统路径、显式 deny 目录 = 红)。
236
+ - 门控改为颜色决策:绿/蓝直接放行(蓝来自会话内批准后的操作指纹);黄进入 GPW 待审批队列;红直接拒绝
237
+ (`tools.guard` + `tools/pre-execute` 双保险,拒绝理由带颜色语义)。
238
+ - 标记与提醒:`/api/w2m/gpw/state` 新增四色计数(colors)、待审批项与最近拦截项的颜色字段;
239
+ 面板显示四色图例与计数,待审批/最近条目带颜色标记;「允许本次会话」动作(allow-session)批准后,
240
+ 同一操作后续以蓝色(已授权常态)放行。
241
+ - 一句话语义:绿=不用管,蓝=管过了,黄=现在管,红=管也不让过(或必须人工硬确认)。
242
+ - 证据:`docs/acceptance/1.8.0/GPW-COLORS.json`。
227
243
  ## [1.7.9] — 2026-10-26
228
244
 
229
245
  **GPW 三项操作性问题修复:拦截即时弹选择、名单长列表折叠、名单文件防篡改。**
230
246
 
231
- - **名单策略调整:文件夹包含子文件夹**:名单条目按「根目录 + 全部子文件夹」递归匹配(`E:\work` 覆盖 `E:\work\a\b\c.txt`,`E:\workshop` 不会被误匹配;嵌套黑名单仍优先);命令工具里的相对路径按会话工作区解析后再分类。新增测试覆盖递归覆盖、同前缀兄弟目录与相对路径。
232
- - **发布门禁稳定性**:`test/mcp-http-live.test.mjs` 的 relay 实连用例在出口网络不可达(或 MCP 工具层报 "could not reach the relay")时明确 `skip`,网络抖动不再把 release 门禁判红;其余用例仍覆盖本地绑定行为。
247
+ - **名单策略调整:文件夹包含子文件夹**:名单条目按「根目录 + 全部子文件夹」递归匹配(`E:\work` 覆盖 `E:\work\a\b\c.txt`,`E:\workshop` 不会被误匹配;嵌套黑名单仍优先);命令工具里的相对路径按会话工作区解析后再分类。新增测试覆盖递归覆盖、同前缀兄弟目录与相对路径。
248
+ - **发布门禁稳定性**:`test/mcp-http-live.test.mjs` 的 relay 实连用例在出口网络不可达(或 MCP 工具层报 "could not reach the relay")时明确 `skip`,网络抖动不再把 release 门禁判红;其余用例仍覆盖本地绑定行为。
233
249
  - **拦截即审批(不再静默挂起)**:`tools/pre-execute` 挂起 C 类调用后,客户端在 **composer 行**
234
250
  (`conversation.input.left`)新增内联提示 `GpwPrompt`:显示工具名与「允许本次 / 拒绝本次 / 打开面板」;
235
251
  多个待审批显示数量。浮窗按钮增加待审批数量徽标,`w2m-gpw-open` 事件让内联提示直接打开面板。
@@ -243,1572 +259,1572 @@ All notable changes to this project are documented here.
243
259
  - 旧版无签名文件在首次启动时一次性迁移为 v2;升级后再次出现的无签名/缺失文件视为篡改。
244
260
  - 诚实边界:DSH 与插件同用户运行时,OS 只读位可被有意的进程绕过;但签名校验使**静默改名单不可能**,
245
261
  篡改会被检测并锁定(需操作员重置)。强隔离需独立用户/服务账号(后续项)。
246
- - **名单继承保证**:黑白名单与其 MAC 密钥位于 `$DSH_HOME/xclient/`(插件包之外),升级、重启、卸载重装都会原样继承;只有面板中的显式「移除 / 重置名单」会改变它。新增 `test/gpw-persistence.test.mjs`(5 项)覆盖。
262
+ - **名单继承保证**:黑白名单与其 MAC 密钥位于 `$DSH_HOME/xclient/`(插件包之外),升级、重启、卸载重装都会原样继承;只有面板中的显式「移除 / 重置名单」会改变它。新增 `test/gpw-persistence.test.mjs`(5 项)覆盖。
247
263
  - 测试:新增 test/gpw-hardening.test.mjs(8 项:签名往返、只读拒绝直写、MAC 不匹配/删除检测、legacy 标记、reset、密钥 sidecar)
248
264
  与 test/gpw-tamper-gate.test.mjs(4 项:篡改 fail-closed → 409 → 重置恢复 → 内联审批放行;legacy 迁移;客户端内联提示/折叠/重置静态断言)。
249
265
  reset、密钥 sidecar)。`client-bundle` 更新为五个 composer 控件;`gpw-activation` 用临时名单文件隔离,
250
266
  不再写真实 `~/.dsh`。
251
267
  - 证据:`gpw-hardening 8/8`、`gpw-folder-policy 9/9`、`gpw-activation 4/4`、`client-bundle 26/26`(node 进程内运行)。
252
268
 
253
- ## [1.7.8] — 2026-10-26
254
-
255
- **GPW 启用失败修复(第二个):`TypeError: Cannot read properties of undefined (reading 'layers')`。**
256
-
257
- - 根因:v1.7.7 为做黑名单同步拒绝,在**激活期**调用根上下文的 `ctx.tools.guard()`。tools 服务是**按 agent 组合**的,
258
- 根作用域实例在激活阶段还没有 layer 表,于是抛 `reading 'layers'`,dsh-gpw 仍无法激活。
259
- - 修复:改为在 `agent/created` 监听里对**每个 agent 自己的上下文** `agent.ctx.tools.guard(...)` 注册黑名单守卫,
260
- 并用 `ctx.effect` 统一释放;根上下文不再读取 tools 服务(`inject` 回到只有 `permissionPresets`)。
261
- - 保留 `tools/pre-execute` 三分类拦截;guard 缺失的宿主不会因此激活失败(pre-execute 仍会拒绝黑名单)。
262
- - 回归测试升级:`test/gpw-activation.test.mjs` 现在用严格 Cordis 语义 Proxy(读取根 `tools` 即抛错)验证激活,
263
- 并通过伪造 `agent/created` 断言守卫注册在 agent 上下文。
264
- - 同时补全 bundle patch 中 GPW v2 的配置项(requireFullAccess / whitelist / blacklist / pendingTimeoutMs)。
265
- - 证据:`docs/acceptance/1.7.8/GPW-AGENT-SCOPE.json`。
266
- ## [1.7.7] — 2026-10-26
267
-
268
- **GPW 子插件启用失败修复:`cannot get property "tools" without inject`。**
269
-
270
- - 根因:`gpw.mjs` 读取 `ctx.tools.guard` 做黑名单同步拦截,但 `inject` 只声明了 `permissionPresets`。
271
- Cordis 对未声明服务读取直接抛错,插件激活因此失败(组件启用失败:1 entry did not activate dsh-gpw)。
272
- - 修复:`inject = ['permissionPresets', 'tools']`;guard 读取改为防御式(老宿主无 guard 扩展时不影响
273
- `tools/pre-execute` 拦截)。
274
- - 新增回归测试 `test/gpw-activation.test.mjs`:用严格 Cordis 语义的 Proxy 上下文调用 `apply()`,
275
- 未声明服务读取即抛错;断言 `tools/pre-execute` 监听已安装。
276
- - 证据:`docs/acceptance/1.7.7/GPW-INJECT.json`。
277
- ## [1.7.6] — 2026-10-25
278
-
279
- **审计修复版:测试零残留工具链 + 修复泄漏的测试夹具,并固化 v1.6.1→v1.7.4 审计报告。**
280
-
281
- - 新增 `scripts/temp-residue.mjs`(`snapshot` / `report` / `clean`):对用户 TEMP 中 `w2m-*`、`gpw-*`
282
- 测试残留做基线快照、前缀统计与按年龄的安全清理;审计实测清理了 15,608 个历史残留目录。
283
- - 新增 `test/helpers/temp-cleanup.mjs`:基线式 per-test 清理,避免并发测试互删夹具。
284
- - 修复会泄漏临时目录的测试:`gpw-folder-policy`、`agent`(workspace 两处 fixture)、`worktree`(makeRepo)。
285
- - 零残留复测:单轮全量测试新增残留从约 1,000 个降至 74 个(剩余前缀已量化,作为下一轮清理清单)。
286
- - 新增审计报告 `docs/audit/AUDIT-1.6.1-1.7.4.md`:14 个正式版的 Release/CI/Compat 全绿记录、
287
- 2930 个测试 0 失败、lint 0 error、实机部署与落库断言、relay 延迟与测试耗时实测。
288
- - 证据:`docs/acceptance/1.7.6/AUDIT-ZERO-RESIDUE.json`。
289
- ## [1.7.5-beta.1] — 2026-10-25
290
-
291
- **GPW 升级为文件夹三分类网关(完全权限下生效):黑名单拒绝 / 白名单放行 / 其余进入 GPW 自己的待审批队列。**
292
-
293
- - 生效条件:仅当会话权限预设为 **完全权限**(`danger-full-access`)时拦截;其它预设交给 DSH 自己的沙箱/审批,
294
- 不重复介入。
295
- - **黑名单(拒绝)**:`ctx.tools.guard()` 同步拦截,返回拒绝原因即不执行;与 `tools/pre-execute` 的监听顺序无关。
296
- - **白名单(通过)**:等价于扩大的工作区,无需再审;仍受自动审批预算约束(每滚动窗口 10 次、间隔 3s),
297
- 预算耗尽后同样进入待审批队列而不是静默放行。
298
- - **其它文件夹(C 类,待审批)**:调用被挂起,进入 GPW 自己的待审批队列;用户在 GPW 面板点「允许本次/拒绝本次」
299
- 才继续(默认 120s 无答复则拒绝)。这不是 DSH 的 approval 策略,所以在 approval=never 的完全权限下依然有效。
300
- - 面板(Node 执行能力菜单内新增「GPW 文件夹策略」):白/黑名单展示与移除;「添加文件夹」通过宿主原生选择器
301
- (Windows FolderBrowserDialog / macOS choose folder / zenity)选路径,读写名单不需要编辑任何 profile YAML;
302
- 名单保存在 `$DSH_HOME/xclient/gpw-folders.json`,另有待审批列表与允许/拒绝按钮。
303
- - 测试:`test/gpw-folder-policy.test.mjs`(6/6,三分类、deny 优先、前缀误判防护、命令路径提取、预算映射)。
304
- ## [1.7.4] — 2026-10-25
305
-
306
- **修复两处桌面宿主兼容性问题:`agent.identity` 崩溃、切换 relay 后旧 device token 导致 401 无法入队。**
307
-
308
- - 崩溃修复:`agent.identity?.()` 在桌面宿主上抛 `TypeError: agent.identity is not a function`,
309
- 且发生在 `agent.start().then(...)` 回调里变成未处理 rejection,把整个 DSH 宿主带崩。
310
- 新增 `agentIdentityOf()` 做类型判断,`agent.start()` 仍为非阻塞并记录失败。
311
- - 迁移修复:relay 切换到新主 relay 后,`device.json` 里的 token 属于旧 relay,新 relay 回
312
- `401 UNAUTHORIZED: missing or invalid device_token`;启动逻辑看到已有 token 就跳过配对,
313
- 导致机器停在"无列表"。现在会先用 token 探测配置的 relay:**明确 401/403 才丢弃并重新配对**,
314
- 网络错误保留 token(不会因短暂故障误触发配对)。
315
- - 新增回归测试:`test/agent-identity-safe.test.mjs`、`test/re-pair-stale-token.test.mjs`。
316
- - 证据:`docs/acceptance/1.7.4/DESKTOP-HOST-COMPAT.json`(含真实崩溃信息与迁移症状)。
317
- ## [1.7.3] — 2026-10-25
318
-
319
- **截图提示改为顶部浮层(5 秒自动消失、可关闭、不挤压输入框);全服计数标注来源 relay 并单列本编队。**
320
-
321
- - 截图成功/失败提示不再插在输入区里:改为聊天面板顶部浮层(绝对定位,不占布局;连续截图只替换同一行,
322
- 不会越堆越多),**5 秒自动消失**,右侧 × 可立即关闭。
323
- - 全服统计行改为「主 relay(地址)全服:X/Y 个编队 · A/B 台在线」,并新增一行「本编队:n/m 台在线」;
324
- 两者语义不同,不再混在一起看。
325
- - relay 角色判定改为按**配置的主 relay**(不再按响应最快,备用 relay 偶然更快时会被误标成 primary)。
326
- - 证据:`docs/acceptance/1.7.3/TOAST-AND-TOTALS.json`(fleet task `01M5ANYMVMSGGQV`;宿主截图 141,768 字节)。
327
- ## [1.7.2] — 2026-10-25
328
-
329
- **按实测反馈调整编队聊天截图/复制:只复制不预览、快捷键 Ctrl+Shift+P、强制可选中。**
330
-
331
- - 截图按钮不再在面板里生成缩略图/下载行:抓到画面后**只写入剪贴板**并给一行提示,聊天区域保持干净。
332
- - 快捷键从 Ctrl+P 改为 **Ctrl+Shift+P(macOS 也支持 Cmd+Shift+P)**;普通 Ctrl+P 不再被劫持,
333
- 浏览器/应用的打印快捷键恢复原样。
334
- - 选中&复制:`user-select:text!important` 应用到整个面板的所有子节点(不再只覆盖几个类),
335
- 并在消息列表上阻止宿主吞掉 mousedown/copy 事件,保证拖选与 Ctrl+C 生效;单条消息的 ⧉ 复制按钮
336
- 仍走「浏览器剪贴板 → 宿主剪贴板」回退链。
337
- - 全服统计只认**配置的主 relay**:主 relay 不可达时不再把备用 relay 的数字(1/1 编队、2/5 在线)当"全服"显示,
338
- 而是显示「主 relay 不可达 · 全服统计不可用」。修复了在线编队/在线机器数量"错误"的观感来源。
339
- - 证据:`docs/acceptance/1.7.2/CHAT-CLIPBOARD-SHORTCUT.json`(fleet task `01M5ANYMVMR3IOB`;
340
- 宿主截图实测 159,561 字节并复制成功)。
341
- ## [1.7.1] — 2026-10-25
342
-
343
- **修复桌面壳下聊天截图/复制失败(permission denied)与插件更新"no supported installer available"。**
344
-
345
- - 截图:浏览器 `getDisplayMedia` 在 DSH 桌面(Electron)被拒绝(`permission denied`)。新增**宿主侧截图**
346
- `POST /api/w2m/capture`(Windows PowerShell System.Drawing;macOS screencapture;返回 JPEG base64),
347
- 按钮与 Ctrl+P 在渲染层捕获失败时自动回退到宿主截图,并在宿主侧复制到剪贴板。
348
- - 复制:`navigator.clipboard` 被拒时新增**宿主剪贴板** `POST /api/w2m/clipboard`(Windows Set-Clipboard /
349
- macOS osascript),单条消息复制按钮与复制文本都走这条回退链。
350
- - 选中:transcript 的 `user-select:text` 改为 `!important`,避免宿主全局样式覆盖导致无法选中。
351
- - 插件更新:`update-install` 的安装器解析新增桌面运行时布局
352
- (`<app>/resources/runtime/<runtime>/dependencies/pnpm/bin/pnpm.mjs`、`~/.dsh/dsh-runtimes` 兜底),
353
- 并用运行时自带的 `node` 执行 pnpm(桌面壳的 `process.execPath` 不是 Node)。修复了更新按钮
354
- "install update: no supported installer available"。
355
-
356
- - 测试:capture-route 4/4、fleet-chat-capture 3/3、client-bundle 26/26、update-resolver 4/4;
357
- 证据 `docs/acceptance/1.7.1/DESKTOP-CAPTURE-UPDATER.json`(fleet task `01M5ANYMVMN9CNQ`)。
358
- ## [1.7.0] — 2026-10-25
359
-
360
- **编队聊天框:截图按钮 + Ctrl+P 快捷键 + 对话内容可选中&复制。**
361
-
362
- - 输入区新增 **📷 截图按钮**:`getDisplayMedia` 让用户选屏幕/窗口 → 抓帧为 PNG → 自动复制到剪贴板,
363
- 面板内显示缩略预览(可下载/再次复制);宿主不支持或权限被拒时在面板内显示原因,不抛未处理异常。
364
- - **Ctrl+P(macOS 也支持 Cmd+P)**:聊天面板打开时触发同一截图流程,并在捕获阶段 `preventDefault`,
365
- 不会弹出浏览器打印对话框。
366
- - **对话内容可选中&复制**:transcript/消息文本 `user-select:text`;每条消息新增 **⧉ 复制** 按钮;
367
- 划选文字时点击消息不会误触发"回复"。
368
-
369
- 已知边界:编队聊天协议仍是纯文本(relay 消息不含图片字段),所以截图是"复制到剪贴板 + 预览/下载",
370
- 不会作为图片消息广播给其他机器——图片传输需要扩展 relay 消息协议(后续版本)。
371
-
372
- - 测试:`test/fleet-chat-capture.test.mjs` 3/3;client-bundle 26/26;
373
- 证据 `docs/acceptance/1.7.0/CHAT-CAPTURE-CONTROLS.json`(fleet task `01M5ANYMVMLRC5H`)。
374
- ## [1.6.9] — 2026-10-24
375
-
376
- **修复 failover 监控从未安装的根因:`startLocalStack` 等待了永不 settle 的 `agent.start()`。**
377
-
378
- 阶段日志(macOS,1.6.8,现场实测):
379
-
380
- ```
381
- W wrap enter relayUrls=[腾讯云, Vultr] ← 配置正确
382
- A startLocalStack enter
383
- C createAgent done
384
- S before agent.start ← 之后没有任何日志
385
- ```
386
-
387
- agent 实际已上线(编队在线),但 `await agent.start()` 的 Promise 永不返回,导致 `startLocalStack` 不返回 →
388
- `startFailoverStack` 永远走不到 `relayPool(cfg)` / `setInterval`,**监控循环从未创建**,自动 failover 不可能触发。
389
-
390
- 修复:
391
-
392
- - `startLocalStack` 不再 await `agent.start()`:后台启动,失败写日志(`w2m: agent start failed`)。
393
- - mount 只需要返回清理句柄,不需要"观察启动完成";agent 会通过 relay 自报在线。
394
- - 新增回归测试 `test/agent-start-nonblocking.test.mjs`:读取源码,若 `startLocalStack` 再次 await
395
- `agent.start()` 即失败(防止该阻塞回归)。
396
- - 证据:`docs/acceptance/1.6.9/FAILOVER-MONITOR-INSTALL.json`(fleet task `01M5ANYMVMKL55O`)。
397
- ## [1.6.8] — 2026-10-24
398
-
399
- **插件面板新增「检查插件更新」按钮:npm 对比 → 自动更新 → 提示重启 DSH。**
400
-
401
- - 客户端:目标机器菜单 relay 区新增按钮。点击后:① GET `/api/w2m/plugin-update` 联网查 npm latest
402
- 并与已装版本比较;② 有新版时 POST `{action:"apply"}` 执行安装;③ 安装完成显示"请重启 DSH 以载入最新插件"。
403
- - 宿主:新增路由 `/api/w2m/plugin-update`。检查逻辑在 `src/plugin/npm-update.mjs`(纯函数,可单测:只认
404
- `latest > installed`,网络失败是错误值而不是"已最新")。
405
- - 安装复用**既有已验证更新器**(`auto-update.mjs` → `update-install.mjs`):下载 tarball、用 release
406
- SHA256SUMS 校验 SHA-256、备份旧包、替换新版——按钮点击不会引入第二套弱校验安装路径。
407
- - 测试:npm-update 4/4、client-bundle 26/26;证据 `docs/acceptance/1.6.8/PLUGIN-UPDATE-BUTTON.json`
408
- (fleet task `01M5ANYMVMI9EHA`)。
409
- - 边界:DSH 插件管理页的「卸载」按钮属于宿主应用(打包在 app.asar 内),插件包无法直接注入其旁边;
410
- 本按钮位于 w2m 插件自己的面板中。若要放进宿主插件页,需要 DSH 侧提供插件动作扩展点(已列为待评估)。
411
- ## [1.6.7] — 2026-10-24
412
-
413
- **修复 v1.6.6 failover 的阻塞 bug:远端 relay 模式下清理函数抛错,切换后无法重启 stack。**
414
-
415
- 现场演练(停主 relay 180s)暴露:机器掉线后没有切到备用,恢复主 relay 后才重新上线。根因:
416
-
417
- - `startLocalStack` 的清理函数无条件执行 `await relay.close()`;而 `relay` 只在"本进程自建本地 relay"
418
- 时才非空,远端 relay 模式下是 `null` → 抛 TypeError;
419
- - failover 的切换流程 `await stop()` 抛错后被 catch 吞掉,**后续的 restart 从未执行**(stack 停了没起来)。
420
-
421
- 修复:
422
- - 清理函数 null-safe/idempotent:仅当 `relay` 是带 `close()` 的对象才调用;`agent.stop()` 失败也继续清理。
423
- - 切换流程:`stop()` 失败不再跳过 restart;并写诊断日志 `xclient/relay-failover.log`(每轮探测/切换/重启错误)。
424
- - 测试:relay-failover 5/5、relay 200/200;证据 `docs/acceptance/1.6.7/FAILOVER-CLEANUP-FIX.json`
425
- (fleet task `01M5ANYMVMFSSP3`)。
426
- ## [1.6.6] — 2026-10-24
427
-
428
- **relay 自动 failover:主 relay 故障时自动切到备用,恢复后自动切回。**
429
-
430
- 现场事故(2026-10-24):主 relay 主机整体不可达(8787 与 SSH 22 都超时),备用 relay 一直健康,
431
- 但每台机器仍死连主 relay——备用链路只是常驻成员,不会接管,编队停摆。
432
-
433
- - 新增 `src/plugin/relay-failover.mjs`:非对称滞回状态机(**连续 3 次**探测失败才切走,**连续 5 次**
434
- 健康才切回;没有可达备选则不切)。纯函数,边界可单测。
435
- - `startLocalStack` 外包一层 `startFailoverStack`:每 10s 探测池内各 relay `/healthz`;
436
- 决策为切换时,停止当前 in-process stack,把 `cfg.rabbitUrl` 设为备 relay 并重启 stack——
437
- **切换是重新入队(fleet key 重新配对),不是重连**(device token 是按 relay 签发的)。
438
- - `w2m_status` 新增 `failover` 快照:primary/active/pool、失败计数、切换次数、最近切换原因与历史。
439
- - 测试:relay-failover 5/5、relay 200/200;证据 `docs/acceptance/1.6.6/RELAY-FAILOVER.json`
440
- (fleet task `01M5ANYMVM9GEKQ`)。
441
- ## [1.6.5] — 2026-10-24
442
-
443
- **Windows 后台自动化不再弹窗:VBScript 隐藏包装 + 计划任务工具 + 文档。**
444
-
445
- 现场故障:给 standby 守护(每 2 分钟)和 TwinsEarth 守望(每 5 分钟)用 `schtasks /TR "node.exe ..."`
446
- 注册任务,导致桌面每隔几分钟弹出一个控制台窗口(守望脚本还会累积进程)。
447
-
448
- - 新增 `src/util/windows-hidden-task.mjs`:纯函数构造 VBScript 包装(`WScript.Shell.Run` 窗口样式 0、
449
- 不等待)与 `schtasks` 参数;命令行/参数先按 Windows 规则引用,再整体做一次 VBScript 转义(避免二次转义)。
450
- - 新增 `bin/w2m-hidden-task.mjs`:`--name/--every-minutes/--command/--arg/--workdir/--delete`,
451
- 一条命令注册“无窗口”计划任务。
452
- - 新增 `docs/WINDOWS-BACKGROUND-TASKS.md`:故障原因、正确做法、原则(分钟级任务只放短检查;一次性任务用完即删)。
453
- - 测试:windows-hidden-task 5/5(含转义与参数校验);证据
454
- `docs/acceptance/1.6.5/HIDDEN-BACKGROUND-TASKS.json`(fleet task `01M5ANYMVM30H98`)。
455
- ## [1.6.4] — 2026-10-24
456
-
457
- **修复:`/healthz` 漏发 `fleets_online`(1.6.3 的 `fleetSummary` 已计算,但 `stats()` 没带到公开字段)。**
458
-
459
- - `stats()` 现在同时输出 `fleets_total` 与 `fleets_online`;
460
- - 面板可正确显示 `全服:X/Y 个编队 · M/T 台在线`(X = 至少一台机器在线的编队数);
461
- - relay 主机升级到 1.6.4(含 1.6.3/1.6.2/1.6.1 全部内容)。
462
- - 测试:relay 200/200、fleet-target 49/49;证据 `docs/acceptance/1.6.4/HEALTHZ-FLEETS-ONLINE.json`
463
- (fleet task `01M5ANYMVLX3MI7`)。
464
- ## [1.6.3] — 2026-10-24
465
-
466
- **编队与机器同构显示:在线编队数/编队总数 + 在线机器/机器总数。**
467
-
468
- - relay `fleetSummary()` 新增 `fleets_online`(至少一台机器在线的编队数),`/healthz` 公开计数同步增加;
469
- - 面板 relay 区显示:`全服:X/Y 个编队 · M/T 台在线`(fleet-target probe 读取 `fleets_online`);
470
- - relay 主机升级到 1.6.3(含 1.6.2 全服概览与 1.6.1 安全修复)。
471
- - 测试:relay 200/200、fleet-target 49/49、client-bundle 26/26;证据
472
- `docs/acceptance/1.6.3/ONLINE-FLEETS.json`(fleet task `01M5ANYMVLWMTSG`)。
473
- ## [1.6.2] — 2026-10-24
474
-
475
- **relay 全服概览:多少个编队、多少台机器在线(healthz + operator /v1/fleets + 面板一行显示)。**
476
-
477
- - `state.fleetSummary()`:按 fleet 聚合设备数/在线数,返回 `fleets_total`、`devices_online`、
478
- `devices_total`、`unfleeted_devices` 与 per-fleet 明细。
479
- - `GET /healthz`(公开)新增上述**聚合计数**(不暴露 per-fleet 明细);
480
- - `GET /v1/fleets`(operator token):返回 per-fleet 明细(其他账号的编队名不向普通设备暴露)。
481
- - 面板 relay 折叠区新增一行:`全服:N 个编队 · M/T 台在线`(fleet-target probe 读取 healthz 计数)。
482
- - relay 主机将升级到 1.6.2(包含 1.6.1 的 Node 执行仅人工/内存态与 allow-list 解释器身份修复)。
483
- - 测试:relay 200/200、fleet-target 49/49、client-bundle 26/26;证据
484
- `docs/acceptance/1.6.2/RELAY-FLEET-OVERVIEW.json`(fleet task `01M5ANYMVLRYXSN`)。
485
- ## [1.6.1] — 2026-10-24
486
-
487
- **Node 执行开关改为「仅人工 UI 手动、进程内存态」:文件/远程/自动化一律无法授权。**
488
-
489
- 本次实机验证暴露了一个必须修掉的现实:grant 任务可以远程写 `~/.w2m/exec-capability.json`,
490
- 从而远程把机器变成可执行任意 Node 代码——与"最高权限只允许人工手动选中"直接冲突。
491
-
492
- - 能力状态改为**进程内存**唯一权威:`readExecCapability` 不再读磁盘;旧文件(含 permanent)被忽略;
493
- 只有 host 路由 `/api/w2m/capability`(composer 人工点击)能写。
494
- - **不随重启保留**:进程退出即回到 denied("永久"= 本次进程生命周期);
495
- `describe` 明确返回 `survives_restart: false`、`manual_only: true`、`file_ignored: true`。
496
- - 三态语义不变:denied / temporary(默认 24h,上限 7 天,过期即失效)/ permanent(本进程内)。
497
- - 测试重写:`test/exec-capability.test.mjs` 6/6(含"磁盘写 permanent 仍 denied""路由是唯一 setter"
498
- "重启即撤销");agent.test 161 项通过(PATH 环境问题已在本机修复,非代码缺陷)。
499
- - 修复 allow-list 解释器身份回归:当 argv[0] 是 **运行 agent 的同一个 node(process.execPath)** 时,
500
- bare 条目 `node` 应放行(此前机器 PATH 里 node 指向别的程序时会误拒,实测 12 个 e2e 失败)。
501
- - 证据 `docs/acceptance/1.6.1/MANUAL-ONLY-CAPABILITY.json`(fleet task `01M5ANYMVLDYWBH`)。
502
- ## [1.6.0] — 2026-10-24
503
-
504
- **一台 relay 承载 N 个编队:每个 DeepSeek 账号一个编队,共享 relay 但互相隔离。**
505
-
506
- - bootstrap 答案从"单个编队的归属"改为"relay 拓扑声明":签名与有效期仍严格,fleet_id 不匹配不再拒绝
507
- (`strictFleet: true` 保留旧行为)。一份 operator 签名即可服务全部编队,新账号机器无需人工重签。
508
- - `scripts/verify-fleet-isolation.mjs` 扩展到 **3 个编队** 14 项检查(设备枚举/任务/聊天/记忆),全过。
509
- - 新增 `docs/MULTI-FLEET.md`:编排模型、运维视图、零人工接入新账号、配额与边界。
510
- - 测试:bootstrap 6/6(含拓扑答案用例)、fleet isolation 14/14;证据 `docs/acceptance/1.6.0/N-FLEET.json`
511
- (fleet task `01M5ANYMVLBU716`)。
512
- ## [1.5.5] — 2026-10-23
513
-
514
- **跨编队隔离:同 relay 上的另一账号机器不再出现在本编队的枚举/任务/聊天/记忆里。**
515
-
516
- 现场:另一账号的机器(自己的 fleet_id、自己的 fleet_key)出现在我们的机器列表里,被读成"非本账号
517
- 机器进入编队"。排查结论:该机器的编队绑定本身是正确的,问题在于 relay 的**读取面没有按 fleet 过滤**
518
- (派发侧自 v0.7.7 起已按 fleet 隔离,operator token 为 relay 级权限属于设计)。
519
-
520
- - `GET /v1/devices`:按请求设备的 fleet 过滤(无 fleet 的旧设备在未配置 fleet 的 relay 上保持历史行为)。
521
- - `GET /v1/tasks`:任务记录携带 `fleet_id`,列表按调用者 fleet 过滤(operator 派发是 relay 级,不会进入
522
- 某个编队的列表)。
523
- - `POST /v1/fleet/messages`:`fleet_id` 取自已认证设备,忽略 body(防止把消息标成别的编队)。
524
- - `GET /v1/fleet/messages`:读取时按调用者 fleet 过滤。
525
- - fleet memory 文档按 fleet 前缀隔离(无 fleet 设备沿用历史键,单编队 relay 不受影响)。
526
- - 新增 `scripts/verify-fleet-isolation.mjs`:真实 relay + 两个 fleet 的黑盒验证(设备/任务/聊天/记忆
527
- 共 11 项检查),可作为部署前门禁脚本运行。
528
- - 验证:脚本 11/11 通过(fleet task `01M5ANYMVL2Q8PD`);relay.test 200/200 通过。
529
- ## [1.5.4] — 2026-10-23
530
-
531
- **验签 bootstrap 的结果覆盖过期的显式 relay(现场故障:设备仍指向 127.0.0.1)。**
532
-
533
- 现场证据:DSH 升级到 v1.5.3 后仍报 `W2M_UNREACHABLE: could not reach the Rabbit at
534
- http://127.0.0.1:8787`——它的 profile 仍显式写着本机地址,v1.5.3 只在“无显式 relay”时才采用
535
- bootstrap 池,于是拿到池却继续拨 loopback。
536
-
537
- - bootstrap 验签成功后**总是采用** `relays[0]` 作为 `rabbitUrl`,并 warn 记录被替换的旧地址;
538
- 想固定 relay 的运维应显式设置 `relayUrls`(此时 bootstrap 分支不执行)。
539
- - 只影响“验签通过”的答案;失败仍保持原配置并写 `w2m_status.bootstrap.last_error`。
540
- - 测试:bootstrap 6/6、bootstrap-cli 6/6;证据 `docs/acceptance/1.5.4/BOOTSTRAP-WINS.json`
541
- (fleet task `01M5ANYMVL0FV9C`)。
542
- ## [1.5.3] — 2026-10-23
543
-
544
- **零接触自动入队:只能升级插件的机器也能自己找到编队。**
545
-
546
- v1.5.2 的验签 bootstrap 需要一个 URL、一个 operator 公钥和 fleetKey 才能触发——新装机器三样都没有,
547
- 于是仍然回落到编译期默认 relay 并报 W2M_UNREACHABLE。v1.5.3 把触发条件做进包里:
548
-
549
- - 包内置默认 bootstrap 端点与 **operator 公钥 pin**(只含公钥;端点无法签发,私钥不出运维机)。
550
- - bootstrap 证明改为可选:机器还没拿到 fleetKey 时,仍可凭"签名 + 有效期 + relay 形状"采用
551
- operator 声明的 relay 池(relay 列表是公开路由信息;加入编队仍需账号派生密钥,是独立一步)。
552
- - `startLocalStack` 在无 relayUrls 时始终尝试 bootstrap(默认值生效),成功即用池(首个 primary),
553
- 失败保持回落并写入 `w2m_status.bootstrap.last_error`。
554
- - 包内默认 patch:`autoStartAgent: true`(本 fork),新装即启动 agent。
555
- - 因此只有"升级插件"这一条通道的机器:升级 → 重启 → 自动取 relay 池 → 账号密钥就绪后自动配对入队。
556
- - 测试:bootstrap 6/6(含 keyless)、bootstrap-cli 6/6、fleet-target 49/49;层模型 clean;
557
- 证据 `docs/acceptance/1.5.3/ZERO-TOUCH-BOOTSTRAP.json`(真实 fleet task `01M5ANYMVKZ7DHH`)。
558
- ## [1.5.2] — 2026-10-23
559
-
560
- **新机器自动获得 relay 归属(验签 bootstrap)+ 一次性配置工具。**
561
-
562
- - 新增 `src/plugin/bootstrap.mjs`:机器用 fleetKey 派生 HMAC 证明请求 relay 列表;响应必须由
563
- operator 公钥(pin)对 canonical payload(fleet_id + 排序后的 relays + issued_at + expires_at)
564
- 验签;过期/错 fleet/被篡改/坏 JSON 全部命名拒绝。缓存每次读取都重验签名与有效期,手改缓存无效。
565
- **任何失败都不改现有 relay 配置**,原因写入 `w2m_status.bootstrap.last_error`。
566
- - `startLocalStack` 接线:无 `relayUrls` 且配置 `bootstrapUrl`+`fleetKey`+operator 公钥时启动即解析
567
- (缓存优先,其次网络),成功则采用该 relay 池(首个为 primary);`w2m_status` 新增
568
- `bootstrap {configured, url, source, relays, expires_at, last_error}`。
569
- - 新增 `bin/w2m-bootstrap.mjs`:一条命令写 `relayUrls`/`fleetKey`/`autoStartAgent` 到 profile patch,
570
- 只改受管键、写前备份、所有输入先校验;`--temporary` 写 `relayTemporary: true`,面板以
571
- **红色“临时 Relay 节点(回退配置)”**显示。
572
- - 新增 `bin/w2m-bootstrap-sign.mjs`:运维侧用 operator 私钥签名 relay 列表,输出可静态托管。
573
- - 控制面板 relay 折叠区支持 temporary 红色标记(仅主题 token,无硬编码颜色)。
574
- - 层模型修复:bootstrap 模块不依赖 chatops 层(layer-check 保持 clean)。
575
- - 测试:bootstrap 5/5、bootstrap-cli 6/6、fleet-target 49/49、client-bundle 26/26;
576
- 证据 `docs/acceptance/1.5.2/VERIFIED-BOOTSTRAP.json`(真实 fleet task `01M5ANYMVKXNU8E`)。
577
- ## [1.5.1] — 2026-10-23
578
-
579
- **备用链路的身份隔离 + 控制面板的 relay 折叠。**
580
-
581
- - `bin/w2m-secondary-link.mjs`:默认给子进程独立身份目录(`<stateDir>/dsh-home`),并同时设置
582
- `DSH_HOME` / `USERPROFILE` / `HOME`,可用 `--identity-home` 指定。修复的是 v1.5.0 现场实测的坑:
583
- 备用链路首次启动覆盖了主链路 `~/.dsh/xclient/device.json`,机器在下一次重启前看不出问题,
584
- 重启后却会以**备用身份**连到**错误的 relay**。这种"重启后才爆"的坑按默认隔离处理。
585
- - 控制面板 relay 折叠:`GET /api/w2m/target` 快照新增 `relays`(`relayUrls` 数组优先,兼容单
586
- `rabbitUrl`),探测 `/healthz` 后按 RTT 排序、标注 primary/standby/offline;超过 3 个节点时只显示
587
- 3 个并给出 "+N 折叠" 提示,展开后显示全部(含离线节点,不隐藏故障)。
588
- - 探针失败保留在列表末尾(`ok:false`),网络异常不会让节点从视图中消失,也不会抛进快照路径。
589
- - 测试:secondary-link 7/7、fleet-target 49/49(新增 relay pool 四个用例);门禁 GREEN ×2;
590
- 证据 `docs/acceptance/1.5.1/IDENTITY-ISOLATION-RELAY-FOLD.json`(真实 fleet task `01M5ANYMVKS3OQZ`)。
591
-
592
- ## [1.5.0] — 2026-10-23
593
-
594
- **多 relay 双通道(active-passive)+ relay 健康看门狗。**
595
-
596
- - 新增 `bin/w2m-secondary-link.mjs`:在一台机器上拉一条通往备用 relay 的常在线链接。
597
- 它监督子进程 `w2m-localside`(同一套协议客户端),子进程退出按指数退避重启并记录每次转换,
598
- 由 systemd/launchd/计划任务只守护这一层——避免"链接一次启动后静默死掉"的已知失败形态。
599
- - active-passive 是**路由规则**:两条链接都常连,任务只在主 relay 派发;主 relay 掉线或延迟严重时
600
- 把同一任务改派到备用 relay(机器已在线,无需重连),正常情况下一台机器只有一个执行者。
601
- - 新增 `deploy/relay/`:`w2m-watchdog.sh` + systemd `.service/.timer`。每 30s 检查
602
- `/healthz` 的 `"ok":true`,连续 2 次失败才重启 relay/stun;每小时最多 5 次重启(避免掩盖根因),
603
- 日志写 `/var/lib/w2m/watchdog/watchdog.log`。relay 因此独立于"是否有编队成员"保持在线。
604
- - 新增 `docs/MULTI-RELAY.md`:拓扑、选路规则(健康且 RTT 最低且目标全在线)、relay 互为成员、
605
- 以及 relay 超过 3 个时客户端折叠显示(`relays: 3 shown · +N online`)的约定。
606
- - 测试:`test/secondary-link.test.mjs` 6/6(参数校验、child argv、退避重启、日志);
607
- 门禁 GREEN ×2;证据 `docs/acceptance/1.5.0/SECONDARY-LINK.json`(真实 fleet task `01M5ANYMVKR311N`)。
608
-
609
- ## [1.4.4] — 2026-10-23
610
-
611
- **relay 重启不再丢失编队归属(devices.json 持久化 fleet_id)。**
612
-
613
- - 持久化修复:`devices.json` 快照新增 `fleet_id` / `fleet_source`,`restore()` 同步恢复。
614
- 在此之前,relay 重启后设备全在、token 仍有效,但每台设备的 fleet 归属全变成 null——
615
- 编队"静默消失"且没有任何错误可看(本次在共享 relay 上实测复现)。
616
- - 桥接 getupdates 看门狗:空闲时每 10 分钟输出 `polling ... connection live`;
617
- 40s 长轮询超时明确记录 `poll timeout after 40s -- reconnecting`;
618
- 连续 5 分钟没有一次成功轮询则退出(exit 3),由 launchd KeepAlive / systemd Restart= 拉起新连接,
619
- 避免"进程活着但没人知道它是否还在轮询"的静默状态。
620
- - 测试:relay 新增 fleet 重启往返用例(pair by fleet_key → 重启 → fleet_id/fleet_source 保留);
621
- bridge 套件 36/36;门禁 GREEN ×2。
622
- - 证据:docs/acceptance/1.4.4/FLEET-PERSISTENCE.json(真实 fleet task `01M5FLEETPERSIST0000000002`)。
623
-
624
- ## [1.4.3] — 2026-10-22
625
-
626
- **微信编队桥接:回复可靠性修复(不回复/静默问题)。**
627
-
628
- - 每条消息独立 try/catch:任何处理异常不再让进程退出,改为回复
629
- `处理失败(阶段,错误码):原因 + /help 提示`(新增 `renderBridgeError`)。
630
- - 桥接启动调用 `ilink/bot/msg/notifystart`:修复"token 有效、进程在跑、但 iLink 不推送消息"的静默状态;
631
- 日志记录 `notifystart : ok`,失败也记录原因。
632
- - 非文本、空消息、未以 `/run` 开头、权限不足、派发失败/超时等路径均保证有回复与下一步提示。
633
- - 运行方式支持 launchd 常驻(KeepAlive 自动拉起;macOS plist 由运维部署,不进入安装包)。
634
-
635
- - **授权优先级修复**:exec-capability 只在任务未携带 grant 时兜底;grant 被拒(replay / 过期 / 机器不符)
636
- 原样上报,不再被能力分支覆盖成路径匹配错误,`GRANT_REPLAY` 语义恢复。
637
-
638
- 真机验收(macmini):`notifystart ok`、错误回复代码在位、launchd state=running、bridge 进程存活;
639
- 证据 `docs/acceptance/1.4.3/BRIDGE-LIVE.json`。测试:chatops-ilink-fleet-bridge 36/36。
640
-
641
- ## [1.4.2] — 2026-10-22
642
-
643
- **Node 代码执行权限成为 UI 可激活的子插件能力(命中真机验收)。**
644
-
645
- - 新增 `src/util/exec-capability.mjs`:机器本地能力状态(拒绝 / 临时 24h / 永久),fail-closed;临时档带绝对
646
- `expires_at_ms`,过期即拒绝;唯一可放行的前缀固定为 `node -e`。
647
- - 新增 host 路由 `GET/POST /api/w2m/capability`(`src/plugin/exec-capability-route.mjs`),同源、在 host
648
- 认证栅栏内;浏览器不接触文件系统或设备 token。
649
- - client `lib/client.js` 在 `conversation.input.left` 新增第三个控制(order 42):
650
- `Node 执行:拒绝 | 临时 24 小时 | 永久`,带 RCE 风险提示与剩余时间。
651
- - agent 每个任务前读取能力文件;命中时结果信封 `authorization.provenance = "exec-capability"`,
652
- warnings 记录 `EXEC_CAPABILITY`;pipeline 各 stage 同样覆盖。
653
-
654
- 真机验收(TwinsEarth,插件 1.4.2-beta.1;先清空 `cordis.patch.yml` 的旧 allow-list 以隔离能力路径):
655
- - 无能力、空 allow-list:`node --version` → `COMMAND_NOT_ALLOWED`
656
- - 能力=永久、空 allow-list:`node -e` → **ok**(`node -e` 是唯一被放行的前缀)
657
- - 能力=临时 5s 到期后:`node -e` → `COMMAND_NOT_ALLOWED`(fail-closed)
658
- - 证据:`docs/acceptance/1.4.2/CAPABILITY-LIVE.json`
659
-
660
- Verified: 单测/路由/客户端契约 43/43;候选 gate GREEN ×2;stable gate 见发布记录。
661
-
662
- ## [1.4.2-beta.1] — 2026-10-22
663
-
664
- **Node 代码执行权限成为 UI 可激活的子插件能力。**
665
-
666
- - 新增 `src/util/exec-capability.mjs`:机器本地能力状态(拒绝 / 临时 24h / 永久),fail-closed,
667
- 临时档带绝对 `expires_at_ms`,过期即拒绝。
668
- - 新增 host 路由 `GET/POST /api/w2m/capability`(`src/plugin/exec-capability-route.mjs`),同源、在
669
- host 认证栅栏内;浏览器不再接触文件系统或设备 token。
670
- - client `lib/client.js` 在 `conversation.input.left` 座位新增第三个控制(order 42):
671
- `Node 执行:拒绝 | 临时 24 小时 | 永久`,带 RCE 风险提示与剩余时间。
672
- - agent 每个任务前读取能力文件;命中时结果信封 `authorization.provenance = "exec-capability"`,
673
- warnings 记录 `EXEC_CAPABILITY`;pipeline 各 stage 同样覆盖。
674
- - 文档 `docs/EXEC-CAPABILITY.md`;测试 `test/exec-capability.test.mjs`(7/7)、
675
- client-bundle/loader-entry/exec-capability 合计 43/43。
676
-
677
- Verified: 单测与路由测试全绿;候选版本真机验收(UI 激活 → agent 下一任务可执行 node -e)。
678
-
679
- ## [1.4.1] — 2026-10-22
680
-
681
- **修复:授权模型在从未 bootstrap 的机器上不可用(真机发现)。**
682
-
683
- - relay 的 offer 对 **grant 任务**也携带 `operator_key`(此前只有 maintenance 请求携带)。在空 allow-list 的机器上,
684
- agent 首次收到 offer 时据此 TOFU pin;修复前该机器只会得到 `GRANT_NO_PINNED_KEY`,即"显式授权"形同虚设。
685
- - 任务台账记录 `operator_key`,relay 重启回放恢复 `workspace / grant / grant_summary / maintenance / operator_key`:
686
- 修复前重启后 re-offer 会丢失授权字段,机器会以错误的理由拒绝任务。
687
-
688
- 真机验收(relay 1.4.1 候选,TwinsEarth 插件 1.4.0、allow-list 为空):
689
-
690
- - 无 grant 的 `node --version` → `COMMAND_NOT_ALLOWED`
691
- - operator grant 授权的 `node -e` → **ok**,输出 `{plugin:"1.4.0", pinned:"^1.4.0", node:"v24.21.0"}`
692
- - relay M1 diag(root runner)→ ok,66,046 bytes
693
- - 证据:`docs/acceptance/1.4.1/TWIN-GRANT-LIVE.json`、`M1-LIVE-DIAG.json`
694
-
695
- Verified: relay TOFU offer 测试 1/1、grant/ledger 相关 3/3;候选部署后真机全部通过。
696
-
697
- ## [1.4.1-beta.1] — 2026-10-22
698
-
699
- **候选:授权模型在"从未 bootstrap 的机器"上可用。**
700
-
701
- - relay 的 offer 现在对 **grant 任务**也携带 `operator_key`(此前只有 maintenance 请求携带),首次见到该键的
702
- agent 因此可以 TOFU pin;在此之前,空 allow-list 的新机器收到 grant 只会得到 `GRANT_NO_PINNED_KEY`。
703
- - 任务台账记录 `operator_key`,重启回放恢复 `workspace / grant / grant_summary / maintenance / operator_key`:
704
- 此前 relay 重启后 re-offer 的任务会丢失授权/工作区字段,机器会以错误的理由拒绝。
705
- - 真机验证路径:TwinsEarth(allow-list 为空、插件 1.4.0)用 operator grant 执行 `node -e` 读取版本。
706
-
707
- Verified: relay TOFU offer 测试 1/1,grant/ledger 相关 3/3;候选部署到 relay 后做真机验收。
708
-
709
- ## [1.4.0] — 2026-10-22
710
-
711
- **稳定版:执行授权、任务、协作三个模型的第一版落地。**
712
-
713
- - **执行授权模型**:`bin/w2m-grant.mjs` 签发单次执行授权(绑定 task_id/argv/机器/≤10min TTL/max_uses);relay 派发预检
714
- (`GRANT_TASK_MISMATCH` / `GRANT_ARGV_MISMATCH` / `GRANT_EXPIRED` → 400);撤销 `POST /v1/grants/revoke`(幂等、
715
- ledger 留痕、派发 `403 GRANT_REVOKED`)与审计 `GET /v1/grants`;结果信封携带 `authorization.provenance`
716
- (grant/allow-list/maintenance);使用台账不可写时 `GRANT_LEDGER_UNAVAILABLE` fail-closed。
717
- - **任务模型**:任务可声明 `workspace {kind:'worktree', base_commit, produce}`;agent 在独立 worktree 中执行、提交、
718
- 生成相对 base 的 patch 并上传内容寻址 blob,结果携带 `{base_commit, head_commit, changed_files, patch_sha256,
719
- patch_bytes, cleaned}`;relay 侧分支 refs(单调、必须声明 from=head)、合并队列(patch sha256 幂等、already_merged)、
720
- 冲突结果带 conflict_sha256 且绝不推进分支。
721
- - **协作模型**:项目 room/board/artifact 索引/decisions/project memory/通知(共享对象事件广播 + 每 reader 未读水位)/
722
- 项目级 SSE(ready + since_seq 补齐 + 实时 project.event)。
723
- - **M7**:指标注册表与 `/metrics` 接线、证据 schema 与 preflight 第 16 项证据门禁;候选版本可用 candidate line
724
- fallback,stable 必须提供本线真机证据。
725
-
726
- **发布时真机状态(如实)**:relay 宿主已运行 1.4.0 候选(1.4.0-beta.1,代码与 stable 相同),relay 侧真机回归 10/10
727
- 通过(board/artifacts/decisions/memory/locks/blobs/refs/merge/notifications/项目 SSE)。M1 维护通道的 diag 在换版后
728
- 未能执行:非特权换版路径(fleet node 通道)替换目录后由 kill 触发的重启没有拉起 `w2m-localside`(部署脚本的
729
- systemctl 被拒绝);在控制台执行 `systemctl restart w2m-localside` 后即可继续完成 M1 复验与 stable 官方资产部署。
730
-
731
- 证据:`docs/acceptance/1.4.0/`(REGRESSION-1.4.0.json、M1-LIVE-MAINTENANCE.json)。
732
-
733
- ## [1.4.0-beta.1] — 2026-10-22
734
-
735
- **v1.4.0 三模型候选:执行授权模型落地、任务模型落地、协作模型落地。**
736
-
737
- 这是把 1.3.12–1.3.14 的 beta 线合成一版的候选,供 stable 前真机验收。
738
-
739
- - **执行授权模型(v1.4.0)**:`bin/w2m-grant.mjs` 签发单次执行授权(绑定 task_id/argv/机器/≤10min TTL);
740
- relay 派发预检(`GRANT_TASK_MISMATCH` / `GRANT_ARGV_MISMATCH` / `GRANT_EXPIRED`);撤销
741
- `POST /v1/grants/revoke`(幂等、ledger 留痕、派发 `403 GRANT_REVOKED`)与审计 `GET /v1/grants`;
742
- 结果信封带 `authorization.provenance`(grant/allow-list/maintenance);使用台账不可写时
743
- `GRANT_LEDGER_UNAVAILABLE` fail-closed(真机发现的 EACCES 崩溃已修)。
744
- - **任务模型(v1.4.1 前身)**:任务可声明 `workspace {kind:'worktree', base_commit, produce}`;
745
- agent 在独立 worktree 执行 → 提交 → 生成相对 base 的 patch → 上传内容寻址 blob → 证据
746
- `{base_commit, head_commit, changed_files, patch_sha256, patch_bytes, cleaned}`;
747
- relay 侧分支 refs(单调、必须声明 from=head)、合并队列(按 patch sha256 幂等、already_merged)、
748
- 冲突结果携带 conflict_sha256 且绝不推进分支;refs/merge-queue HTTP 路由。
749
- - **协作模型(v1.4.2 前身)**:项目 room(成员/角色/快照+增量事件)、board、artifact 索引、
750
- decisions(append-only)、project memory(版本文档)、通知(共享对象事件广播 + 每 reader 未读水位)、
751
- 项目级 SSE(ready + since_seq 补齐 + 实时 project.event + keepalive)。
752
-
753
- M7 证据门禁对候选版本增加 candidate line fallback:beta 候选可引用其所基于发布线的真机证据(其产物在机器升级前构建),stable 仍必须提供本线真机证据。
754
-
755
- Verified: gate GREEN ×2;relay 测试、agent 测试、workspace/refs/board/notification/SSE 用例全绿。
756
-
757
- ## [1.3.12-beta.4] — 2026-10-22
758
-
759
- **执行授权模型真机修复:使用台账不可写时 fail closed,而不是 crashed。**
760
-
761
- 在 relay 宿主上实测时发现"授权了但一执行就崩":维护状态目录 `/var/lib/w2m/maintenance` 由 root 拥有且 0755,agent 以
762
- 非特权用户运行,grant 第一次通过后写 `grant-uses.json` 报 EACCES,异常冒泡成任务 `crashed` —— 授权判定其实已经通过
763
- (同一 argv 无 grant 时是 `COMMAND_NOT_ALLOWED`),失败发生在记录环节。
764
-
765
- - `grant-gate`:台账写入包 try/catch,失败返回 `GRANT_LEDGER_UNAVAILABLE`,不执行"未记录的一次授权"(否则单次授权会变成
766
- 可重放漏洞),也不再把整个 handler 打崩。
767
- - `bootstrap.sh`:`$BASE` 改为 `root:w2m 0775`,让默认安装就能写台账;queue 仍 0770、done/claimed 0750。
768
- - 回归用例:usedFile 指向不可写路径,断言返回该 code 而非抛异常。
769
-
770
- 真机对比(修复前、relay 宿主):
771
- - 无 grant 的 `uname -s` → `refused/COMMAND_NOT_ALLOWED`;
772
- - 有合法 grant 的同一 argv → 通过 gate,随后因台账 EACCES 报 crashed(本次修复的目标)。
773
-
774
- Verified: grant-gate 7/7(含新用例);gate GREEN twice。
775
-
776
- ## [1.3.12-beta.3] — 2026-10-22
777
-
778
- **执行授权模型收尾:每个结果都说明"是谁授权的"。**
779
-
780
- beta.1 让操作员能授权一次执行,beta.2 让授权可撤销、可审计;这一版补上最后一块——结果自身携带授权来源,而不是只能在
781
- relay 台账里反查。
782
-
783
- - 结果信封新增 `authorization` 字段:`{provenance: "grant" | "allow-list" | "maintenance", capability_id?, grant_id?}`。
784
- grant 路径带 `grant_id`(可与 ledger 对账),maintenance 路径带 request_id(可与 M1 台账对账),allow-list 路径标记
785
- provenance 即可。
786
- - 一个决定、两个受众:gate 决定"跑不跑",信封记录"这是哪一种决定";refused 路径不伪造 provenance。
787
- - e2e 断言:空 allow-list + 合法 grant 的结果 `authorization.provenance === "grant"` 且带 grant_id。
788
- - 边界如实写进文档:provenance 是执行机器对自身判定的陈述,relay 存储但不验证它;签名仍由 agent 验证,派发仍由 relay
789
- 预检,撤销仍由 relay 记账——台账才是权威。
790
-
791
- Verified: agent 159/157(含 grant replay e2e 的 provenance 断言);gate GREEN twice。
792
-
793
- ## [1.3.12-beta.2] — 2026-10-22
794
-
795
- **执行授权模型补完:撤销与操作员审计。**
796
-
797
- beta.1 让操作员能"授权某一次执行";这一版让授权可以被收回、可以被查账,而不是只能等 TTL 过期。
798
-
799
- - **撤销**:`POST /v1/grants/revoke`(operator token)`{grant_id, reason?}`。幂等(第二次 `already_revoked: true`),写入
800
- ledger `grant.revoked`,带 reason 与操作者。
801
- - **派发拒绝**:携带已撤销 grant 的任务在 `POST /v1/task` 被拒,`403 GRANT_REVOKED`,机器收不到 offer;revocation 先于
802
- argv/task 绑定检查,理由更直接。
803
- - **审计视图**:`GET /v1/grants`(operator token)返回 `tasks`(task_id / grant_id / argv_hash / key_id / mode / created_at)
804
- 与 `revoked` 列表;device token 为 401,授权台账不属于机器工作面。
805
- - **边界**:撤销阻止的是**新派发**;agent 断网时仍会执行已到手 grant 里绑定的那一次(正确性仍取决于 relay 派发,与 M2 §4
806
- 一致)。撤销后要重新授权必须签发新 grant_id。
807
-
808
- Verified: grant 13/13;grant-issue 3/3;relay 191/191(新增撤销+审计用例);gate GREEN twice。
809
-
810
- ## [1.3.12-beta.1] — 2026-10-22
811
-
812
- **执行授权模型:静态 allow-list 之外,操作员可以显式授权某一次执行。**
813
-
814
- 在此之前授权只有一层:机器本地配置的 allow-list,一个长期决定,没有"就这一次"的途径。这一版把 M2 grant 补成一条可用的
815
- 路径——签发、派发、验证、审计——而不是一个只在测试里存在的协议。
816
-
817
- - **`bin/w2m-grant.mjs`**:操作员签发一条 grant。绑定一个 task_id(必须先确定)、一个 argv、若干 machine_id、≤10 分钟 TTL
818
- 和 max_uses(默认 1);stdout 输出可直接放进 `POST /v1/task` 的 `grant` 字段,stderr 打印 task_id/argv_hash/expires_at。
819
- 该 CLI 同时登记为 package bin `w2m-grant`。
820
- - **relay 派发预检**:task_id 不符 → `GRANT_TASK_MISMATCH`,argv_hash 不符 → `GRANT_ARGV_MISMATCH`,已过期 →
821
- `GRANT_EXPIRED`,都是 HTTP 400 的调用方错误;relay 不替 agent 做授权裁决。
822
- - **审计**:task summary 增加 `argv_hash` 与 `grant_key_id`(`grant_id` 已有),让"谁授权了哪条命令"可查。
823
- - **agent 仍是验证者**:签名、任务/机器绑定、argv、TTL、max_uses、重放(`GRANT_REPLAY`)由机器本地用 pin 的操作员公钥判定;
824
- 空 allow-list + 合法 grant 执行,无 grant 仍 `COMMAND_NOT_ALLOWED`(既有 e2e 覆盖)。
825
- - **文档**:`docs/COLLAB/EXECUTION-AUTH-MODEL.md` 状态改为已实现,补上"取 task_id → 签发 → 携带派发 → 两处检查 → 审计字段"
826
- 的完整路径和仍然不做的边界(无长期委托、无自动签发、无跨项目授权)。
827
-
828
- Verified: grant-issue 3/3(真实 CLI 子进程签发并用签名载荷验证);grant 13/13;relay 190/190(新增派发预检用例);gate GREEN twice。
829
-
830
- ## [1.3.11] — 2026-10-22
831
-
832
- **Stable release: the collaboration stack's first seven modules, with the gate that measures them.**
833
-
834
- This is the first stable since 1.3.9 (docs) that ships runtime work, and the first beta line to end with its acceptance evidence
835
- enforced by the pre-flight rather than stored beside it. What is in it, by module:
836
-
837
- - **M1 maintenance channel** (live on the relay host): signed update/restart/diag requests, operator-key pinning, a root runner and
838
- a bootstrap for Linux/Windows. Accepted on the real machine: diag (9 sections / 62 KB), relay restart with a new relay_id and
839
- the result delivered after it, update to 1.3.10-beta.10 (download + sha256 + atomic swap), and a post-update diag. Evidence:
840
- docs/acceptance/1.3.10-beta.10/.
841
- - **M2 grants**: single-use, task-bound Ed25519 grants with argv hash, TTL, use ledger and named refusals; e2e covers an empty
842
- allow-list succeeding under a grant, replay, argv tampering and the no-grant refusal.
843
- - **M3 Git-native tasks**: isolated worktrees at the caller's base commit, a content-addressed patch artifact contract, a merge
844
- queue with merge-slot serialisation, strict/replay policies, already_merged and a structured conflict artifact. Module-level,
845
- 24 tests across the three slices plus two composed workflow tests.
846
- - **M4 project room**: project objects, membership roles, a deny-by-default action matrix, snapshot + incremental events, task
847
- attribution (implicit default project, explicit projects refused when unknown), and the /v1/projects routes.
848
- - **M5 multi-client primitives**: branch/file locks (leases, renewal, owner force-release, merge preflight conflicts), reviews
849
- (self-review guard, duplicate-verdict refusal, approval gate by patch sha) and notifications (dedupe, digest, per-reader
850
- watermarks, retention), with the lock routes live and their events projected into the project stream.
851
- - **M6 cross-network reliability**: chunked, resumable blob uploads with verified commits, Range downloads, and an agent result
852
- outbox (atomic, persisted exponential backoff, cross-restart recovery, bounded).
853
- - **M7 metrics and acceptance**: a 39-metric contract with a cardinality guard, live on /metrics; a validator for acceptance
854
- evidence; and the pre-flight's 16th check, which fails a release whose evidence does not validate or whose release line has no
855
- record.
856
-
857
- Verified: pre-flight GREEN twice (16/16 checks, 2828 tests: 2814 pass, 0 fail, 14 skip); beta line beta.1 through beta.25 shipped on
858
- GitHub prereleases only.
859
-
860
- Known limitations, stated rather than implied: the merge queue and the review/notification routes are not wired into the task path
861
- yet; a request that restarts the agent itself loses the ferry result (relay restarts are fine); M4 role checks cover the lock routes
862
- and reads, not every future write; the relay runs beta.10 and is upgraded separately from this tag.
863
-
864
- ## [1.3.10-beta.25] — 2026-10-22
865
-
866
- **M7 final core slice: acceptance evidence is a gate, not a folder.**
867
-
868
- The pre-flight grew its 16th check. `acceptance evidence (every committed record validates, and the release line has one)`
869
- does three things a file list cannot:
870
-
871
- 1. every JSON record under `docs/acceptance/` is validated with the M7 schema (`src/acceptance/evidence.mjs`) — a record that
872
- does not say which machine ran it, when, by whom and under which task id fails the gate;
873
- 2. the current release line must have at least one record: shipping a beta that claims an accepted capability while pointing at
874
- no evidence from its own `1.3.x` line is RED;
875
- 3. it reports what it read ("4 acceptance record(s) validated; 4 for the 1.3.10 line"), so a green row is a statement about a
876
- specific set of records rather than about an empty directory.
877
-
878
- The check is bound to `docs/acceptance` in `GATE_INVENTORY`, so deleting the directory or the check breaks the inventory's
879
- both-directions rule; the evidence validator's accepted-orphan entry is removed now that the gate imports it.
880
-
881
- Not claimed: this gate proves the records are well-formed and current, not that the described runs happened — that is what the
882
- task id, the machine ids and the executor field are for, and they are now mandatory.
883
-
884
- Verified: gate GREEN twice with 16/16 registered checks; the evidence check passes on the four M1 records.
885
-
886
- ## [1.3.10-beta.24] — 2026-10-22
887
-
888
- **M7 second slice: acceptance evidence has a schema, and the committed evidence is validated against it.**
889
-
890
- - `src/acceptance/evidence.mjs` validates every required field of the M7 evidence record: version, uppercase case id, kind
891
- (positive/negative/chaos), requirement, at least one machine, ISO `executed_at`, executor (lead/human/agent), a result with a
892
- verdict, and — for a positive case — the real `task_id`. Optional fields are validated when present: argv arrays,
893
- `[from, to]` ledger ranges, `{sha256, bytes}` artifacts and an assertions object. Every problem is returned at once, because a
894
- release check should list what to fix rather than one field per run.
895
- - `summarizeEvidence()` counts valid records by kind and version and names the invalid ones, which is what a release note or a
896
- gate summary reads.
897
- - The test suite validates the **actual committed files** under `docs/acceptance/` — a schema that rejected the project's own
898
- evidence would be a schema written for a fictional project. It also pins that both the flat `result.outcome` and the nested
899
- `result.task.outcome` (the live restart record) are accepted as the same fact.
900
-
901
- Registered as an accepted library with its exit named (the preflight evidence gate).
902
-
903
- Still to wire for M7: the preflight check that a release's evidence exists and validates, and the remaining instrumentation.
904
-
905
- Verified: evidence 6/6; gate GREEN twice.
906
-
907
- ## [1.3.10-beta.23] — 2026-10-22
908
-
909
- **M7 wiring: the registry is live on /metrics.**
910
-
911
- - Relay state owns the M7 registry. The project room now notifies a listener per event, and that callback counts
912
- `wm_project_events_total{type}` once for both the room's own events (project created, member changes, task linked) and the
913
- lock/review/notification events projected into it — a projection is appended, then counted through the room hook, so it cannot
914
- be double-counted.
915
- - `GET /metrics` appends the registry body under a `# reliability registry` marker. A relay that never records into the registry
916
- renders exactly what it rendered before, which is why the existing exposition tests still pass unchanged.
917
- - Lock routes count `wm_lock_acquired_total{kind}` and `wm_lock_conflict_total{kind}`.
918
- - One route test: after creating a project and acquiring a branch lock, `/metrics` carries
919
- `wm_project_events_total{type="project.created"}` and `wm_lock_acquired_total{kind="branch"}`.
920
-
921
- Verified: relay 189/189 (one new test); the registry and room suites are unchanged; gate GREEN twice.
922
-
923
- ## [1.3.10-beta.22] — 2026-10-22
924
-
925
- **M7 first slice: one metrics contract for the whole collaboration stack.** A local module and eight tests; the relay /metrics
926
- route and the call sites do not record into it yet.
927
-
928
- - `src/relay/reliability-metrics.mjs` declares every M7 metric name, its type and the labels it may carry — 39 definitions across tasks,
929
- authorization, data plane, collaboration and transport. An undeclared name, an undeclared label, or a high-cardinality label
930
- (`task_id`, `dedupe_key`, ids) is refused at record time, so a metrics endpoint cannot silently become a memory leak.
931
- - Counters refuse decrements; gauges hold the last value; histograms keep cumulative buckets plus sum/count. `renderPrometheus()`
932
- emits `_bucket/_sum/_count` with sorted, escaped label values, and `snapshot()` gives the same data as plain JSON for the panel.
933
- - Eight tests: label aggregation, undeclared metric/label refusal, wrong-type and decrement refusal, gauge semantics, cumulative
934
- buckets, Prometheus text (including `+Inf`), label escaping, and the contract's own shape.
935
-
936
- Registered as an accepted library with its exit named (the /metrics route and instrumentation). Note on the name: the exposition module was already called metrics.mjs, so after a naming collision during this work its file and test were restored, and this registry ships as reliability-metrics.mjs; both test files pass.
937
-
938
- Still to wire for M7: the /metrics route, the instrumentation call sites, the acceptance-evidence validator and the preflight
939
- evidence check.
940
-
941
- Verified: metrics 8/8; gate GREEN twice.
942
-
943
- ## [1.3.10-beta.21] — 2026-10-22
944
-
945
- **M6 second slice: the agent result outbox.** A local module and eight tests; the agent delivery path still uses the existing
946
- spool and has not switched to it yet.
947
-
948
- - `src/agent/outbox.mjs` — a result is written to disk (temp + rename, so a crash leaves either the old state or the complete
949
- entry) before any network attempt, keyed by `(task_id, attempt)` so a retried handler is one entry. Delivery removes the entry
950
- only after the relay acknowledged it; the relay's own dedupe makes a re-delivery safe when that acknowledgement was lost.
951
- - Retries use exponential backoff persisted with the entry (`1s, 2s, 4s …` capped at 60s), so closing the laptop for an hour is
952
- a resume rather than a restart. Past `max_attempts` the entry moves to `dead/` and is reported as exhausted.
953
- - Limits: 200 entries / 16 MiB; beyond that the oldest entries are dropped and the caller sees `outbox_full: true`
954
- (`OUTBOX_FULL` in the changelog's words) rather than a silent disk fill. A single oversize result is refused.
955
- - Eight tests: atomic persist + depth, duplicate enqueue, restart recovery with attempt counts, backoff growth and exhaustion,
956
- ack-only removal, count-limit drop keeping the newest, corrupt-file quarantine, oversize refusal.
957
-
958
- Registered as an accepted library with its exit named (the agent delivery switch).
959
-
960
- Still to wire for M6: the agent's upload/download decision for large artifacts, and P2P as acceleration-only with metrics.
961
-
962
- Verified: outbox 8/8; gate GREEN twice.
963
-
964
- ## [1.3.10-beta.20] — 2026-10-22
965
-
966
- **M6 wiring: chunk session and Range routes.** Large artifacts now travel over HTTP without restarting from zero.
967
-
968
- - `POST /v1/fleet/blob-sessions` declares `{sha256, size, chunk_bytes?}`; `PUT …/:sid/chunks/:index` uploads one idempotent chunk
969
- (octet-stream); `GET …/:sid` returns received/missing so an interrupted upload resumes; `POST …/:sid/commit` verifies the
970
- assembled sha256 and, on success, stores the bytes through the existing content-addressed blob store. A missing chunk answers
971
- `409 BLOB_INCOMPLETE` with the indices; a mismatch deletes the session and creates nothing.
972
- - `GET /v1/fleet/blobs/:sha?range=start-end` answers `206` with `Content-Range`, so a client can fetch a large patch (or a conflict
973
- artifact) in slices instead of one allocation.
974
- - The blob-session module's responses are now uniformly `{ok, session: {...}}`, which the routes and the original tests share.
975
-
976
- Two HTTP tests: a chunked upload that commits and is then read whole plus as a Range, and an incomplete commit naming `[0,1]`.
977
-
978
- Still to wire for M6: the agent result outbox, and P2P as acceleration-only with its observable success rate.
979
-
980
- Verified: blob-sessions 9/9, relay 188/188 (2 new route tests); gate GREEN twice.
981
-
982
- ## [1.3.10-beta.19] — 2026-10-22
983
-
984
- **M6 first slice: large artifacts can be uploaded in resumable chunks.** A local module and nine tests; the routes and the
985
- agent upload path are the wiring slice after it.
986
-
987
- - `src/relay/blob-sessions.mjs` — a client declares the final `sha256` and `size` once, then uploads chunks in any order
988
- (each index idempotent), asks what is missing, and commits. Only a commit whose assembled bytes hash to the declared sha256
989
- becomes a blob; a mismatch deletes the session and returns `BLOB_SHA_MISMATCH`, so nothing can be referenced that does not
990
- exist.
991
- - Limits follow the spec: default 1 MiB chunks, `≤8 MiB` per chunk, `≤256` chunks and `≤64 MiB` per blob, two open sessions and
992
- 512 MiB unfinished bytes per machine, 24h session TTL. A resume after a disconnect is `status()` plus the missing indices,
993
- which is exactly the "don't restart a 40 MiB patch" property.
994
- - Nine tests: plan/status, out-of-order + idempotent chunks, verified commit through the store hook, incomplete commit naming
995
- the missing chunks, sha mismatch deleting the session, chunk length/size refusals, quotas, TTL expiry, and blob/chunk ceilings.
996
-
997
- Registered as an accepted library with its exit named (the blob-session routes + agent upload).
998
-
999
- Still to wire for M6: the HTTP chunk routes and Range downloads, the agent outbox for offline results, and P2P as acceleration
1000
- only.
1001
-
1002
- Verified: blob-sessions 9/9; gate GREEN twice.
1003
-
1004
- ## [1.3.10-beta.18] — 2026-10-22
1005
-
1006
- **M5 wiring, part 1: lock routes and one event stream for the primitives.**
1007
-
1008
- - Relay state now owns the three M5 primitives (locks, reviews, notifications) and projects their events into the project room
1009
- stream with the module sequence attached, so a client can reconstruct "what happened" from one cursor instead of three.
1010
- - `/v1/projects/:id/locks`: `GET` list, `POST` acquire, `POST …/:lock_id/renew`, `DELETE …/:lock_id` (holder release), and
1011
- `DELETE …?force=1&reason=…` (owner only). A conflict is an answer, not a transport error: `409` with the holder and expiry, so a
1012
- client can show who is editing instead of parsing a failure. Role checks sit on M4 membership (default project treats a paired
1013
- machine as worker); a non-member receives `404` and learns nothing about the project.
1014
- - Two route tests: acquire/list/conflict/foreign-release/ordered-release, plus non-member `404` and `LOCK_FORCE_DENIED` for a
1015
- worker trying to force release.
1016
-
1017
- Still to wire: review and notification routes, the merge/review gate, and the panel views.
1018
-
1019
- Verified: relay 186/186 (2 new lock route tests); gate GREEN twice.
1020
-
1021
- ## [1.3.10-beta.17] — 2026-10-22
1022
-
1023
- **M5 third primitive: notifications.** A local module and eight tests; no routes or SSE projection use it yet.
1024
-
1025
- - `src/relay/project-notifications.mjs` — notifications for a project addressed to a member or broadcast, each with kind,
1026
- subject, refs and `requires_action`. Every row is also a `notification.created` event for the live stream, and the list route
1027
- is the catch-up path.
1028
- - Semantics pinned by tests: the same (kind, primary ref, target) inside 60s is one row; different refs/kinds are distinct;
1029
- overflow beyond 120 per minute collapses into one digest row that carries the count; unread is counted after a per-reader
1030
- watermark; a watermark never moves backwards; paging is `since_ms` + limit; retention drops rows older than 7 days (or beyond
1031
- 2000 per project).
1032
- - The reader is a member identity (machine or operator), not a transport: an offline client reconnects and reads the same
1033
- watermark it left.
1034
-
1035
- Registered as an accepted library with its exit named (the notification routes).
1036
-
1037
- M5 primitives are now all designed-and-implemented at module level: locks (beta.15), reviews (beta.16), notifications (beta.17).
1038
- Still missing for M5: the routes, the merge/ref gates using them, and the panel views.
1039
-
1040
- Verified: project-notifications 8/8; gate GREEN twice.
1041
-
1042
- ## [1.3.10-beta.16] — 2026-10-22
1043
-
1044
- **M5 second primitive: review tasks.** A local module and ten tests; the review routes and the merge gate do not call it yet.
1045
-
1046
- - `src/relay/project-reviews.mjs` — a review points at a content-addressed `{kind, sha256, source_task_id, diffstat}` target and
1047
- collects decisions from listed reviewers. Verdicts are records, not a mutable flag: `approve`/`changes_requested`/`comment`
1048
- accumulate, approvals are counted per distinct reviewer, and reaching `required_approvals` moves the review to `approved`.
1049
- - Safety rules the tests pin: self-review is refused unless an owner overrides it (and the override is its own event); reviewers
1050
- must be maintainer/owner; only listed reviewers may decide; a reviewer cannot record the same verdict twice; a
1051
- `changes_requested` closes the round and refuses a later approve; a due date expires a pending review and late decisions are
1052
- refused; `approvedFor(project, sha256)` answers the merge gate for an exact patch.
1053
- - Every state change is emitted as an event (`review.requested/decided/approved/changes_requested/expired/self_review_override`).
1054
-
1055
- Registered as an accepted library with its exit named (the review routes + merge-requires-review gate).
1056
-
1057
- Not in this slice: notifications (§4), the third M5 primitive.
1058
-
1059
- Verified: project-reviews 10/10; gate GREEN twice.
1060
-
1061
- ## [1.3.10-beta.15] — 2026-10-22
1062
-
1063
- **M5 first primitive: branch and file locks.** A local module and eight tests; no routes or merge preflight call it yet.
1064
-
1065
- - `src/relay/project-locks.mjs` — one exclusive holder per `(project, branch)` or `(project, path)`, leases with renewal, lazy
1066
- expiry that frees the target and records `lock.expired`, owner-only force release with a mandatory reason recorded as
1067
- `lock.force_released`, per-project/per-holder limits (20/10), and a `conflicts()` preflight that answers which live locks cover a
1068
- branch or a set of paths.
1069
- - Every mutation is returned as an event (`lock.acquired/renewed/released/expired/force_released`) so the project stream can carry
1070
- it; the module holds no relay state and takes the clock as an injection.
1071
- - Eight tests: acquire/list/release, LOCK_HELD naming the holder, expired re-entry with both events, renew ownership and
1072
- non-revival, force-release permission and reason, limits, preflight conflicts for branch and paths, and malformed-input refusal.
1073
-
1074
- Registered as an accepted library with its exit named (the lock routes and the merge preflight check).
1075
-
1076
- Not in this slice: review tasks (§3) and notifications (§4), which are the next two M5 primitives.
1077
-
1078
- Verified: project-locks 8/8; gate GREEN twice.
1079
-
1080
- ## [1.3.10-beta.14] — 2026-10-22
1081
-
1082
- **M4 third slice: the `/v1/projects` routes.**
1083
-
1084
- - `GET /v1/projects` — the default project plus the projects this machine is a member of (device token).
1085
- - `GET /v1/projects/:id` — snapshot with `last_event_seq`; a machine that is not a member gets `NOT_FOUND`, so a project's
1086
- existence is not leaked. The default project stays readable for any paired machine because it is the implicit home of unnamed
1087
- tasks.
1088
- - `GET /v1/projects/:id/events?since_seq=N` — the incremental slice and `next_seq`, so a client can resume after a disconnect.
1089
- - `POST /v1/projects` and `POST /v1/projects/:id/members` — operator token only (the dispatch credential), because these change
1090
- who may act on shared state; the body's initial `members` array is seeded atomically with the project.
1091
- - The routes authenticate per method and therefore sit above the shared device guard: an operator token is not a device token,
1092
- and the earlier placement made every write answer `UNAUTHORIZED`.
1093
- - Three HTTP tests: list/create/snapshot/events with a member, a non-member seeing `NOT_FOUND`, the operator requirement, and an
1094
- unknown project answering `NOT_FOUND`.
1095
-
1096
- Still missing: artifact/memory/decision mounts, the panel view, and multi-client e2e.
1097
-
1098
- Verified: relay 184/184 (3 new route tests); gate GREEN twice.
1099
-
1100
- ## [1.3.10-beta.13] — 2026-10-22
1101
-
1102
- **M4 second slice: tasks belong to a project, and the room records it.** Relay state now owns a project room; no HTTP routes yet.
1103
-
1104
- - `POST /v1/task` (internal `createTask`) accepts an optional `project_id`. Unnamed tasks land in the implicit `default`
1105
- project, which is created lazily on first use; an explicit unknown project is refused with `BAD_REQUEST` rather than being
1106
- created silently.
1107
- - The project room records a `task.linked` event for every task, so a snapshot's `last_event_seq` moves with the work and a
1108
- second client can apply it as an incremental event.
1109
- - State exposes `projectSnapshot(projectId)` and `projectEvents(projectId, {sinceSeq})`; the task summary carries
1110
- `project_id`; the replay path restores it from the ledger.
1111
- - Three attribution tests: implicit default + linked event consistency, explicit project visible on the summary, and an unknown
1112
- project refused without creating one.
1113
-
1114
- Still missing, stated: the `/v1/projects` HTTP routes, artefact/memory/decision mounts, the panel view, and multi-client e2e.
1115
-
1116
- Verified: project-attribution 3/3, project-room 8/8, relay 181/181; gate GREEN twice.
1117
-
1118
- ## [1.3.10-beta.12] — 2026-10-22
1119
-
1120
- **M4 first slice: the project room object.** A local module and eight tests; the relay routes and the panel do not call it yet.
1121
-
1122
- - `src/relay/project-room.mjs` — a project aggregate (project id, name, origin, settings, members), the four roles
1123
- (owner/maintainer/worker/observer), a deny-by-default action matrix (`read/dispatch/artifact/lock/memory/decide/merge/
1124
- settings/members`), member add/remove/role-change as recorded events, a snapshot carrying `last_event_seq`, incremental
1125
- events since a sequence, and per-member project listing.
1126
- - Safety rules the tests pin: the last owner cannot be removed; a duplicate member is refused by name; an unknown project is
1127
- refused rather than creating an empty one; the permission check returns the required and current role rather than a bare
1128
- false; a role change records `{from,to}`.
1129
-
1130
- Registered as an accepted library with its exit named (the `/v1/projects` routes and the room panel), because nothing reaches it
1131
- yet.
1132
-
1133
- Also in this tree since beta.11: the M1 live-acceptance evidence files under `docs/acceptance/1.3.10-beta.10/`.
1134
-
1135
- Verified: project-room 8/8; gate GREEN twice.
1136
-
1137
- ## [1.3.10-beta.11] — 2026-10-22
1138
-
1139
- **M1 update path fixed and the full acceptance set recorded.**
1140
-
1141
- The live update acceptance exposed two defects that made `update` unusable even though diag and restart already worked:
1142
-
1143
- 1. **EXDEV on swap** — staging unpacked under `/tmp` (often tmpfs) and the swap is a rename into `/opt/w2m/app`, so the kernel
1144
- refused with `EXDEV: cross-device link`. Staging now lives on the same filesystem as the app directory
1145
- (`/opt/w2m/.maintenance-staging`).
1146
- 2. **Ferry reported success for a failed action** — the ferry returning a result file only means the runner wrote one; the
1147
- runner's own `status` was not inspected, so a failed update was reported as `ok`. The agent now reports
1148
- `MAINTENANCE_FAILED` with the runner's code, and a test covers the shape.
1149
-
1150
- Acceptance evidence now in the tree:
1151
- - `docs/acceptance/1.3.10-beta.10/M1-LIVE-DIAG.json` (9 sections, 62,238 bytes, runner ok)
1152
- - `M1-LIVE-RESTART.json` (relay restarted, new relay_id, result delivered)
1153
- - `M1-LIVE-UPDATE.json` (download + sha256 + swap to 1.3.10-beta.10, no auto-restart)
1154
- - `M1-LIVE-POSTUPDATE-DIAG.json` (after update + restart, a new diag still returns 59,581 bytes)
1155
-
1156
- Known limitation unchanged and documented: a request restarting the agent itself kills the ferry before it reads the result
1157
- file, so the accepted restart target is the relay; delayed restart is the next fix.
1158
-
1159
- Verified: gate GREEN twice; actions/ferry unit tests pass.
1160
-
1161
- ## [1.3.10-beta.10] — 2026-10-22
1162
-
1163
- **M1 live acceptance: the maintenance channel works end to end on a real machine.**
1164
-
1165
- The relay host (jp-shared) was bootstrapped and two signed requests were executed by the root runner:
1166
-
1167
- - **diag** — the runner produced 9 sections (version/services/journal/disk/ports), the agent returned the task as `ok` with
1168
- 62,238 bytes of result, and the runner's own result file confirms `status: ok`. Evidence:
1169
- `docs/acceptance/1.3.10-beta.10/M1-LIVE-DIAG.json`.
1170
- - **restart (rabbit)** — the relay was restarted by the runner, the agent survived it and delivered the task result, and the
1171
- relay came back with a new `relay_id` and a fresh uptime. Evidence:
1172
- `docs/acceptance/1.3.10-beta.10/M1-LIVE-RESTART.json`.
1173
-
1174
- The run found four real defects, all fixed here:
1175
-
1176
- 1. The agent's envelope builder hashes and sizes the output; the maintenance ferry passed an execution object without
1177
- `stdout_sha256/stdout_bytes/stderr_sha256/stderr_bytes`, so every maintenance task ended as `crashed` **after** the action had
1178
- already run. The ferry now fills them.
1179
- 2. `--operator-key` without `--operator-key-id` claimed an empty key id, which the pin store correctly refused as a mismatch;
1180
- an absent id now means "trust the key that arrives".
1181
- 3. `bootstrap.sh` made the queue root-only, so the unprivileged agent could never enqueue a request; the queue is now
1182
- `root:w2m 0770` (write but not execute) and `done` is `root:w2m 0750`.
1183
- 4. Known limitation, documented rather than hidden: a request that restarts **the agent itself** kills the process still waiting
1184
- for its result file, so that result is lost (the action still runs). Restarting the relay is unaffected, which is why the
1185
- accepted case uses that target; the delayed-restart fix is next.
1186
-
1187
- Also verified: gate GREEN twice; the local agent suite still passes with the ferry envelope fix.
1188
-
1189
- ## [1.3.10-beta.9] — 2026-10-22
1190
-
1191
- **M3 conflict contract completed: MERGE_CONFLICT now carries the conflict itself.**
1192
-
1193
- When `git apply --3way` cannot apply a patch, the queue previously reported the changed files and the git message. It now also
1194
- captures the state git left in the merge worktree (conflict markers included) as a `conflictArtifact`:
1195
- `{ sha256, bytes, text }`. That is the evidence a reviewer needs to decide whether to resubmit against the new base or resolve
1196
- by hand, and it is the same content-addressed shape the patch artifacts use.
1197
-
1198
- The head still does not move on a conflict; the test asserts both the artifact (with conflict markers) and the unchanged head,
1199
- and the composed M3 workflow test still passes.
1200
-
1201
- Verified: merge-queue 7/7 (conflict artifact assertion included), m3-workflow 2/2; gate GREEN twice.
1202
-
1203
- ## [1.3.10-beta.8] — 2026-10-22
1204
-
1205
- **M3 composed: worktree -> patch -> merge queue, proven together.**
1206
-
1207
- Two integration tests drive the three modules the way a real task pair would: both tasks start from the same base in isolated
1208
- worktrees, each commits its change and produces a patch artifact, and the queue merges them one after the other. The disjoint
1209
- case ends with both files changed and the branch advanced to the second merge; the same-file case ends with a structured
1210
- MERGE_CONFLICT listing the file, the head still on the first approved commit, and the first task's change intact.
1211
-
1212
- No runtime wiring yet: the agent's task handler does not create worktrees, and the relay does not run the queue. This release
1213
- records that the modules agree with each other before either becomes reachable, which is the point of composing them now.
1214
-
1215
- M1 real-machine acceptance remains pending: the relay host agent is offline and root SSH authentication is refused, so the
1216
- privileged bootstrap has not run.
1217
-
1218
- Verified: m3-workflow 2/2, merge-queue 7/7, worktree 5/5, patch-artifact 5/5; gate GREEN twice.
1219
-
1220
- ## [1.3.10-beta.7] — 2026-10-22
1221
-
1222
- **M3 third block: the merge queue.** A local module and seven real-git tests; the relay task path does not import it yet.
1223
-
1224
- - `src/relay/merge-queue.mjs` — one queue per project+branch with a single merge slot (holder + TTL), items named by the
1225
- patch sha256, `strict` and `replay` base policies, and terminal verdicts: MERGED (branch head advances), `already_merged`
1226
- for a repeat submission, `BASE_MISMATCH` under strict, and a structured `MERGE_CONFLICT` with the changed files that never
1227
- advances the head.
1228
- - Application happens in a detached throwaway worktree, then the repository's branch and working tree are moved to the new
1229
- commit — a merge that committed only in the worktree would leave the next reviewer looking at a head that never moved.
1230
- - Seven tests cover merge-and-advance, idempotent re-submission, stale base, conflict (head unchanged), replay onto a moved
1231
- head, merge-slot serialisation, and patch naming.
1232
-
1233
- Registered as an accepted library with its exit named (the relay merge executor dispatch), because nothing reaches it yet.
1234
-
1235
- Still pending, unchanged: the M1 real-machine acceptance (relay runs beta.5 and carries the maintenance field live, but the
1236
- relay host's agent is offline and root SSH authentication has not been restored), and the M2/M3 live runs.
1237
-
1238
- Verified: merge-queue 7/7; the rest of the suite is unchanged; gate GREEN twice.
1239
-
1240
- ## [1.3.10-beta.6] — 2026-10-21
1241
-
1242
- **M3 first two blocks: isolated worktrees and a patch artifact contract. Plus the grant replay e2e.**
1243
-
1244
- - `src/agent/worktree.mjs` — one git worktree per task, created detached at the caller's `base_commit`; hooks disabled,
1245
- commit identity fixed, a porcelain snapshot before and after, commit-if-changed, `format-patch` relative to the base, and
1246
- a removal that reports failure instead of leaking a directory. Unknown base commit and missing repository are named refusals.
1247
- - `src/agent/patch-artifact.mjs` — the transportable result: sha256 over the patch bytes, byte count, base/head commits,
1248
- sorted changed files, inline/blob mode, and `no_change` as a successful outcome for an empty patch. Non-ASCII patches are
1249
- measured in UTF-8 bytes, not characters.
1250
- - Five real-git tests for the worktree lifecycle and five for the artifact contract.
1251
- - `test/agent.test.mjs` gains the grant **replay** e2e: the same signed grant is used twice, the first run executes, the second
1252
- is refused with `GRANT_REPLAY` — the use ledger is what makes "single-use" true across processes.
1253
-
1254
- Not in this slice (stated, not implied): the merge queue itself (strict/replay bases, merge-slot serialization,
1255
- `MERGE_CONFLICT`), review tasks and locks (M5), and the project room (M4). The M1 real-machine acceptance is also still pending:
1256
- the relay was upgraded to beta.5 and the maintenance field is verified live, but the relay host's agent is offline and root SSH
1257
- authentication is refused, so the privileged bootstrap has not run.
1258
-
1259
- Verified: agent 159/157 (replay e2e included), relay 181/181, grant 10/10, grant-gate 6/6, maintenance 17/17, ferry 4/4,
1260
- worktree 5/5, patch-artifact 5/5, layer-model 24/24, claims 47/47; gate GREEN twice.
1261
-
1262
- ## [1.3.10-beta.5] — 2026-10-21
1263
-
1264
- **M2 grant e2e: the explicit-authorisation path is proven end to end.**
1265
-
1266
- Four fake-relay tests now drive the real agent process:
1267
-
1268
- - an empty allow-list executes a command **only** because a valid grant authorises that exact argv (the envelope is `ok` and
1269
- the stdout hash matches);
1270
- - the same command with no grant is still `COMMAND_NOT_ALLOWED`;
1271
- - a grant that signed a different argv is refused with `GRANT_ARGV_MISMATCH`;
1272
- - a grant that names another machine is refused with `GRANT_WRONG_MACHINE`.
1273
-
1274
- Two integration defects the e2e exposed, both fixed: the per-stage allow-list re-check ran again after step 5, so a grant that
1275
- had authorised the task was refused at the stage boundary; and the matcher's result carries `allowed` (not `ok`), so both the
1276
- gate result and the stage override had to use that shape or the refusal fell back to `COMMAND_NOT_ALLOWED`.
1277
-
1278
- Verified: agent 158/156 (4 new e2e cases), relay 181/181, grant 10/10, grant-gate 6/6, maintenance 17/17, ferry 4/4,
1279
- layer-model 24/24, claims 47/47; gate GREEN twice.
1280
-
1281
- ## [1.3.10-beta.4] — 2026-10-21
1282
-
1283
- **M1 bootstrap tooling: one privileged step, then the channel is remote.** No runtime behaviour changes.
1284
-
1285
- - `deploy/maintenance/bootstrap.sh` (Linux, run once as root) — installs the fixed-action path unit/service, creates the
1286
- queue/done/claimed directories with root ownership, optionally pins the operator public key, and enables the watcher. It
1287
- refuses to run without `--machine-id` and refuses a tree where `bin/w2m-maintenance.mjs` is absent.
1288
- - `deploy/maintenance/bootstrap.ps1` (Windows, run once as Administrator) — same shape via a SYSTEM scheduled task; it
1289
- substitutes the app directory and machine id into the task XML rather than editing the shipped file.
1290
- - `docs/COLLAB/M1-ACCEPTANCE-RUNBOOK.md` — the real-machine acceptance cases (diag/restart/update/rollback plus four named
1291
- refusals), what "green" means, and the limitations that must appear in the release note (privileged bootstrap, TOFU pinning,
1292
- no automatic recovery if a restarted machine cannot be reached).
1293
-
1294
- Both scripts install **only** the fixed update/restart/diag runner; neither installs a shell, and neither touches the
1295
- allow-list. The design's security boundary is unchanged: the agent ferries a signed request, the root runner decides.
1296
-
1297
- Honest gap, unchanged: the M1 real-machine acceptance run itself still needs a machine with a privileged bootstrap and a
1298
- relay carrying the maintenance fields; and the M2 grant path still lacks a fake-relay e2e case. Both are next.
1299
-
1300
- Verified: gate GREEN twice; pack carries the new scripts and document.
1301
-
1302
- ## [1.3.10-beta.3] — 2026-10-21
1303
-
1304
- **M2 first slice: task-level grants become the explicit authorisation channel.** Design: `docs/COLLAB/M2-TASK-GRANT.md`.
1305
-
1306
- - `src/maintenance/grant.mjs` — grant protocol: task binding, named machine ids, argv hash + expanded argv for the operator
1307
- to read, max uses, TTL capped at 10 minutes, nonce, and constraint fields that may only tighten the task (mode/cwd/write/
1308
- write_scope). Verification has a named refusal for every distinct failure: wrong machine, wrong task, argv mismatch,
1309
- expired, future, TTL too long, replay, uses exhausted, revoked key, unknown key, bad signature, constraint widening.
1310
- - `src/agent/grant-gate.mjs` — the agent-side gate: pins the operator key on first sight, verifies the grant against the
1311
- concrete argv, and persists use counts so a spent grant stays spent across restarts. A corrupt use ledger is never read as
1312
- "nothing used yet".
1313
- - The agent asks the grant gate **before** falling through to the allow-list refusal, and reports a grant refusal with its own
1314
- `GRANT_*` code instead of folding it into `COMMAND_NOT_ALLOWED`.
1315
- - The relay accepts an optional `grant` on `POST /v1/task` (8 KiB cap, `grant_id` + 64-hex `argv_hash` checked), carries it on
1316
- the offer, writes `grant_summary` into the ledger and exposes `grant_id` on the task summary.
1317
-
1318
- Honest gap: the grant path has unit coverage (protocol 10, gate 6) and the existing agent/relay suites are unchanged-green,
1319
- but there is no fake-relay e2e yet for "empty allow-list executes under a grant". That case and the real two-machine run land
1320
- in the next slice together with the M2 acceptance evidence.
1321
-
1322
- Verified: agent 154/152, relay 181/181, grant 10/10, grant-gate 6/6, maintenance 17/17, ferry 4/4, layer-model 24/24; gate
1323
- GREEN twice.
1324
-
1325
- ## [1.3.10-beta.2] — 2026-10-21
1326
-
1327
- **M1 maintenance channel: relay passthrough + agent ferry.** The remote loop now has both ends.
1328
-
1329
- - The relay accepts an optional `maintenance` request and `operator_key` on `POST /v1/task` (operator token already required),
1330
- registers the key once (`operator.key.registered` in the ledger, replayed on restart), carries the request on the offer, and
1331
- exposes `maintenance_request_id` on the task summary.
1332
- - `src/agent/maintenance-ferry.mjs` — the agent pins the operator key on first sight (TOFU, rotation chain thereafter), verifies
1333
- the request against the pin, writes it atomically into the queue the root runner watches, waits for the runner's result file,
1334
- and reports it as the task result. A refusal is a named value (`MAINTENANCE_NO_PINNED_KEY`, `MAINTENANCE_BAD_SIGNATURE`,
1335
- `MAINTENANCE_RUNNER_TIMEOUT`, ...), never a thrown error, so the task result says why.
1336
- - The agent never gains the privilege to run update/restart/diag: the root runner (beta.1) remains the only executor.
1337
- - Layer model declares `maintenance` as a capability layer with an `agent -> maintenance` edge, so the new dependency is
1338
- explained rather than excused.
1339
-
1340
- Known gap, stated: no fake-runner agent-e2e test for the ferry yet (the ferry's own four cases cover pin/verify/queue/refusal);
1341
- the end-to-end case lands with the real-machine acceptance run in beta.3.
1342
-
1343
- Verified: gate GREEN twice with the bundled node on PATH: 2731 tests / 2717 pass / 0 fail / 14 skip in 135 files
1344
- (agent 154/152, relay 181/181, maintenance 17/17, ferry 4/4, layer-model 24/24, claims 44/44).
1345
-
1346
- ## [1.3.10-beta.1] — 2026-10-21
1347
-
1348
- **M1 maintenance channel, first implementation slice.** Design: `docs/COLLAB/M1-MAINTENANCE.md` (the design series names this slice v1.3.2-beta; it ships as 1.3.10-beta.1 because 1.3.9 was already published as a stable release and versions must stay ordered).
1349
-
1350
- The maintenance channel lets an operator authorise exactly one of three fixed actions on one machine (update, restart, diag)
1351
- with an Ed25519-signed request, executed by a root/SYSTEM runner that reads a queue the unprivileged agent may write to. An
1352
- allow-list entry is not needed: the signature is the authorisation, and widening the allow-list is never the answer.
1353
-
1354
- ### What this slice contains
1355
-
1356
- - `src/maintenance/protocol.mjs` — request envelope, canonical signing bytes, parameter whitelists for the three actions,
1357
- and the complete refusal vocabulary (wrong machine, expired, future, TTL too long, replay, revoked key, unknown key,
1358
- bad signature, bad action, bad params, too large, missing signature). TTL is capped at 10 minutes and only the pinned
1359
- operator key can sign.
1360
- - `src/maintenance/operator-key.mjs` — Ed25519 key generation, relay registration payload, trust-on-first-use pinning with a
1361
- rotation chain (a new key must be signed by the pinned one), revocation, and a corrupt pin store that refuses instead of
1362
- trusting nothing.
1363
- - `src/maintenance/runner.mjs` — queue scanning, claim-before-verify, signature verification, persisted replay sets
1364
- (request id + nonce), idempotent re-runs, and a result file for every request it looks at, refusals included.
1365
- - `src/maintenance/actions.mjs` — the three fixed actions with no shell interpolation: diag (fixed read-only command set,
1366
- capped output), restart (fixed unit order and health waits), update (https download, sha256 verify, staged extraction,
1367
- version check inside the artifact, backup, atomic swap, health check, automatic rollback).
1368
- - `bin/w2m-maintenance.mjs` — the root entry point (once/watch, pin bootstrap, root guard).
1369
- - `deploy/server/w2m-maintenance.{service,path}` and `deploy/windows/w2m-maintenance.xml` — the runner as a systemd path
1370
- unit / SYSTEM scheduled task.
1371
-
1372
- ### What this slice deliberately does not contain
1373
-
1374
- The relay routes that distribute the operator public key and the agent-side ferry that carries a request from a task offer
1375
- to the queue (and the result back) are the next slice (1.3.10-beta.2). Until then the channel is exercised by placing a
1376
- signed request in the queue directory directly — which is also the offline/bootstrap path the design keeps.
1377
-
1378
- Tests: 17 new cases cover the protocol, pinning/rotation/revocation and the runner's refusal and replay behaviour. The
1379
- destructive actions are not executed in tests; they are covered by the real-machine acceptance runbook (M7 evidence).
1380
-
1381
- Verified: gate GREEN twice.
1382
-
1383
- ## [1.3.9] — 2026-10-21
1384
-
1385
- **Design-first release: the collaboration line is specified, not yet implemented.**
1386
-
1387
- This release ships the design specification for the multi-machine / multi-client collaboration capability, and no behaviour change: the
1388
- plugin, relay and agent are byte-identical in function to 1.3.1. It exists so the plan is reviewable, versioned, and citable before any
1389
- of it is built.
1390
-
1391
- ### What is in this release
1392
-
1393
- - `docs/MULTI-MACHINE-COLLABORATION-PLAN.md` — capability audit (what exists today is dispatch + aggregation, not collaboration),
1394
- root causes (missing execution-authorization model, missing workspace model, missing shared-object model), and the M1–M7 module plan.
1395
- - `docs/COLLAB/M1-MAINTENANCE.md` — v1.3.2-beta design: signed maintenance requests executed by a root/SYSTEM unit through a queue
1396
- directory; remote update/restart/diag without SSH and without widening the allow-list.
1397
- - `docs/COLLAB/M2-TASK-GRANT.md` — v1.3.3-beta design: operator-signed per-execution grants (argv hash, machine ids, TTL, task binding),
1398
- agent-side verification, ledger audit, revocation; the allow-list stops being the only execution authority.
1399
- - `docs/COLLAB/M3-GIT-NATIVE-TASKS.md` — v1.3.4-beta design: per-task git worktrees, patch artifacts as the transportable result, a
1400
- serialized merge queue, and MERGE_CONFLICT as a first-class outcome.
1401
- - `docs/COLLAB/M4-PROJECT-ROOM.md` — v1.3.5-beta design: relay-side project objects (members, board, artifacts, memory, decisions,
1402
- refs), snapshot + incremental event protocol, role-based permissions.
1403
- - `docs/COLLAB/M5-MULTI-CLIENT-PRIMITIVES.md` — v1.3.6-beta design: branch/file locks, review tasks with findings and merge gating,
1404
- and notifications with delivery guarantees and read watermarks.
1405
- - `docs/COLLAB/M6-CROSS-NETWORK-RELIABILITY.md` — v1.3.7-beta design: relay as the authoritative channel, chunked/resumable blobs,
1406
- agent result outbox, and P2P demoted to opportunistic acceleration.
1407
- - `docs/COLLAB/M7-METRICS-ACCEPTANCE.md` — v1.3.8-beta design: every capability must be triggerable by a real task, negative tests are
1408
- mandatory, metrics are computed by the relay, and evidence is checked by pre-flight.
1409
-
1410
- ### What is explicitly not in this release
1411
-
1412
- No code for M1–M7. The three model-level designs (v1.4.0 execution-authorization, v1.4.1 task, v1.4.2 collaboration) are not written yet.
1413
- The next stable release that changes behaviour will be the one that implements M1 + M2 (the remote-operations loop), with real-machine
1414
- acceptance evidence recorded as described in M7.
1415
-
1416
- Verified: gate GREEN twice on this tree; the package differs from 1.3.1 only in documentation and version metadata.
1417
-
1418
- ## [1.3.1] — 2026-10-21
1419
-
1420
- **A red Compat and a green local gate: the loader-entry test was the gap.**
1421
-
1422
- v1.3.0 shipped with a failing Compat job: `test/loader-entry.test.mjs` still listed the fifteen tools it expected the packed
1423
- artifact to register, and the artifact registers sixteen (`w2m_reduce`). The gate ran from the tree, where that test's expectation
1424
- matched the *other* fixture it derives from, so the disagreement only appeared in the packed-copy mount. The expectation now names
1425
- `w2m_reduce`, and this patch release is the artifact that matches it.
1426
-
1427
- No behaviour changes: the fix is the expectation, not the tool surface. beta.2 completes the fix: `EXPECTED_TOOLS` in that same test is the list the loader gate compares against, and beta.1 patched only the other fixture. v1.3.0 is superseded.
1428
-
1429
- This is the stable fix for v1.3.0's red Compat: the loader gate and the packed artifact now agree at sixteen tools. v1.3.0 is superseded on every channel; the artifact behaviour is unchanged. Verified: gate GREEN twice (2710 tests / 2696 pass / 0 fail / 14 skip in 133 files), and Compat on this tag is green before it is called stable.
1430
-
1431
- ## [1.3.0] — 2026-10-21
1432
-
1433
- The first stable release of the collaboration line. It consolidates the four betas that followed 1.2.7 and changes no protocol in a
1434
- way that a 1.2.7 peer cannot ignore; every addition is additive and named.
1435
-
1436
- ### Data plane
1437
-
1438
- **A reduce phase you can actually run.** `w2m_reduce` dispatches a reduce task only from complete data: it refuses unless every
1439
- declared shard reported as a blob and the named reducer reported (`W2M_REDUCE_NOT_READY` with the missing indexes), never picks a
1440
- reducer, and refuses inline inputs that cannot be carried to another machine. The relay validates, stores, ledgers and offers
1441
- `reduce_of` + `reduce_inputs`; the agent fetches those content-addressed blobs before the reduce runs and exposes
1442
- `W2M_REDUCE_INPUTS_DIR` / `W2M_REDUCE_MANIFEST` / `W2M_REDUCE_OF`; a failed fetch refuses the whole task rather than reducing a
1443
- partial fleet.
1444
-
1445
- **A suggestion that dispenses no action.** `w2m_wait` carries `reduce_suggestion`: ready only when every declared shard reported and
1446
- a reducer was named, the missing indexes otherwise, the candidate machines, and the literal next step. It is derived, never stored,
1447
- and it dispatches nothing.
1448
-
1449
- **Message ids survive a restart, artifacts are one size everywhere.** Chat ids carry a random per-process relay epoch
1450
- (`msg-<epoch>-<seq>`) so a restart cannot recycle an id a client persisted; the blob transport frame is 16 MiB, matching the agent's
1451
- dataflow hard cap, instead of 1 MiB against it.
1452
-
1453
- ### Collaboration
1454
-
1455
- **The panel shows the scheduler's own buckets.** The fleet-chat panel renders 准备 / 进行 / 等待 / 阻塞 counts beside the relay's task
1456
- list, derived from the relay's `status` and `waiting_on`; read-only, no dispatch.
1457
-
1458
- **Conflicts name their next step.** `w2m_memory append` returns `merge_hint` with a 409: `re-read-then-append`, `behind_by`, the
1459
- unseen count, the sample size and the literal instruction to re-read, compare and append against the current revision -- never an
1460
- automatic merge.
1461
-
1462
- **Decisions leave a trail.** `w2m_council fleet-decide` reports `trail` (`appended` / `refused` / `error` / `not-written`), and the
1463
- test asserts that status instead of implying a record exists.
1464
-
1465
- ### Verified
1466
-
1467
- Gate GREEN twice, 15 of 15: 2710 tests / 2696 pass / 0 fail / 14 skip in 133 files. Two-machine live runs over the real relay cover
1468
- the turn state machine (ask/await over SSE with a refusal), dependency scheduling, and the missing-shard refusal, plus the panel and
1469
- proxy behaviour above.
1470
-
1471
- ### Known limits, stated
1472
-
1473
- - `pipeline` keeps `index_total` at 1; blob map results come from plain commands above the inline cap.
1474
- - The reduce suggestion is advisory: the caller dispatches the reduce (this release does not let the relay decide to run a command).
1475
- - The composer panel does not render the board buckets on a host without the Connection service; the tools still work.
1476
-
1477
- ## [1.2.11-beta.1] — 2026-10-21
1478
-
1479
- A beta by policy: GitHub prerelease only, never npm.
1480
-
1481
- ### The task board in the composer panel
1482
-
1483
- The fleet-chat panel already listed the relay's tasks; it now also shows the four buckets the scheduler uses — 准备 / 进行 / 等待 / 阻塞 — derived from the relay's own `status` and `waiting_on`, never from a classification of its own. Read-only: the panel dispatches nothing.
1484
-
1485
- ### Merge hint on memory conflicts
1486
-
1487
- `w2m_memory append` returns `merge_hint` with a 409: state `re-read-then-append`, `behind_by`, the number of unseen records, the sample size, and the literal next step (re-read, compare, append against the current revision — do not merge unseen records). Pure function, no automatic merge.
1488
-
1489
- ### Council decisions leave a trail
1490
-
1491
- `w2m_council fleet-decide` reports `trail` (`appended` / `refused` / `error` / `not-written`) instead of implying a record exists; the test now asserts that status.
1492
-
1493
- Verified: gate GREEN twice, 2704 tests / 2690 pass / 0 fail / 14 skip in 132 files; client-bundle 25/25 after the board line; the merge-hint and trail tests are new.
1494
-
1495
- ## [1.2.10-beta.1] — 2026-10-21
1496
-
1497
- A beta by policy: GitHub prerelease only, never npm. This one makes the reduce phase executable.
1498
-
1499
- ### Reduce execution (caller-dispatched)
1500
-
1501
- `w2m_reduce` dispatches a reduce only from complete data: it refuses unless every declared shard reported as a blob and the named reducer reported (`W2M_REDUCE_NOT_READY`, with the missing indexes). It never picks a reducer and never invents inputs; inline results are refused (`W2M_REDUCE_INLINE_INPUT`) because they cannot be carried to another machine.
1502
-
1503
- The relay validates and carries `reduce_of` + `reduce_inputs` (at most 16 artifacts, sha256 only) on the task, the ledger and the offer; the agent fetches those content-addressed blobs BEFORE running the reduce and exposes `W2M_REDUCE_INPUTS_DIR` / `W2M_REDUCE_MANIFEST` / `W2M_REDUCE_OF` to the command. A failed fetch refuses the task (`REDUCE_INPUT_FETCH_FAILED`) rather than reducing half the fleet.
1504
-
1505
- Verified: gate GREEN twice, 2701 tests / 2687 pass / 0 fail / 14 skip in 131 files; new agent e2e proves the reduce reads the exact artifact bytes; the tool has a dispatch test and a refusal test.
1506
-
1507
- ## [1.2.9-beta.1] — 2026-10-21
1508
-
1509
- A beta by policy: **GitHub prerelease only, never npm.** The theme is the data plane and the collaboration plane: the design is
1510
- written down (`docs/DATA-PLANE-COLLABORATION.md`) and this release ships its first slice.
1511
-
1512
- ### `reduce_suggestion` on `w2m_wait`
1513
-
1514
- Every terminal wait now answers three questions without acting on them: is the data complete, who could reduce it, and what the
1515
- caller is supposed to do next (`dispatch the reduce yourself; this tool does not run it`). It is a pure function over the machine
1516
- facts the aggregate already publishes:
1517
-
1518
- - **ready** only when every declared shard reported and a reducer was named;
1519
- - **missing indexes** when a shard never reported (an expired lease cannot claim one);
1520
- - **candidates** listing the machines that reported, with the reason;
1521
- - a named reducer that never reported is refused by name (`REDUCER_UNREPORTED`) rather than silently replaced -- changing the
1522
- machine changes what the reduce runs against;
1523
- - a caller that named no reducer gets `REDUCER_MISSING` and candidates, never an invented choice.
1524
-
1525
- Nothing is stored and nothing is dispatched. Two readers of the same aggregate get the same suggestion, and reading does not
1526
- change relay state.
1527
-
1528
- ### The design document states the open question
1529
-
1530
- Caller-dispatched reduce (current direction) versus auto-reduce in the relay. The design leans caller-dispatched, because a relay
1531
- that decides to run a command is the boundary every other decision avoids; a beta that flipped that default would be the wrong
1532
- place to discover it. The document also names the next collaboration slices: conflict-merge UI, council decision trail to shared
1533
- memory, and the board in the composer panel.
1534
-
1535
- ### Verified
1536
-
1537
- Gate GREEN twice: 2699 tests / 2685 pass / 0 fail / 14 skip in 131 files. New: `suggestReduce` unit tests (7) covering complete,
1538
- partial-with-missing, no-reducer, unreported-reducer and mixed inline+blob inputs; `w2m_wait` carries the suggestion for every
1539
- aggregate.
1540
- ## [1.2.8-beta.1] — 2026-10-21
1541
-
1542
- A beta, by policy: **not published to npm**. It is a GitHub prerelease, installable by a developer who asks for it.
1543
-
1544
- ### Message ids are unique across a relay restart
1545
-
1546
- The relay's chat sequence resets on restart, so a bare `msg-000001` named a different message after every deploy. Clients persist
1547
- those ids -- an open turn, a read watermark, `reply_to` -- and live testing caught exactly that: a Windows `ask` recorded a turn, the
1548
- relay restarted, a new message reused the id, and the wait settled the OLD turn as `answered_after_timeout` while the reply was
1549
- fresh. Message ids now carry a random per-process epoch (`msg-<epoch>-<seq>`), so a restart cannot recycle an id and old clients that
1550
- render ids verbatim keep working.
1551
-
1552
- ### One size for "how large may one artifact be"
1553
-
1554
- The blob transport frame was 1 MiB while the agent's dataflow hard cap is 16 MiB, so an artifact between the two was accepted by the
1555
- agent and then failed its upload (`BLOB_UPLOAD_FAILED`) on a relay that looked fine. `BLOB_FRAME_BYTES` is now 16 MiB, matching the
1556
- agent, while every JSON route keeps the 64 KiB frame limit.
1557
-
1558
- ### Verified
1559
-
1560
- Gate GREEN twice: 2691 tests / 2677 pass / 0 fail / 14 skip in 130 files. New tests: an id is unique across two store instances
1561
- (same sequence, different process), an explicit epoch is honoured, and a 2 MiB blob survives the transport intact.
1562
-
1563
- ### Still open (unchanged from 1.2.7)
1564
-
1565
- - `pipeline` keeps `index_total` at 1; blob map results come from plain commands above the inline cap.
1566
- - macmini runs 1.2.6 from its own checkout and needs a `git pull` + restart for the plain-command blob behaviour.
1567
- ## [1.2.7] — 2026-10-21
1568
-
1569
- **The scheduler enforces the graph, a wait survives a restart and a dropped stream, and a partial fleet can no longer call itself
1570
- a complete reduce.**
1571
-
1572
- ### Turns are a state machine on disk
1573
-
1574
- A question written with `action:'ask'` becomes a persisted turn (`ask_id`, deadline). `await` attaches to that record, so a process
1575
- restart does not lose the question or turn a late answer into silence; a late answer is recorded as `answered_after_timeout`. The
1576
- wait prefers the relay's event stream, re-subscribes when the stream drops, and polls only when streaming is unavailable. A reply
1577
- now carries `reply_state: 'answered'|'refused'`, so a refusal is a state the waiter reads rather than wording it guesses.
1578
-
1579
- ### Dependencies block dispatch, and release when their prerequisite settles
1580
-
1581
- A task with `depends_on` is not offered: its leases are `waiting`, which redelivery skips and the sweeper ignores. When every
1582
- prerequisite is settled -- **by a result, an expiry or a refusal** -- `_releaseDependents` moves those leases to offers. The graph
1583
- edges and `assignee` are written into the task ledger, because live testing showed a restarted relay losing them and keeping a
1584
- waiting task waiting forever. `assignee` is enforced, not recorded: only that machine gets a lease, and an unknown assignee is
1585
- refused by name. `GET /v1/tasks` now publishes a derived `status` (`waiting`/`active`/`done`/`failed`/`cancelled`) and
1586
- `waiting_on`, so the board and the scheduler cannot disagree.
1587
-
1588
- ### Dataflow: an expected shard count, and a reduce that refuses partial input
1589
-
1590
- `buildReducePlan` takes `expectedIndexTotal` from the task (`index_total`) and refuses `MISSING_SHARD` (with the missing indexes),
1591
- `MAP_INDEX_MISSING` and `MAP_INDEX_OUT_OF_RANGE`; only machines that actually reported can claim a shard, so an expired lease no
1592
- longer makes a partial run look complete. Two production gaps were found by live testing and closed with it:
1593
-
1594
- - **A large plain-command stdout was never a blob.** The blob path lived only in the pipeline loop, so a split/mapreduce map result
1595
- was always inline and a reduce plan could never see what it reduces. Any command output above the inline cap now uploads a blob
1596
- exactly as a pipeline stage does (best effort: a failed upload keeps the command successful and says why).
1597
- - **The relay's own frame limit made the blob channel unusable.** `MAX_FRAME_BYTES` (64 KiB) equals the inline threshold, so every
1598
- blob was by construction larger than the frame cap and a real upload failed with `FRAME_TOO_LARGE` while the fake relay in tests
1599
- accepted it. Blob PUTs now get a bounded 1 MiB frame (`BLOB_FRAME_BYTES`); every JSON route keeps the 64 KiB cap.
1600
- - The aggregate now publishes `blob_sha256`, `output_mode`, `stdout_head` and `stdout_bytes` per machine: `w2m_wait` used to read
1601
- `aggregate.detail.machines`, a shape the production `/v1/tasks` response never carries, so the reduce plan was unreachable
1602
- outside tests.
1603
-
1604
- ### Verified
1605
-
1606
- Gate GREEN twice, 15 of 15: 2687 tests / 2673 pass / 0 fail / 14 skip in 130 files.
1607
-
1608
- Two-machine live runs on Windows (ToFuture) and jp-shared over the real relay:
1609
-
1610
- - **await over SSE**: Windows asks, jp-shared replies `reply_state:'refused'`; the wait returns `state=refused, via=stream`, and the
1611
- turn is settled on disk.
1612
- - **dependency chain**: B with `depends_on: [A]` is `waiting`, A settles, B is released (observed `status=active` with offers).
1613
- - **missing shard**: a split task with `index_total: 5` whose map results are blobs is refused as
1614
- `MISSING_SHARD {missing:[1,3,4], expectedIndexTotal:5}` with `dataflow_degraded: true` -- the first live exercise of the blob
1615
- channel end to end.
1616
-
1617
- Both live-testing defects above (release only on a result; graph edges missing from the ledger) were found by these runs and are
1618
- covered by tests now.
1619
-
1620
- ### Limits, stated
1621
-
1622
- - A pipeline task runs the whole chain per machine, so `index_total` stays 1 for it; blob map results come from plain commands above
1623
- the inline cap.
1624
- - The blob transport frame is 1 MiB while the agent's dataflow hard cap is 16 MiB: a single artifact above 1 MiB fails its upload
1625
- explicitly (`BLOB_UPLOAD_FAILED`) rather than being split across frames.
1626
- - The jp-shared agent was upgraded to this build; macmini runs 1.2.6 from its own checkout and needs a `git pull` + restart to get
1627
- the plain-command blob behaviour.
1628
- ## [1.2.6] — 2026-10-20
1629
-
1630
- **Security surface: signatures, a permission matrix, and output that no longer carries secrets — plus the reliability the chat
1631
- needed before it could be trusted with real messages.**
1632
-
1633
- ### Command signatures
1634
-
1635
- The agent signs `{task_id, argv_hash, machine_id, ts}` with an Ed25519 key derived from its device key (`DeviceKey` is X25519 and
1636
- cannot sign; deriving keeps one secret per machine). The envelope carries `signature {alg, value, public_key, ts}`, and
1637
- `w2m_wait` reports every machine as `signed`, `unsigned` or `bad_signature`, with a task-level `signature_warning`. A tampered
1638
- `argv_hash` verifies as `bad_signature` -- tested at the unit level and end to end through a real agent run.
1639
-
1640
- Honest limit: the public half travels inside the signature. That proves the result matches the key it carries, not that the key
1641
- belongs to the machine. Binding it to the relay's device record is the remaining step for non-repudiation across machines, and it
1642
- is named here rather than implied.
1643
-
1644
- ### Permission matrix
1645
-
1646
- `src/util/policy-matrix.mjs` is the single table: `observer < worker < owner` against `read_private | vote | join | dispatch |
1647
- approve | manage_members`. A refusal is `FORBIDDEN_ACTION` naming the action, the level required and the level held. The relay
1648
- enforces it where a role actually acts: an observer may read the fleet flow and may not post into it. Unknown means worker, never
1649
- owner.
1650
-
1651
- ### Output redaction
1652
-
1653
- `stdout_head`/`tail` and `stderr_head`/`tail` are filtered for API keys, `npm_`/`github_pat_` tokens and private-key blocks,
1654
- replaced in place with `[redacted:class]` so line numbers survive. Hashes and blobs are never touched: they are evidence of the raw
1655
- bytes. `redactOutput: false` is allowed and marks the envelope `unredacted: true`.
1656
-
1657
- ### End-to-end encrypted chat
1658
-
1659
- Sending encrypts with AES-256-GCM under a group key derived as `HKDF(fleetKey, 'fleet-chat-v1')`; the relay stores ciphertext and
1660
- cannot open it. Reading decrypts what the key covers and marks the rest `encrypted: true` with a `decrypt_error`. Plaintext
1661
- messages keep working, so older clients neither break nor are lied to. The relay validates the encryption envelope's shape and
1662
- refuses a malformed one (`BAD_ENCRYPTION`), and cannot search content -- which is the point, and is written down as the cost.
1663
-
1664
- ### Reliability
1665
-
1666
- - **Role persistence**: `member.role` ledger entries replay on restart, so a relay restart no longer silently demotes every owner.
1667
- Verified live: the role that was lost before the deploy came back after it.
1668
- - **Chat cursor**: `list` continues where the last page ended (`since_ms` from a persisted per-session cursor) and advances only
1669
- over what was actually read.
1670
- - **Offline spool**: a send that cannot reach the relay is written to disk with an idempotency key and replayed on the next call;
1671
- a replay that the relay already received returns the first message instead of posting twice.
1672
-
1673
- ### Verified
1674
-
1675
- Gate GREEN twice, 15 of 15: 2666 tests / 2652 pass / 0 fail / 14 skip in 130 files. New suites: chat-crypto (8),
1676
- envelope-signature (6), policy-matrix (9); agent 152, relay 173, tools 132, fleet-messages 14.
1677
-
1678
- Relay redeployed twice from hash-verified tarballs, with state backups, and the new surfaces exercised against the live relay:
1679
- encrypted message stored as ciphertext and read back, malformed encryption refused with 400, roles restored after restart.
1680
-
1681
- ### Known gaps
1682
-
1683
- - The GitHub Packages mirror still holds an orphan `1.2.4` published by a diagnostic dispatch; removal needs a token with package
1684
- administration (this one sees 404 on the package API).
1685
- - The signing public key is not yet bound to the relay's device record (see above).
1686
- - Two-machine end-to-end runs of the new chat surface are still not done; the HTTP and tool surfaces are tested against fakes and
1687
- exercised once against jp-shared.
1688
- ## [1.2.5] — 2026-10-20
1689
-
1690
- **Who is in the fleet, what they have read, who was named, and a retry that cannot double-post.**
1691
-
1692
- ### Members and roles
1693
-
1694
- `GET /v1/fleet/members` lists every machine with its name, role, join time, presence and last-seen time. `PUT
1695
- /v1/fleet/members/{id}` changes a role and requires the **operator** token, so the route sits with dispatch rather than behind the
1696
- device gate. Roles are `owner | worker | observer` and live in their own table: a device record is written by pairing and by
1697
- reconnect, and a role is an operator decision that must survive both. An unknown machine is a `worker`, never an `owner` -- a
1698
- re-pair cannot gain authority. The last owner cannot be demoted, because then nobody could readmit a machine.
1699
-
1700
- ### Read watermarks
1701
-
1702
- `PUT /v1/fleet/read/{session}` records how far this machine has read a conversation (its own token decides which machine);
1703
- `GET /v1/fleet/read/{session}` answers the watermark and an unread count of messages from others newer than it. Only forward: a
1704
- stale marker arriving late must not move the watermark backwards, or the count would grow for messages already seen.
1705
-
1706
- ### Mentions
1707
-
1708
- A message may carry `mentions: [machine_id]` (at most 10, validated). The row is still **one** row -- a mention is a
1709
- notification hint, not a delivery mechanism -- and the SSE notification says only whether **this** listener was named. It never
1710
- carries the text, and it does not carry the mention list.
1711
-
1712
- ### Idempotent posts
1713
-
1714
- `POST /v1/fleet/messages` accepts `idempotency_key` (per session). A retry after a lost response returns the **first** message
1715
- with `deduped: true` instead of storing the sentence twice. The key is bounded, like the message log.
1716
-
1717
- ### Two defects the 1.2.4 tag carried, fixed here
1718
-
1719
- The 1.2.4 commit (tag `v1.2.4`) had a red CI and a red Compat, both found before approval:
1720
-
1721
- - **Compat**: `w2m_wait`'s `reducer_machine_id` declaration had been pasted **inside** `poll_ms`. Every local check was green;
1722
- the real host refused the whole entry ("parameters.poll_ms.reducer_machine_id is not supported by the value schema DSL"). The
1723
- parameter now sits where it belongs, and `schema-spec.mjs` gained `strayParameterProblems()` — a parameter declaration
1724
- nested inside another parameter is now refused where the tool is written, with two tests.
1725
- - **CI**: the cancellation HTTP test had been inserted inside an enclosing `it`. Node 24 ran it; Node 20/22 reported
1726
- `cancelledByParent`. The test now lives in its own top-level `describe`.
1727
-
1728
- ### Verified
1729
-
1730
- Gate GREEN twice, 15 of 15: 2628 tests / 2614 pass / 0 fail / 14 skip. New suites: fleet-members (6); fleet-messages grew to 11;
1731
- relay grew to 171 including two HTTP cases for members and watermarks.
1732
-
1733
- The relay was deployed a fourth time from a hash-verified tarball with a state backup, and the new routes were exercised against
1734
- the live relay (members returned 8 machines; a role change returned 200; a watermark round-tripped; unread answered 0).
1735
-
1736
- ### Not in this release
1737
-
1738
- - **Offline replay of outbound chat** is not implemented. The agent's spool already holds undeliverable *results*; chat text is
1739
- not yet spooled and replayed.
1740
- - A two-machine end-to-end run of mentions/watermarks. The HTTP surface is tested against the fake relay and exercised once
1741
- against jp-shared, but not by two physical machines.
1742
- ## [1.2.4] — 2026-10-20
1743
-
1744
- **Fleet collaboration, part two: cancellation, dependencies, a task board, questions on disk, and output that travels.**
1745
-
1746
- ### Cancelling a task over HTTP
1747
-
1748
- `state.cancelTask` has existed since the task ledger gained a cancel flag, but **no route reached it**. `POST /v1/tasks/{id}/cancel`
1749
- now does, with the same operator authorization dispatch requires, and `w2m_cancel` is the tool over it. Cancelling twice is not
1750
- an error: the ledger is the answer.
1751
-
1752
- ### Prerequisites and a task board
1753
-
1754
- - `POST /v1/task` accepts `depends_on[]`. Validated at **enqueue** time: unknown prerequisite, self-edge, duplicate edge and
1755
- cycles are refused by name (`src/relay/task-deps.mjs`), because a graph checked only when a worker picks the task up puts the
1756
- refusal on a machine that did not create the problem.
1757
- - `depends_on` / `assignee` travel on the task record.
1758
- - `w2m_tasks` (the fifteenth tool) is the board: it classifies every visible task as ready / active / waiting / blocked / done
1759
- (`src/plugin/task-graph.mjs`). A prerequisite outside the page is treated as **waiting**, never as absent.
1760
-
1761
- ### Questions that survive a restart
1762
-
1763
- `src/plugin/ask-store.mjs` keeps open questions in a JSONL file next to the profile (`ask-store.jsonl`, atomic rewrite, cap on
1764
- records). A reply that arrives after the deadline is recorded as `answered_after_timeout` rather than overwriting the timeout or
1765
- being dropped, and a settled question is never reopened. `w2m_chat` gained `action: "asks"`, and `await` settles the stored ask.
1766
-
1767
- ### Output that travels
1768
-
1769
- `src/util/dataflow.mjs` (moved from `src/plugin/` — the layer check refused an `agent -> plugin` edge) is now wired:
1770
-
1771
- - a pipeline stage whose stdout exceeds 64 KiB **uploads it as a blob** (`PUT /v1/fleet/blobs/{sha256}`) and the envelope names
1772
- it (`blob_sha256`, `output_mode: "blob"`) instead of inlining head/tail;
1773
- - a failed upload **fails the stage** by name (`BLOB_UPLOAD_FAILED`) rather than leaving a reference to nothing;
1774
- - `w2m_wait` builds the reduce plan with `buildReducePlan` from the **raw** envelopes and marks the verdict `degraded` when a
1775
- shard is missing — but only when the caller declared `reducer_machine_id`, so a large pipeline result is not mistaken for a
1776
- partial reduce.
1777
-
1778
- The end-to-end fixture this release adds (`test/agent.test.mjs`) runs a real agent against a fake relay: it asserts the blob name
1779
- is the hash of the bytes sent and that a refused upload fails the stage. Two real defects surfaced while writing it and are fixed
1780
- here: the stage stdout arrives as a `Buffer` (it was handed to the signature helper as a string), and a 70 KiB test argv exceeds
1781
- Windows' `CreateProcess` limit.
1782
-
1783
- ### Retry and hand-off
1784
-
1785
- - `POST /v1/task` accepts `max_attempts` (1..5, default 1) and `retry_on` (`expired|failed|partial|timeout`). Defaults keep
1786
- at-most-once for every existing caller; a malformed policy is `BAD_REQUEST`, not silently ignored.
1787
- - On lease expiry, `retry_on` containing `expired` performs the hand-off `halt=now` always had: attempt+1 and a re-offer to idle
1788
- machines (`_takeover`, which already refuses writing tasks to avoid double application).
1789
-
1790
- ### mapreduce
1791
-
1792
- `mode: "mapreduce"` is accepted with `reduce_argv` and an optional `reducer_machine_id`. The map half is ordinary dispatch; the
1793
- reduce half is **described, never launched** — the caller runs it as an explicit second task after `w2m_wait` says every shard is
1794
- present, so one task id never describes two executions.
1795
-
1796
- ### Relay
1797
-
1798
- The relay was deployed three times during this release (cancel route, retry policy, mapreduce) from hash-verified tarballs, each
1799
- with a state backup, a health check, and a 401 check on the pre-existing routes. **The relay name is separate from the package
1800
- version**: the tarball deployed was the 1.2.3-labelled build carrying this code, and is re-deployed from the released 1.2.4 tarball
1801
- below.
1802
-
1803
- ### Verified
1804
-
1805
- Gate GREEN twice, 15 of 15: 2610 tests / 2596 pass / 0 fail / 14 skip. Notable suites: agent 149 (147 pass, 2 skipped),
1806
- relay 169, tools 127, task-deps 13, task-graph 11, ask-store 8, turn 5, dataflow 7, dataflow-wiring 7.
1807
-
1808
- ### Not verified
1809
-
1810
- A two-machine run of cancel / retry / mapreduce end to end. The code paths are tested against a fake relay and against the real
1811
- relay's HTTP surface, but no test here runs two physical machines exchanging an expired lease.
269
+ ## [1.7.8] — 2026-10-26
270
+
271
+ **GPW 启用失败修复(第二个):`TypeError: Cannot read properties of undefined (reading 'layers')`。**
272
+
273
+ - 根因:v1.7.7 为做黑名单同步拒绝,在**激活期**调用根上下文的 `ctx.tools.guard()`。tools 服务是**按 agent 组合**的,
274
+ 根作用域实例在激活阶段还没有 layer 表,于是抛 `reading 'layers'`,dsh-gpw 仍无法激活。
275
+ - 修复:改为在 `agent/created` 监听里对**每个 agent 自己的上下文** `agent.ctx.tools.guard(...)` 注册黑名单守卫,
276
+ 并用 `ctx.effect` 统一释放;根上下文不再读取 tools 服务(`inject` 回到只有 `permissionPresets`)。
277
+ - 保留 `tools/pre-execute` 三分类拦截;guard 缺失的宿主不会因此激活失败(pre-execute 仍会拒绝黑名单)。
278
+ - 回归测试升级:`test/gpw-activation.test.mjs` 现在用严格 Cordis 语义 Proxy(读取根 `tools` 即抛错)验证激活,
279
+ 并通过伪造 `agent/created` 断言守卫注册在 agent 上下文。
280
+ - 同时补全 bundle patch 中 GPW v2 的配置项(requireFullAccess / whitelist / blacklist / pendingTimeoutMs)。
281
+ - 证据:`docs/acceptance/1.7.8/GPW-AGENT-SCOPE.json`。
282
+ ## [1.7.7] — 2026-10-26
283
+
284
+ **GPW 子插件启用失败修复:`cannot get property "tools" without inject`。**
285
+
286
+ - 根因:`gpw.mjs` 读取 `ctx.tools.guard` 做黑名单同步拦截,但 `inject` 只声明了 `permissionPresets`。
287
+ Cordis 对未声明服务读取直接抛错,插件激活因此失败(组件启用失败:1 entry did not activate dsh-gpw)。
288
+ - 修复:`inject = ['permissionPresets', 'tools']`;guard 读取改为防御式(老宿主无 guard 扩展时不影响
289
+ `tools/pre-execute` 拦截)。
290
+ - 新增回归测试 `test/gpw-activation.test.mjs`:用严格 Cordis 语义的 Proxy 上下文调用 `apply()`,
291
+ 未声明服务读取即抛错;断言 `tools/pre-execute` 监听已安装。
292
+ - 证据:`docs/acceptance/1.7.7/GPW-INJECT.json`。
293
+ ## [1.7.6] — 2026-10-25
294
+
295
+ **审计修复版:测试零残留工具链 + 修复泄漏的测试夹具,并固化 v1.6.1→v1.7.4 审计报告。**
296
+
297
+ - 新增 `scripts/temp-residue.mjs`(`snapshot` / `report` / `clean`):对用户 TEMP 中 `w2m-*`、`gpw-*`
298
+ 测试残留做基线快照、前缀统计与按年龄的安全清理;审计实测清理了 15,608 个历史残留目录。
299
+ - 新增 `test/helpers/temp-cleanup.mjs`:基线式 per-test 清理,避免并发测试互删夹具。
300
+ - 修复会泄漏临时目录的测试:`gpw-folder-policy`、`agent`(workspace 两处 fixture)、`worktree`(makeRepo)。
301
+ - 零残留复测:单轮全量测试新增残留从约 1,000 个降至 74 个(剩余前缀已量化,作为下一轮清理清单)。
302
+ - 新增审计报告 `docs/audit/AUDIT-1.6.1-1.7.4.md`:14 个正式版的 Release/CI/Compat 全绿记录、
303
+ 2930 个测试 0 失败、lint 0 error、实机部署与落库断言、relay 延迟与测试耗时实测。
304
+ - 证据:`docs/acceptance/1.7.6/AUDIT-ZERO-RESIDUE.json`。
305
+ ## [1.7.5-beta.1] — 2026-10-25
306
+
307
+ **GPW 升级为文件夹三分类网关(完全权限下生效):黑名单拒绝 / 白名单放行 / 其余进入 GPW 自己的待审批队列。**
308
+
309
+ - 生效条件:仅当会话权限预设为 **完全权限**(`danger-full-access`)时拦截;其它预设交给 DSH 自己的沙箱/审批,
310
+ 不重复介入。
311
+ - **黑名单(拒绝)**:`ctx.tools.guard()` 同步拦截,返回拒绝原因即不执行;与 `tools/pre-execute` 的监听顺序无关。
312
+ - **白名单(通过)**:等价于扩大的工作区,无需再审;仍受自动审批预算约束(每滚动窗口 10 次、间隔 3s),
313
+ 预算耗尽后同样进入待审批队列而不是静默放行。
314
+ - **其它文件夹(C 类,待审批)**:调用被挂起,进入 GPW 自己的待审批队列;用户在 GPW 面板点「允许本次/拒绝本次」
315
+ 才继续(默认 120s 无答复则拒绝)。这不是 DSH 的 approval 策略,所以在 approval=never 的完全权限下依然有效。
316
+ - 面板(Node 执行能力菜单内新增「GPW 文件夹策略」):白/黑名单展示与移除;「添加文件夹」通过宿主原生选择器
317
+ (Windows FolderBrowserDialog / macOS choose folder / zenity)选路径,读写名单不需要编辑任何 profile YAML;
318
+ 名单保存在 `$DSH_HOME/xclient/gpw-folders.json`,另有待审批列表与允许/拒绝按钮。
319
+ - 测试:`test/gpw-folder-policy.test.mjs`(6/6,三分类、deny 优先、前缀误判防护、命令路径提取、预算映射)。
320
+ ## [1.7.4] — 2026-10-25
321
+
322
+ **修复两处桌面宿主兼容性问题:`agent.identity` 崩溃、切换 relay 后旧 device token 导致 401 无法入队。**
323
+
324
+ - 崩溃修复:`agent.identity?.()` 在桌面宿主上抛 `TypeError: agent.identity is not a function`,
325
+ 且发生在 `agent.start().then(...)` 回调里变成未处理 rejection,把整个 DSH 宿主带崩。
326
+ 新增 `agentIdentityOf()` 做类型判断,`agent.start()` 仍为非阻塞并记录失败。
327
+ - 迁移修复:relay 切换到新主 relay 后,`device.json` 里的 token 属于旧 relay,新 relay 回
328
+ `401 UNAUTHORIZED: missing or invalid device_token`;启动逻辑看到已有 token 就跳过配对,
329
+ 导致机器停在"无列表"。现在会先用 token 探测配置的 relay:**明确 401/403 才丢弃并重新配对**,
330
+ 网络错误保留 token(不会因短暂故障误触发配对)。
331
+ - 新增回归测试:`test/agent-identity-safe.test.mjs`、`test/re-pair-stale-token.test.mjs`。
332
+ - 证据:`docs/acceptance/1.7.4/DESKTOP-HOST-COMPAT.json`(含真实崩溃信息与迁移症状)。
333
+ ## [1.7.3] — 2026-10-25
334
+
335
+ **截图提示改为顶部浮层(5 秒自动消失、可关闭、不挤压输入框);全服计数标注来源 relay 并单列本编队。**
336
+
337
+ - 截图成功/失败提示不再插在输入区里:改为聊天面板顶部浮层(绝对定位,不占布局;连续截图只替换同一行,
338
+ 不会越堆越多),**5 秒自动消失**,右侧 × 可立即关闭。
339
+ - 全服统计行改为「主 relay(地址)全服:X/Y 个编队 · A/B 台在线」,并新增一行「本编队:n/m 台在线」;
340
+ 两者语义不同,不再混在一起看。
341
+ - relay 角色判定改为按**配置的主 relay**(不再按响应最快,备用 relay 偶然更快时会被误标成 primary)。
342
+ - 证据:`docs/acceptance/1.7.3/TOAST-AND-TOTALS.json`(fleet task `01M5ANYMVMSGGQV`;宿主截图 141,768 字节)。
343
+ ## [1.7.2] — 2026-10-25
344
+
345
+ **按实测反馈调整编队聊天截图/复制:只复制不预览、快捷键 Ctrl+Shift+P、强制可选中。**
346
+
347
+ - 截图按钮不再在面板里生成缩略图/下载行:抓到画面后**只写入剪贴板**并给一行提示,聊天区域保持干净。
348
+ - 快捷键从 Ctrl+P 改为 **Ctrl+Shift+P(macOS 也支持 Cmd+Shift+P)**;普通 Ctrl+P 不再被劫持,
349
+ 浏览器/应用的打印快捷键恢复原样。
350
+ - 选中&复制:`user-select:text!important` 应用到整个面板的所有子节点(不再只覆盖几个类),
351
+ 并在消息列表上阻止宿主吞掉 mousedown/copy 事件,保证拖选与 Ctrl+C 生效;单条消息的 ⧉ 复制按钮
352
+ 仍走「浏览器剪贴板 → 宿主剪贴板」回退链。
353
+ - 全服统计只认**配置的主 relay**:主 relay 不可达时不再把备用 relay 的数字(1/1 编队、2/5 在线)当"全服"显示,
354
+ 而是显示「主 relay 不可达 · 全服统计不可用」。修复了在线编队/在线机器数量"错误"的观感来源。
355
+ - 证据:`docs/acceptance/1.7.2/CHAT-CLIPBOARD-SHORTCUT.json`(fleet task `01M5ANYMVMR3IOB`;
356
+ 宿主截图实测 159,561 字节并复制成功)。
357
+ ## [1.7.1] — 2026-10-25
358
+
359
+ **修复桌面壳下聊天截图/复制失败(permission denied)与插件更新"no supported installer available"。**
360
+
361
+ - 截图:浏览器 `getDisplayMedia` 在 DSH 桌面(Electron)被拒绝(`permission denied`)。新增**宿主侧截图**
362
+ `POST /api/w2m/capture`(Windows PowerShell System.Drawing;macOS screencapture;返回 JPEG base64),
363
+ 按钮与 Ctrl+P 在渲染层捕获失败时自动回退到宿主截图,并在宿主侧复制到剪贴板。
364
+ - 复制:`navigator.clipboard` 被拒时新增**宿主剪贴板** `POST /api/w2m/clipboard`(Windows Set-Clipboard /
365
+ macOS osascript),单条消息复制按钮与复制文本都走这条回退链。
366
+ - 选中:transcript 的 `user-select:text` 改为 `!important`,避免宿主全局样式覆盖导致无法选中。
367
+ - 插件更新:`update-install` 的安装器解析新增桌面运行时布局
368
+ (`<app>/resources/runtime/<runtime>/dependencies/pnpm/bin/pnpm.mjs`、`~/.dsh/dsh-runtimes` 兜底),
369
+ 并用运行时自带的 `node` 执行 pnpm(桌面壳的 `process.execPath` 不是 Node)。修复了更新按钮
370
+ "install update: no supported installer available"。
371
+
372
+ - 测试:capture-route 4/4、fleet-chat-capture 3/3、client-bundle 26/26、update-resolver 4/4;
373
+ 证据 `docs/acceptance/1.7.1/DESKTOP-CAPTURE-UPDATER.json`(fleet task `01M5ANYMVMN9CNQ`)。
374
+ ## [1.7.0] — 2026-10-25
375
+
376
+ **编队聊天框:截图按钮 + Ctrl+P 快捷键 + 对话内容可选中&复制。**
377
+
378
+ - 输入区新增 **📷 截图按钮**:`getDisplayMedia` 让用户选屏幕/窗口 → 抓帧为 PNG → 自动复制到剪贴板,
379
+ 面板内显示缩略预览(可下载/再次复制);宿主不支持或权限被拒时在面板内显示原因,不抛未处理异常。
380
+ - **Ctrl+P(macOS 也支持 Cmd+P)**:聊天面板打开时触发同一截图流程,并在捕获阶段 `preventDefault`,
381
+ 不会弹出浏览器打印对话框。
382
+ - **对话内容可选中&复制**:transcript/消息文本 `user-select:text`;每条消息新增 **⧉ 复制** 按钮;
383
+ 划选文字时点击消息不会误触发"回复"。
384
+
385
+ 已知边界:编队聊天协议仍是纯文本(relay 消息不含图片字段),所以截图是"复制到剪贴板 + 预览/下载",
386
+ 不会作为图片消息广播给其他机器——图片传输需要扩展 relay 消息协议(后续版本)。
387
+
388
+ - 测试:`test/fleet-chat-capture.test.mjs` 3/3;client-bundle 26/26;
389
+ 证据 `docs/acceptance/1.7.0/CHAT-CAPTURE-CONTROLS.json`(fleet task `01M5ANYMVMLRC5H`)。
390
+ ## [1.6.9] — 2026-10-24
391
+
392
+ **修复 failover 监控从未安装的根因:`startLocalStack` 等待了永不 settle 的 `agent.start()`。**
393
+
394
+ 阶段日志(macOS,1.6.8,现场实测):
395
+
396
+ ```
397
+ W wrap enter relayUrls=[腾讯云, Vultr] ← 配置正确
398
+ A startLocalStack enter
399
+ C createAgent done
400
+ S before agent.start ← 之后没有任何日志
401
+ ```
402
+
403
+ agent 实际已上线(编队在线),但 `await agent.start()` 的 Promise 永不返回,导致 `startLocalStack` 不返回 →
404
+ `startFailoverStack` 永远走不到 `relayPool(cfg)` / `setInterval`,**监控循环从未创建**,自动 failover 不可能触发。
405
+
406
+ 修复:
407
+
408
+ - `startLocalStack` 不再 await `agent.start()`:后台启动,失败写日志(`w2m: agent start failed`)。
409
+ - mount 只需要返回清理句柄,不需要"观察启动完成";agent 会通过 relay 自报在线。
410
+ - 新增回归测试 `test/agent-start-nonblocking.test.mjs`:读取源码,若 `startLocalStack` 再次 await
411
+ `agent.start()` 即失败(防止该阻塞回归)。
412
+ - 证据:`docs/acceptance/1.6.9/FAILOVER-MONITOR-INSTALL.json`(fleet task `01M5ANYMVMKL55O`)。
413
+ ## [1.6.8] — 2026-10-24
414
+
415
+ **插件面板新增「检查插件更新」按钮:npm 对比 → 自动更新 → 提示重启 DSH。**
416
+
417
+ - 客户端:目标机器菜单 relay 区新增按钮。点击后:① GET `/api/w2m/plugin-update` 联网查 npm latest
418
+ 并与已装版本比较;② 有新版时 POST `{action:"apply"}` 执行安装;③ 安装完成显示"请重启 DSH 以载入最新插件"。
419
+ - 宿主:新增路由 `/api/w2m/plugin-update`。检查逻辑在 `src/plugin/npm-update.mjs`(纯函数,可单测:只认
420
+ `latest > installed`,网络失败是错误值而不是"已最新")。
421
+ - 安装复用**既有已验证更新器**(`auto-update.mjs` → `update-install.mjs`):下载 tarball、用 release
422
+ SHA256SUMS 校验 SHA-256、备份旧包、替换新版——按钮点击不会引入第二套弱校验安装路径。
423
+ - 测试:npm-update 4/4、client-bundle 26/26;证据 `docs/acceptance/1.6.8/PLUGIN-UPDATE-BUTTON.json`
424
+ (fleet task `01M5ANYMVMI9EHA`)。
425
+ - 边界:DSH 插件管理页的「卸载」按钮属于宿主应用(打包在 app.asar 内),插件包无法直接注入其旁边;
426
+ 本按钮位于 w2m 插件自己的面板中。若要放进宿主插件页,需要 DSH 侧提供插件动作扩展点(已列为待评估)。
427
+ ## [1.6.7] — 2026-10-24
428
+
429
+ **修复 v1.6.6 failover 的阻塞 bug:远端 relay 模式下清理函数抛错,切换后无法重启 stack。**
430
+
431
+ 现场演练(停主 relay 180s)暴露:机器掉线后没有切到备用,恢复主 relay 后才重新上线。根因:
432
+
433
+ - `startLocalStack` 的清理函数无条件执行 `await relay.close()`;而 `relay` 只在"本进程自建本地 relay"
434
+ 时才非空,远端 relay 模式下是 `null` → 抛 TypeError;
435
+ - failover 的切换流程 `await stop()` 抛错后被 catch 吞掉,**后续的 restart 从未执行**(stack 停了没起来)。
436
+
437
+ 修复:
438
+ - 清理函数 null-safe/idempotent:仅当 `relay` 是带 `close()` 的对象才调用;`agent.stop()` 失败也继续清理。
439
+ - 切换流程:`stop()` 失败不再跳过 restart;并写诊断日志 `xclient/relay-failover.log`(每轮探测/切换/重启错误)。
440
+ - 测试:relay-failover 5/5、relay 200/200;证据 `docs/acceptance/1.6.7/FAILOVER-CLEANUP-FIX.json`
441
+ (fleet task `01M5ANYMVMFSSP3`)。
442
+ ## [1.6.6] — 2026-10-24
443
+
444
+ **relay 自动 failover:主 relay 故障时自动切到备用,恢复后自动切回。**
445
+
446
+ 现场事故(2026-10-24):主 relay 主机整体不可达(8787 与 SSH 22 都超时),备用 relay 一直健康,
447
+ 但每台机器仍死连主 relay——备用链路只是常驻成员,不会接管,编队停摆。
448
+
449
+ - 新增 `src/plugin/relay-failover.mjs`:非对称滞回状态机(**连续 3 次**探测失败才切走,**连续 5 次**
450
+ 健康才切回;没有可达备选则不切)。纯函数,边界可单测。
451
+ - `startLocalStack` 外包一层 `startFailoverStack`:每 10s 探测池内各 relay `/healthz`;
452
+ 决策为切换时,停止当前 in-process stack,把 `cfg.rabbitUrl` 设为备 relay 并重启 stack——
453
+ **切换是重新入队(fleet key 重新配对),不是重连**(device token 是按 relay 签发的)。
454
+ - `w2m_status` 新增 `failover` 快照:primary/active/pool、失败计数、切换次数、最近切换原因与历史。
455
+ - 测试:relay-failover 5/5、relay 200/200;证据 `docs/acceptance/1.6.6/RELAY-FAILOVER.json`
456
+ (fleet task `01M5ANYMVM9GEKQ`)。
457
+ ## [1.6.5] — 2026-10-24
458
+
459
+ **Windows 后台自动化不再弹窗:VBScript 隐藏包装 + 计划任务工具 + 文档。**
460
+
461
+ 现场故障:给 standby 守护(每 2 分钟)和 TwinsEarth 守望(每 5 分钟)用 `schtasks /TR "node.exe ..."`
462
+ 注册任务,导致桌面每隔几分钟弹出一个控制台窗口(守望脚本还会累积进程)。
463
+
464
+ - 新增 `src/util/windows-hidden-task.mjs`:纯函数构造 VBScript 包装(`WScript.Shell.Run` 窗口样式 0、
465
+ 不等待)与 `schtasks` 参数;命令行/参数先按 Windows 规则引用,再整体做一次 VBScript 转义(避免二次转义)。
466
+ - 新增 `bin/w2m-hidden-task.mjs`:`--name/--every-minutes/--command/--arg/--workdir/--delete`,
467
+ 一条命令注册“无窗口”计划任务。
468
+ - 新增 `docs/WINDOWS-BACKGROUND-TASKS.md`:故障原因、正确做法、原则(分钟级任务只放短检查;一次性任务用完即删)。
469
+ - 测试:windows-hidden-task 5/5(含转义与参数校验);证据
470
+ `docs/acceptance/1.6.5/HIDDEN-BACKGROUND-TASKS.json`(fleet task `01M5ANYMVM30H98`)。
471
+ ## [1.6.4] — 2026-10-24
472
+
473
+ **修复:`/healthz` 漏发 `fleets_online`(1.6.3 的 `fleetSummary` 已计算,但 `stats()` 没带到公开字段)。**
474
+
475
+ - `stats()` 现在同时输出 `fleets_total` 与 `fleets_online`;
476
+ - 面板可正确显示 `全服:X/Y 个编队 · M/T 台在线`(X = 至少一台机器在线的编队数);
477
+ - relay 主机升级到 1.6.4(含 1.6.3/1.6.2/1.6.1 全部内容)。
478
+ - 测试:relay 200/200、fleet-target 49/49;证据 `docs/acceptance/1.6.4/HEALTHZ-FLEETS-ONLINE.json`
479
+ (fleet task `01M5ANYMVLX3MI7`)。
480
+ ## [1.6.3] — 2026-10-24
481
+
482
+ **编队与机器同构显示:在线编队数/编队总数 + 在线机器/机器总数。**
483
+
484
+ - relay `fleetSummary()` 新增 `fleets_online`(至少一台机器在线的编队数),`/healthz` 公开计数同步增加;
485
+ - 面板 relay 区显示:`全服:X/Y 个编队 · M/T 台在线`(fleet-target probe 读取 `fleets_online`);
486
+ - relay 主机升级到 1.6.3(含 1.6.2 全服概览与 1.6.1 安全修复)。
487
+ - 测试:relay 200/200、fleet-target 49/49、client-bundle 26/26;证据
488
+ `docs/acceptance/1.6.3/ONLINE-FLEETS.json`(fleet task `01M5ANYMVLWMTSG`)。
489
+ ## [1.6.2] — 2026-10-24
490
+
491
+ **relay 全服概览:多少个编队、多少台机器在线(healthz + operator /v1/fleets + 面板一行显示)。**
492
+
493
+ - `state.fleetSummary()`:按 fleet 聚合设备数/在线数,返回 `fleets_total`、`devices_online`、
494
+ `devices_total`、`unfleeted_devices` 与 per-fleet 明细。
495
+ - `GET /healthz`(公开)新增上述**聚合计数**(不暴露 per-fleet 明细);
496
+ - `GET /v1/fleets`(operator token):返回 per-fleet 明细(其他账号的编队名不向普通设备暴露)。
497
+ - 面板 relay 折叠区新增一行:`全服:N 个编队 · M/T 台在线`(fleet-target probe 读取 healthz 计数)。
498
+ - relay 主机将升级到 1.6.2(包含 1.6.1 的 Node 执行仅人工/内存态与 allow-list 解释器身份修复)。
499
+ - 测试:relay 200/200、fleet-target 49/49、client-bundle 26/26;证据
500
+ `docs/acceptance/1.6.2/RELAY-FLEET-OVERVIEW.json`(fleet task `01M5ANYMVLRYXSN`)。
501
+ ## [1.6.1] — 2026-10-24
502
+
503
+ **Node 执行开关改为「仅人工 UI 手动、进程内存态」:文件/远程/自动化一律无法授权。**
504
+
505
+ 本次实机验证暴露了一个必须修掉的现实:grant 任务可以远程写 `~/.w2m/exec-capability.json`,
506
+ 从而远程把机器变成可执行任意 Node 代码——与"最高权限只允许人工手动选中"直接冲突。
507
+
508
+ - 能力状态改为**进程内存**唯一权威:`readExecCapability` 不再读磁盘;旧文件(含 permanent)被忽略;
509
+ 只有 host 路由 `/api/w2m/capability`(composer 人工点击)能写。
510
+ - **不随重启保留**:进程退出即回到 denied("永久"= 本次进程生命周期);
511
+ `describe` 明确返回 `survives_restart: false`、`manual_only: true`、`file_ignored: true`。
512
+ - 三态语义不变:denied / temporary(默认 24h,上限 7 天,过期即失效)/ permanent(本进程内)。
513
+ - 测试重写:`test/exec-capability.test.mjs` 6/6(含"磁盘写 permanent 仍 denied""路由是唯一 setter"
514
+ "重启即撤销");agent.test 161 项通过(PATH 环境问题已在本机修复,非代码缺陷)。
515
+ - 修复 allow-list 解释器身份回归:当 argv[0] 是 **运行 agent 的同一个 node(process.execPath)** 时,
516
+ bare 条目 `node` 应放行(此前机器 PATH 里 node 指向别的程序时会误拒,实测 12 个 e2e 失败)。
517
+ - 证据 `docs/acceptance/1.6.1/MANUAL-ONLY-CAPABILITY.json`(fleet task `01M5ANYMVLDYWBH`)。
518
+ ## [1.6.0] — 2026-10-24
519
+
520
+ **一台 relay 承载 N 个编队:每个 DeepSeek 账号一个编队,共享 relay 但互相隔离。**
521
+
522
+ - bootstrap 答案从"单个编队的归属"改为"relay 拓扑声明":签名与有效期仍严格,fleet_id 不匹配不再拒绝
523
+ (`strictFleet: true` 保留旧行为)。一份 operator 签名即可服务全部编队,新账号机器无需人工重签。
524
+ - `scripts/verify-fleet-isolation.mjs` 扩展到 **3 个编队** 14 项检查(设备枚举/任务/聊天/记忆),全过。
525
+ - 新增 `docs/MULTI-FLEET.md`:编排模型、运维视图、零人工接入新账号、配额与边界。
526
+ - 测试:bootstrap 6/6(含拓扑答案用例)、fleet isolation 14/14;证据 `docs/acceptance/1.6.0/N-FLEET.json`
527
+ (fleet task `01M5ANYMVLBU716`)。
528
+ ## [1.5.5] — 2026-10-23
529
+
530
+ **跨编队隔离:同 relay 上的另一账号机器不再出现在本编队的枚举/任务/聊天/记忆里。**
531
+
532
+ 现场:另一账号的机器(自己的 fleet_id、自己的 fleet_key)出现在我们的机器列表里,被读成"非本账号
533
+ 机器进入编队"。排查结论:该机器的编队绑定本身是正确的,问题在于 relay 的**读取面没有按 fleet 过滤**
534
+ (派发侧自 v0.7.7 起已按 fleet 隔离,operator token 为 relay 级权限属于设计)。
535
+
536
+ - `GET /v1/devices`:按请求设备的 fleet 过滤(无 fleet 的旧设备在未配置 fleet 的 relay 上保持历史行为)。
537
+ - `GET /v1/tasks`:任务记录携带 `fleet_id`,列表按调用者 fleet 过滤(operator 派发是 relay 级,不会进入
538
+ 某个编队的列表)。
539
+ - `POST /v1/fleet/messages`:`fleet_id` 取自已认证设备,忽略 body(防止把消息标成别的编队)。
540
+ - `GET /v1/fleet/messages`:读取时按调用者 fleet 过滤。
541
+ - fleet memory 文档按 fleet 前缀隔离(无 fleet 设备沿用历史键,单编队 relay 不受影响)。
542
+ - 新增 `scripts/verify-fleet-isolation.mjs`:真实 relay + 两个 fleet 的黑盒验证(设备/任务/聊天/记忆
543
+ 共 11 项检查),可作为部署前门禁脚本运行。
544
+ - 验证:脚本 11/11 通过(fleet task `01M5ANYMVL2Q8PD`);relay.test 200/200 通过。
545
+ ## [1.5.4] — 2026-10-23
546
+
547
+ **验签 bootstrap 的结果覆盖过期的显式 relay(现场故障:设备仍指向 127.0.0.1)。**
548
+
549
+ 现场证据:DSH 升级到 v1.5.3 后仍报 `W2M_UNREACHABLE: could not reach the Rabbit at
550
+ http://127.0.0.1:8787`——它的 profile 仍显式写着本机地址,v1.5.3 只在“无显式 relay”时才采用
551
+ bootstrap 池,于是拿到池却继续拨 loopback。
552
+
553
+ - bootstrap 验签成功后**总是采用** `relays[0]` 作为 `rabbitUrl`,并 warn 记录被替换的旧地址;
554
+ 想固定 relay 的运维应显式设置 `relayUrls`(此时 bootstrap 分支不执行)。
555
+ - 只影响“验签通过”的答案;失败仍保持原配置并写 `w2m_status.bootstrap.last_error`。
556
+ - 测试:bootstrap 6/6、bootstrap-cli 6/6;证据 `docs/acceptance/1.5.4/BOOTSTRAP-WINS.json`
557
+ (fleet task `01M5ANYMVL0FV9C`)。
558
+ ## [1.5.3] — 2026-10-23
559
+
560
+ **零接触自动入队:只能升级插件的机器也能自己找到编队。**
561
+
562
+ v1.5.2 的验签 bootstrap 需要一个 URL、一个 operator 公钥和 fleetKey 才能触发——新装机器三样都没有,
563
+ 于是仍然回落到编译期默认 relay 并报 W2M_UNREACHABLE。v1.5.3 把触发条件做进包里:
564
+
565
+ - 包内置默认 bootstrap 端点与 **operator 公钥 pin**(只含公钥;端点无法签发,私钥不出运维机)。
566
+ - bootstrap 证明改为可选:机器还没拿到 fleetKey 时,仍可凭"签名 + 有效期 + relay 形状"采用
567
+ operator 声明的 relay 池(relay 列表是公开路由信息;加入编队仍需账号派生密钥,是独立一步)。
568
+ - `startLocalStack` 在无 relayUrls 时始终尝试 bootstrap(默认值生效),成功即用池(首个 primary),
569
+ 失败保持回落并写入 `w2m_status.bootstrap.last_error`。
570
+ - 包内默认 patch:`autoStartAgent: true`(本 fork),新装即启动 agent。
571
+ - 因此只有"升级插件"这一条通道的机器:升级 → 重启 → 自动取 relay 池 → 账号密钥就绪后自动配对入队。
572
+ - 测试:bootstrap 6/6(含 keyless)、bootstrap-cli 6/6、fleet-target 49/49;层模型 clean;
573
+ 证据 `docs/acceptance/1.5.3/ZERO-TOUCH-BOOTSTRAP.json`(真实 fleet task `01M5ANYMVKZ7DHH`)。
574
+ ## [1.5.2] — 2026-10-23
575
+
576
+ **新机器自动获得 relay 归属(验签 bootstrap)+ 一次性配置工具。**
577
+
578
+ - 新增 `src/plugin/bootstrap.mjs`:机器用 fleetKey 派生 HMAC 证明请求 relay 列表;响应必须由
579
+ operator 公钥(pin)对 canonical payload(fleet_id + 排序后的 relays + issued_at + expires_at)
580
+ 验签;过期/错 fleet/被篡改/坏 JSON 全部命名拒绝。缓存每次读取都重验签名与有效期,手改缓存无效。
581
+ **任何失败都不改现有 relay 配置**,原因写入 `w2m_status.bootstrap.last_error`。
582
+ - `startLocalStack` 接线:无 `relayUrls` 且配置 `bootstrapUrl`+`fleetKey`+operator 公钥时启动即解析
583
+ (缓存优先,其次网络),成功则采用该 relay 池(首个为 primary);`w2m_status` 新增
584
+ `bootstrap {configured, url, source, relays, expires_at, last_error}`。
585
+ - 新增 `bin/w2m-bootstrap.mjs`:一条命令写 `relayUrls`/`fleetKey`/`autoStartAgent` 到 profile patch,
586
+ 只改受管键、写前备份、所有输入先校验;`--temporary` 写 `relayTemporary: true`,面板以
587
+ **红色“临时 Relay 节点(回退配置)”**显示。
588
+ - 新增 `bin/w2m-bootstrap-sign.mjs`:运维侧用 operator 私钥签名 relay 列表,输出可静态托管。
589
+ - 控制面板 relay 折叠区支持 temporary 红色标记(仅主题 token,无硬编码颜色)。
590
+ - 层模型修复:bootstrap 模块不依赖 chatops 层(layer-check 保持 clean)。
591
+ - 测试:bootstrap 5/5、bootstrap-cli 6/6、fleet-target 49/49、client-bundle 26/26;
592
+ 证据 `docs/acceptance/1.5.2/VERIFIED-BOOTSTRAP.json`(真实 fleet task `01M5ANYMVKXNU8E`)。
593
+ ## [1.5.1] — 2026-10-23
594
+
595
+ **备用链路的身份隔离 + 控制面板的 relay 折叠。**
596
+
597
+ - `bin/w2m-secondary-link.mjs`:默认给子进程独立身份目录(`<stateDir>/dsh-home`),并同时设置
598
+ `DSH_HOME` / `USERPROFILE` / `HOME`,可用 `--identity-home` 指定。修复的是 v1.5.0 现场实测的坑:
599
+ 备用链路首次启动覆盖了主链路 `~/.dsh/xclient/device.json`,机器在下一次重启前看不出问题,
600
+ 重启后却会以**备用身份**连到**错误的 relay**。这种"重启后才爆"的坑按默认隔离处理。
601
+ - 控制面板 relay 折叠:`GET /api/w2m/target` 快照新增 `relays`(`relayUrls` 数组优先,兼容单
602
+ `rabbitUrl`),探测 `/healthz` 后按 RTT 排序、标注 primary/standby/offline;超过 3 个节点时只显示
603
+ 3 个并给出 "+N 折叠" 提示,展开后显示全部(含离线节点,不隐藏故障)。
604
+ - 探针失败保留在列表末尾(`ok:false`),网络异常不会让节点从视图中消失,也不会抛进快照路径。
605
+ - 测试:secondary-link 7/7、fleet-target 49/49(新增 relay pool 四个用例);门禁 GREEN ×2;
606
+ 证据 `docs/acceptance/1.5.1/IDENTITY-ISOLATION-RELAY-FOLD.json`(真实 fleet task `01M5ANYMVKS3OQZ`)。
607
+
608
+ ## [1.5.0] — 2026-10-23
609
+
610
+ **多 relay 双通道(active-passive)+ relay 健康看门狗。**
611
+
612
+ - 新增 `bin/w2m-secondary-link.mjs`:在一台机器上拉一条通往备用 relay 的常在线链接。
613
+ 它监督子进程 `w2m-localside`(同一套协议客户端),子进程退出按指数退避重启并记录每次转换,
614
+ 由 systemd/launchd/计划任务只守护这一层——避免"链接一次启动后静默死掉"的已知失败形态。
615
+ - active-passive 是**路由规则**:两条链接都常连,任务只在主 relay 派发;主 relay 掉线或延迟严重时
616
+ 把同一任务改派到备用 relay(机器已在线,无需重连),正常情况下一台机器只有一个执行者。
617
+ - 新增 `deploy/relay/`:`w2m-watchdog.sh` + systemd `.service/.timer`。每 30s 检查
618
+ `/healthz` 的 `"ok":true`,连续 2 次失败才重启 relay/stun;每小时最多 5 次重启(避免掩盖根因),
619
+ 日志写 `/var/lib/w2m/watchdog/watchdog.log`。relay 因此独立于"是否有编队成员"保持在线。
620
+ - 新增 `docs/MULTI-RELAY.md`:拓扑、选路规则(健康且 RTT 最低且目标全在线)、relay 互为成员、
621
+ 以及 relay 超过 3 个时客户端折叠显示(`relays: 3 shown · +N online`)的约定。
622
+ - 测试:`test/secondary-link.test.mjs` 6/6(参数校验、child argv、退避重启、日志);
623
+ 门禁 GREEN ×2;证据 `docs/acceptance/1.5.0/SECONDARY-LINK.json`(真实 fleet task `01M5ANYMVKR311N`)。
624
+
625
+ ## [1.4.4] — 2026-10-23
626
+
627
+ **relay 重启不再丢失编队归属(devices.json 持久化 fleet_id)。**
628
+
629
+ - 持久化修复:`devices.json` 快照新增 `fleet_id` / `fleet_source`,`restore()` 同步恢复。
630
+ 在此之前,relay 重启后设备全在、token 仍有效,但每台设备的 fleet 归属全变成 null——
631
+ 编队"静默消失"且没有任何错误可看(本次在共享 relay 上实测复现)。
632
+ - 桥接 getupdates 看门狗:空闲时每 10 分钟输出 `polling ... connection live`;
633
+ 40s 长轮询超时明确记录 `poll timeout after 40s -- reconnecting`;
634
+ 连续 5 分钟没有一次成功轮询则退出(exit 3),由 launchd KeepAlive / systemd Restart= 拉起新连接,
635
+ 避免"进程活着但没人知道它是否还在轮询"的静默状态。
636
+ - 测试:relay 新增 fleet 重启往返用例(pair by fleet_key → 重启 → fleet_id/fleet_source 保留);
637
+ bridge 套件 36/36;门禁 GREEN ×2。
638
+ - 证据:docs/acceptance/1.4.4/FLEET-PERSISTENCE.json(真实 fleet task `01M5FLEETPERSIST0000000002`)。
639
+
640
+ ## [1.4.3] — 2026-10-22
641
+
642
+ **微信编队桥接:回复可靠性修复(不回复/静默问题)。**
643
+
644
+ - 每条消息独立 try/catch:任何处理异常不再让进程退出,改为回复
645
+ `处理失败(阶段,错误码):原因 + /help 提示`(新增 `renderBridgeError`)。
646
+ - 桥接启动调用 `ilink/bot/msg/notifystart`:修复"token 有效、进程在跑、但 iLink 不推送消息"的静默状态;
647
+ 日志记录 `notifystart : ok`,失败也记录原因。
648
+ - 非文本、空消息、未以 `/run` 开头、权限不足、派发失败/超时等路径均保证有回复与下一步提示。
649
+ - 运行方式支持 launchd 常驻(KeepAlive 自动拉起;macOS plist 由运维部署,不进入安装包)。
650
+
651
+ - **授权优先级修复**:exec-capability 只在任务未携带 grant 时兜底;grant 被拒(replay / 过期 / 机器不符)
652
+ 原样上报,不再被能力分支覆盖成路径匹配错误,`GRANT_REPLAY` 语义恢复。
653
+
654
+ 真机验收(macmini):`notifystart ok`、错误回复代码在位、launchd state=running、bridge 进程存活;
655
+ 证据 `docs/acceptance/1.4.3/BRIDGE-LIVE.json`。测试:chatops-ilink-fleet-bridge 36/36。
656
+
657
+ ## [1.4.2] — 2026-10-22
658
+
659
+ **Node 代码执行权限成为 UI 可激活的子插件能力(命中真机验收)。**
660
+
661
+ - 新增 `src/util/exec-capability.mjs`:机器本地能力状态(拒绝 / 临时 24h / 永久),fail-closed;临时档带绝对
662
+ `expires_at_ms`,过期即拒绝;唯一可放行的前缀固定为 `node -e`。
663
+ - 新增 host 路由 `GET/POST /api/w2m/capability`(`src/plugin/exec-capability-route.mjs`),同源、在 host
664
+ 认证栅栏内;浏览器不接触文件系统或设备 token。
665
+ - client `lib/client.js` 在 `conversation.input.left` 新增第三个控制(order 42):
666
+ `Node 执行:拒绝 | 临时 24 小时 | 永久`,带 RCE 风险提示与剩余时间。
667
+ - agent 每个任务前读取能力文件;命中时结果信封 `authorization.provenance = "exec-capability"`,
668
+ warnings 记录 `EXEC_CAPABILITY`;pipeline 各 stage 同样覆盖。
669
+
670
+ 真机验收(TwinsEarth,插件 1.4.2-beta.1;先清空 `cordis.patch.yml` 的旧 allow-list 以隔离能力路径):
671
+ - 无能力、空 allow-list:`node --version` → `COMMAND_NOT_ALLOWED`
672
+ - 能力=永久、空 allow-list:`node -e` → **ok**(`node -e` 是唯一被放行的前缀)
673
+ - 能力=临时 5s 到期后:`node -e` → `COMMAND_NOT_ALLOWED`(fail-closed)
674
+ - 证据:`docs/acceptance/1.4.2/CAPABILITY-LIVE.json`
675
+
676
+ Verified: 单测/路由/客户端契约 43/43;候选 gate GREEN ×2;stable gate 见发布记录。
677
+
678
+ ## [1.4.2-beta.1] — 2026-10-22
679
+
680
+ **Node 代码执行权限成为 UI 可激活的子插件能力。**
681
+
682
+ - 新增 `src/util/exec-capability.mjs`:机器本地能力状态(拒绝 / 临时 24h / 永久),fail-closed,
683
+ 临时档带绝对 `expires_at_ms`,过期即拒绝。
684
+ - 新增 host 路由 `GET/POST /api/w2m/capability`(`src/plugin/exec-capability-route.mjs`),同源、在
685
+ host 认证栅栏内;浏览器不再接触文件系统或设备 token。
686
+ - client `lib/client.js` 在 `conversation.input.left` 座位新增第三个控制(order 42):
687
+ `Node 执行:拒绝 | 临时 24 小时 | 永久`,带 RCE 风险提示与剩余时间。
688
+ - agent 每个任务前读取能力文件;命中时结果信封 `authorization.provenance = "exec-capability"`,
689
+ warnings 记录 `EXEC_CAPABILITY`;pipeline 各 stage 同样覆盖。
690
+ - 文档 `docs/EXEC-CAPABILITY.md`;测试 `test/exec-capability.test.mjs`(7/7)、
691
+ client-bundle/loader-entry/exec-capability 合计 43/43。
692
+
693
+ Verified: 单测与路由测试全绿;候选版本真机验收(UI 激活 → agent 下一任务可执行 node -e)。
694
+
695
+ ## [1.4.1] — 2026-10-22
696
+
697
+ **修复:授权模型在从未 bootstrap 的机器上不可用(真机发现)。**
698
+
699
+ - relay 的 offer 对 **grant 任务**也携带 `operator_key`(此前只有 maintenance 请求携带)。在空 allow-list 的机器上,
700
+ agent 首次收到 offer 时据此 TOFU pin;修复前该机器只会得到 `GRANT_NO_PINNED_KEY`,即"显式授权"形同虚设。
701
+ - 任务台账记录 `operator_key`,relay 重启回放恢复 `workspace / grant / grant_summary / maintenance / operator_key`:
702
+ 修复前重启后 re-offer 会丢失授权字段,机器会以错误的理由拒绝任务。
703
+
704
+ 真机验收(relay 1.4.1 候选,TwinsEarth 插件 1.4.0、allow-list 为空):
705
+
706
+ - 无 grant 的 `node --version` → `COMMAND_NOT_ALLOWED`
707
+ - operator grant 授权的 `node -e` → **ok**,输出 `{plugin:"1.4.0", pinned:"^1.4.0", node:"v24.21.0"}`
708
+ - relay M1 diag(root runner)→ ok,66,046 bytes
709
+ - 证据:`docs/acceptance/1.4.1/TWIN-GRANT-LIVE.json`、`M1-LIVE-DIAG.json`
710
+
711
+ Verified: relay TOFU offer 测试 1/1、grant/ledger 相关 3/3;候选部署后真机全部通过。
712
+
713
+ ## [1.4.1-beta.1] — 2026-10-22
714
+
715
+ **候选:授权模型在"从未 bootstrap 的机器"上可用。**
716
+
717
+ - relay 的 offer 现在对 **grant 任务**也携带 `operator_key`(此前只有 maintenance 请求携带),首次见到该键的
718
+ agent 因此可以 TOFU pin;在此之前,空 allow-list 的新机器收到 grant 只会得到 `GRANT_NO_PINNED_KEY`。
719
+ - 任务台账记录 `operator_key`,重启回放恢复 `workspace / grant / grant_summary / maintenance / operator_key`:
720
+ 此前 relay 重启后 re-offer 的任务会丢失授权/工作区字段,机器会以错误的理由拒绝。
721
+ - 真机验证路径:TwinsEarth(allow-list 为空、插件 1.4.0)用 operator grant 执行 `node -e` 读取版本。
722
+
723
+ Verified: relay TOFU offer 测试 1/1,grant/ledger 相关 3/3;候选部署到 relay 后做真机验收。
724
+
725
+ ## [1.4.0] — 2026-10-22
726
+
727
+ **稳定版:执行授权、任务、协作三个模型的第一版落地。**
728
+
729
+ - **执行授权模型**:`bin/w2m-grant.mjs` 签发单次执行授权(绑定 task_id/argv/机器/≤10min TTL/max_uses);relay 派发预检
730
+ (`GRANT_TASK_MISMATCH` / `GRANT_ARGV_MISMATCH` / `GRANT_EXPIRED` → 400);撤销 `POST /v1/grants/revoke`(幂等、
731
+ ledger 留痕、派发 `403 GRANT_REVOKED`)与审计 `GET /v1/grants`;结果信封携带 `authorization.provenance`
732
+ (grant/allow-list/maintenance);使用台账不可写时 `GRANT_LEDGER_UNAVAILABLE` fail-closed。
733
+ - **任务模型**:任务可声明 `workspace {kind:'worktree', base_commit, produce}`;agent 在独立 worktree 中执行、提交、
734
+ 生成相对 base 的 patch 并上传内容寻址 blob,结果携带 `{base_commit, head_commit, changed_files, patch_sha256,
735
+ patch_bytes, cleaned}`;relay 侧分支 refs(单调、必须声明 from=head)、合并队列(patch sha256 幂等、already_merged)、
736
+ 冲突结果带 conflict_sha256 且绝不推进分支。
737
+ - **协作模型**:项目 room/board/artifact 索引/decisions/project memory/通知(共享对象事件广播 + 每 reader 未读水位)/
738
+ 项目级 SSE(ready + since_seq 补齐 + 实时 project.event)。
739
+ - **M7**:指标注册表与 `/metrics` 接线、证据 schema 与 preflight 第 16 项证据门禁;候选版本可用 candidate line
740
+ fallback,stable 必须提供本线真机证据。
741
+
742
+ **发布时真机状态(如实)**:relay 宿主已运行 1.4.0 候选(1.4.0-beta.1,代码与 stable 相同),relay 侧真机回归 10/10
743
+ 通过(board/artifacts/decisions/memory/locks/blobs/refs/merge/notifications/项目 SSE)。M1 维护通道的 diag 在换版后
744
+ 未能执行:非特权换版路径(fleet node 通道)替换目录后由 kill 触发的重启没有拉起 `w2m-localside`(部署脚本的
745
+ systemctl 被拒绝);在控制台执行 `systemctl restart w2m-localside` 后即可继续完成 M1 复验与 stable 官方资产部署。
746
+
747
+ 证据:`docs/acceptance/1.4.0/`(REGRESSION-1.4.0.json、M1-LIVE-MAINTENANCE.json)。
748
+
749
+ ## [1.4.0-beta.1] — 2026-10-22
750
+
751
+ **v1.4.0 三模型候选:执行授权模型落地、任务模型落地、协作模型落地。**
752
+
753
+ 这是把 1.3.12–1.3.14 的 beta 线合成一版的候选,供 stable 前真机验收。
754
+
755
+ - **执行授权模型(v1.4.0)**:`bin/w2m-grant.mjs` 签发单次执行授权(绑定 task_id/argv/机器/≤10min TTL);
756
+ relay 派发预检(`GRANT_TASK_MISMATCH` / `GRANT_ARGV_MISMATCH` / `GRANT_EXPIRED`);撤销
757
+ `POST /v1/grants/revoke`(幂等、ledger 留痕、派发 `403 GRANT_REVOKED`)与审计 `GET /v1/grants`;
758
+ 结果信封带 `authorization.provenance`(grant/allow-list/maintenance);使用台账不可写时
759
+ `GRANT_LEDGER_UNAVAILABLE` fail-closed(真机发现的 EACCES 崩溃已修)。
760
+ - **任务模型(v1.4.1 前身)**:任务可声明 `workspace {kind:'worktree', base_commit, produce}`;
761
+ agent 在独立 worktree 执行 → 提交 → 生成相对 base 的 patch → 上传内容寻址 blob → 证据
762
+ `{base_commit, head_commit, changed_files, patch_sha256, patch_bytes, cleaned}`;
763
+ relay 侧分支 refs(单调、必须声明 from=head)、合并队列(按 patch sha256 幂等、already_merged)、
764
+ 冲突结果携带 conflict_sha256 且绝不推进分支;refs/merge-queue HTTP 路由。
765
+ - **协作模型(v1.4.2 前身)**:项目 room(成员/角色/快照+增量事件)、board、artifact 索引、
766
+ decisions(append-only)、project memory(版本文档)、通知(共享对象事件广播 + 每 reader 未读水位)、
767
+ 项目级 SSE(ready + since_seq 补齐 + 实时 project.event + keepalive)。
768
+
769
+ M7 证据门禁对候选版本增加 candidate line fallback:beta 候选可引用其所基于发布线的真机证据(其产物在机器升级前构建),stable 仍必须提供本线真机证据。
770
+
771
+ Verified: gate GREEN ×2;relay 测试、agent 测试、workspace/refs/board/notification/SSE 用例全绿。
772
+
773
+ ## [1.3.12-beta.4] — 2026-10-22
774
+
775
+ **执行授权模型真机修复:使用台账不可写时 fail closed,而不是 crashed。**
776
+
777
+ 在 relay 宿主上实测时发现"授权了但一执行就崩":维护状态目录 `/var/lib/w2m/maintenance` 由 root 拥有且 0755,agent 以
778
+ 非特权用户运行,grant 第一次通过后写 `grant-uses.json` 报 EACCES,异常冒泡成任务 `crashed` —— 授权判定其实已经通过
779
+ (同一 argv 无 grant 时是 `COMMAND_NOT_ALLOWED`),失败发生在记录环节。
780
+
781
+ - `grant-gate`:台账写入包 try/catch,失败返回 `GRANT_LEDGER_UNAVAILABLE`,不执行"未记录的一次授权"(否则单次授权会变成
782
+ 可重放漏洞),也不再把整个 handler 打崩。
783
+ - `bootstrap.sh`:`$BASE` 改为 `root:w2m 0775`,让默认安装就能写台账;queue 仍 0770、done/claimed 0750。
784
+ - 回归用例:usedFile 指向不可写路径,断言返回该 code 而非抛异常。
785
+
786
+ 真机对比(修复前、relay 宿主):
787
+ - 无 grant 的 `uname -s` → `refused/COMMAND_NOT_ALLOWED`;
788
+ - 有合法 grant 的同一 argv → 通过 gate,随后因台账 EACCES 报 crashed(本次修复的目标)。
789
+
790
+ Verified: grant-gate 7/7(含新用例);gate GREEN twice。
791
+
792
+ ## [1.3.12-beta.3] — 2026-10-22
793
+
794
+ **执行授权模型收尾:每个结果都说明"是谁授权的"。**
795
+
796
+ beta.1 让操作员能授权一次执行,beta.2 让授权可撤销、可审计;这一版补上最后一块——结果自身携带授权来源,而不是只能在
797
+ relay 台账里反查。
798
+
799
+ - 结果信封新增 `authorization` 字段:`{provenance: "grant" | "allow-list" | "maintenance", capability_id?, grant_id?}`。
800
+ grant 路径带 `grant_id`(可与 ledger 对账),maintenance 路径带 request_id(可与 M1 台账对账),allow-list 路径标记
801
+ provenance 即可。
802
+ - 一个决定、两个受众:gate 决定"跑不跑",信封记录"这是哪一种决定";refused 路径不伪造 provenance。
803
+ - e2e 断言:空 allow-list + 合法 grant 的结果 `authorization.provenance === "grant"` 且带 grant_id。
804
+ - 边界如实写进文档:provenance 是执行机器对自身判定的陈述,relay 存储但不验证它;签名仍由 agent 验证,派发仍由 relay
805
+ 预检,撤销仍由 relay 记账——台账才是权威。
806
+
807
+ Verified: agent 159/157(含 grant replay e2e 的 provenance 断言);gate GREEN twice。
808
+
809
+ ## [1.3.12-beta.2] — 2026-10-22
810
+
811
+ **执行授权模型补完:撤销与操作员审计。**
812
+
813
+ beta.1 让操作员能"授权某一次执行";这一版让授权可以被收回、可以被查账,而不是只能等 TTL 过期。
814
+
815
+ - **撤销**:`POST /v1/grants/revoke`(operator token)`{grant_id, reason?}`。幂等(第二次 `already_revoked: true`),写入
816
+ ledger `grant.revoked`,带 reason 与操作者。
817
+ - **派发拒绝**:携带已撤销 grant 的任务在 `POST /v1/task` 被拒,`403 GRANT_REVOKED`,机器收不到 offer;revocation 先于
818
+ argv/task 绑定检查,理由更直接。
819
+ - **审计视图**:`GET /v1/grants`(operator token)返回 `tasks`(task_id / grant_id / argv_hash / key_id / mode / created_at)
820
+ 与 `revoked` 列表;device token 为 401,授权台账不属于机器工作面。
821
+ - **边界**:撤销阻止的是**新派发**;agent 断网时仍会执行已到手 grant 里绑定的那一次(正确性仍取决于 relay 派发,与 M2 §4
822
+ 一致)。撤销后要重新授权必须签发新 grant_id。
823
+
824
+ Verified: grant 13/13;grant-issue 3/3;relay 191/191(新增撤销+审计用例);gate GREEN twice。
825
+
826
+ ## [1.3.12-beta.1] — 2026-10-22
827
+
828
+ **执行授权模型:静态 allow-list 之外,操作员可以显式授权某一次执行。**
829
+
830
+ 在此之前授权只有一层:机器本地配置的 allow-list,一个长期决定,没有"就这一次"的途径。这一版把 M2 grant 补成一条可用的
831
+ 路径——签发、派发、验证、审计——而不是一个只在测试里存在的协议。
832
+
833
+ - **`bin/w2m-grant.mjs`**:操作员签发一条 grant。绑定一个 task_id(必须先确定)、一个 argv、若干 machine_id、≤10 分钟 TTL
834
+ 和 max_uses(默认 1);stdout 输出可直接放进 `POST /v1/task` 的 `grant` 字段,stderr 打印 task_id/argv_hash/expires_at。
835
+ 该 CLI 同时登记为 package bin `w2m-grant`。
836
+ - **relay 派发预检**:task_id 不符 → `GRANT_TASK_MISMATCH`,argv_hash 不符 → `GRANT_ARGV_MISMATCH`,已过期 →
837
+ `GRANT_EXPIRED`,都是 HTTP 400 的调用方错误;relay 不替 agent 做授权裁决。
838
+ - **审计**:task summary 增加 `argv_hash` 与 `grant_key_id`(`grant_id` 已有),让"谁授权了哪条命令"可查。
839
+ - **agent 仍是验证者**:签名、任务/机器绑定、argv、TTL、max_uses、重放(`GRANT_REPLAY`)由机器本地用 pin 的操作员公钥判定;
840
+ 空 allow-list + 合法 grant 执行,无 grant 仍 `COMMAND_NOT_ALLOWED`(既有 e2e 覆盖)。
841
+ - **文档**:`docs/COLLAB/EXECUTION-AUTH-MODEL.md` 状态改为已实现,补上"取 task_id → 签发 → 携带派发 → 两处检查 → 审计字段"
842
+ 的完整路径和仍然不做的边界(无长期委托、无自动签发、无跨项目授权)。
843
+
844
+ Verified: grant-issue 3/3(真实 CLI 子进程签发并用签名载荷验证);grant 13/13;relay 190/190(新增派发预检用例);gate GREEN twice。
845
+
846
+ ## [1.3.11] — 2026-10-22
847
+
848
+ **Stable release: the collaboration stack's first seven modules, with the gate that measures them.**
849
+
850
+ This is the first stable since 1.3.9 (docs) that ships runtime work, and the first beta line to end with its acceptance evidence
851
+ enforced by the pre-flight rather than stored beside it. What is in it, by module:
852
+
853
+ - **M1 maintenance channel** (live on the relay host): signed update/restart/diag requests, operator-key pinning, a root runner and
854
+ a bootstrap for Linux/Windows. Accepted on the real machine: diag (9 sections / 62 KB), relay restart with a new relay_id and
855
+ the result delivered after it, update to 1.3.10-beta.10 (download + sha256 + atomic swap), and a post-update diag. Evidence:
856
+ docs/acceptance/1.3.10-beta.10/.
857
+ - **M2 grants**: single-use, task-bound Ed25519 grants with argv hash, TTL, use ledger and named refusals; e2e covers an empty
858
+ allow-list succeeding under a grant, replay, argv tampering and the no-grant refusal.
859
+ - **M3 Git-native tasks**: isolated worktrees at the caller's base commit, a content-addressed patch artifact contract, a merge
860
+ queue with merge-slot serialisation, strict/replay policies, already_merged and a structured conflict artifact. Module-level,
861
+ 24 tests across the three slices plus two composed workflow tests.
862
+ - **M4 project room**: project objects, membership roles, a deny-by-default action matrix, snapshot + incremental events, task
863
+ attribution (implicit default project, explicit projects refused when unknown), and the /v1/projects routes.
864
+ - **M5 multi-client primitives**: branch/file locks (leases, renewal, owner force-release, merge preflight conflicts), reviews
865
+ (self-review guard, duplicate-verdict refusal, approval gate by patch sha) and notifications (dedupe, digest, per-reader
866
+ watermarks, retention), with the lock routes live and their events projected into the project stream.
867
+ - **M6 cross-network reliability**: chunked, resumable blob uploads with verified commits, Range downloads, and an agent result
868
+ outbox (atomic, persisted exponential backoff, cross-restart recovery, bounded).
869
+ - **M7 metrics and acceptance**: a 39-metric contract with a cardinality guard, live on /metrics; a validator for acceptance
870
+ evidence; and the pre-flight's 16th check, which fails a release whose evidence does not validate or whose release line has no
871
+ record.
872
+
873
+ Verified: pre-flight GREEN twice (16/16 checks, 2828 tests: 2814 pass, 0 fail, 14 skip); beta line beta.1 through beta.25 shipped on
874
+ GitHub prereleases only.
875
+
876
+ Known limitations, stated rather than implied: the merge queue and the review/notification routes are not wired into the task path
877
+ yet; a request that restarts the agent itself loses the ferry result (relay restarts are fine); M4 role checks cover the lock routes
878
+ and reads, not every future write; the relay runs beta.10 and is upgraded separately from this tag.
879
+
880
+ ## [1.3.10-beta.25] — 2026-10-22
881
+
882
+ **M7 final core slice: acceptance evidence is a gate, not a folder.**
883
+
884
+ The pre-flight grew its 16th check. `acceptance evidence (every committed record validates, and the release line has one)`
885
+ does three things a file list cannot:
886
+
887
+ 1. every JSON record under `docs/acceptance/` is validated with the M7 schema (`src/acceptance/evidence.mjs`) — a record that
888
+ does not say which machine ran it, when, by whom and under which task id fails the gate;
889
+ 2. the current release line must have at least one record: shipping a beta that claims an accepted capability while pointing at
890
+ no evidence from its own `1.3.x` line is RED;
891
+ 3. it reports what it read ("4 acceptance record(s) validated; 4 for the 1.3.10 line"), so a green row is a statement about a
892
+ specific set of records rather than about an empty directory.
893
+
894
+ The check is bound to `docs/acceptance` in `GATE_INVENTORY`, so deleting the directory or the check breaks the inventory's
895
+ both-directions rule; the evidence validator's accepted-orphan entry is removed now that the gate imports it.
896
+
897
+ Not claimed: this gate proves the records are well-formed and current, not that the described runs happened — that is what the
898
+ task id, the machine ids and the executor field are for, and they are now mandatory.
899
+
900
+ Verified: gate GREEN twice with 16/16 registered checks; the evidence check passes on the four M1 records.
901
+
902
+ ## [1.3.10-beta.24] — 2026-10-22
903
+
904
+ **M7 second slice: acceptance evidence has a schema, and the committed evidence is validated against it.**
905
+
906
+ - `src/acceptance/evidence.mjs` validates every required field of the M7 evidence record: version, uppercase case id, kind
907
+ (positive/negative/chaos), requirement, at least one machine, ISO `executed_at`, executor (lead/human/agent), a result with a
908
+ verdict, and — for a positive case — the real `task_id`. Optional fields are validated when present: argv arrays,
909
+ `[from, to]` ledger ranges, `{sha256, bytes}` artifacts and an assertions object. Every problem is returned at once, because a
910
+ release check should list what to fix rather than one field per run.
911
+ - `summarizeEvidence()` counts valid records by kind and version and names the invalid ones, which is what a release note or a
912
+ gate summary reads.
913
+ - The test suite validates the **actual committed files** under `docs/acceptance/` — a schema that rejected the project's own
914
+ evidence would be a schema written for a fictional project. It also pins that both the flat `result.outcome` and the nested
915
+ `result.task.outcome` (the live restart record) are accepted as the same fact.
916
+
917
+ Registered as an accepted library with its exit named (the preflight evidence gate).
918
+
919
+ Still to wire for M7: the preflight check that a release's evidence exists and validates, and the remaining instrumentation.
920
+
921
+ Verified: evidence 6/6; gate GREEN twice.
922
+
923
+ ## [1.3.10-beta.23] — 2026-10-22
924
+
925
+ **M7 wiring: the registry is live on /metrics.**
926
+
927
+ - Relay state owns the M7 registry. The project room now notifies a listener per event, and that callback counts
928
+ `wm_project_events_total{type}` once for both the room's own events (project created, member changes, task linked) and the
929
+ lock/review/notification events projected into it — a projection is appended, then counted through the room hook, so it cannot
930
+ be double-counted.
931
+ - `GET /metrics` appends the registry body under a `# reliability registry` marker. A relay that never records into the registry
932
+ renders exactly what it rendered before, which is why the existing exposition tests still pass unchanged.
933
+ - Lock routes count `wm_lock_acquired_total{kind}` and `wm_lock_conflict_total{kind}`.
934
+ - One route test: after creating a project and acquiring a branch lock, `/metrics` carries
935
+ `wm_project_events_total{type="project.created"}` and `wm_lock_acquired_total{kind="branch"}`.
936
+
937
+ Verified: relay 189/189 (one new test); the registry and room suites are unchanged; gate GREEN twice.
938
+
939
+ ## [1.3.10-beta.22] — 2026-10-22
940
+
941
+ **M7 first slice: one metrics contract for the whole collaboration stack.** A local module and eight tests; the relay /metrics
942
+ route and the call sites do not record into it yet.
943
+
944
+ - `src/relay/reliability-metrics.mjs` declares every M7 metric name, its type and the labels it may carry — 39 definitions across tasks,
945
+ authorization, data plane, collaboration and transport. An undeclared name, an undeclared label, or a high-cardinality label
946
+ (`task_id`, `dedupe_key`, ids) is refused at record time, so a metrics endpoint cannot silently become a memory leak.
947
+ - Counters refuse decrements; gauges hold the last value; histograms keep cumulative buckets plus sum/count. `renderPrometheus()`
948
+ emits `_bucket/_sum/_count` with sorted, escaped label values, and `snapshot()` gives the same data as plain JSON for the panel.
949
+ - Eight tests: label aggregation, undeclared metric/label refusal, wrong-type and decrement refusal, gauge semantics, cumulative
950
+ buckets, Prometheus text (including `+Inf`), label escaping, and the contract's own shape.
951
+
952
+ Registered as an accepted library with its exit named (the /metrics route and instrumentation). Note on the name: the exposition module was already called metrics.mjs, so after a naming collision during this work its file and test were restored, and this registry ships as reliability-metrics.mjs; both test files pass.
953
+
954
+ Still to wire for M7: the /metrics route, the instrumentation call sites, the acceptance-evidence validator and the preflight
955
+ evidence check.
956
+
957
+ Verified: metrics 8/8; gate GREEN twice.
958
+
959
+ ## [1.3.10-beta.21] — 2026-10-22
960
+
961
+ **M6 second slice: the agent result outbox.** A local module and eight tests; the agent delivery path still uses the existing
962
+ spool and has not switched to it yet.
963
+
964
+ - `src/agent/outbox.mjs` — a result is written to disk (temp + rename, so a crash leaves either the old state or the complete
965
+ entry) before any network attempt, keyed by `(task_id, attempt)` so a retried handler is one entry. Delivery removes the entry
966
+ only after the relay acknowledged it; the relay's own dedupe makes a re-delivery safe when that acknowledgement was lost.
967
+ - Retries use exponential backoff persisted with the entry (`1s, 2s, 4s …` capped at 60s), so closing the laptop for an hour is
968
+ a resume rather than a restart. Past `max_attempts` the entry moves to `dead/` and is reported as exhausted.
969
+ - Limits: 200 entries / 16 MiB; beyond that the oldest entries are dropped and the caller sees `outbox_full: true`
970
+ (`OUTBOX_FULL` in the changelog's words) rather than a silent disk fill. A single oversize result is refused.
971
+ - Eight tests: atomic persist + depth, duplicate enqueue, restart recovery with attempt counts, backoff growth and exhaustion,
972
+ ack-only removal, count-limit drop keeping the newest, corrupt-file quarantine, oversize refusal.
973
+
974
+ Registered as an accepted library with its exit named (the agent delivery switch).
975
+
976
+ Still to wire for M6: the agent's upload/download decision for large artifacts, and P2P as acceleration-only with metrics.
977
+
978
+ Verified: outbox 8/8; gate GREEN twice.
979
+
980
+ ## [1.3.10-beta.20] — 2026-10-22
981
+
982
+ **M6 wiring: chunk session and Range routes.** Large artifacts now travel over HTTP without restarting from zero.
983
+
984
+ - `POST /v1/fleet/blob-sessions` declares `{sha256, size, chunk_bytes?}`; `PUT …/:sid/chunks/:index` uploads one idempotent chunk
985
+ (octet-stream); `GET …/:sid` returns received/missing so an interrupted upload resumes; `POST …/:sid/commit` verifies the
986
+ assembled sha256 and, on success, stores the bytes through the existing content-addressed blob store. A missing chunk answers
987
+ `409 BLOB_INCOMPLETE` with the indices; a mismatch deletes the session and creates nothing.
988
+ - `GET /v1/fleet/blobs/:sha?range=start-end` answers `206` with `Content-Range`, so a client can fetch a large patch (or a conflict
989
+ artifact) in slices instead of one allocation.
990
+ - The blob-session module's responses are now uniformly `{ok, session: {...}}`, which the routes and the original tests share.
991
+
992
+ Two HTTP tests: a chunked upload that commits and is then read whole plus as a Range, and an incomplete commit naming `[0,1]`.
993
+
994
+ Still to wire for M6: the agent result outbox, and P2P as acceleration-only with its observable success rate.
995
+
996
+ Verified: blob-sessions 9/9, relay 188/188 (2 new route tests); gate GREEN twice.
997
+
998
+ ## [1.3.10-beta.19] — 2026-10-22
999
+
1000
+ **M6 first slice: large artifacts can be uploaded in resumable chunks.** A local module and nine tests; the routes and the
1001
+ agent upload path are the wiring slice after it.
1002
+
1003
+ - `src/relay/blob-sessions.mjs` — a client declares the final `sha256` and `size` once, then uploads chunks in any order
1004
+ (each index idempotent), asks what is missing, and commits. Only a commit whose assembled bytes hash to the declared sha256
1005
+ becomes a blob; a mismatch deletes the session and returns `BLOB_SHA_MISMATCH`, so nothing can be referenced that does not
1006
+ exist.
1007
+ - Limits follow the spec: default 1 MiB chunks, `≤8 MiB` per chunk, `≤256` chunks and `≤64 MiB` per blob, two open sessions and
1008
+ 512 MiB unfinished bytes per machine, 24h session TTL. A resume after a disconnect is `status()` plus the missing indices,
1009
+ which is exactly the "don't restart a 40 MiB patch" property.
1010
+ - Nine tests: plan/status, out-of-order + idempotent chunks, verified commit through the store hook, incomplete commit naming
1011
+ the missing chunks, sha mismatch deleting the session, chunk length/size refusals, quotas, TTL expiry, and blob/chunk ceilings.
1012
+
1013
+ Registered as an accepted library with its exit named (the blob-session routes + agent upload).
1014
+
1015
+ Still to wire for M6: the HTTP chunk routes and Range downloads, the agent outbox for offline results, and P2P as acceleration
1016
+ only.
1017
+
1018
+ Verified: blob-sessions 9/9; gate GREEN twice.
1019
+
1020
+ ## [1.3.10-beta.18] — 2026-10-22
1021
+
1022
+ **M5 wiring, part 1: lock routes and one event stream for the primitives.**
1023
+
1024
+ - Relay state now owns the three M5 primitives (locks, reviews, notifications) and projects their events into the project room
1025
+ stream with the module sequence attached, so a client can reconstruct "what happened" from one cursor instead of three.
1026
+ - `/v1/projects/:id/locks`: `GET` list, `POST` acquire, `POST …/:lock_id/renew`, `DELETE …/:lock_id` (holder release), and
1027
+ `DELETE …?force=1&reason=…` (owner only). A conflict is an answer, not a transport error: `409` with the holder and expiry, so a
1028
+ client can show who is editing instead of parsing a failure. Role checks sit on M4 membership (default project treats a paired
1029
+ machine as worker); a non-member receives `404` and learns nothing about the project.
1030
+ - Two route tests: acquire/list/conflict/foreign-release/ordered-release, plus non-member `404` and `LOCK_FORCE_DENIED` for a
1031
+ worker trying to force release.
1032
+
1033
+ Still to wire: review and notification routes, the merge/review gate, and the panel views.
1034
+
1035
+ Verified: relay 186/186 (2 new lock route tests); gate GREEN twice.
1036
+
1037
+ ## [1.3.10-beta.17] — 2026-10-22
1038
+
1039
+ **M5 third primitive: notifications.** A local module and eight tests; no routes or SSE projection use it yet.
1040
+
1041
+ - `src/relay/project-notifications.mjs` — notifications for a project addressed to a member or broadcast, each with kind,
1042
+ subject, refs and `requires_action`. Every row is also a `notification.created` event for the live stream, and the list route
1043
+ is the catch-up path.
1044
+ - Semantics pinned by tests: the same (kind, primary ref, target) inside 60s is one row; different refs/kinds are distinct;
1045
+ overflow beyond 120 per minute collapses into one digest row that carries the count; unread is counted after a per-reader
1046
+ watermark; a watermark never moves backwards; paging is `since_ms` + limit; retention drops rows older than 7 days (or beyond
1047
+ 2000 per project).
1048
+ - The reader is a member identity (machine or operator), not a transport: an offline client reconnects and reads the same
1049
+ watermark it left.
1050
+
1051
+ Registered as an accepted library with its exit named (the notification routes).
1052
+
1053
+ M5 primitives are now all designed-and-implemented at module level: locks (beta.15), reviews (beta.16), notifications (beta.17).
1054
+ Still missing for M5: the routes, the merge/ref gates using them, and the panel views.
1055
+
1056
+ Verified: project-notifications 8/8; gate GREEN twice.
1057
+
1058
+ ## [1.3.10-beta.16] — 2026-10-22
1059
+
1060
+ **M5 second primitive: review tasks.** A local module and ten tests; the review routes and the merge gate do not call it yet.
1061
+
1062
+ - `src/relay/project-reviews.mjs` — a review points at a content-addressed `{kind, sha256, source_task_id, diffstat}` target and
1063
+ collects decisions from listed reviewers. Verdicts are records, not a mutable flag: `approve`/`changes_requested`/`comment`
1064
+ accumulate, approvals are counted per distinct reviewer, and reaching `required_approvals` moves the review to `approved`.
1065
+ - Safety rules the tests pin: self-review is refused unless an owner overrides it (and the override is its own event); reviewers
1066
+ must be maintainer/owner; only listed reviewers may decide; a reviewer cannot record the same verdict twice; a
1067
+ `changes_requested` closes the round and refuses a later approve; a due date expires a pending review and late decisions are
1068
+ refused; `approvedFor(project, sha256)` answers the merge gate for an exact patch.
1069
+ - Every state change is emitted as an event (`review.requested/decided/approved/changes_requested/expired/self_review_override`).
1070
+
1071
+ Registered as an accepted library with its exit named (the review routes + merge-requires-review gate).
1072
+
1073
+ Not in this slice: notifications (§4), the third M5 primitive.
1074
+
1075
+ Verified: project-reviews 10/10; gate GREEN twice.
1076
+
1077
+ ## [1.3.10-beta.15] — 2026-10-22
1078
+
1079
+ **M5 first primitive: branch and file locks.** A local module and eight tests; no routes or merge preflight call it yet.
1080
+
1081
+ - `src/relay/project-locks.mjs` — one exclusive holder per `(project, branch)` or `(project, path)`, leases with renewal, lazy
1082
+ expiry that frees the target and records `lock.expired`, owner-only force release with a mandatory reason recorded as
1083
+ `lock.force_released`, per-project/per-holder limits (20/10), and a `conflicts()` preflight that answers which live locks cover a
1084
+ branch or a set of paths.
1085
+ - Every mutation is returned as an event (`lock.acquired/renewed/released/expired/force_released`) so the project stream can carry
1086
+ it; the module holds no relay state and takes the clock as an injection.
1087
+ - Eight tests: acquire/list/release, LOCK_HELD naming the holder, expired re-entry with both events, renew ownership and
1088
+ non-revival, force-release permission and reason, limits, preflight conflicts for branch and paths, and malformed-input refusal.
1089
+
1090
+ Registered as an accepted library with its exit named (the lock routes and the merge preflight check).
1091
+
1092
+ Not in this slice: review tasks (§3) and notifications (§4), which are the next two M5 primitives.
1093
+
1094
+ Verified: project-locks 8/8; gate GREEN twice.
1095
+
1096
+ ## [1.3.10-beta.14] — 2026-10-22
1097
+
1098
+ **M4 third slice: the `/v1/projects` routes.**
1099
+
1100
+ - `GET /v1/projects` — the default project plus the projects this machine is a member of (device token).
1101
+ - `GET /v1/projects/:id` — snapshot with `last_event_seq`; a machine that is not a member gets `NOT_FOUND`, so a project's
1102
+ existence is not leaked. The default project stays readable for any paired machine because it is the implicit home of unnamed
1103
+ tasks.
1104
+ - `GET /v1/projects/:id/events?since_seq=N` — the incremental slice and `next_seq`, so a client can resume after a disconnect.
1105
+ - `POST /v1/projects` and `POST /v1/projects/:id/members` — operator token only (the dispatch credential), because these change
1106
+ who may act on shared state; the body's initial `members` array is seeded atomically with the project.
1107
+ - The routes authenticate per method and therefore sit above the shared device guard: an operator token is not a device token,
1108
+ and the earlier placement made every write answer `UNAUTHORIZED`.
1109
+ - Three HTTP tests: list/create/snapshot/events with a member, a non-member seeing `NOT_FOUND`, the operator requirement, and an
1110
+ unknown project answering `NOT_FOUND`.
1111
+
1112
+ Still missing: artifact/memory/decision mounts, the panel view, and multi-client e2e.
1113
+
1114
+ Verified: relay 184/184 (3 new route tests); gate GREEN twice.
1115
+
1116
+ ## [1.3.10-beta.13] — 2026-10-22
1117
+
1118
+ **M4 second slice: tasks belong to a project, and the room records it.** Relay state now owns a project room; no HTTP routes yet.
1119
+
1120
+ - `POST /v1/task` (internal `createTask`) accepts an optional `project_id`. Unnamed tasks land in the implicit `default`
1121
+ project, which is created lazily on first use; an explicit unknown project is refused with `BAD_REQUEST` rather than being
1122
+ created silently.
1123
+ - The project room records a `task.linked` event for every task, so a snapshot's `last_event_seq` moves with the work and a
1124
+ second client can apply it as an incremental event.
1125
+ - State exposes `projectSnapshot(projectId)` and `projectEvents(projectId, {sinceSeq})`; the task summary carries
1126
+ `project_id`; the replay path restores it from the ledger.
1127
+ - Three attribution tests: implicit default + linked event consistency, explicit project visible on the summary, and an unknown
1128
+ project refused without creating one.
1129
+
1130
+ Still missing, stated: the `/v1/projects` HTTP routes, artefact/memory/decision mounts, the panel view, and multi-client e2e.
1131
+
1132
+ Verified: project-attribution 3/3, project-room 8/8, relay 181/181; gate GREEN twice.
1133
+
1134
+ ## [1.3.10-beta.12] — 2026-10-22
1135
+
1136
+ **M4 first slice: the project room object.** A local module and eight tests; the relay routes and the panel do not call it yet.
1137
+
1138
+ - `src/relay/project-room.mjs` — a project aggregate (project id, name, origin, settings, members), the four roles
1139
+ (owner/maintainer/worker/observer), a deny-by-default action matrix (`read/dispatch/artifact/lock/memory/decide/merge/
1140
+ settings/members`), member add/remove/role-change as recorded events, a snapshot carrying `last_event_seq`, incremental
1141
+ events since a sequence, and per-member project listing.
1142
+ - Safety rules the tests pin: the last owner cannot be removed; a duplicate member is refused by name; an unknown project is
1143
+ refused rather than creating an empty one; the permission check returns the required and current role rather than a bare
1144
+ false; a role change records `{from,to}`.
1145
+
1146
+ Registered as an accepted library with its exit named (the `/v1/projects` routes and the room panel), because nothing reaches it
1147
+ yet.
1148
+
1149
+ Also in this tree since beta.11: the M1 live-acceptance evidence files under `docs/acceptance/1.3.10-beta.10/`.
1150
+
1151
+ Verified: project-room 8/8; gate GREEN twice.
1152
+
1153
+ ## [1.3.10-beta.11] — 2026-10-22
1154
+
1155
+ **M1 update path fixed and the full acceptance set recorded.**
1156
+
1157
+ The live update acceptance exposed two defects that made `update` unusable even though diag and restart already worked:
1158
+
1159
+ 1. **EXDEV on swap** — staging unpacked under `/tmp` (often tmpfs) and the swap is a rename into `/opt/w2m/app`, so the kernel
1160
+ refused with `EXDEV: cross-device link`. Staging now lives on the same filesystem as the app directory
1161
+ (`/opt/w2m/.maintenance-staging`).
1162
+ 2. **Ferry reported success for a failed action** — the ferry returning a result file only means the runner wrote one; the
1163
+ runner's own `status` was not inspected, so a failed update was reported as `ok`. The agent now reports
1164
+ `MAINTENANCE_FAILED` with the runner's code, and a test covers the shape.
1165
+
1166
+ Acceptance evidence now in the tree:
1167
+ - `docs/acceptance/1.3.10-beta.10/M1-LIVE-DIAG.json` (9 sections, 62,238 bytes, runner ok)
1168
+ - `M1-LIVE-RESTART.json` (relay restarted, new relay_id, result delivered)
1169
+ - `M1-LIVE-UPDATE.json` (download + sha256 + swap to 1.3.10-beta.10, no auto-restart)
1170
+ - `M1-LIVE-POSTUPDATE-DIAG.json` (after update + restart, a new diag still returns 59,581 bytes)
1171
+
1172
+ Known limitation unchanged and documented: a request restarting the agent itself kills the ferry before it reads the result
1173
+ file, so the accepted restart target is the relay; delayed restart is the next fix.
1174
+
1175
+ Verified: gate GREEN twice; actions/ferry unit tests pass.
1176
+
1177
+ ## [1.3.10-beta.10] — 2026-10-22
1178
+
1179
+ **M1 live acceptance: the maintenance channel works end to end on a real machine.**
1180
+
1181
+ The relay host (jp-shared) was bootstrapped and two signed requests were executed by the root runner:
1182
+
1183
+ - **diag** — the runner produced 9 sections (version/services/journal/disk/ports), the agent returned the task as `ok` with
1184
+ 62,238 bytes of result, and the runner's own result file confirms `status: ok`. Evidence:
1185
+ `docs/acceptance/1.3.10-beta.10/M1-LIVE-DIAG.json`.
1186
+ - **restart (rabbit)** — the relay was restarted by the runner, the agent survived it and delivered the task result, and the
1187
+ relay came back with a new `relay_id` and a fresh uptime. Evidence:
1188
+ `docs/acceptance/1.3.10-beta.10/M1-LIVE-RESTART.json`.
1189
+
1190
+ The run found four real defects, all fixed here:
1191
+
1192
+ 1. The agent's envelope builder hashes and sizes the output; the maintenance ferry passed an execution object without
1193
+ `stdout_sha256/stdout_bytes/stderr_sha256/stderr_bytes`, so every maintenance task ended as `crashed` **after** the action had
1194
+ already run. The ferry now fills them.
1195
+ 2. `--operator-key` without `--operator-key-id` claimed an empty key id, which the pin store correctly refused as a mismatch;
1196
+ an absent id now means "trust the key that arrives".
1197
+ 3. `bootstrap.sh` made the queue root-only, so the unprivileged agent could never enqueue a request; the queue is now
1198
+ `root:w2m 0770` (write but not execute) and `done` is `root:w2m 0750`.
1199
+ 4. Known limitation, documented rather than hidden: a request that restarts **the agent itself** kills the process still waiting
1200
+ for its result file, so that result is lost (the action still runs). Restarting the relay is unaffected, which is why the
1201
+ accepted case uses that target; the delayed-restart fix is next.
1202
+
1203
+ Also verified: gate GREEN twice; the local agent suite still passes with the ferry envelope fix.
1204
+
1205
+ ## [1.3.10-beta.9] — 2026-10-22
1206
+
1207
+ **M3 conflict contract completed: MERGE_CONFLICT now carries the conflict itself.**
1208
+
1209
+ When `git apply --3way` cannot apply a patch, the queue previously reported the changed files and the git message. It now also
1210
+ captures the state git left in the merge worktree (conflict markers included) as a `conflictArtifact`:
1211
+ `{ sha256, bytes, text }`. That is the evidence a reviewer needs to decide whether to resubmit against the new base or resolve
1212
+ by hand, and it is the same content-addressed shape the patch artifacts use.
1213
+
1214
+ The head still does not move on a conflict; the test asserts both the artifact (with conflict markers) and the unchanged head,
1215
+ and the composed M3 workflow test still passes.
1216
+
1217
+ Verified: merge-queue 7/7 (conflict artifact assertion included), m3-workflow 2/2; gate GREEN twice.
1218
+
1219
+ ## [1.3.10-beta.8] — 2026-10-22
1220
+
1221
+ **M3 composed: worktree -> patch -> merge queue, proven together.**
1222
+
1223
+ Two integration tests drive the three modules the way a real task pair would: both tasks start from the same base in isolated
1224
+ worktrees, each commits its change and produces a patch artifact, and the queue merges them one after the other. The disjoint
1225
+ case ends with both files changed and the branch advanced to the second merge; the same-file case ends with a structured
1226
+ MERGE_CONFLICT listing the file, the head still on the first approved commit, and the first task's change intact.
1227
+
1228
+ No runtime wiring yet: the agent's task handler does not create worktrees, and the relay does not run the queue. This release
1229
+ records that the modules agree with each other before either becomes reachable, which is the point of composing them now.
1230
+
1231
+ M1 real-machine acceptance remains pending: the relay host agent is offline and root SSH authentication is refused, so the
1232
+ privileged bootstrap has not run.
1233
+
1234
+ Verified: m3-workflow 2/2, merge-queue 7/7, worktree 5/5, patch-artifact 5/5; gate GREEN twice.
1235
+
1236
+ ## [1.3.10-beta.7] — 2026-10-22
1237
+
1238
+ **M3 third block: the merge queue.** A local module and seven real-git tests; the relay task path does not import it yet.
1239
+
1240
+ - `src/relay/merge-queue.mjs` — one queue per project+branch with a single merge slot (holder + TTL), items named by the
1241
+ patch sha256, `strict` and `replay` base policies, and terminal verdicts: MERGED (branch head advances), `already_merged`
1242
+ for a repeat submission, `BASE_MISMATCH` under strict, and a structured `MERGE_CONFLICT` with the changed files that never
1243
+ advances the head.
1244
+ - Application happens in a detached throwaway worktree, then the repository's branch and working tree are moved to the new
1245
+ commit — a merge that committed only in the worktree would leave the next reviewer looking at a head that never moved.
1246
+ - Seven tests cover merge-and-advance, idempotent re-submission, stale base, conflict (head unchanged), replay onto a moved
1247
+ head, merge-slot serialisation, and patch naming.
1248
+
1249
+ Registered as an accepted library with its exit named (the relay merge executor dispatch), because nothing reaches it yet.
1250
+
1251
+ Still pending, unchanged: the M1 real-machine acceptance (relay runs beta.5 and carries the maintenance field live, but the
1252
+ relay host's agent is offline and root SSH authentication has not been restored), and the M2/M3 live runs.
1253
+
1254
+ Verified: merge-queue 7/7; the rest of the suite is unchanged; gate GREEN twice.
1255
+
1256
+ ## [1.3.10-beta.6] — 2026-10-21
1257
+
1258
+ **M3 first two blocks: isolated worktrees and a patch artifact contract. Plus the grant replay e2e.**
1259
+
1260
+ - `src/agent/worktree.mjs` — one git worktree per task, created detached at the caller's `base_commit`; hooks disabled,
1261
+ commit identity fixed, a porcelain snapshot before and after, commit-if-changed, `format-patch` relative to the base, and
1262
+ a removal that reports failure instead of leaking a directory. Unknown base commit and missing repository are named refusals.
1263
+ - `src/agent/patch-artifact.mjs` — the transportable result: sha256 over the patch bytes, byte count, base/head commits,
1264
+ sorted changed files, inline/blob mode, and `no_change` as a successful outcome for an empty patch. Non-ASCII patches are
1265
+ measured in UTF-8 bytes, not characters.
1266
+ - Five real-git tests for the worktree lifecycle and five for the artifact contract.
1267
+ - `test/agent.test.mjs` gains the grant **replay** e2e: the same signed grant is used twice, the first run executes, the second
1268
+ is refused with `GRANT_REPLAY` — the use ledger is what makes "single-use" true across processes.
1269
+
1270
+ Not in this slice (stated, not implied): the merge queue itself (strict/replay bases, merge-slot serialization,
1271
+ `MERGE_CONFLICT`), review tasks and locks (M5), and the project room (M4). The M1 real-machine acceptance is also still pending:
1272
+ the relay was upgraded to beta.5 and the maintenance field is verified live, but the relay host's agent is offline and root SSH
1273
+ authentication is refused, so the privileged bootstrap has not run.
1274
+
1275
+ Verified: agent 159/157 (replay e2e included), relay 181/181, grant 10/10, grant-gate 6/6, maintenance 17/17, ferry 4/4,
1276
+ worktree 5/5, patch-artifact 5/5, layer-model 24/24, claims 47/47; gate GREEN twice.
1277
+
1278
+ ## [1.3.10-beta.5] — 2026-10-21
1279
+
1280
+ **M2 grant e2e: the explicit-authorisation path is proven end to end.**
1281
+
1282
+ Four fake-relay tests now drive the real agent process:
1283
+
1284
+ - an empty allow-list executes a command **only** because a valid grant authorises that exact argv (the envelope is `ok` and
1285
+ the stdout hash matches);
1286
+ - the same command with no grant is still `COMMAND_NOT_ALLOWED`;
1287
+ - a grant that signed a different argv is refused with `GRANT_ARGV_MISMATCH`;
1288
+ - a grant that names another machine is refused with `GRANT_WRONG_MACHINE`.
1289
+
1290
+ Two integration defects the e2e exposed, both fixed: the per-stage allow-list re-check ran again after step 5, so a grant that
1291
+ had authorised the task was refused at the stage boundary; and the matcher's result carries `allowed` (not `ok`), so both the
1292
+ gate result and the stage override had to use that shape or the refusal fell back to `COMMAND_NOT_ALLOWED`.
1293
+
1294
+ Verified: agent 158/156 (4 new e2e cases), relay 181/181, grant 10/10, grant-gate 6/6, maintenance 17/17, ferry 4/4,
1295
+ layer-model 24/24, claims 47/47; gate GREEN twice.
1296
+
1297
+ ## [1.3.10-beta.4] — 2026-10-21
1298
+
1299
+ **M1 bootstrap tooling: one privileged step, then the channel is remote.** No runtime behaviour changes.
1300
+
1301
+ - `deploy/maintenance/bootstrap.sh` (Linux, run once as root) — installs the fixed-action path unit/service, creates the
1302
+ queue/done/claimed directories with root ownership, optionally pins the operator public key, and enables the watcher. It
1303
+ refuses to run without `--machine-id` and refuses a tree where `bin/w2m-maintenance.mjs` is absent.
1304
+ - `deploy/maintenance/bootstrap.ps1` (Windows, run once as Administrator) — same shape via a SYSTEM scheduled task; it
1305
+ substitutes the app directory and machine id into the task XML rather than editing the shipped file.
1306
+ - `docs/COLLAB/M1-ACCEPTANCE-RUNBOOK.md` — the real-machine acceptance cases (diag/restart/update/rollback plus four named
1307
+ refusals), what "green" means, and the limitations that must appear in the release note (privileged bootstrap, TOFU pinning,
1308
+ no automatic recovery if a restarted machine cannot be reached).
1309
+
1310
+ Both scripts install **only** the fixed update/restart/diag runner; neither installs a shell, and neither touches the
1311
+ allow-list. The design's security boundary is unchanged: the agent ferries a signed request, the root runner decides.
1312
+
1313
+ Honest gap, unchanged: the M1 real-machine acceptance run itself still needs a machine with a privileged bootstrap and a
1314
+ relay carrying the maintenance fields; and the M2 grant path still lacks a fake-relay e2e case. Both are next.
1315
+
1316
+ Verified: gate GREEN twice; pack carries the new scripts and document.
1317
+
1318
+ ## [1.3.10-beta.3] — 2026-10-21
1319
+
1320
+ **M2 first slice: task-level grants become the explicit authorisation channel.** Design: `docs/COLLAB/M2-TASK-GRANT.md`.
1321
+
1322
+ - `src/maintenance/grant.mjs` — grant protocol: task binding, named machine ids, argv hash + expanded argv for the operator
1323
+ to read, max uses, TTL capped at 10 minutes, nonce, and constraint fields that may only tighten the task (mode/cwd/write/
1324
+ write_scope). Verification has a named refusal for every distinct failure: wrong machine, wrong task, argv mismatch,
1325
+ expired, future, TTL too long, replay, uses exhausted, revoked key, unknown key, bad signature, constraint widening.
1326
+ - `src/agent/grant-gate.mjs` — the agent-side gate: pins the operator key on first sight, verifies the grant against the
1327
+ concrete argv, and persists use counts so a spent grant stays spent across restarts. A corrupt use ledger is never read as
1328
+ "nothing used yet".
1329
+ - The agent asks the grant gate **before** falling through to the allow-list refusal, and reports a grant refusal with its own
1330
+ `GRANT_*` code instead of folding it into `COMMAND_NOT_ALLOWED`.
1331
+ - The relay accepts an optional `grant` on `POST /v1/task` (8 KiB cap, `grant_id` + 64-hex `argv_hash` checked), carries it on
1332
+ the offer, writes `grant_summary` into the ledger and exposes `grant_id` on the task summary.
1333
+
1334
+ Honest gap: the grant path has unit coverage (protocol 10, gate 6) and the existing agent/relay suites are unchanged-green,
1335
+ but there is no fake-relay e2e yet for "empty allow-list executes under a grant". That case and the real two-machine run land
1336
+ in the next slice together with the M2 acceptance evidence.
1337
+
1338
+ Verified: agent 154/152, relay 181/181, grant 10/10, grant-gate 6/6, maintenance 17/17, ferry 4/4, layer-model 24/24; gate
1339
+ GREEN twice.
1340
+
1341
+ ## [1.3.10-beta.2] — 2026-10-21
1342
+
1343
+ **M1 maintenance channel: relay passthrough + agent ferry.** The remote loop now has both ends.
1344
+
1345
+ - The relay accepts an optional `maintenance` request and `operator_key` on `POST /v1/task` (operator token already required),
1346
+ registers the key once (`operator.key.registered` in the ledger, replayed on restart), carries the request on the offer, and
1347
+ exposes `maintenance_request_id` on the task summary.
1348
+ - `src/agent/maintenance-ferry.mjs` — the agent pins the operator key on first sight (TOFU, rotation chain thereafter), verifies
1349
+ the request against the pin, writes it atomically into the queue the root runner watches, waits for the runner's result file,
1350
+ and reports it as the task result. A refusal is a named value (`MAINTENANCE_NO_PINNED_KEY`, `MAINTENANCE_BAD_SIGNATURE`,
1351
+ `MAINTENANCE_RUNNER_TIMEOUT`, ...), never a thrown error, so the task result says why.
1352
+ - The agent never gains the privilege to run update/restart/diag: the root runner (beta.1) remains the only executor.
1353
+ - Layer model declares `maintenance` as a capability layer with an `agent -> maintenance` edge, so the new dependency is
1354
+ explained rather than excused.
1355
+
1356
+ Known gap, stated: no fake-runner agent-e2e test for the ferry yet (the ferry's own four cases cover pin/verify/queue/refusal);
1357
+ the end-to-end case lands with the real-machine acceptance run in beta.3.
1358
+
1359
+ Verified: gate GREEN twice with the bundled node on PATH: 2731 tests / 2717 pass / 0 fail / 14 skip in 135 files
1360
+ (agent 154/152, relay 181/181, maintenance 17/17, ferry 4/4, layer-model 24/24, claims 44/44).
1361
+
1362
+ ## [1.3.10-beta.1] — 2026-10-21
1363
+
1364
+ **M1 maintenance channel, first implementation slice.** Design: `docs/COLLAB/M1-MAINTENANCE.md` (the design series names this slice v1.3.2-beta; it ships as 1.3.10-beta.1 because 1.3.9 was already published as a stable release and versions must stay ordered).
1365
+
1366
+ The maintenance channel lets an operator authorise exactly one of three fixed actions on one machine (update, restart, diag)
1367
+ with an Ed25519-signed request, executed by a root/SYSTEM runner that reads a queue the unprivileged agent may write to. An
1368
+ allow-list entry is not needed: the signature is the authorisation, and widening the allow-list is never the answer.
1369
+
1370
+ ### What this slice contains
1371
+
1372
+ - `src/maintenance/protocol.mjs` — request envelope, canonical signing bytes, parameter whitelists for the three actions,
1373
+ and the complete refusal vocabulary (wrong machine, expired, future, TTL too long, replay, revoked key, unknown key,
1374
+ bad signature, bad action, bad params, too large, missing signature). TTL is capped at 10 minutes and only the pinned
1375
+ operator key can sign.
1376
+ - `src/maintenance/operator-key.mjs` — Ed25519 key generation, relay registration payload, trust-on-first-use pinning with a
1377
+ rotation chain (a new key must be signed by the pinned one), revocation, and a corrupt pin store that refuses instead of
1378
+ trusting nothing.
1379
+ - `src/maintenance/runner.mjs` — queue scanning, claim-before-verify, signature verification, persisted replay sets
1380
+ (request id + nonce), idempotent re-runs, and a result file for every request it looks at, refusals included.
1381
+ - `src/maintenance/actions.mjs` — the three fixed actions with no shell interpolation: diag (fixed read-only command set,
1382
+ capped output), restart (fixed unit order and health waits), update (https download, sha256 verify, staged extraction,
1383
+ version check inside the artifact, backup, atomic swap, health check, automatic rollback).
1384
+ - `bin/w2m-maintenance.mjs` — the root entry point (once/watch, pin bootstrap, root guard).
1385
+ - `deploy/server/w2m-maintenance.{service,path}` and `deploy/windows/w2m-maintenance.xml` — the runner as a systemd path
1386
+ unit / SYSTEM scheduled task.
1387
+
1388
+ ### What this slice deliberately does not contain
1389
+
1390
+ The relay routes that distribute the operator public key and the agent-side ferry that carries a request from a task offer
1391
+ to the queue (and the result back) are the next slice (1.3.10-beta.2). Until then the channel is exercised by placing a
1392
+ signed request in the queue directory directly — which is also the offline/bootstrap path the design keeps.
1393
+
1394
+ Tests: 17 new cases cover the protocol, pinning/rotation/revocation and the runner's refusal and replay behaviour. The
1395
+ destructive actions are not executed in tests; they are covered by the real-machine acceptance runbook (M7 evidence).
1396
+
1397
+ Verified: gate GREEN twice.
1398
+
1399
+ ## [1.3.9] — 2026-10-21
1400
+
1401
+ **Design-first release: the collaboration line is specified, not yet implemented.**
1402
+
1403
+ This release ships the design specification for the multi-machine / multi-client collaboration capability, and no behaviour change: the
1404
+ plugin, relay and agent are byte-identical in function to 1.3.1. It exists so the plan is reviewable, versioned, and citable before any
1405
+ of it is built.
1406
+
1407
+ ### What is in this release
1408
+
1409
+ - `docs/MULTI-MACHINE-COLLABORATION-PLAN.md` — capability audit (what exists today is dispatch + aggregation, not collaboration),
1410
+ root causes (missing execution-authorization model, missing workspace model, missing shared-object model), and the M1–M7 module plan.
1411
+ - `docs/COLLAB/M1-MAINTENANCE.md` — v1.3.2-beta design: signed maintenance requests executed by a root/SYSTEM unit through a queue
1412
+ directory; remote update/restart/diag without SSH and without widening the allow-list.
1413
+ - `docs/COLLAB/M2-TASK-GRANT.md` — v1.3.3-beta design: operator-signed per-execution grants (argv hash, machine ids, TTL, task binding),
1414
+ agent-side verification, ledger audit, revocation; the allow-list stops being the only execution authority.
1415
+ - `docs/COLLAB/M3-GIT-NATIVE-TASKS.md` — v1.3.4-beta design: per-task git worktrees, patch artifacts as the transportable result, a
1416
+ serialized merge queue, and MERGE_CONFLICT as a first-class outcome.
1417
+ - `docs/COLLAB/M4-PROJECT-ROOM.md` — v1.3.5-beta design: relay-side project objects (members, board, artifacts, memory, decisions,
1418
+ refs), snapshot + incremental event protocol, role-based permissions.
1419
+ - `docs/COLLAB/M5-MULTI-CLIENT-PRIMITIVES.md` — v1.3.6-beta design: branch/file locks, review tasks with findings and merge gating,
1420
+ and notifications with delivery guarantees and read watermarks.
1421
+ - `docs/COLLAB/M6-CROSS-NETWORK-RELIABILITY.md` — v1.3.7-beta design: relay as the authoritative channel, chunked/resumable blobs,
1422
+ agent result outbox, and P2P demoted to opportunistic acceleration.
1423
+ - `docs/COLLAB/M7-METRICS-ACCEPTANCE.md` — v1.3.8-beta design: every capability must be triggerable by a real task, negative tests are
1424
+ mandatory, metrics are computed by the relay, and evidence is checked by pre-flight.
1425
+
1426
+ ### What is explicitly not in this release
1427
+
1428
+ No code for M1–M7. The three model-level designs (v1.4.0 execution-authorization, v1.4.1 task, v1.4.2 collaboration) are not written yet.
1429
+ The next stable release that changes behaviour will be the one that implements M1 + M2 (the remote-operations loop), with real-machine
1430
+ acceptance evidence recorded as described in M7.
1431
+
1432
+ Verified: gate GREEN twice on this tree; the package differs from 1.3.1 only in documentation and version metadata.
1433
+
1434
+ ## [1.3.1] — 2026-10-21
1435
+
1436
+ **A red Compat and a green local gate: the loader-entry test was the gap.**
1437
+
1438
+ v1.3.0 shipped with a failing Compat job: `test/loader-entry.test.mjs` still listed the fifteen tools it expected the packed
1439
+ artifact to register, and the artifact registers sixteen (`w2m_reduce`). The gate ran from the tree, where that test's expectation
1440
+ matched the *other* fixture it derives from, so the disagreement only appeared in the packed-copy mount. The expectation now names
1441
+ `w2m_reduce`, and this patch release is the artifact that matches it.
1442
+
1443
+ No behaviour changes: the fix is the expectation, not the tool surface. beta.2 completes the fix: `EXPECTED_TOOLS` in that same test is the list the loader gate compares against, and beta.1 patched only the other fixture. v1.3.0 is superseded.
1444
+
1445
+ This is the stable fix for v1.3.0's red Compat: the loader gate and the packed artifact now agree at sixteen tools. v1.3.0 is superseded on every channel; the artifact behaviour is unchanged. Verified: gate GREEN twice (2710 tests / 2696 pass / 0 fail / 14 skip in 133 files), and Compat on this tag is green before it is called stable.
1446
+
1447
+ ## [1.3.0] — 2026-10-21
1448
+
1449
+ The first stable release of the collaboration line. It consolidates the four betas that followed 1.2.7 and changes no protocol in a
1450
+ way that a 1.2.7 peer cannot ignore; every addition is additive and named.
1451
+
1452
+ ### Data plane
1453
+
1454
+ **A reduce phase you can actually run.** `w2m_reduce` dispatches a reduce task only from complete data: it refuses unless every
1455
+ declared shard reported as a blob and the named reducer reported (`W2M_REDUCE_NOT_READY` with the missing indexes), never picks a
1456
+ reducer, and refuses inline inputs that cannot be carried to another machine. The relay validates, stores, ledgers and offers
1457
+ `reduce_of` + `reduce_inputs`; the agent fetches those content-addressed blobs before the reduce runs and exposes
1458
+ `W2M_REDUCE_INPUTS_DIR` / `W2M_REDUCE_MANIFEST` / `W2M_REDUCE_OF`; a failed fetch refuses the whole task rather than reducing a
1459
+ partial fleet.
1460
+
1461
+ **A suggestion that dispenses no action.** `w2m_wait` carries `reduce_suggestion`: ready only when every declared shard reported and
1462
+ a reducer was named, the missing indexes otherwise, the candidate machines, and the literal next step. It is derived, never stored,
1463
+ and it dispatches nothing.
1464
+
1465
+ **Message ids survive a restart, artifacts are one size everywhere.** Chat ids carry a random per-process relay epoch
1466
+ (`msg-<epoch>-<seq>`) so a restart cannot recycle an id a client persisted; the blob transport frame is 16 MiB, matching the agent's
1467
+ dataflow hard cap, instead of 1 MiB against it.
1468
+
1469
+ ### Collaboration
1470
+
1471
+ **The panel shows the scheduler's own buckets.** The fleet-chat panel renders 准备 / 进行 / 等待 / 阻塞 counts beside the relay's task
1472
+ list, derived from the relay's `status` and `waiting_on`; read-only, no dispatch.
1473
+
1474
+ **Conflicts name their next step.** `w2m_memory append` returns `merge_hint` with a 409: `re-read-then-append`, `behind_by`, the
1475
+ unseen count, the sample size and the literal instruction to re-read, compare and append against the current revision -- never an
1476
+ automatic merge.
1477
+
1478
+ **Decisions leave a trail.** `w2m_council fleet-decide` reports `trail` (`appended` / `refused` / `error` / `not-written`), and the
1479
+ test asserts that status instead of implying a record exists.
1480
+
1481
+ ### Verified
1482
+
1483
+ Gate GREEN twice, 15 of 15: 2710 tests / 2696 pass / 0 fail / 14 skip in 133 files. Two-machine live runs over the real relay cover
1484
+ the turn state machine (ask/await over SSE with a refusal), dependency scheduling, and the missing-shard refusal, plus the panel and
1485
+ proxy behaviour above.
1486
+
1487
+ ### Known limits, stated
1488
+
1489
+ - `pipeline` keeps `index_total` at 1; blob map results come from plain commands above the inline cap.
1490
+ - The reduce suggestion is advisory: the caller dispatches the reduce (this release does not let the relay decide to run a command).
1491
+ - The composer panel does not render the board buckets on a host without the Connection service; the tools still work.
1492
+
1493
+ ## [1.2.11-beta.1] — 2026-10-21
1494
+
1495
+ A beta by policy: GitHub prerelease only, never npm.
1496
+
1497
+ ### The task board in the composer panel
1498
+
1499
+ The fleet-chat panel already listed the relay's tasks; it now also shows the four buckets the scheduler uses — 准备 / 进行 / 等待 / 阻塞 — derived from the relay's own `status` and `waiting_on`, never from a classification of its own. Read-only: the panel dispatches nothing.
1500
+
1501
+ ### Merge hint on memory conflicts
1502
+
1503
+ `w2m_memory append` returns `merge_hint` with a 409: state `re-read-then-append`, `behind_by`, the number of unseen records, the sample size, and the literal next step (re-read, compare, append against the current revision — do not merge unseen records). Pure function, no automatic merge.
1504
+
1505
+ ### Council decisions leave a trail
1506
+
1507
+ `w2m_council fleet-decide` reports `trail` (`appended` / `refused` / `error` / `not-written`) instead of implying a record exists; the test now asserts that status.
1508
+
1509
+ Verified: gate GREEN twice, 2704 tests / 2690 pass / 0 fail / 14 skip in 132 files; client-bundle 25/25 after the board line; the merge-hint and trail tests are new.
1510
+
1511
+ ## [1.2.10-beta.1] — 2026-10-21
1512
+
1513
+ A beta by policy: GitHub prerelease only, never npm. This one makes the reduce phase executable.
1514
+
1515
+ ### Reduce execution (caller-dispatched)
1516
+
1517
+ `w2m_reduce` dispatches a reduce only from complete data: it refuses unless every declared shard reported as a blob and the named reducer reported (`W2M_REDUCE_NOT_READY`, with the missing indexes). It never picks a reducer and never invents inputs; inline results are refused (`W2M_REDUCE_INLINE_INPUT`) because they cannot be carried to another machine.
1518
+
1519
+ The relay validates and carries `reduce_of` + `reduce_inputs` (at most 16 artifacts, sha256 only) on the task, the ledger and the offer; the agent fetches those content-addressed blobs BEFORE running the reduce and exposes `W2M_REDUCE_INPUTS_DIR` / `W2M_REDUCE_MANIFEST` / `W2M_REDUCE_OF` to the command. A failed fetch refuses the task (`REDUCE_INPUT_FETCH_FAILED`) rather than reducing half the fleet.
1520
+
1521
+ Verified: gate GREEN twice, 2701 tests / 2687 pass / 0 fail / 14 skip in 131 files; new agent e2e proves the reduce reads the exact artifact bytes; the tool has a dispatch test and a refusal test.
1522
+
1523
+ ## [1.2.9-beta.1] — 2026-10-21
1524
+
1525
+ A beta by policy: **GitHub prerelease only, never npm.** The theme is the data plane and the collaboration plane: the design is
1526
+ written down (`docs/DATA-PLANE-COLLABORATION.md`) and this release ships its first slice.
1527
+
1528
+ ### `reduce_suggestion` on `w2m_wait`
1529
+
1530
+ Every terminal wait now answers three questions without acting on them: is the data complete, who could reduce it, and what the
1531
+ caller is supposed to do next (`dispatch the reduce yourself; this tool does not run it`). It is a pure function over the machine
1532
+ facts the aggregate already publishes:
1533
+
1534
+ - **ready** only when every declared shard reported and a reducer was named;
1535
+ - **missing indexes** when a shard never reported (an expired lease cannot claim one);
1536
+ - **candidates** listing the machines that reported, with the reason;
1537
+ - a named reducer that never reported is refused by name (`REDUCER_UNREPORTED`) rather than silently replaced -- changing the
1538
+ machine changes what the reduce runs against;
1539
+ - a caller that named no reducer gets `REDUCER_MISSING` and candidates, never an invented choice.
1540
+
1541
+ Nothing is stored and nothing is dispatched. Two readers of the same aggregate get the same suggestion, and reading does not
1542
+ change relay state.
1543
+
1544
+ ### The design document states the open question
1545
+
1546
+ Caller-dispatched reduce (current direction) versus auto-reduce in the relay. The design leans caller-dispatched, because a relay
1547
+ that decides to run a command is the boundary every other decision avoids; a beta that flipped that default would be the wrong
1548
+ place to discover it. The document also names the next collaboration slices: conflict-merge UI, council decision trail to shared
1549
+ memory, and the board in the composer panel.
1550
+
1551
+ ### Verified
1552
+
1553
+ Gate GREEN twice: 2699 tests / 2685 pass / 0 fail / 14 skip in 131 files. New: `suggestReduce` unit tests (7) covering complete,
1554
+ partial-with-missing, no-reducer, unreported-reducer and mixed inline+blob inputs; `w2m_wait` carries the suggestion for every
1555
+ aggregate.
1556
+ ## [1.2.8-beta.1] — 2026-10-21
1557
+
1558
+ A beta, by policy: **not published to npm**. It is a GitHub prerelease, installable by a developer who asks for it.
1559
+
1560
+ ### Message ids are unique across a relay restart
1561
+
1562
+ The relay's chat sequence resets on restart, so a bare `msg-000001` named a different message after every deploy. Clients persist
1563
+ those ids -- an open turn, a read watermark, `reply_to` -- and live testing caught exactly that: a Windows `ask` recorded a turn, the
1564
+ relay restarted, a new message reused the id, and the wait settled the OLD turn as `answered_after_timeout` while the reply was
1565
+ fresh. Message ids now carry a random per-process epoch (`msg-<epoch>-<seq>`), so a restart cannot recycle an id and old clients that
1566
+ render ids verbatim keep working.
1567
+
1568
+ ### One size for "how large may one artifact be"
1569
+
1570
+ The blob transport frame was 1 MiB while the agent's dataflow hard cap is 16 MiB, so an artifact between the two was accepted by the
1571
+ agent and then failed its upload (`BLOB_UPLOAD_FAILED`) on a relay that looked fine. `BLOB_FRAME_BYTES` is now 16 MiB, matching the
1572
+ agent, while every JSON route keeps the 64 KiB frame limit.
1573
+
1574
+ ### Verified
1575
+
1576
+ Gate GREEN twice: 2691 tests / 2677 pass / 0 fail / 14 skip in 130 files. New tests: an id is unique across two store instances
1577
+ (same sequence, different process), an explicit epoch is honoured, and a 2 MiB blob survives the transport intact.
1578
+
1579
+ ### Still open (unchanged from 1.2.7)
1580
+
1581
+ - `pipeline` keeps `index_total` at 1; blob map results come from plain commands above the inline cap.
1582
+ - macmini runs 1.2.6 from its own checkout and needs a `git pull` + restart for the plain-command blob behaviour.
1583
+ ## [1.2.7] — 2026-10-21
1584
+
1585
+ **The scheduler enforces the graph, a wait survives a restart and a dropped stream, and a partial fleet can no longer call itself
1586
+ a complete reduce.**
1587
+
1588
+ ### Turns are a state machine on disk
1589
+
1590
+ A question written with `action:'ask'` becomes a persisted turn (`ask_id`, deadline). `await` attaches to that record, so a process
1591
+ restart does not lose the question or turn a late answer into silence; a late answer is recorded as `answered_after_timeout`. The
1592
+ wait prefers the relay's event stream, re-subscribes when the stream drops, and polls only when streaming is unavailable. A reply
1593
+ now carries `reply_state: 'answered'|'refused'`, so a refusal is a state the waiter reads rather than wording it guesses.
1594
+
1595
+ ### Dependencies block dispatch, and release when their prerequisite settles
1596
+
1597
+ A task with `depends_on` is not offered: its leases are `waiting`, which redelivery skips and the sweeper ignores. When every
1598
+ prerequisite is settled -- **by a result, an expiry or a refusal** -- `_releaseDependents` moves those leases to offers. The graph
1599
+ edges and `assignee` are written into the task ledger, because live testing showed a restarted relay losing them and keeping a
1600
+ waiting task waiting forever. `assignee` is enforced, not recorded: only that machine gets a lease, and an unknown assignee is
1601
+ refused by name. `GET /v1/tasks` now publishes a derived `status` (`waiting`/`active`/`done`/`failed`/`cancelled`) and
1602
+ `waiting_on`, so the board and the scheduler cannot disagree.
1603
+
1604
+ ### Dataflow: an expected shard count, and a reduce that refuses partial input
1605
+
1606
+ `buildReducePlan` takes `expectedIndexTotal` from the task (`index_total`) and refuses `MISSING_SHARD` (with the missing indexes),
1607
+ `MAP_INDEX_MISSING` and `MAP_INDEX_OUT_OF_RANGE`; only machines that actually reported can claim a shard, so an expired lease no
1608
+ longer makes a partial run look complete. Two production gaps were found by live testing and closed with it:
1609
+
1610
+ - **A large plain-command stdout was never a blob.** The blob path lived only in the pipeline loop, so a split/mapreduce map result
1611
+ was always inline and a reduce plan could never see what it reduces. Any command output above the inline cap now uploads a blob
1612
+ exactly as a pipeline stage does (best effort: a failed upload keeps the command successful and says why).
1613
+ - **The relay's own frame limit made the blob channel unusable.** `MAX_FRAME_BYTES` (64 KiB) equals the inline threshold, so every
1614
+ blob was by construction larger than the frame cap and a real upload failed with `FRAME_TOO_LARGE` while the fake relay in tests
1615
+ accepted it. Blob PUTs now get a bounded 1 MiB frame (`BLOB_FRAME_BYTES`); every JSON route keeps the 64 KiB cap.
1616
+ - The aggregate now publishes `blob_sha256`, `output_mode`, `stdout_head` and `stdout_bytes` per machine: `w2m_wait` used to read
1617
+ `aggregate.detail.machines`, a shape the production `/v1/tasks` response never carries, so the reduce plan was unreachable
1618
+ outside tests.
1619
+
1620
+ ### Verified
1621
+
1622
+ Gate GREEN twice, 15 of 15: 2687 tests / 2673 pass / 0 fail / 14 skip in 130 files.
1623
+
1624
+ Two-machine live runs on Windows (ToFuture) and jp-shared over the real relay:
1625
+
1626
+ - **await over SSE**: Windows asks, jp-shared replies `reply_state:'refused'`; the wait returns `state=refused, via=stream`, and the
1627
+ turn is settled on disk.
1628
+ - **dependency chain**: B with `depends_on: [A]` is `waiting`, A settles, B is released (observed `status=active` with offers).
1629
+ - **missing shard**: a split task with `index_total: 5` whose map results are blobs is refused as
1630
+ `MISSING_SHARD {missing:[1,3,4], expectedIndexTotal:5}` with `dataflow_degraded: true` -- the first live exercise of the blob
1631
+ channel end to end.
1632
+
1633
+ Both live-testing defects above (release only on a result; graph edges missing from the ledger) were found by these runs and are
1634
+ covered by tests now.
1635
+
1636
+ ### Limits, stated
1637
+
1638
+ - A pipeline task runs the whole chain per machine, so `index_total` stays 1 for it; blob map results come from plain commands above
1639
+ the inline cap.
1640
+ - The blob transport frame is 1 MiB while the agent's dataflow hard cap is 16 MiB: a single artifact above 1 MiB fails its upload
1641
+ explicitly (`BLOB_UPLOAD_FAILED`) rather than being split across frames.
1642
+ - The jp-shared agent was upgraded to this build; macmini runs 1.2.6 from its own checkout and needs a `git pull` + restart to get
1643
+ the plain-command blob behaviour.
1644
+ ## [1.2.6] — 2026-10-20
1645
+
1646
+ **Security surface: signatures, a permission matrix, and output that no longer carries secrets — plus the reliability the chat
1647
+ needed before it could be trusted with real messages.**
1648
+
1649
+ ### Command signatures
1650
+
1651
+ The agent signs `{task_id, argv_hash, machine_id, ts}` with an Ed25519 key derived from its device key (`DeviceKey` is X25519 and
1652
+ cannot sign; deriving keeps one secret per machine). The envelope carries `signature {alg, value, public_key, ts}`, and
1653
+ `w2m_wait` reports every machine as `signed`, `unsigned` or `bad_signature`, with a task-level `signature_warning`. A tampered
1654
+ `argv_hash` verifies as `bad_signature` -- tested at the unit level and end to end through a real agent run.
1655
+
1656
+ Honest limit: the public half travels inside the signature. That proves the result matches the key it carries, not that the key
1657
+ belongs to the machine. Binding it to the relay's device record is the remaining step for non-repudiation across machines, and it
1658
+ is named here rather than implied.
1659
+
1660
+ ### Permission matrix
1661
+
1662
+ `src/util/policy-matrix.mjs` is the single table: `observer < worker < owner` against `read_private | vote | join | dispatch |
1663
+ approve | manage_members`. A refusal is `FORBIDDEN_ACTION` naming the action, the level required and the level held. The relay
1664
+ enforces it where a role actually acts: an observer may read the fleet flow and may not post into it. Unknown means worker, never
1665
+ owner.
1666
+
1667
+ ### Output redaction
1668
+
1669
+ `stdout_head`/`tail` and `stderr_head`/`tail` are filtered for API keys, `npm_`/`github_pat_` tokens and private-key blocks,
1670
+ replaced in place with `[redacted:class]` so line numbers survive. Hashes and blobs are never touched: they are evidence of the raw
1671
+ bytes. `redactOutput: false` is allowed and marks the envelope `unredacted: true`.
1672
+
1673
+ ### End-to-end encrypted chat
1674
+
1675
+ Sending encrypts with AES-256-GCM under a group key derived as `HKDF(fleetKey, 'fleet-chat-v1')`; the relay stores ciphertext and
1676
+ cannot open it. Reading decrypts what the key covers and marks the rest `encrypted: true` with a `decrypt_error`. Plaintext
1677
+ messages keep working, so older clients neither break nor are lied to. The relay validates the encryption envelope's shape and
1678
+ refuses a malformed one (`BAD_ENCRYPTION`), and cannot search content -- which is the point, and is written down as the cost.
1679
+
1680
+ ### Reliability
1681
+
1682
+ - **Role persistence**: `member.role` ledger entries replay on restart, so a relay restart no longer silently demotes every owner.
1683
+ Verified live: the role that was lost before the deploy came back after it.
1684
+ - **Chat cursor**: `list` continues where the last page ended (`since_ms` from a persisted per-session cursor) and advances only
1685
+ over what was actually read.
1686
+ - **Offline spool**: a send that cannot reach the relay is written to disk with an idempotency key and replayed on the next call;
1687
+ a replay that the relay already received returns the first message instead of posting twice.
1688
+
1689
+ ### Verified
1690
+
1691
+ Gate GREEN twice, 15 of 15: 2666 tests / 2652 pass / 0 fail / 14 skip in 130 files. New suites: chat-crypto (8),
1692
+ envelope-signature (6), policy-matrix (9); agent 152, relay 173, tools 132, fleet-messages 14.
1693
+
1694
+ Relay redeployed twice from hash-verified tarballs, with state backups, and the new surfaces exercised against the live relay:
1695
+ encrypted message stored as ciphertext and read back, malformed encryption refused with 400, roles restored after restart.
1696
+
1697
+ ### Known gaps
1698
+
1699
+ - The GitHub Packages mirror still holds an orphan `1.2.4` published by a diagnostic dispatch; removal needs a token with package
1700
+ administration (this one sees 404 on the package API).
1701
+ - The signing public key is not yet bound to the relay's device record (see above).
1702
+ - Two-machine end-to-end runs of the new chat surface are still not done; the HTTP and tool surfaces are tested against fakes and
1703
+ exercised once against jp-shared.
1704
+ ## [1.2.5] — 2026-10-20
1705
+
1706
+ **Who is in the fleet, what they have read, who was named, and a retry that cannot double-post.**
1707
+
1708
+ ### Members and roles
1709
+
1710
+ `GET /v1/fleet/members` lists every machine with its name, role, join time, presence and last-seen time. `PUT
1711
+ /v1/fleet/members/{id}` changes a role and requires the **operator** token, so the route sits with dispatch rather than behind the
1712
+ device gate. Roles are `owner | worker | observer` and live in their own table: a device record is written by pairing and by
1713
+ reconnect, and a role is an operator decision that must survive both. An unknown machine is a `worker`, never an `owner` -- a
1714
+ re-pair cannot gain authority. The last owner cannot be demoted, because then nobody could readmit a machine.
1715
+
1716
+ ### Read watermarks
1717
+
1718
+ `PUT /v1/fleet/read/{session}` records how far this machine has read a conversation (its own token decides which machine);
1719
+ `GET /v1/fleet/read/{session}` answers the watermark and an unread count of messages from others newer than it. Only forward: a
1720
+ stale marker arriving late must not move the watermark backwards, or the count would grow for messages already seen.
1721
+
1722
+ ### Mentions
1723
+
1724
+ A message may carry `mentions: [machine_id]` (at most 10, validated). The row is still **one** row -- a mention is a
1725
+ notification hint, not a delivery mechanism -- and the SSE notification says only whether **this** listener was named. It never
1726
+ carries the text, and it does not carry the mention list.
1727
+
1728
+ ### Idempotent posts
1729
+
1730
+ `POST /v1/fleet/messages` accepts `idempotency_key` (per session). A retry after a lost response returns the **first** message
1731
+ with `deduped: true` instead of storing the sentence twice. The key is bounded, like the message log.
1732
+
1733
+ ### Two defects the 1.2.4 tag carried, fixed here
1734
+
1735
+ The 1.2.4 commit (tag `v1.2.4`) had a red CI and a red Compat, both found before approval:
1736
+
1737
+ - **Compat**: `w2m_wait`'s `reducer_machine_id` declaration had been pasted **inside** `poll_ms`. Every local check was green;
1738
+ the real host refused the whole entry ("parameters.poll_ms.reducer_machine_id is not supported by the value schema DSL"). The
1739
+ parameter now sits where it belongs, and `schema-spec.mjs` gained `strayParameterProblems()` — a parameter declaration
1740
+ nested inside another parameter is now refused where the tool is written, with two tests.
1741
+ - **CI**: the cancellation HTTP test had been inserted inside an enclosing `it`. Node 24 ran it; Node 20/22 reported
1742
+ `cancelledByParent`. The test now lives in its own top-level `describe`.
1743
+
1744
+ ### Verified
1745
+
1746
+ Gate GREEN twice, 15 of 15: 2628 tests / 2614 pass / 0 fail / 14 skip. New suites: fleet-members (6); fleet-messages grew to 11;
1747
+ relay grew to 171 including two HTTP cases for members and watermarks.
1748
+
1749
+ The relay was deployed a fourth time from a hash-verified tarball with a state backup, and the new routes were exercised against
1750
+ the live relay (members returned 8 machines; a role change returned 200; a watermark round-tripped; unread answered 0).
1751
+
1752
+ ### Not in this release
1753
+
1754
+ - **Offline replay of outbound chat** is not implemented. The agent's spool already holds undeliverable *results*; chat text is
1755
+ not yet spooled and replayed.
1756
+ - A two-machine end-to-end run of mentions/watermarks. The HTTP surface is tested against the fake relay and exercised once
1757
+ against jp-shared, but not by two physical machines.
1758
+ ## [1.2.4] — 2026-10-20
1759
+
1760
+ **Fleet collaboration, part two: cancellation, dependencies, a task board, questions on disk, and output that travels.**
1761
+
1762
+ ### Cancelling a task over HTTP
1763
+
1764
+ `state.cancelTask` has existed since the task ledger gained a cancel flag, but **no route reached it**. `POST /v1/tasks/{id}/cancel`
1765
+ now does, with the same operator authorization dispatch requires, and `w2m_cancel` is the tool over it. Cancelling twice is not
1766
+ an error: the ledger is the answer.
1767
+
1768
+ ### Prerequisites and a task board
1769
+
1770
+ - `POST /v1/task` accepts `depends_on[]`. Validated at **enqueue** time: unknown prerequisite, self-edge, duplicate edge and
1771
+ cycles are refused by name (`src/relay/task-deps.mjs`), because a graph checked only when a worker picks the task up puts the
1772
+ refusal on a machine that did not create the problem.
1773
+ - `depends_on` / `assignee` travel on the task record.
1774
+ - `w2m_tasks` (the fifteenth tool) is the board: it classifies every visible task as ready / active / waiting / blocked / done
1775
+ (`src/plugin/task-graph.mjs`). A prerequisite outside the page is treated as **waiting**, never as absent.
1776
+
1777
+ ### Questions that survive a restart
1778
+
1779
+ `src/plugin/ask-store.mjs` keeps open questions in a JSONL file next to the profile (`ask-store.jsonl`, atomic rewrite, cap on
1780
+ records). A reply that arrives after the deadline is recorded as `answered_after_timeout` rather than overwriting the timeout or
1781
+ being dropped, and a settled question is never reopened. `w2m_chat` gained `action: "asks"`, and `await` settles the stored ask.
1782
+
1783
+ ### Output that travels
1784
+
1785
+ `src/util/dataflow.mjs` (moved from `src/plugin/` — the layer check refused an `agent -> plugin` edge) is now wired:
1786
+
1787
+ - a pipeline stage whose stdout exceeds 64 KiB **uploads it as a blob** (`PUT /v1/fleet/blobs/{sha256}`) and the envelope names
1788
+ it (`blob_sha256`, `output_mode: "blob"`) instead of inlining head/tail;
1789
+ - a failed upload **fails the stage** by name (`BLOB_UPLOAD_FAILED`) rather than leaving a reference to nothing;
1790
+ - `w2m_wait` builds the reduce plan with `buildReducePlan` from the **raw** envelopes and marks the verdict `degraded` when a
1791
+ shard is missing — but only when the caller declared `reducer_machine_id`, so a large pipeline result is not mistaken for a
1792
+ partial reduce.
1793
+
1794
+ The end-to-end fixture this release adds (`test/agent.test.mjs`) runs a real agent against a fake relay: it asserts the blob name
1795
+ is the hash of the bytes sent and that a refused upload fails the stage. Two real defects surfaced while writing it and are fixed
1796
+ here: the stage stdout arrives as a `Buffer` (it was handed to the signature helper as a string), and a 70 KiB test argv exceeds
1797
+ Windows' `CreateProcess` limit.
1798
+
1799
+ ### Retry and hand-off
1800
+
1801
+ - `POST /v1/task` accepts `max_attempts` (1..5, default 1) and `retry_on` (`expired|failed|partial|timeout`). Defaults keep
1802
+ at-most-once for every existing caller; a malformed policy is `BAD_REQUEST`, not silently ignored.
1803
+ - On lease expiry, `retry_on` containing `expired` performs the hand-off `halt=now` always had: attempt+1 and a re-offer to idle
1804
+ machines (`_takeover`, which already refuses writing tasks to avoid double application).
1805
+
1806
+ ### mapreduce
1807
+
1808
+ `mode: "mapreduce"` is accepted with `reduce_argv` and an optional `reducer_machine_id`. The map half is ordinary dispatch; the
1809
+ reduce half is **described, never launched** — the caller runs it as an explicit second task after `w2m_wait` says every shard is
1810
+ present, so one task id never describes two executions.
1811
+
1812
+ ### Relay
1813
+
1814
+ The relay was deployed three times during this release (cancel route, retry policy, mapreduce) from hash-verified tarballs, each
1815
+ with a state backup, a health check, and a 401 check on the pre-existing routes. **The relay name is separate from the package
1816
+ version**: the tarball deployed was the 1.2.3-labelled build carrying this code, and is re-deployed from the released 1.2.4 tarball
1817
+ below.
1818
+
1819
+ ### Verified
1820
+
1821
+ Gate GREEN twice, 15 of 15: 2610 tests / 2596 pass / 0 fail / 14 skip. Notable suites: agent 149 (147 pass, 2 skipped),
1822
+ relay 169, tools 127, task-deps 13, task-graph 11, ask-store 8, turn 5, dataflow 7, dataflow-wiring 7.
1823
+
1824
+ ### Not verified
1825
+
1826
+ A two-machine run of cancel / retry / mapreduce end to end. The code paths are tested against a fake relay and against the real
1827
+ relay's HTTP surface, but no test here runs two physical machines exchanging an expired lease.
1812
1828
  ## [1.2.3] — 2026-10-20
1813
1829
 
1814
1830
  **The packed artifact did not mount. This fixes that, and the reason it was not caught here.**