init: workbuddy_skills 重建,仅收录 session-mechanism

- 按用户指示清空原有 25 技能内容,只提交 session-mechanism(57 文件)
- 附 .gitignore(产物 + 本机凭据)
- 令牌明文已脱敏(历史 .neodata_token 与 pitfalls 引用均不入库)
- 本提交为孤儿提交(父提交为空),历史自此重新开始
This commit is contained in:
admin committed 2026-10-05 14:13:24 +08:00
commit 19101acd65
58 files changed
+36134

No files matched your search

@@ -0,0 +1,114 @@
# 动手前必过 · 六条(动手层)
> `pitfalls.md`=**知识**(47 条,问「为什么」时查)。
> 本档=**动作**(开工前照做)。**⛔ 别开工前通读 pitfalls。**
同一天栽过四次,同一个形状:**判据锚点自己没先验 ⇒ 推出了错结论。**
## ⓿ 🔴🔴 说「会怎样」之前,先取证那个后果真的存在
> **这条是 2026-10-03 23:20 立的,当天我又犯了一次(10-04 17:1x)。**
⛔ **不许把自己推测的后果写成事实。** 否则「讲清后果」会退化成
「**编一个后果、说得更像回事**」——**比原句更坏**(原句至少只是啰嗦;编的后果是**假情报**,
会被当真话用)。
**今天(10-04)栽的那次**:我说「没启动脚本 ⇒ 常驻挂在会话进程树上 ⇒ 会话一关就被带走」。
**实测**:pid 19424 连跑 **24.2 h / 8695 轮**,父进程早已退出(孤儿进程)⇒ **不受会话结束影响**。
⇒ 我说的后果**不存在**。而**正确答案早写在 `supervise-persistence.md` 里**(还明写
「⛔ 不能用它论证这种起法也能长期」⇒ 我把它读反了)。
✅ **动手前问两句**:
1. 我要写的这个后果,**能不能拿一条读数证明**?
2. 判据在哪?(**先查判据 ⇒ 再写结论**,⛔ 不是反过来)
📌 同源的第二条红线(也是 10-03 立的):
⚠️ **改文案/解释改到第三轮还说不清 ⇒ 该问的是「这行要不要留」,⛔ 不是继续润色。**
(「看不懂」往往不是措辞问题,是**这行本身就不该在** —— 10-03 与 10-04 两天各撞一次。)
## ① 查状态 → 问那个程序自己
⛔ **不许用 `glob`/手拼路径证明"某文件不存在"**(`glob('**/x')` **不穿透 `.workbuddy` 这类点目录** ⇒ 零命中 ≠ 不存在。今天就是这么把"常驻没跑"报错的)。
```python
import collabd
ok, det = collabd.supervise_alive() # ✅ 让它自己判
hb = collabd.SUP_HB # ✅ 路径问常量
```
| 东西 | 真身 | 程序自判 |
|---|---|---|
| 常驻心跳 | `<WS>/.workbuddy/collab/logs/supervise-heartbeat.json` | `collabd.supervise_alive()` |
| 停止标志 | `<WS>/tmp/supervise-inbox/guard.stop` | `collabd._guard_says_stop()` |
| 目标状态 | `<WS>/tmp/supervise-inbox/goal.json` | `collabd.goal_lifecycle()` |
| 台账四态 | `<WS>/tmp/supervise-inbox/tasks.json` | ⛔ 唯一权威,⛔ 别读 `queue.json`(已退役) |
| 配置 | `<WS>/.workbuddy/collab/collabd.config.json` | — |
## ② 引用读数 → 先看它什么时候写的
台账 V5 写「PASS 48」,真跑 73;V1 写「pid 8024」,现活 19424。`acceptance_state` 每条带 `_更新`(本例停在 **10-02 13:58**)=**"当时验过",不是"现在仍成立"**。
> **凡带「现在/已/还差」的结论,读数一律现取。** pid · 心跳 · 计数 · pass 数 · 端口 · 排期状态——全都会随时间变。
## ③ 写判据 → 先让基线全绿,再谈抓得住
判据正则 `width:\s*\d` 误伤 CSS 里正当的 `max-width:420px` ⇒ 这条**永远红** ⇒ 跑任何变异都"报红" ⇒ **5/5 全 ✅ 全是假象**。
> **恒红比漏网更坏**:它让所有变异都"红" ⇒ 验证作废,而**表面看起来完美**。
> 跑完变异先问「**基线绿吗**」,不是「几条报红」。
三条硬规矩:
1. **基线先全绿** —— 数 `✗` 且 ⛔ 排除「报告型」那些行(既有存量不计入成败)
2. **锚在「结构」或「唯一位置」** —— ⛔ 锚在"文本出现过"上(判 CSS 要**剥注释**+只取那条规则块)
3. **变异脚本带两道护栏** —— `ast.parse` 判据源码 + 必须真找到那条用例(否则"无输出"会被当成"全绿")
④ 🔴 **每个变异改完先验「被检对象真被改了」**(长度差/md5)—— 今天栽过:脚本里锚点写错 ⇒
`replace` 什么都没改 ⇒ 变体被 `⏭` 跳过 ⇒ 汇总却按总数打"6/6 全 ✅" ⇒ **整轮验证作废**。
⇒ **⏭ 跳过必须计入失败**,⛔ 不许从分母里去掉。
### 🔴🔴 写了一天纪律还是栽 ⇒ 那就**上机器**(2026-10-04 收口)
上面 ①~⑤ 是**纪律**。纪律拦不住 —— 我写完当天照样栽。**根因收敛成两条**(⛔ 不是六条各自独立的失误):
| 根因 | 今天的表现(≥6 次) | 机器抓手 |
|---|---|---|
| **A. 判据与被检对象之间的「连接」没被验证** | 判据里 `lambda` **自己重算**;锚在「文本出现过」;夹具只覆盖顺带跑到的分支;锚点凭记忆写 | `judge_audit.py --audit selftest.py` |
| **B. 验证动作本身出错,而我不检查它** | 变异脚本自己 `IndexError` 却照样打 PASS;只重置一份文件 ⇒ 累积污染 | `judge_audit.py --mutate` |
⚠️ **两者的共同签名**:**判据绿灯与被检对象状态无关**。所以解法不是「更小心」,
是**让验证器自己的失败变成读数**(rc≠0 且明说原因),⛔ 不许它输出得像结论。
```bash
# 🔴 每改完判据必跑(⛔ 别手写变异脚本 —— 手写的今天栽了三次)
python scripts/judge_audit.py --audit scripts/selftest.py # 元体检:扫弱锚点写法
python scripts/judge_audit.py --mutate <被检文件> '旧文本' '新文本' -k <用例关键字>
# ↳ rc=2 = 变异没生效/等价(⛔ 作废,⛔ 不许当通过)|报红才算数|自动核对还原 md5
```
⚠️ **元规则表也会假红**:`--audit` 扫的是**写法**,分不清「引用教训」与「犯错误」
(实测把「自建夹具证明 glob 会漏」那条**教训本身**判成了错误用法)⇒ 误报要进
`FALSE_POSITIVE` 且**写清为什么**,⛔ 不许默默加。
## ④ 写沉淀 → 简明,**判据要点一个不许丢**
用户 2026-10-04 明令:「**沉淀的经验不要记成流水账,要简明扼要有效**」(**这条本身也是经验**)。
体检:`pitfalls.md` 47 条 **140 KB**,单条最长 **11 KB**;**我当天新写的两条各 3 KB** —— 比真复杂的 P0-41/22/23 还长 ⇒ **拿流水账充数**。
> **单条上限 6 KB。超了就是流水账**:论证过程、对比表格、逐条展开,⛔ 全删。
> **判据要点不许丢** —— 精简后必须**关键词对账**(长度达标但判据被删 = 更坏,假绿)。
⚠️ 精简的顺序:**先删论证 → 再压表格 → 最后才动措辞**;⛔ 别一上来就重写(会把判据改丢)。
## ⑤ 报结论前 → 四问
1. 这个数字是**现取**的,还是从文档/台账抄的?
2. 我判"不存在/没在跑"时,**问过程序自己了吗**(还是只 glob 了)?
3. 判据改完了吗,**基线绿吗**(不是"几条报红")?
4. 🔴 **我写的每个「会怎样」,都有读数证明吗?**(⛔ 拿不出读数的 ⇒ 删掉那句,
别让它"说得更像回事" —— **编出来的后果比原句更坏**。10-03 立,10-04 又犯。)
---
🔴 **读这份的时机**:开工前 | 报「已完成」之前。**其余时候问「为什么」才去 `pitfalls.md`。**
@@ -0,0 +1,53 @@
# 文档索引 · 干什么事该看哪一篇(★ 2026-10-04 建)
> 🔴 **本档只解决一件事**:**别通读文档。** 按下表跳,⛔ 不许「先都读一遍」。
> ⚠️ 落它的起因:文档已 14 篇 + `SKILL.md`,**改流程/改机制时不知道必看哪几篇**
> ⇒ 出现过「**昨天写好的红线,今天换个会话又踩一遍**」(2026-10-03 立的「⛔ 写会怎样前先取证」,
> 只躺在工作区日志里,换会话读不到 ⇒ 10-04 复发)。
## ⓿ 🔴 改流程/改机制前的**必读三篇**(顺序别换)
| 序 | 必读 | 读多久 | 为什么是它 |
|---|---|---|---|
| 1 | **`00-动手前必过.md`** | **3 分钟** | 六条动作红线。⛔ **开工前只读这一篇就够**,⛔ 别通读 pitfalls |
| 2 | **`rules.md`** | 5 分钟 | 现行规则本体(⛔ 不是过程记录;历史在 `pitfalls.md`) |
| 3 | **`manifest.md`** | 2 分钟 | 清单:哪些文件是什么、**哪个是权威**。⛔ 改之前先确认你改的那份是权威 |
**一句话**:⓿ 三篇加起来 **10 分钟**,能避开今天栽的每一类。
## 一、按「你要干什么」跳
| 你要干的事 | 必看 | ⛔ 不用看 |
|---|---|---|
| **改流程/改机制/改正则** | `00-动手前必过.md` + `rules.md` + `manifest.md` | `pitfalls.md` 全篇 |
| 说错了话、想找根因 | `pitfalls.md`(**按编号查**,⛔ 别通读) | — |
| 常驻挂了/要开机自启 | `supervise-persistence.md`(唯一权威) | `architecture.md` |
| 看板显示不对 | `collab-detail.md` | `architecture.md` |
| 换机器/装钩子 | `deploy.md` + `install.py` | 其余全部 |
| 会话卡住/日志爆了/抢锁 | `SKILL.md` §1 加载块 | `pitfalls.md` |
| 查某个历史会话干了啥 | `forensics.md` | — |
| 任务图/多会话分工 | `taskgraph.md` + `collab.md` | — |
| 怎么回话/排版 | `03-回复排版-核心块.md` + `02-功能优先协作协议.md` | — |
| 对方甩来一句没头绪的话 | `02-功能优先协作协议.md` | — |
| 只想要速查 | `99-速查清单.md` | — |
| **问「这是什么、为什么这样」** | `architecture.md` | — |
## 二、🔴 文档四条规则(写文档/改文档时必守)
1. **分类索引** —— 新增文档必须在这张表里登记(⛔ 没登记 = 别人找不到 = 白写)。
2. **结论在最前,过程记录在后** —— 读者要的是「现在是什么样」,⛔ 不是「我改了几轮」。
3. **历史记录按时间倒排** —— **新的在前面,旧的在后面**(⛔ 追加只能往前插,⛔ 不许接在末尾)。
4. **简明扼要有效** —— **单条 ≤6 KB**;⛔ 论证过程/对比表格/逐条展开全删;
⚠️ 但**判据要点一个不许丢**(长度达标而判据被删 = **更坏**,那是假绿)。
## 三、🔴 为什么「存档」不等于「读得到」(10-04 实证)
| 档位 | 装什么 | 跨会话可见 |
|---|---|---|
| `references/*.md` + `SKILL.md` | **规矩** | ✅ 技能会自动加载 |
| `.workbuddy/memory/<日期>.md` | **过程记录**(当天做了什么) | ❌ 只有那个工作区翻才看得到 |
| 源码注释 + git 历史 | 细节与来路 | ⛔ 没人会去看 |
⚠️ **10-04 的教训**:一条红线立在对的地方(工作区日志 197 KB),仍然等于没立
⇒ **凡是「下次必须做到」的事,必须落在 `references/` 或 `SKILL.md`,⛔ 不能只写日志。**
判据:`selftest.py::t_no_invented_consequence` 量的就是这个(红线在动手层**第一段**)。
@@ -0,0 +1,350 @@
# 功能优先协作协议(用户只提功能卡 · AI 自主决策)
> ## 🔴🔴 归属:**2026-10-04 起本档归 `session-mechanism`**(用户定案逐字:「这个属于会话的自决策方法,应该属于会话逻辑」)
>
> **为什么搬**:这套协议答的是「**在会话里做事时,该自己定还是该问用户**」——
> 而 `session-mechanism` 管的就是「**主会话 ↔ 执行会话怎么协作、活怎么派、结果怎么验**」。
> 判据落在会话机制里 ⇒ **主会话派活时与任务会话执行时读的是同一份**,⛔ 不会两处口径打架。
> **provenance**:原技能 `dsh-feature-first`(**已退役**)全文 + `dsh-decision/references/01`
>  (2026-10-04 **逐行搬入,内容守恒**;⛔ 只改档名与 skill 名,**判据正文一字未动**)。
> ✅ **本档即权威**(2026-10-04 改,用户定案:"只想复制一个技能到别的机器,这些功能都可以正常使用"):
>  **自决策白名单(§2)· 只准提报用户 3 类(§3)· 功能卡 4 问(§1)· 语言转换表(§3.3)**
>  的**判据实体就在本档正文里** ⇒ 只复制 `session-mechanism` 一个技能即可用,⛔ 不依赖任何其他技能。
> 📌 **来路**(保留以便追溯):本档 2026-10-04 从 `dsh-decision/references/01` 逐行搬入;
>  其判据 2026-09-12 起的「完整版」曾寄放在 `agent-operating-rules §1.6/§1/§2/§2.5`(跨项目通用技能)。
>  ⇒ **本机若同时装了那个技能,以本档为准**(避免两处各存一份、改一处忘另一处)。
> ⚠️ **`dsh-decision/references/01` 侧的残留摘要仍在**(那份技能本机也有)⇒ 若两处都读,
>  **改判据以本档为准**;`dsh-decision` 那份视为副本,不同步。
---
# dsh-feature-first — 功能优先协作协议
> **一句话**:把「事前请示」改成「**默认自主 + 事后可推翻**」。
> 用户的输入只到**功能层**(谁、在哪、做什么、怎样算成功);**技术层全部由 AI 自主决定并记录**。
---
## 0. 为什么要有这条协议(复盘证据,2026-09-12)
### 0.1 事实:用户的时间被技术问题吃掉了
对 `dsh-server-docs/04-调整方案/` 全部档案按「触发来源」分类(含「触发」字段的 42 份):
| 类别 | 份数 | 占比 | 含义 |
|---|---|---|---|
| **用户的功能 / 交互需求**(用户的主场) | 15 | 36% | 16 / 18 / 19 / 31 / 34 / 36 / 37b / 38b / 53 / 54 / 56① / 57 / 60 / 61 / 62 |
| **技术类**(用户被拉进技术判断,或 AI 做技术排查) | 17 | 40% | 12 / 14 / 22 / 23 / 26 / 27 / 29 / 32 / 33 / 35 / 37a / 38a / 42 / 55 / 58 / 64 / 65 |
| **用户报障**(用户只能看到现象,被迫当测试) | 10 | 24% | 15 / 17 / 21 / 24 / 25 / 30 / 49 / 51 / 56② / 59 |
⇒ **技术类 + 报障类 = 27/42 ≈ 64%** —— 用户 2/3 的注意力花在自己不擅长的领域。
> 复核命令:`cd dsh-server-docs/调整方案 && grep -Hn "触发" *.md`(分类可按档案号逐一核验)
> **⚠️ 该比例是快照,会随档案增长变化;引用时须重新取数。**
### 0.2 五个根因(按影响排序)
| # | 根因 | 表现 |
|---|---|---|
| **1** | **技术问题被"决策化"了** | AI 把"我不确定选哪条技术路径"包装成「决策点」提报用户:走 CF 代理还是改 DNS、要不要 watchdog、内存能否降到 512M、要不要 enablePatch。用户没有判断依据,只能凭感觉点头 |
| **2** | **需求没有"规格层"** | 用户说「AI 生成的文件看不到、下不了」→ AI 直接跳到技术方案(新增端点 + 注入脚本 + 面板)。中间的"谁在什么场景要看到什么、怎样算成功"没有沉淀 → 每次都要重新谈技术 |
| **3** | **报障驱动** | 平台上线后用户输入大量来自"坏了"而不是"我要什么";报障的根因排查天然是技术沟通 |
| **4** | **没有"技术默认值"** | 项目其实有大量技术原则(红线 R1-R8、只准收窄、最小代价路径、能一行代码就不改平台),但**没有一条"AI 自主技术决策的授权边界"** → 每次都重新分析、重新提报用户 |
| **5** | **交付语言是技术语言** | AI 汇报用 commit / 文件 / md5 / 端点;用户只能靠技术语言参与验收 → 反过来推高了技术沟通量 |
---
## 1. 用户的输入:只填「功能卡」4 问
> 用户**不需要**描述技术方案、不需要指定改哪个文件、不需要选技术路径。只回答这 4 件事(缺的 AI 自己补默认值并标注)。
```
1. 谁用? (角色:admin / 普通用户 / 两者)
2. 在哪用? (哪个界面:门户某页 / 实例内设置 / 会话页 / 右下角…)
3. 要做什么? (用户视角的动作与结果,一句话)
4. 怎样算成功? (用户能亲眼看到/亲手验证的验收点)
```
**示例(用户侧的正确写法)**
> 「普通用户要能在设置里看到自己能启停哪些功能插件,勾选后确认,重启后生效。」
**示例(不要这样写)**
> ~~「在 business-plugins 的 settings.section 里加一个 id、调 /api/plugins/mine/apply、重启实例生效」~~
> —— 这是实现,不是需求。
**AI 侧**:拿到功能卡后,缺什么自己定;定完在档案里记「技术选择」段(见 §5)。
---
## 2. AI 自主决策的 9 类白名单(**永不问用户**) → **本档 §2 即权威**
> ✅ **判据实体=本档下方那张表**(2026-10-04 起;⛔ 不再是"内联摘要",它就是权威)。
> 命中以下任一类 ⇒ **直接定、直接做**,只在档案里记一句「我选了什么(可推翻)」——
| # | 类别 |
|---|---|
| 1 | 技术选型(API / 表 / 库 / 协议 / 存储格式) |
| 2 | 实现路径(改哪一层、配置还是改码、复用哪个扩展点) |
| 3 | 命名与结构(变量 / 路由 / 表名 / 目录 / 文件拆分) |
| 4 | 性能与资源调参(内存 / 并发 / 超时 / 缓存 TTL / 退避) |
| 5 | 部署与同步流程(scp 姿势 / 权限位 / 行尾 / 对账 / 脚本) |
| 6 | 排查方法(journalctl 还是埋点、怎么复现与取证) |
| 7 | 版本与依赖(选版本 / 预装还是按需拉 / 打包方式) |
| 8 | 兼容与降级(显式抛 `unsupported`,不假装支持) |
| 9 | 文档与档案的技术内容(编号 / 模板 / 章节 / 脚本实现) |
**判断口诀**:**「用户能不能从功能视角判断这个选项的好坏?」** 能 → 可提报用户(见 §3);不能 → **这就是 AI 的工作,不要问**。
## 3. 只准提报用户的 3 类(+ 语言转换表)
> **唯一判据(用户 2026-09-12 原话):只问「超过现有判断方法边界」的问题。**
> 边界内 → 一律自决策;边界外(§3.1–§3.4)→ 必须问。**没有第三条路。**
### 3.1 (a) 功能语义分叉 —— 选项之间存在**用户能感知的差别**
例:技能随插件包投放 vs 走平台共享层(差别 = 用户"要不要分开管理");新开会话 vs 迁移老会话(差别 = 用户"要不要重开一个");提示 vs 自动改档位(差别 = "AI 会不会悄悄改我的设置")。
### 3.2 (b) 红线门禁 —— 必须用户点头
- **扩大**权限/可见面(R5)
- **批量 / 全仓写入**(R7,>10 文件)
- **不可逆的破坏性操作**(删数据 / 迁 DB / 清目录 ⇒ 先出清单)
- ⛔ **「中断在线用户」已不是门禁**(2026-09-13 用户明令 + 2026-09-16 收口):`R8` 原文 —— **47 是「开发环境服务器」,不用担心中断用户** ⇒ 重启服务 / 停实例 scope / drain / 改配额·env / 改 nginx·nft **均可直接做**,只需**动手前一句话说明**在动什么。
⚠️ **原表述「只有真会中断的(重启服务 / 停实例 / drain / 改配额·env / 改 nginx·nft)才提报用户」是会话 `ddea70b7` 提报用户的直接依据** —— AI 据此把"106 旧控制面要不要停"拿去问用户,而**用户随后两次(U5 / U7)自己给出"删除"**。⛔ 别再拿"重启 / 停实例 / nginx"当提报用户理由。
- ⚠️ **判据:按「实际影响面」判,不按「动作名字」判**(2026-09-13 用户纠正)—— 看到「部署 / 上线 / 生产 / 铺包」就提报用户是**过度套用**:
**不中断在线用户的上线**(传产物 / 换 profile 包 / 改静态页 / 候选池投放)**属边界内的执行细节 ⇒ 做完即上线,自决策**。详见 `dsh-decision-method` **U20 / X9**。
- 🔑 **"平台级" ≠ "别人的"**(2026-09-16 收口):`dshs*`·`dsh-*` systemd 单元、`/var/lib/dshs/**`、nginx·nft、端口,**在我们自己的 47 / 106 / 本工作区上 ⇒ 本平台自己的资源 ⇒ 直接做**。只有**别人的 / 归属不明**的才"只报告不动手"。**判据看"归属",不看"是不是平台组件"。**
### 3.3 提报用户格式(**强制**) → **本档 §3.3 即权威**(下方即全表)
> **完整对照表已收敛到通用技能**。**最小摘要**(3 条铁律):
> ① **提报给用户时不出现**包名 / 环境变量 / 文件路径 / commit / API 路径 / 代码标识符 —— 出现即是没转换。
> ② 翻译方向 = **从"技术选项"翻成"用户能感知的差别"**。例:不问「要不要启用 `enablePatch`」,而问「要让用户**自己装插件**,还是只由管理员统一装?」
> ③ **一轮最多一个问题**,且**同类不连问两次**。
### 3.4 (c) 超出决策方法边界的事 —— **必须问**(用户 2026-09-12 明确)
> **用户原话**:「有超过现有决策方法边界的事情,还是需要问的。」
> 即:**方法覆盖得到 → 自己定;覆盖不到 → 必须问**。这条是 §2 的**例外出口**,也是闸门的 fail-open 依据。
> ⚠️ 判据是「**有没有客观可判的优劣**」:有 → 方法能判,自己定;没有 → 那是用户的取向,必须问。
| # | 边界外的类别 | 例子 | 为什么方法判不了 |
|---|---|---|---|
| 1 | **业务目标与优先级** | 先做哪个功能、这个功能要不要做、值不值得投入 | 方法只能判"**怎么做得最优**",判不了"**该不该做**"。<br>⚠️ **出口(2026-09-16 加)**:**方向已定**(用户已说"要做这几个功能 / 该谁处理就开发对应功能")时,"**先做哪个**"若候选有**客观排序**(如"先做能闭环真实缺陷的那项"、"3 项 P0 阻塞真实场景")⇒ **属边界内,自决策**。只有"几个都该做、用户对**节奏/取舍**有偏好"才提报用户。<br>**实证**:会话 `ddea70b7` 把"D1–D6 先做哪些"归进本类 ⇒ 提报用户 ⇒ 用户回「**按照你的规划执行**」(AI 自己其实已排出顺序并写了"我倾向先 D1+D2")|
| 2 | **成本与资源承诺** | 买云资源 / 开第三方账号 / 花钱买额度 | 涉及用户的钱,不是技术优劣问题 |
| 3 | **对外承诺与合规** | ICP 备案、资质、合同、对外 SLA、对外品牌文案 | 法律与商业后果,超出工程判断 |
| 4 | **需要用户提供的第三方凭据 / 审批** | API Key、DNS 权限、企业审批、账号授权 | 只有用户有 |
| 5 | **用户体验偏好(无客观优劣)** | 审美与文案语气、默认值取向、提示语措辞 | 没有"更优",只有用户"更喜欢" |
| 6 | **影响面超出本平台** | 会波及其他系统 / 他人数据 / 不可逆的对外影响 | 影响半径超出我能判断的范围 |
| 7 | **红线门禁**(R5 扩大 / R7 批量>10 / 不可逆破坏性操作) | — | 安全与影响面变更**必须**用户知情同意。⛔ **不含 R8** —— "中断在线用户"已于 2026-09-13 修正(开发环境服务器 ⇒ 直接做,只须动手前一句话说明) |
| 8 | **方法确实判不准** | 两边都无依据、事实不足以判断 | **宁可问,不要卡死** |
**提报用户方式**:这 8 类**照实说人话**说明「你为什么需要他决定」,不要包装成技术选项。
### 3.5 方法本身的演进 —— 改为自决策(用户 2026-09-12 明确)
> **用户原话**:「以后决策方法的问题就不需要再问了。」
- **方法的日常应用不问**:边界内的一切判断,按 §2 + `dsh-decision-method §4.4` 自己定。
- **方法本身的修订也不问**:发现方法有缺口 / 有反例 / 需要加规则时,**直接改、直接记录**(改完在交付回执里说一句"我更新了方法:因为 X"),不要拿"要不要加这条规则""这么写好不好"去问用户。
- **唯一例外**:方法修订若**扩大**了 AI 的自主权或收窄了用户的门禁 → 属边界外的第 1/7 类,**必须问**。
### 3.6 ⛔ 拆包提报用户:红线问题**不得**与技术方案捆在一起问(2026-09-12 实证)
> **实证**(会话「任务执行2」14:39):AI 把「**要不要现在重启服务**(R8,该问)」和「**用 A 还是 B 实现**(技术项,不该问)」**捆成一个提问**,用户被迫先读懂两套技术方案才能回答那个红线问题
> → 用户的体感依然是"**又在让我确认技术问题**"。**这才是"设置了却没用"的真正形态**:不是问得太多,而是**把该问的和不该问的混在一次提问里**。
**三条规则**:
1. **剥出红线问题单独问**,且只问用户能判断的维度:**要不要现在动生产 / 影响谁 / 断多久 / 能否避开**。
2. **技术形态自己定**,作为**已定项**写进回复("我按 B 做,因为…;可推翻"),**不要做成选项让用户选**。
3. **一轮最多一个问题**;同一个问题**不要连问两次**(实证:11:16 与 11:42 两次问"用哪个账号验收",第二次只是细化 → 本可合并成一次,或直接自定)。
**该会话 6 次提报用户的逐条判定(反例对照)**:
| 时间 | 问的什么 | 判定 |
|---|---|---|
| 10:34 | 21 处 Windows 专属指引怎么处理(一并改 / 只改单子 / 先出清单) | ❌ **纯技术项**(范围 + 实现路径)——该自己定 |
| 10:21 | 行尾 CRLF 怎么处理 | ✅ 该问(命中 **R7**:批量换行符转换需授权) |
| 11:16 / 11:42 | 验收用哪个实例 / 账号 | ✅ 该问(影响面)· ❌ **连问两次**,可合并 |
| 13:32 | admin 崩溃怎么修 | ✅ 该问(R8 + 跨会话)· ❌ 选项里混了"我拆 / 转给对方 / 只给命令"三种**技术路径** |
| 14:39 | 选 A/B 方案 **+** 要不要重启 | ✅ 红线该问 · ❌ 技术方案不该问 → **典型捆包** |
**正确写法(照这个格式写)**:
```
我先按 B 做(平台侧自动摘除坏插件,覆盖所有装法)——**已定,可推翻**。
但它要重启 dshs:
· 影响谁:当前所有在线用户(刚实测 guest 实例在跑)
· 断多久:数秒;实例"访问即拉起",会话数据不丢
· 能否避开:可以等空闲;或并入 T04 那批改动只重启一次
**请定:现在就重启,还是等窗口?**
```
---
## 4. 报障闭环前置(让用户**不必**报障)
报障类占 24%,大部分可以从源头消掉。凡涉及"用户会撞上的状态",**必须同时交付**:
| # | 要求 | 已落地先例 |
|---|---|---|
| 1 | **错误页给人话 + 下一步**,禁止裸 JSON / 英文原文 | 档案 25(`not_running` 不再吐 JSON) |
| 2 | **等待必有可见反馈**(导航路径 + XHR 路径都要有) | 档案 59(3 秒后亮覆盖层) |
| 3 | **状态可自查**(能力清单 / 我的文件 / 档位提示) | 档案 56 |
| 4 | **能自愈就不报错**(401 透明重放、回收后自动恢复) | 档案 24 / 45 / 49 / 51 |
| 5 | **缓存/后台任务的状态可见**("更新于 X 前"、可手动重拉) | 档案 62 |
> 判断口诀:**「用户遇到这个情况,会不会只能来问我?」** 会 → 先把反馈做出来。
---
## 5. 答复与交付格式(功能性语言优先)
### 5.1 结论骨架(回答「是否已实现 / 能不能 / 为什么不行」类提问)
> **2026-09-13 用户纠正原话**:「**需要告诉我的是 是否已实现,如果未实现:为什么不能,需要我拍板可以问我**」
> 触发场景:用户问「是否已实现」,AI 却用「我自己的失误(一并交代)+ 探针怎么被污染 + 版本流水 + 下一步三步计划」作答 —— **要的结论被埋在第 5 段之后**,用户只能再追问一句。
**固定四节,顺序不许换;没有的节整节删掉,不要留空标题:**
> ⚠️ **「需要你拍板」必须是整条回复的最后一节**(2026-09-15 用户明令:「**要把需要我确认或决策的内容放在 最后,别隐藏在回复内容中间** | **按照有序段落展示**」)—— 它后面**不许再有任何节**,且必须**逐条编号**(有序段落),不写成散文。理由:夹在中间 ⇒ 用户扫不到、漏答。
> ✅ **配套的反向要求(同一句原话前半段)**:「**能根据决策方法 自行决策的就自决策继续处理**」⇒ **能自决策的不要停下来问**,直接做完并陈述;**只有不能自决策的**(真门禁 / 需你给凭据或窗口)才收进这一节。
> 🎯 **提报用户标准 = 存在"真取舍"**(2026-09-15 用户明令:「**需要我确认的方案需要说明优点和缺点,现在没法判断,假如只有优点或只有缺点那不需要我判断**」):把候选各写 **优点 + 缺点** —— 某个**只有优点**(明显更优)或**只有缺点** ⇒ **不需要用户判断**,自己拍掉再陈述;只有**各有优有劣、且客观标准分不出高下**才算真取舍,才提报用户,且**必须逐项列出优点与缺点**(只写"差别在哪"不算)。
> 📐 **候选方案必须"竖排成段"**(2026-09-15 用户明令:「**每个需要我决策的问题的潜在解决方案 A B C 也按照段落式排版,别横着排列**」):**A / B / C 各占一行(各自成段)**;⛔ **不许**写成 `A:… · B:…` 一行横排,⛔ **也不许**把候选做成**表格的列**。段内「优点…;缺点…」连写即可 —— 不必每个字段再拆行,否则撞 §5.4 硬约束 3「每节 ≤7 行」。
```markdown
## 判定
❌ 未实现 / ⚠️ 部分可用 / ✅ 已实现 —— <一句话>
| 目标 | 状态 |
|---|---|
| <用户列的第 1 项> | ✅/❌ + 半句依据 |
| <第 2 项> | … |
## 为什么不行(只在有 ❌ 时写;最多 3 层,结论层零技术标识)
- 已经排除的:<今天已经修完、不再是原因的> —— 一句话带过
- 当前唯一卡点:<一句人话>
- 为什么难(可选):<1–2 句;不确定性要写进**结论句**,例:「还不能说做不到,只能说这一步还没试」>
## 我接着做
- 下一步 <X> —— **陈述句,不是征询句**
## 需要你拍板(**最后一节**;真需要才写,不需要 → 整节删掉;**逐条编号**)
**1. <问题一句话>**
**A** —— 优点:…;缺点:…
**B** —— 优点:…;缺点:…
我倾向 **A**(一句话理由,可推翻)
**2. <下一件>** —— 同上
```
### 5.2 交付回执(已完成功能的交付)
AI 交付时**先说功能,技术细节折叠在后**:
```markdown
## 做了什么(功能)
<2-3 句:现在多了一个什么能力,在哪儿>
## 你现在能看到
- <具体位置> 出现了 <什么>;点它会发生 <什么>
- 验证方式:<用户亲手可做的一步>
## 不用你决策的技术选择(已定,可随时推翻)
- <易感知的一条,一句话>(若你希望反过来,说一声即可)
…
## 技术附录(可选读)
<文件 / commit / md5 / 端点 / 档案号>
```
> 第 3 段是这条协议的关键:**把"事前请示"改成"事后可推翻"** —— 用户获得了知情权与推翻权,但不需要在读之前做判断。
---
### 5.3 五条铁律(都来自本工作区的真实失手)
| # | 铁律 | 反例(实证) |
|---|---|---|
| 1 | **回答主位 = 用户问的那件事**;AI 的进度 / 失误 / 计划**不得占前两节** | 用户问「是否已实现」,AI 先写「五、我自己的失误(一并交代)」+ 版本流水 → 用户被迫再追问一句才拿到结论 |
| 2 | **结论层零技术标识**(版本号 / commit / 包名 / 内部函数名 / 路径 / 探针名)—— 最多放「技术附录」 | `0.2.19 / 0.2.20 / 0.2.23`、`TDZ`、`chr(10)`、`requestOverUnixSocket` 全在主线,用户读不出结论 |
| 3 | **禁止征询式收尾**:「要我接着做吗 / 说一声即可 / 你看怎么弄」—— 下一步**已定**且不命中门禁(现行门禁 = **不可逆破坏性操作**,见 `CODEBUDDY.md §3 R8`)⇒ **直接做**,用陈述句交代。⚠️ 本条禁的是**形态**;**位置**要求见铁律 4 | 「要我现在接着做,说一声即可」—— 把本该自己拍的执行细节又推回用户(同 **X9** 一族) |
| 4 | **能自决策的继续做;不能自决策的收到最后一节** —— 能按决策方法自决策 ⇒ **自决策 + 继续处理**(不为"要不要继续"而停);不能自决策(真门禁 / 需凭据·窗口)⇒ **收进整条回复的最后一节,按有序段落逐条编号**(每条 = 问题 + 选项**优缺点** + 我的倾向),⛔ 不许夹在中间,也不许散在正文里问 | 2026-09-15 用户原话:「**能根据决策方法 自行决策的就自决策继续处理,不能决策的问题和需确认内容放在回复的最后,按照有序段落展示**」—— 待拍板项夹在「进度 + 下一步」之间 ⇒ 用户扫不到、漏答 |
| 5 | **提报给用户前先过「取舍筛」**:把候选各写 **优点 + 缺点** —— ① 某个**只有优点**(明显更优)或**只有缺点** ⇒ **不需要用户判断**,自己拍掉再陈述;② 只有**各有优有劣、客观标准分不出高下**(真取舍)才提报用户;③ 提报给用户时**必须逐项列出优点与缺点**(只写"差别在哪"不算);④ 候选**竖排成段**(A / B / C 各占一行),⛔ 不横排、不做成表格的列 | 2026-09-15 用户原话两段:「**需要我确认的方案需要说明优点和缺点,现在没法判断,假如只有优点或只有缺点那不需要我判断**」+「**每个需要我决策的问题的潜在解决方案 A B C 也按照段落式排版,别横着排列**」—— 此前只写"可感知差别"且**横排**,用户**没法判断** |
> **自己失误的交代**:只在两种情况下写 —— ① 它**改变了结论**(例:某次改动引入了新问题);② 用户**问根因**。否则放最后一节一行,或先不提。
---
### 5.4 排版规范(让长回答**可扫读**) → **本包 `references/03-回复排版-核心块.md`(随包,⛔ 不依赖外部)**
> ✅ **排版契约随包自带** ⇒ `references/03-回复排版-核心块.md`(`reply-style-guard.py` **每轮现读**注入,⛔ 不设冷却)。
> 完整十条硬约束 + 十四条反模式 + 去 AI 味(删填充词/破公式/变节奏/信任读者/删金句)见本档 §5.3 自检与下方骨架表。
> **最小摘要 —— 十条硬约束的自检问法**:
| # | 自检怎么数 |
|---|---|
| 1 | 前 3 行有没有判定(✅/⚠️/❌ + 一句) |
| 2 | 有没有第 4 级标题(层级须 ≤3) |
| 3 | 有没有连续 >12 行的文字墙(每节 ≤7 行) |
| 4 | 每节加粗是否 ≤2 处、有没有整句加粗 |
| 5 | 表格是否 >5 列、单元格塞整句 |
| 6 | 同一条信息有没有重复出现 |
| 7 | 翻到最后一节 —— 是不是拍板项、有没有编号 |
| 8 | 每个候选是否优缺点各至少一条 |
| 9 | 有没有 `A:… · B:…` 一行塞多个候选(须竖排) |
| 10 | 有没有 `①…;②…;③…` 挤在一段(须逐条独占一段) |
**待用户拍板项**:位置 = **最后一节**|形态 = **逐条编号**|语气 = **陈述句**。
**去 AI 味**(说话像人):**本档 §5.3** —— 删填充词、破公式、变节奏、信任读者、删金句。
## 6. AI 自检清单(每次动手前 / 交付前)
**动手前**
0. **用户本轮点名了某个技能 / 方法吗**(如「参考决策方法」「按你的规划」「别问我」「自行决策」)?—— 点名了 ⇒ **必须先用 Skill 工具加载对应技能**,⛔ 不得以"我已经知道判据"为由跳过。
(机制层有 `dsh-server-docs/07-scripts/skill-load-guard.py` 钩子强制注入提醒 · 2026-09-16 加;实证:会话 `ddea70b7` 用户点名后 AI 全程 `Skill` 调用 **0 次**,仍按旧判据提报用户)
1. 我手上有一张**功能卡**吗(谁/在哪/做什么/怎样算成功)?没有 → 先问这 4 条,**不要问技术**。
2. 我准备提报用户的每一件事,**用户能从功能视角判断吗**?不能 → 撤掉,自己定。
3. 我提报用户的文案里,有没有包名 / 环境变量 / 路径 / commit?有 → 翻译成"能感知的差别"。
4. 有没有命中红线(**R5 扩大 / R7 批量>10 / 不可逆破坏性操作**)?**按实际影响面判,不按动作名字判**(X9)—— 命中 → 必须提报用户并说明影响面;⛔ **R8 不算门禁**(开发环境服务器 ⇒ 重启 / 停实例 / drain / nginx·nft **直接做**,动手前一句话说明即可);**没命中就别拿「这是生产操作 / 会中断用户」当理由停下**。
**交付前**
5. 我有没有按 §5 的格式给"做了什么 / 你能看到什么"?还是又只给了技术清单?
6. 用户会不会因为**缺少某个反馈**又来报障?(对照 §4 五条)
7. 技术选择我**记进档案**了吗(§2 的 9 类,做完要留痕,否则下次重新吵)。
8. 用户问的是「是否 / 能不能 / 为什么不行」吗?—— 是 → 用 §5.1 结论骨架(**判定在前,过程在后**)。
9. 结论层有没有技术标识(版本号 / commit / 包名 / 内部函数名 / 路径)?有 → 挪进「技术附录」。
10. 我有没有用「要我接着做吗 / 说一声即可」收尾?—— 有 → 改成陈述句,并**直接去做**(除非命中门禁 = 不可逆破坏性操作)。
11. 这条回复**能被扫吗**?—— 首屏 3 行给判定 / 层级 ≤3 / 每节 ≤7 行 / 加粗 ≤2 处每节 / 表格 ≤5 列 / 一条信息只说一次 / **待拍板项在末节**(§5.4;执行信息·报障·提问**各有骨架**)
12. **需要用户确认 / 决策的内容,在整条回复的最后一节吗**?—— 它后面若还有别的节 ⇒ **挪到最后**(2026-09-15 用户明令);且必须**逐条编号**(有序段落),不是散文一段;形态是**陈述句**,不是征询句。**反向也查一遍**:能自决策的事,我是不是停下来问了?
13. 我要提报用户的每一项,**优缺点都写了吗**?—— 若某个候选**只有优点**或**只有缺点** ⇒ **不该问**,自己拍掉再陈述(§5.3 铁律 5)。
14. 候选**竖排成段**了吗(A / B / C **各占一行**)?—— 横排成 `A:… · B:…` 或塞进表格的列 ⇒ **改成竖排**(§5.4 硬约束 9 · 2026-09-15 用户明令)。
---
## 7. 与其他约定/技能的关系
- **红线优先级最高**:R1–R7 / R9–R11 命中一律先停手 —— 本协议不构成豁免,只约束"该不该问"。
⛔ **唯一例外 = R8**:它已按用户 2026-09-13 明令**修正为"开发环境服务器 ⇒ 不必等确认"** ⇒ **不构成提报用户理由**(只保留"动手前一句话说明")。把 R8 当门禁 = 本站最常见的过度提报用户(`ddea70b7` 实证)。
- **`dsh-decision-method`**:管"怎么想、怎么定"(判定矩阵、十问、验收分级)。本协议管"**谁定什么、用什么语言问**"。
- **`dsh-change-workflow`**:管"怎么落地"(六阶段、档案模板、并行调度)。
- **`01-规范/06-工作台UI规范.md`**:前端强制基线,冲突以其实测 Token 为准。
- **单一来源**:本协议即唯一来源(技能必须本地加载才生效);文档库 `INDEX.md` 只放**指针**,不复制全文。
---
## 变更历史(原 frontmatter · 逐字保留)
```text
name: dsh-feature-first
description: dsh 多租户平台(ai1net.com)的「功能优先」协作协议 —— **用户只提功能需求,AI 自主完成全部技术决策**。当用户提出任何平台功能/交互需求、报障、说「我要把精力放在功能搭建上」,或问「**先做哪个 / 优先级怎么排 / 做不做**」(⇒ 属边界外第①类,该问)时使用;也用于 AI 自查「这件事该不该拿去问用户」。核心 = 复盘证据(技术类+报障类占 64%)+ 用户的「功能卡」4 问 + **AI 自主决策的 9 类白名单(永不问)** + **只准提报用户的 3 类(功能语义分叉 / 红线门禁 / 超出方法边界)** + **决策方法边界定义(边界外必须问的 8 类)** + 提报用户格式 + 报障闭环前置 + 交付回执格式 + 方法本身演进改为自决策 + **§3.6 拆包提报用户:红线问题不得与技术方案捆在一起问** · **§3.2 红线按「实际影响面」判、不按「动作名字」判** · **§5.1 结论骨架(答「是否已实现 / 为什么不行」)** + **§5.3 铁律 4–5 / §5.4 硬约束 7–9:待确认项 = 回复的最后一节、逐条编号;每个候选各占一段(竖排、不横排)且必须写「优点 / 缺点」;只有优点或只有缺点 ⇒ 自决策、自决策**(2026-09-15 用户明令)。**配套:`dsh-decision-method`(怎么想)· `dsh-change-workflow`(怎么落地)。**
version: 1.0.0
updated_at: 2026-09-22
last_change: 【2026-09-22 按要求统一版本号】frontmatter `version` → `1.0.0`(原 v1.8.1);正文与历史中的版本号为当时记录,未改动。此前 v1.8.1(2026-09-22):补触发说法「先做哪个 / 优先级怎么排 / 做不做」—— 实测盲区:这三类说法原本命不中本技能(而它们恰是「该不该问用户」的典型场景);顺带补齐缺失的 `last_change` 字段(12 个技能里只有本技能没有)。
agent_created: true
```
@@ -0,0 +1,18 @@
# 回复排版与格式 · 核心块(**机器可读 · 权威源**)
> **这份是权威源**(跨工作区、跨机器)。两个消费者:
> ① 钩子 `scripts/hooks/reply-style-guard.py` **每轮现读本块**注入会话;
> ② 注入器 `scripts/apply-reply-rules.py` 把它写进**各环境的规则文件**(`CODEBUDDY.md` / `AGENTS.md`)。
> ⛔ **不要在本文件之外再抄一份规则文本** —— 抄了就成第二真相源,两边必然漂。
> 用户令(2026-10-02):「所有会话中回复排版和格式要求和规则,也要整合到会话技能中,使用时配置到对应环境文件中」。
<!-- REPLY-CORE:BEGIN (钩子每轮**现读本块**;改口径只改这里,⛔ 别去改钩子) -->
- **骨架**:拆两层 —— `#` 大类别(已完成/待处理任务)→ `##` 具体事项。每件事先写「当前状态」(每条一个圆点,用**一句陈述句**说重点,依据与细节放**句末圆括号**),再写「待处理事项」(用序号 `1、2、3、`,每条可不止一句)。
- **层级与顺序**:大类标题必须比任务名大一号;**已完成的大类放最前**,待处理放最后;附件写在**所属板块最末一行**;⛔ 不出四级标题。
- **首屏**:开头 3 行内先给判定(✅/⚠️/❌ + 一句),细节放后面。
- **三禁**(⛔ 任一命中 = 该条回复**作废、重写后再发**):**表格** / **长散文** / **碎标签堆叠**。
- **并列内容竖排**:多个候选、多项并列各占一段、逐条编号;⛔ 不横排、⛔ 不挤进一段、⛔ 不塞成表格的列。
- **待拍板项**:放回复**最后一节**,逐条编号;每条写清「问题 + 说明(影响谁/断多久/花多少钱/有无不可逆)+ 各候选的优点与缺点 + 倾向」。
- ⛔ **不用征询句收尾**(「要我…吗/请确认/你看怎么办」);能自决策的直接做完,只留一句"我选了什么(可推翻)"。
- ⚠️ 若**本工作区另有更新的定稿**(环境文件里有更细的排版节)⇒ **以那份为准**,本块是通用形态。
<!-- REPLY-CORE:END -->
@@ -0,0 +1,110 @@
# 速查清单 · 常见坑一句话版
> 📌 **来路**:原嵌在 `pitfalls.md` 的 **P0-2** 条里(占那条 79% 的篇幅 ⇒ 把一个条目撑成 11.6 KB)。
> **2026-10-04 拆出**:⛔ **内容逐字保留**(一字未改),只是搬家。
> **什么时候用**:已知坑名、想快速确认「这个形状是不是老坑」⇒ 先扫这份;
> 要**根因与实证** ⇒ 去 `pitfalls.md` 查对应编号。
> 📌 **注意**:这��条只给「现象 / 根因 / 修法」三行,⛔ 不含证据;
> **别拿它当结论** —— 当年 P0-2 里的旧结论就因为"只有弱相关性"被作废过。
## P0 ⛔ 子进程不加 `CREATE_NO_WINDOW` ⇒ 桌面**反复闪黑窗**(用户:"一会弹出来一会弹出来的,影响我操作电脑")
- **现象**:守护/常驻程序跑起来后,桌面每隔 10~30 秒**闪一个控制台窗口**。
- **根因**:程序里有 `subprocess.run(["netstat", "-ano"], stdout=PIPE, …)`(**认网关端口**用)——
**`netstat` 是控制台程序**,不带 `creationflags=0x08000000` 就会**新建一个控制台窗口**;而该函数**每轮都被调**(常驻程序 ~10 s/投递 ~30 s)⇒ 桌面反复闪。
- **修法**:✅ **所有** `subprocess` 调用一律带 `creationflags=0x08000000`(CREATE_NO_WINDOW)。
⛔ **只重定向 `stdout`/`stderr` 是没用的** —— 窗口照样会建。
- **验收**:桌面从"反复闪"变为"零窗口";**功能侧**用"能否照常认出网关端口"核对(日志/`wakeups.jsonl` 仍有记录)。
- 🔑 **推广**:任何**长期循环**里的外部命令调用,先过一遍"**它会不会开窗**"。
> 本档给**现象 / 根因 / 修法**。**架构层判据 ⇒ `references/architecture.md`**;原技能存档 ⇒ `references/collab-detail.md §6`。
## P1 ⛔ 用「会话后台任务」跑常驻 ⇒ 宿主"卡死"
- **现象**:用户说"卡住了/发消息不恢复";会话永不回 idle。
- **根因**:会话后台任务**每轮输出都会唤醒宿主会话**。
- **修法**:🔴 **首选=根本不要常驻** —— 由**宿主钩子按需唤起**一次性进程(见 **P0-3**)。
⛔ 旧建议"用 `run_in_background` 起常驻"是**错的**:那正是 **P0-2**(任务记在该会话名下 ⇒ 该会话卡输出)。
真需要常驻时,只能**会话之外**起(启动文件夹/独立窗口),并接受它**拿不到口令 ⇒ 只能落盘、不能投递**。
- ⚠️ **`cmd start` / `wmic process call create` / PowerShell `Start-Process` 常被安全策略拦** ⇒ ⛔ 别在"独立起进程"上耗时间。
## P2 ⛔ 自愈没有去抖 ⇒ 反复杀掉正在服务的好实例("自愈比故障更伤")
- **现象**:监控到的端口**反复通/断**;日志里"launching → 又被杀"循环。
- **根因**:启动器常有**幂等闸**("进程活着但端口不通 ⇒ 杀掉旧的再拉一份");若守护进程**每次探测失败就触发**,就变成**抖动**,把可能正在服务的好实例打死。
- **修法**:**连续 N 次(≥3)失败才动手** + **冷却(数百秒)** + 探测用**socket 连一下**(⛔ 别发真请求)。
## P3 ⛔ 粗粒度域锁 ⇒ 自造串行瓶颈,整轮白开
- **现象**:别的会话**抢锁失败 ⇒ 什么都不做就退出**(纯浪费的会话)。
- **根因**:把**整个工作区**声明成一个域 ⇒ 所有写操作串行。
- **修法**:**按节点涉及的文件/子目录声明域**;不重叠即可并行。**机制层才独占**。
## P4 ⛔ `fail-open` 掩盖字段名错误 ⇒ 视图静默为空
- **现象**:`rc=0`、日志无异常,但**某个区块一直是空的**。
- **根因**:异常被 `except: pass` 吞掉;查询写错列名(真实例:给 `sessions` 查了不存在的 `name` 列)。
- **修法**:① **必须核对输出非空**(⛔ 不能只看退出码)② 关键查询先 `pragma table_info(<表>)` 核对列名 ③ 定期 `diff` 视图一眼。
## P5 ⛔ 把"接续任务"当成果 ⇒ 被"空转链条"骗
- **现象**:看起来一直在推进("下一棒已派"),实际**总工期不动**。
- **根因**:**排期 ≠ 成果**。
- **修法**:**证据分级**(真成果/接续任务/刚开跑/哑火);"在跑的棒 N 个"是排期数,⛔ 不是成果数。
## P6 ⛔ 用"加自动化"回应一切需求 ⇒ 会话洪泛
- **现象**:一个需求挂上 7 个自动化,其中 5 个同用途。
- **修法**:**白名单+确认制**(只有"接续会话/派活"可免确认)+ **配额 ≤2**。建前自问:**非得开新会话吗?已有的能不能覆盖?**
## P7 ⚠️ 一次性自动化跑完 `status` 仍为 ACTIVE ⇒ 统计虚高
- **现象**:数"在挂的棒"= 6,实际只有 2 个真在等。
- **修法**:**一律用 `next_run_at > now` 过滤**;⛔ 不看 `status` 单独判断。清理僵尸旧件(已跑完的 once)。
## P8 ⛔ 诊断数据不落盘/不落库 ⇒ 只能靠"自述"
- **根因**:把"谁干完了、结论是什么"寄托在文件扫描/人报告上。
- **修法**:**读宿主已落的运行记录**(`automation_runs.thread_title` 等)⇒ **0 token、不轮询**。
- ⚠️ 但要**核对该列是否真有内容**(`IN_PROGRESS` 时标题可能为空 ⇒ 判"在跑"看 `status`)。
## P9 ⛔ 校验"可用"时只看"能跑起来"
- **现象**:结论写"八项判据起停各一遍全绿",但**端口此刻并不通**。
- **判据**:**"能跑起来" ≠ "可用"**。**可用 = 现在这一刻服务可达,且能被维持住**。
## P10 ⚠️ 长前台命令被沙箱杀,连带杀掉先前后台起的进程
- **现象**:命令无输出、`Exit -1`;随后发现后台进程也没了。
- **修法**:**单条前台命令控制在 ~90 秒内**;长活**拆短步**或**走异步**。
## P12 ⛔ 反复重启常驻程序 ⇒ **把主会话自己弄卡**(最容易被忽视的一条)
- **现象**:**每次进入"改常驻程序"的阶段,主会话就变卡、被反复打断**(用户原话:"为什么每次一改到这里就把自己的会话弄卡")。
- **根因(两条叠加 + 一条次因)**:
1. 🔴 用**会话后台任务**起常驻 ⇒ 该进程成为**本会话的附带物**;**每次 kill / launch 都产生一条 `failed` 任务通知** ⇒ **每次都打断会话**。实测:一个阶段里重启 **7~8 次** ⇒ **7~8 次打断**。
2. 🔴 **同一会话里做了 500+ 次工具调用** + 反复读写长文件 ⇒ 上下文极大 ⇒ 每轮推理显著变慢。
3. ⚠️ 注入钩子每轮往上下文加 ~1KB(应压在几百字符)。
- **修法**:
1. **攒批重启**:代码改动**攒到一次**再重启(≈3 处以上/或等功能自测全过)。⛔ **不要"改一行重启一次"**。
2. **优先热加载**:把**规则/配置/任务图/队列**做成**程序每轮读文件** ⇒ 改这些**根本不用重启**。
3. **长任务换会话**:一个会话**不要既"建系统"又"跑长验收"** ⇒ 到阈值就**交棒接续**(这正是本机制存在的意义)。
4. **注入瘦身**:`additionalContext` 压到几百字符。
5. 🔴 **改成"拉取模型"**(最根治):**程序只写队列/文件,⛔ 不调用会话、不通知、不起后台任务**;会话在"用户发话 / 棒收尾 / 需要时"三个时机**主动拉**队首 ⇒ **没有"推"就没有打断**。详见 `references/collab-detail.md` §1.3。
> 🔑 **一句话**:**"改代码 → 重启常驻 → 产生失败通知 → 打断自己"这个循环,是主会话变卡的机制性原因**,而不是外部故障。
> ⇒ **"拉"取代"推",是这个问题的根治解。**
- **现象**:`SyntaxError: unterminated string literal`。
- **根因**:在双引号字符串里写了 ASCII 双引号。
- **修法**:文案里的引号一律用 **`「」`**;改完 **`ast.parse` 校验**(比 `py_compile` 报错更清楚)。
## P13 ⛔ 常驻"活不长" ∧ 钩子指向无唤醒码的旧副本 ⇒ 机制**静默停摆**(最阴的一条:没人会发现)
- **现象**:唤醒回路代码写完、也实测到 1 次真投递,但此后**再也不投**。查下去:常驻进程**已消失**、单例端口 `Connection refused`、日志停在某一刻;而"唯一会自动跑"的钩子那条路,跑的是**另一份旧的精简副本**(没有队列闸门、没有唤醒码)⇒ 整个机械层**静默停摆**,且**没有任何告警**——因为告警也是那个程序发的。
- **根因**:① **从会话里起的常驻会随其宿主会话结束被回收**(实测:`13:09` 起的 pid,`13:12` 之后再无一轮);② **同目录另存过一份旧副本且钩子指向它** ⇒ "代码在 A、钩子在 B" ⇒ **接了线等于没接**;③ 认口取"`uptime` 最大"的口,而**那个口可能没有活会话** ⇒ 有活会话也照样落 `no-live-session` 跳过。
- **修法**:
1. 🔴 **"能自动跑起来"的路只有一条 = 钩子**(每个会话收尾跑一轮、零 token、不占会话)⇒ **把机制的关键环节挂在这条路上**,⛔ 别押在"常驻一直活着"上。
2. **钩子必须指向最新那份**(含队列/唤醒);**同目录⛔ 不要留旧副本**,或让旧副本显式**转调**新版(fail-open + 静默 + 超时,⛔ 不改自己原有行为)。
3. **认口=「第一个带活会话的口」**,⛔ 不是 `uptime` 最大者(实测两者常不同)—— 同 cwd 多实例时,这条正是**投得出去 / 投不出去**的分水岭。
4. **判"接线成立"看产物**:每轮覆写的视图/队列/状态文件 **mtime 是否跟着"会话收尾"推进** —— 比读代码可靠得多(本节即靠这一条定位的)。
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,966 @@
> ## ⚠️ 本件=**原 `multi-session-collab` 技能的 `SKILL.md`**(2026-10-01 并入本包)
> · 🔴🔴 **2026-10-03 07:2x 新增状态块(本条最优先,⛔ 排在下面 10-01 那条之上)——「上报/投递」整套退役**:
> ⛔ **队列投递已真删**(`supervise()` 203→71 行、两个 `_deliver_str()` 调用点删除、`wake_enable` true→false)
> ⇒ 本件里凡讲**投递/上报/唤醒回路/跟进会话收件**的段落,**读到即跳过**(含 §1.4a、§0.5 里的 `wakeups.jsonl` 面板)。
> ✅ **现行机制只有两条腿**:**接收**=`--report` 写四态台账;**处理**=建 `[检查]-…` 排期。
> 🔴 **「常驻投递」这个角色名也别再用** —— 角色本来就叫「**协作程序**」,"常驻"是**运行形态、不是角色名**(P0-37)。
> ⚠️ 本件 §0.5 的**看板版面纪律仍然全部有效**(2026-10-03 两格合一就是按它做的);⛔ 只有"图上画着投递"那部分作废。
> ⇒ 冲突时以 `architecture.md` 当前结论节 + `SKILL.md` 文首 10-03 状态块为准。
> · 为什么带进来:本包 `references/{architecture,deploy,pitfalls,taskgraph}.md` 都写着「**主干判据在 `SKILL.md §x`**」
> —— 指的就是**这一份**。不带进来,包内就有 8 处悬空引用。
> · 🔴 **权威关系**:`references/architecture.md` 是**唯一权威**(其头部有「当前结论」节)。
> **凡本件与它冲突,一律以 `architecture.md` 为准**;本件只作**细则补充 + 溯源**。
> · 🔴🔴 **本件内所有「零常驻/按需唤起/跑完即退/不需要常驻/不用常驻描述主体」的表述,一律已被推翻**
> (2026-10-01 定案:**协作与投递一直运行(常驻)**;理由「可能不是所有队列都是钩子产生的」+「定时任务的方案已经废弃了」
> —— ⚠️ **2026-10-03 订正**:"投递"那半边已退役,⛔ 只剩"常驻程序一直运行")
> ⇒ **读到即跳过**,现行口径见 `architecture.md §4/§5-1`。
> · 原件 mtime `2026-10-01 13:56`;原件全文(含 YAML 头)⇒ `<工作区>/归档/技能-退役-20261001/multi-session-collab/SKILL.md`。
### 🔴 读法索引(**先看这张表,⛔ 别从头顺读**)
| 章节 | 性质 | 怎么读 |
|---|---|---|
| §0.5 · §0.5.0–§0.5.6(看板) | **本件独有 = 活判据** | ✅ 必读:看板总则、版面纪律、文案纪律、形状语义、改完怎么验 |
| §1.3 拉取模型 · §1.4a 唤醒回路判据 · §6 自愈纪律 · §8 派活前自检 · §9 `collabd.py` 维护要点 | **本件独有 = 活判据** | ✅ 用到时读 |
| §3 任务图 · §4 证据分级 · §5 自动化白名单 · §2 主会话只做两件 · §7 落地三个件 | **细则** | ✅ 用到时读(§3 亦见 `taskgraph.md`;§5 亦见项目层 `CODEBUDDY.md §F`) |
| §0 五个主体 · §0.05 目标 · §1 四条通道 · §10 防打转判据 · §11 术语表 | **已被取代** | ⛔ 跳过 ⇒ 读 `architecture.md` 的 §1/§0.1/§3/§6/§7 |
| §11.1 命名铁律 · §12 文档权威 | 摘要 | ⚠️ 略读即可 |
---
> ⚠️ **存档说明**:本件正文取自原技能 `SKILL.md` 的**主体**。
> 原件的 **YAML 头部**(`name` / `description` / `version` / `updated_at` / **逐轮 `last_change` 变更流水** / `agent_created`)**已于 2026-10-01 删除** ——
> 那是技能加载器的元数据 + 发布流水,放在参考件里对使用零价值、且会随时间腐坏。
> 原件全文(含该 YAML 头)见 `<工作区>/归档/技能-退役-20261001/multi-session-collab/SKILL.md`。
# multi-session-collab — 主会话 × 多子会话执行机制
> 🔴 **一句话**:**派活用自动化(唯一能开新会话的通道)|收结果直读宿主库(0 token)|机械判定下沉到本地只读脚本|AI 只在需要判断时被叫起|用任务图+关键路径防干等。**
> ## 🔴 当前结论(**先读这里** · 最后更新 2026-09-30 12:30)
> ⚠️ 本文件**通篇是追加式写法** ⇒ 下文可能残留**已被取代**的内容(已就地标注)。
> **冲突时以「本节 + `references/architecture.md` 的「当前结论」节 + 两文件 §迭代记录最新条」为准**,并请顺手把冲突处就地标注。
>
> | 项 | **当前结论** |
> |---|---|
> | **唯一权威** | 架构 ⇒ `references/architecture.md`(⛔ 别处⛔ 不再写第二份)。`交付物/` 里 4 份"多会话协同"平行件 + `落地清单-唤醒回路` ⇒ **2026-09-30 已加退役标注**,⛔ 不作用判据。 |
> | **运行形态** | 🔴 **本条已订正(2026-09-30)· 原 09-29 定案「协作程序与监督程序一直运行」已废弃**。实测判据(用户原话「**你把监督程序停掉关了 整个流程还是照样跑**」):`guard` 自 09-29 23:59 停到现在,**投递一路正常**(当日 8 条 `http=200`)⇒ 停掉常驻,流程照跑。⇒ **现形态 = 两个模式都由宿主钩子按需唤起**:`--once`(协作程序 · 维护队列)/`--tick`(**投递** · 唯一投递方)。⚠️ 「监督程序」**这个名字已退役** —— 它从来不是「一个该常驻的进程」(用户:「**跟监督程序没有半毛钱关系**」)。 |
> | **本机起法** | 唯一可行 = **宿主后台任务机制 + `stdout` 全重定向到文件**。⚠️ `detached` 活不过工具调用边界;`schtasks`/`reg`/`wmic`/`sc`/`wsl` 被**内置程序黑名单**硬拦。 |
> | **派活模板必带** | ① **`开工第 0 步:抢域锁`**(域按**显式 domain** 切,⛔ 不按 cwd/⛔ 不按工作区)② **`收尾自判 ⇒ 有缺口就接下一棒;blocked 则⛔ 不接、改喊用户`**。 |
> | **看板(三层)** | 🔴 **只读旁路观测**(用户 2026-09-30:「看板**不能影响程序执行**,可以**异步**、可以**延迟**」)⇒ `board.py --serve`:**后台线程产快照,请求线程只读内存缓存**(⛔ 不碰 DB/⛔ 不读文件),开几个标签页都不加宿主负载。🔴 架构图**三层**:用户/主会话/**分工板块(按线,不是历史会话列表)**/常驻程序+投递 + 宿主地基带;每格带**真状态**(会话取宿主库、程序取状态戳 mtime)。详见 §0.5。 |
> | 🔴 **看板=单实例**(2026-09-30 加护栏) | Windows 的 `SO_REUSEADDR` 允许**同端口重复绑定且不报错** ⇒ 多个实例**静默并存**、同一 URL 被**随机**应答 ⇒ 快照/代码版本**打架**(实拍:已有看板时第二个照样打印"看板已起")。⇒ `board.py --serve` 起前探 `/healthz` 签名,已有 ⇒ **拒绝启动**;换新代码用 **`--takeover`**。停机 ⇒ `stop-collab.py` ④。详 `references/pitfalls.md` **P0-9**。 |
> | **已知待修** | ① ✅ `goals_open()` 已补读 `acceptance_state`(2026-09-30)② ⚠️ `wake_round()` 已停用未删 ③ 常驻与钩子的**投递唯一性**未按新方案收口(现靠三重去重兜着)④ ⚠️ `board.py` 改完**必须重启服务**(进程内已 import,不重启就跑旧代码)⑤ ✅ **`--once` 已认「已停」总闸**(2026-10-01 修:`goal_paused()`/`paused_round()` + 自测 6 项)⑥ ⚠️ **"全节点 done 但验收未声明"** ⇒ 已补验收判据(V1–V5)+ 日志节流 600 s;⚠️ **判据仍由对话说明**(`goalctl declare`),⛔ 别拿本次这五条当项目常量 ⑦ ✅ **多目标 tab**(2026-10-01 加:`build()` 出 `goals[]` + 看板 `renderTabs()`/`paintScope()`;**tab 第一行=完整目标名**,⛔ 不许退回"只给简称")。数据源=`<inbox>/goals/*.json`(一个文件一个目标,**活跃排第一**);⛔ **没有 `goals/` 时行为与旧版逐字一致**(`goals[]` 只有 1 项)。⚠️ 台账按 `line ∈ 目标 topics` 切到各目标;**不属任何目标**的件归**活跃目标**且照常显示(⛔ 不静默丢)。改 `board.py` 记得走 ④ 的重启 |
| 🔴 **2026-10-01 体检新增三修** | ① **`--once`/`--tick` 认总闸**(`run != active` ⇒ 只写一行"已停"、清告警投影、⛔ 不投递)② **主会话解析只认活会话**(`_pick_live`)—— 实测断点:解析出一条**已 `completed`** 的「主控」当投递目标 ⇒ 一路 `main-not-live`(**程序在跑、通知永远发不出去**);修后判读转为 `target-busy` ③ **退役线清队列**:台账/任务图 18 条全是**已退役的手机接入线** ⇒ 队首长期指向退役需求(已归档、按当前类别重建) |
| 🔴 **唤醒主会话:只画了格,会话没建**(2026-10-01 实测) | 会话表里**零条** `[唤醒]` 前缀会话(带「唤醒」字样的 6 条全是 `completed` 的旧棒)⇒ 技能 §1.4a 那条"**随需求确定时创建**"从未执行 ⇒ 看板上「还没建」虽然按定性是**正常态**,但**那条腿实际是空的**。叠加"网关排期=死能力"(`CODEBUDDY_DISABLE_CRON=1`)⇒ **全员静止时零唤醒**。**处置:本次未擅自建**(属"派活/接续"白名单外 ⇒ 待用户拍板) |
>
> 🔴 **⑤ 已停 ≠ 静默**(2026-10-01 实测 · ⛔ 见 `tmp/协作机制体检-2026-10-01.md`):
> `goal.json.run=paused` 时,`collabd.py` 全文**零处**引用 `run`/`paused`(只有 `wake_enable` 管**投递**)
> ⇒ `--once` 照跑、照写 `STALL.md`/`NEXT.md`/`digest.md`/`TO-MAIN.md`,而 `digest.md` 的内容
> **经钩子注入每个会话的每一轮** ⇒ 一个"人为停掉"的需求被读成"机制坏了"。
> **正解**:`--once` 开头加总闸,`run != active` ⇒ 只写一行"已停",⛔ 不产告警。
> 🔴 **⑥ 判不出完成 ⇒ 唤醒永不暂停**:任务图 18/18 done、队列 0 待办,但 `acceptance_state` 只有说明行、无判据
> ⇒ `goal_state` 判「判不出来」⇒ `goals_open()=true` ⇒ §1.4a ②「已完成 ⇒ 唤醒任务暂停」**永不成立**,
> `TO-MAIN.md` 每轮照发。**正解**:补判据,或改成「全 done 且未声明 ⇒ 喊一次就静默」。
> 🔴🔴 **架构正文不在本文件** ⇒ **`references/architecture.md`** =「多会话协作」的**唯一架构文档**(2026-09-29 用户定案)。
> 🔴🔴 **本机制的一切迭代都在本 skill 内进行**(用户 2026-09-29 明令:「都要在技能中去迭代」)——
> 架构 ⇒ `references/architecture.md`|部署与起法 ⇒ `references/deploy.md`|踩坑 ⇒ `references/pitfalls.md`|操作 ⇒ 本文件|代码与配置 ⇒ `scripts/`。
> ⛔ **不在工作区或别处另开平行文档**;⛔ 不把机制文档散进业务件(业务件只留"用了哪条机制"的指针)。
> 🔴🔴 **改完必跑回归自测**:`python scripts/selftest.py` —— **全绿(rc=0)才算改完**。
> 它把**所有异常情况**做成了用例(⚠️ **条数别写死** —— 会随迭代增长,`grep -c '^@case(' scripts/selftest.py` 数一下才准:
> **看板与收尾确认的归属判据必须同款**/投递成对重复/**纯投影不消费队列**/**投不出不消费**/
> 两级命名/目标闸/台账四态/启动对账/僵尸/不显窗/假绿/**会话哑掉(诊断日志撞上限)**/常量完整/优雅退出 …),
> **⛔ 测试不碰生产**(用 `tmp/selftest/` 独立工作区)。
> 🔴 **写用例的两条纪律**(2026-10-01 各踩到一次):
> ① **"前提不成立就 SKIP"要把前提判「全」,⛔ 别靠副作用反推一半** —— 例:僵尸用例只看 `adopted` 就断言,
> 漏了「无 IN_PROGRESS 运行」那半 ⇒ **自动化会话跑自测时必然假红**(它自己就是那条运行)。
> ② **用例自造的假数据每轮清干净**(放 `_prepare()` 里清),⛔ 别让上一轮的留在原地 ——
> 断言"精确集合"的用例会被跨轮残留多算几条 ⇒ **第二次跑才红**(最难查的一型)。
> 📌 用例是从**真实事故**里长出来的 ⇒ 每踩一个新坑,**先把它加成一条用例**,再改代码。
> · **改架构 ⇒ 只改那一份**(五个主体 / 任务队列=**上报制** / 唤醒=**静默判据** / 四条通道 / 三种节奏 / 防打转判据 / 术语表)。
> · **本文件 = 操作入口**(怎么派活、怎么收尾、怎么排坑);下文出现的架构内容为**就近速查**,**冲突一律以 `architecture.md` 为准**。
> · ⚠️ **【已作废 · 2026-09-30 就地标注】** 此处原写"⛔ **零常驻** —— 凡见『常驻程序』字样均属旧说法"。
> **该说法已被用户 2026-09-29 定案推翻**(见上表「运行形态」:常驻程序与投递**一直运行**)。
> ⛔ 保留此行只为拦住"翻到旧文又照做";**判据一律以上表 + `architecture.md §5` 为准**。
## 0 第 0 步:先认清**五个主体**(本机制的**唯一主体清单**)
> 🔴 **架构里只有这 4 个主体**:**用户 · 主会话 · 任务会话 · 常驻程序**。
> 🔴 **「主会话」是*解析出来的*,不是*登记出来的***(2026-09-30 用户定则:「**以 一个工作区 为主会话的工作区**」):
> 登记(`roles[sid]=="main"`)只在**该会话确实活着**时才有效;失效则按**工作区**重新认
> (`cwd == 本工作区` 且标题**不带** `[协作]`),仍认不出 ⇒ **⛔ 拒绝盲投**、报 `no-main-session` 并喊用户。
> ⛔ 曾有「目标不在活会话里就随手取第一条口」的**盲选回落**(=零判据投递,会投错窗口),**已删除**。
> 判据唯一实体=`resolve_main()`;完整分析见 `references/architecture.md §7`。
>
> 🔴 **命名定则(2026-09-30 用户)**:**主会话 / 接续会话的标题前缀 = `主控`**,形如
> **`主控 · <任务类别> · <具体>`** —— 例:`主控 · 机制线(查唤醒为什么断)`。
> · **⛔ 中段写「任务类别」,不是目标名**(用户原话:「而且后面也不叫 手机接入」);
> · 🔴 **「任务类别」是同一个词的四处**(⛔ 别当成四件事):**`goal.json.topics` 的一项 ≡
> 会话标题里的 `<任务类别>` ≡ 台账条目的 `line` ≡ 分工板的一行**。⛔ `topics` 缺省才回落 `short`。
> · 🔴 **同一工作区可有多个任务类别**(2026-09-30 用户:「之前的协作会话是**跨工作区**的,
> 要能支持**同一个工作区** 多会话协作(…**通过协作会话名称前缀**区分具体执行会话,
> 所有主会话,协作会话,**自动唤醒任务**,都在**一个工作区**)」)⇒
> **每个类别各有自己的主会话**;投递**按件的类别**选主会话(`resolve_mains()` + `main_for_topic()`)。
> ⛔ **自动唤醒任务的排期名也必须带类别前缀**(`[协作]-<类别>-<具体>`)(⚠️ 仅**代偿形态**下适用;定案时钟=常驻投递,不开会话、无排期名)——
> 实测踩到:`[协作]-唤醒A · <工作区>` 缺第 2 级 ⇒ 解析不出类别。
> 全文 ⇒ `references/architecture.md §2.3.0`。
> · **⛔ 主会话的接续会话不许带 `[协作]` 前缀** —— `[协作]` 是**协作棒**的标记,带上它会被本机制
> 当成棒**排除掉**(我犯过:把接续会话建成 `[协作]-[手机接入]-接续…` ⇒ 自己排除自己);
> · `主控` 前缀是**显式标记** ⇒ `resolve_main()` **优先认它**(比"最近活动"可信:那是用户自己标的,不是猜的);
> · ⚠️ 实测坑:网关 `POST /api/v1/sessions/{id}/rename` **回 204 但不落宿主库**(显示名与库标题可能是两套)
> ⇒ 改名后**必须回读 `sessions.title` 确认**,⛔ 别只看 204。
> ⚠️ **2026-09-30 订正**:原来的第 5 个主体「投递」**已退役** —— 它其实只是**同一个 `collabd.py` 的 `--tick` 模式**(投递),⛔ **从来不是一个独立进程**。
> ⚠️ **宿主(WorkBuddy 本体)是地基,不是第 6 个主体**;它只提供四样:状态(只读 3 表)/**调度(自动化排期 = 唯一能开新会话的通道)**/事件(钩子)/互斥(域锁)。
> 🔴 **⛔ 不用"角色"这个词**(历史文档里"三个角色"的说法已被**本表「主体」**取代,见 §12);也 ⛔ 不用"常驻"描述任何主体 —— 本机制的设计前提是**零常驻**。
> ⚠️ **但「主体」与「会话类别」是两根不同的轴**,⛔ 别混:
> · **本表(§12)= 主体** —— 谁负责什么(含**用户**与**两个程序**:常驻程序/投递)。
> · **会话类别 = 四类**(2026-10-01 用户口径)—— **① 主会话 ② 协作会话 ③ 唤醒会话 ④ 队列上报的跟进会话**,
> 同工作区、靠**标题两级前缀**区分 ⇒ 见 `collab.md §4` = `architecture.md §2.3.0b`(接续=**形态**,⛔ 不是类别)/**§2.3.0c**(第④类,未落地)。
> ⚠️ **已知缺口**:第 ③④ 类**尚未列进本表**(本表是"主体"视角,唤醒会话/跟进会话都是**会话**,落点在会话类别那根轴上)。
| # | 主体 | 只做什么 | ⛔ 不做什么 | 与谁接口 |
|---|---|---|---|---|
| 1 | **用户** | 🔴 **职责(2026-09-30 用户原话):制定目标 · 调整方向 · 做决策**。可做的动作:看板 · 放行授权 · 改目标 · 喊停 | ⛔ 不管过程;⛔ 不必催进度 | → 主会话:需求/判据/授权 |
| 2 | **主会话** | **判断 + 派活**:读库+看板判缺口 ⇒ **写一行排期**;收尾自判 | ⛔ 不替别线干活;⛔ 不做机械判定;⛔ 不搬砖(除非最靠前那步自己就能做) | → 宿主:一行排期;→ 用户:看板/待授权 |
| 3 | **任务会话** | **一棒一线,一次一件**:做本棒;**收尾两件**=写产出 + 判本线缺口**接下一棒** | ⛔ 不跨线;⛔ 不夹带;⛔ 不常驻 | → 宿主:运行结论(自动落库) |
| 4 | **目标检查** | 持有**需求台账**;收上报/判定/告警/体检/看板覆写/单例(`--once` **投影轮**) | 🔴 **⛔ 不派活**(不写排期、不开会话);🔴 **⛔ 不投递、⛔ 不推进队列**(`mutate=False`) | → 用户:看板;→ 主会话:机械摘要 |
| 5 | **投递**(⚠️ 旧名「监督程序」,2026-09-30 退役。**它是同一个 `collabd.py` 的 `--tick` 模式**,⛔ 不是一个独立主体/进程) | **逐条读队列** ⇒ **投给主会话**(单条+握手/三条件唤醒)⇒ **唯一的投递方+唯一的队列推进方** | ⛔ 不开会话;⛔ 不派活;⛔ 口令不落盘/不进日志/不回显 | → 主会话:通知 |
**🔴 判定你是哪一个**:你是被派出去干一件具体事的那个 ⇒ **任务会话**;你是"入口"、负责判缺口与派活 ⇒ **主会话**;你是脚本 ⇒ **投递**(只读只判)或**目标检查**(其余全部,但不派活)。
---
## 0.05 🔴🔴 **目标不是固定配置 —— 它在「调用本技能的那次对话」里说明**(2026-10-01 用户订正)
> 用户原话:「**目标是 通过对话在调用 会话协作skill时说明的,不是固定的**」
**这条规矩治的是本技能 2026-09-30 犯过的一个错**:把 `goal.json.topics` 按**工作区里现有的
接续入口/接续包文件名**填了 4 类,还当成"待拍板事项"去问用户要不要**定死**。
⇒ 那是**猜**,不是**说明**;而且一旦写进 `goal.json`,机制就会照它跑(**判归属、选主会话**)——
**猜错了不会报错,只会静默漏管/错投**。
| 要说明什么 | 落到哪 | 谁来说 |
|---|---|---|
| **目标**(一句话说清要做成什么) | `goal.json.title` | 用户(对话里) |
| **为什么 / 边界** | `goal.json.why` | 用户(对话里) |
| **任务类别**(同一工作区靠它分前缀) | `goal.json.topics` | 用户说 / 主会话梳理 |
| **验收判据** | `goal.json.acceptance_state` | 用户说 / 主会话提 |
**🔴 三条硬规矩**
1. **技能侧与脚本 ⛔ 不许预设目标与类别,⛔ 更不许从目录名/文件名/接续入口名去"推"**
—— 推出来的东西会**静默决定**「哪些会话算本项目、投递往哪条主会话去」。
2. **登记有唯一落点**(="说明"这个动作的**唯一出口**):
```
python .workbuddy/collab/goalctl.py declare --title "…" [--why "…"] [--topics "A,B"] [--kpi "V1=pass"] --yes
```
⛔ 默认**干跑**;`--title` **必填**(脚本**不替你编目标**);省略某个参数 ⇒ **不动那一项**
(⛔ 不拿旧值凑数);`--topics ""` ⇒ 显式清空(回到单类别回落)。
3. **可以被覆盖** —— 下次调用技能时在对话里再说一遍 ⇒ 用 `declare` 覆盖即可;
⛔ 别把上一次说明的目标当成**项目常量**(这正是出错的根源)。
**⚠️ 未说明时的行为(⛔ 不许静默)**:`topics` 缺省 ⇒ 回落 `goal.short`(单类别,向后兼容),
但**看板必须显式标出「⚠ 未声明 ⇒ 暂回落目标简称」**,⛔ 不许让人以为"这就是定下来的类别"。
落点:`board.py::_goal_topics_source()` → `project.topics_source.kind ∈ {declared, fallback, none}`
(`declared` 时一并显示说明时间/说明人)。回归用例:`selftest.py::看板:任务类别来源必须说得出口`。
## 0.5 🔴 看板(`scripts/board.py` + `assets/board.html`)
### 0.5.0 🔴🔴 「看板」默认指**那个实时动态看板** —— 它是**要起着的**(2026-10-01 用户点破)
> 用户原话:「**我说的看板是 实时动态看板 现在被关闭了**」
⚠️ 别用"一份离线快照页"去顶 —— 快照只是**复核手段**(见 §0.5.4),**用户要看的是活着的那一个**。
| 项 | 值 |
|---|---|
| 地址 | **`http://127.0.0.1:8788/`**(⛔ 只绑回环) |
| 起法 | **会话后台任务 + stdout 落文件**(本机唯一可行的起法) |
| 命令 | `COLLABD_CONFIG=<ws>/.workbuddy/collab/collabd.config.json DSH_COLLAB_WS=<ws> "<python>" "<技能>/scripts/board.py" --serve 8788 --takeover > <ws>/tmp/board-serve.out.log 2>&1` |
| 换代码重起 | 加 **`--takeover`**(先停旧的再接管)。⛔ **别裸起第二个** —— Windows `SO_REUSEADDR` 会让两实例**静默并存**、同一 URL 随机应答("改了、也重启了、却还是旧的"就是这么来的)。单实例护栏:已有看板 ⇒ **拒绝启动** |
| 改完必须重启 | ⚠️ 服务进程**已 import** 技能里的 `board.py` ⇒ 改快照内容后**不重启就跑旧代码** |
| 停机出口 | `.workbuddy/collab/stop-collab.py`(dry-run 只报告)|`goalctl stop --yes` 会顺带停它 |
**⚠️ 两条如实登记的边界(⛔ 别当没这回事)**
1. 它是**会话后台任务** ⇒ **关会话/关宿主就停**。本机**没有**真正的常驻手段(detached spawn 活不过
工具调用边界;`schtasks`/`reg` 等持久化工具在内置程序黑名单里)⇒ **这不是"忘了常驻",是做不到**。
2. ⚠️ **该会话只要挂着 `pending`/`running` 的后台任务,宿主的 `idle` 钩子会被静默压制**
(2026-09-30 实测被一个僵尸任务压过 6h20m)⇒ 起看板的那个会话,其**唤醒/投递回路会哑**。
⇒ 正解=**让"起看板"这个动作落在一个不承担派活职责的会话里**,⛔ 别让主会话干这件事。
### 0.5.1 铁律:**看板不能影响程序执行**(用户 2026-09-30 明令)
> 用户原话:「**看板不能影响程序执行,可以异步 可以延迟**」。
三条落地(照这个改,⛔ 别把看板做成"第二个常驻程序"):
| # | 约束 | 做法 |
|---|---|---|
| ① | **解耦** | 常驻程序/守护程序**一行都不引用** `board.py`(可用 `grep -rn "board" scripts/*.py` 复核);`board.py` **从不写任何账本**(只写 `board.json`,且仅在 CLI 模式)。 |
| ② | **异步** | `--serve` 起**一个后台线程**按 `--interval`(默认 3 s)产快照 ⇒ **请求线程只吐内存缓存字节**(⛔ 不碰 DB、⛔ 不读文件)⇒ 宿主库查询频率**恒定 1/interval**,与标签页数量无关。页面轮询节奏**跟随** `board.refresh_interval`(⛔ 不自行加频)。 |
| ③ | **降级不静默** | 宿主库只读连接 + `busy_timeout=300`(撞写锁 **0.3 s 就放弃**,⛔ 不排队);任一块读不到 ⇒ 记进 `board.json.warn` 并在界面顶部黄条显示,⛔ **不伪装成"0 个会话"**(那是假情报)。刷新失败 ⇒ **保留上一份快照**,界面显示「数据延迟 N 秒」。 |
⚠️ **改完 `board.py` 必须重启看板服务**(进程内已 import,不重启跑的还是旧代码)。⚠️ 页面每请求重读 `board.html` ⇒ **只改 HTML 不用重启**。
⚠️ 起服务时把 `stdout` 重定向到文件(`> tmp/board-serve.log 2>&1`)—— 会话后台任务的每轮 stdout 会**唤醒宿主会话**。
### 0.5.2 架构图三层布局(用户 2026-09-30 指定)
> 🔴🔴 **2026-10-01:第三层语义又变了一次(第三次)⇒ 本节正文里"分工板块(按线)"已作废。**
> 用户原话:「**主会话 下面 那一排只放协作会话**,横着排 有几个放几个,当前没有对应协作会话 就空着」
> +「**放一个空的框 说明 暂无协作会话**」+「**唤醒机制如果是主会话的 事就放到主会话框里去**」。
> ⇒ **以 §0.5.6 为准**(那一排 = 任务会话排;任务类别搬进主会话框)。下文保留作历史。
```
用户
↓ 发消息 · 看板
主会话
↓ ① 派活 · 自动化排期
分工板块(按线)…每格:最近协作任务 / 承接会话 / 件进度
↓ ② 上报 · --report
目标检查 ──队列──▶ 投递 ──③ 投递 · reply/唤醒──▶ (右侧竖井回主会话)
↑ ④ 收结果 · 直读宿主库
宿主 WorkBuddy(地基)
```
- 🔴 **第三层是「分工板块」,⛔ 不是历史会话列表**(用户:「协作会话**不是历史记录**,是**展示分工**的板块」)。
**分工位 = 线**(`goal.json.lines` ∪ 台账里出现过的线);每格展示该线**最近的协作任务**(用户追加要求)、
当前**承接会话**、**件汇总**。三色:**有会话在跑**(绿)/**⚠ 有件没人在跑 = 缺口**(黄,最有用的信号)/**件已全完**(灰)。
- 🔴 **承接会话怎么认**:拿"在跑会话标题里的件 id"匹配(长 id 优先,防 `N1` 抢 `N10`)。
⛔ **不用 cwd 推断** —— §2.3 明令。匹配不上就如实写「当前无会话在跑」,⛔ 不硬凑。
- 🔴 **会话归属判据(本项目 vs 别的项目)**:`board.py` 与 `collabd.py` 各有一份 `in_project()`/`_in_project()`,
**必须逐字同款** —— 一旦漂移就会出现「看板说没人跑、`--ready-next` 却被别的项目拦死」。
已有回归用例 `selftest.py::归属判据:看板 ≡ 收尾确认` 逐例比对(⛔ 别删)。
- 🔴 **常驻程序节点带「队列计数」**(用户 2026-09-30 追加要求):**队列 = 需求台账 `tasks.json`**(不是 `queue.json`,
`architecture.md` §迭代记录明载)⇒ 显示 `队列 N 件 · 已完成 X · 未完 Y(· 受阻 Z)`,四态 `pending/running/done/blocked`。
⚠️ **机制里没有「待验收」这个态** —— 用户问「待验收队列数量」时**按四态如实给数,⛔ 不臆造一个数字**;
要不要真加一道「验收」关口,属**机制变更,待用户拍板**。
- ⚠️ **【本条已作废 · 2026-09-30】** 原文讲「投递显示『已停』是正常的」—— **前提已不存在**:该名字已退役,看板不再显示「已停」(见 §0.5.2b/§11.1)。以下原文仅作历史:
- ~~🔴🔴 **投递显示「已停」是正常的、有据的止损,⛔ 别当故障去"修"**(出处 `.workbuddy/memory/2026-09-29.md` 2424–2435 行
+ `architecture.md §5-1`,代码常量 `board.py::GUARD_STOP_REASON`):
2026-09-29 23:59 主会话 `guard.py --stop` 主动停 —— 守护当时是用「**会话内后台任务**」起的 ⇒ 任务**归属发起会话**
⇒ 该会话被判定"一直挂着长跑任务" ⇒ 实测「**一启动会话就卡消息输出**」。
🔑 **根因 =「有网关口令」与「不占会话」不可兼得**。**正解 = 拆两件**:「发现」走**会话外**常驻(启动文件夹/独立窗口,
⛔ 但拿不到口令)、「投递」走**宿主起的**通道(钩子 `--tick` / 低频排期 —— 宿主起的子进程**天生有口令**)。
~~⇒ 现投递**已由钩子 `--tick` 事件驱动**,守护常驻**按设计不再需要**;⚠️ **代价 = 没有独立唤醒时钟**
(即用户点破的「心跳成摆设」)—— **此点仍待用户拍板**是否恢复常驻。~~
🔴 **2026-10-02 标注(⛔ 不是口径变更)**:**这段已作废** —— 用户 **2026-10-01 已拍板**:「**协作与投递一直运行(常驻)**」+「**定时任务的方案已经废弃了**」⇒ **常驻必须回来**(见 §5-1)。⛔ 别再把上面那句当现状读。
### 0.5.2b 🔴 看板版面纪律:**小字描述只进右上角 `?`**
用户 2026-09-30:「把**各板块的小字描述**放到各板块对应**右上角 `?` 号图标**中,鼠标移上去显示
(**只保留标题,主体,类别标签**这类信息)」,并点名**删除**三处。
| 版面只留 | 进 `?`(`.qtip`) |
|---|---|
| **标题**(`h2`)· **主体**(表格/chips/架构图)· **类别标签**(`h2 .hint`,如 `tasks.json`/`组件状态`) | 该板块的**说明性小字**(为什么这样、判据是什么、口径提醒) |
- 实现:`.qh`(右上角圆点 `?`)+ 内嵌 `.qtip`,CSS `:hover / :focus-visible` 才显示 ⇒ **纯 CSS,零 JS**;
用 `<span class="qh" tabindex="0">` ⇒ **键盘也能看**。
- 🔴 **动态的长解释也要进来**(如「投递为什么是停的」)—— 做法:给 `.qtip` 一个 id,
每轮渲染先 `tip.innerHTML = tip.dataset.base`(首轮存静态原文)再按条件追加,⛔ 防重复追加。
- ⛔ **别再往版面摊解释性小字**;警告/读数这类**主体内容**不算小字,可以留在版面。
- **回归用例**:`selftest.py::看板:版面纪律`(`?` 数量与 `.qtip` 配对 + 被点名删除的三句不得复活)。
- 🔴 **「延迟」与「保留的快照时刻」是例外:必须**固定显示**在「协作架构」板块**右上角**,⛔ 不许收进 `?`
(用户 2026-09-30:「延迟 = 你看到的时间 − `<快照时刻>` **保留时间** 放在**协作架构板块右上角**」)。
表单:`.herometa` 两行 —— 第 1 行**只放快照时刻**(`<b id="ts2">`),第 2 行放 **`延迟 <span id="lag">`**。
⚠️ 2026-09-30 用户又划掉一次:**「延迟 = 你看到的时间 −」这句不要**,时间下面直接写「延迟 X 秒」即可
⇒ ⛔ 不要再加解释性前缀。由 `paintAge()` 每秒刷新,**超过 15 秒整块转警示色**。
⛔ 别在 header 再放第二份时刻(已去掉)。
### 0.5.1b 🔴🔴 技能 / 使用方分离(用户 2026-09-30 定则)
> 用户原话:「**技能就是技能 程序就是程序,谁用产生的文件 放在他自己那里**」
**技能 = 通用能力**(`SKILL.md` / `references/` / `scripts/` 的代码 / `assets/` / 范例配置)。
**使用方产生的东西 ⇒ 放使用方自己那里**:部署配置、运行日志、编译产物、看板扩展、部署启动器。
| 件 | 归谁 | 落点 |
|---|---|---|
| **部署配置** | 使用方 | `<工作区>/.workbuddy/collab/collabd.config.json`(技能里⛔ 不放;只有 `collabd.config.example.json` 范例) |
| **运行日志** | 使用方 | 配置键 `log`(默认落在工作区内);⛔ `LOG = HERE / …` 是旧写法,**已禁** |
| **看板「前置」块** | 使用方 | 配置键 `board_ext` → `.workbuddy/collab/board_ext.py`,契约 `build(ws) -> {title,tag,chips,paragraphs,tip}` |
| **部署启动器** | 使用方 | 工作区自己的 `start-guard.cmd`(里面写死本机 python 路径 ⇒ 属部署件) |
🔴 **配置查找链(`collabd.py`)**:① 环境变量 `COLLABD_CONFIG` → ② `<COLLABD_WORKSPACE 或 cwd>/.workbuddy/collab/collabd.config.json` → ③ **都没有 ⇒ 拒跑 rc=2**。
⚠️ **为什么 ③ 必须拒跑**:不配配置时 `workspace` 会回落 **cwd**,而 cwd 常常就是**技能目录** ⇒ 技能里长出 `tmp/supervise-inbox/` 与 `_collabd.log`。
⛔ 连"未找到配置"那条告警**都不许写文件**(它自己就会造成同样的污染)⇒ 只打 stderr。
🔴 **接线方(钩子/启动器)必须显式传 `COLLABD_CONFIG` + `COLLABD_WORKSPACE`**;⛔ **不许做"回落同目录旧副本"的兜底** —— 那是静默换版本的经典来源。
🔴 **回归用例**:`selftest.py::技能侧零项目串`(技能目录无结构性产物/日志不挂 HERE/`board.py` 代码零项目串/看板不回读废弃项目键)。
⚠️ **迁移期尾巴**:改完这套后,**迁移前起的旧进程**仍会用旧代码路径写文件(实测 `wb-supervisor-watch.py --interval 10`)⇒ 等它结束才彻底干净。
### 0.5.2c 🔴 文案纪律(**去 AI 味**)—— **看板 + 注入给会话的正文**
用户 2026-09-30:「**现在很多文案不是抓不住重点,就是描述太AI味**」。
用户 2026-10-01:「**会话的反馈信息排版 非常不利于阅读,改为段落排版**」+ 给出目标形态:
**一行 header + 其后每行「标签:一段话」**。
🔴 **适用范围(2026-10-01 扩写 · 原来只管看板,是个漏洞)**:本节纪律**同时管**两处 ——
① **看板板面**;② 🔴 **注入给会话的正文**:`digest.md`(机械摘要)+ 三个信号文件
(`STALL.md` / `VACUUM.md` / `READY.md`)。为什么必须一致:后者是**每轮钩子注入给每个会话**的,
排版差 = **每个会话每轮都要多花注意力去拆句**,是**全平台共用的阅读成本**;
而且用户**根本分不清**哪句来自看板、哪句来自注入 —— 两处形态不一致就等于纪律没落地。
⚠️ **载体**:注入正文的唯一生成处 = `scripts/collabd.py::digest_text()`(`digest.md`)与 `signals()`(三个信号文件)。
**改文案=改这两处**,⛔ 别去改生成的 `.md`(下一轮就被覆写)。
**规则来源**:技能 `humanizer-zh`(24 类 AI 写作模式)+ `agent-operating-rules §2.5`。两处文案都按这两份过。
| ⛔ 别写 | ✅ 改成 | 实例 |
|---|---|---|
| **否定式排比**「不是 X,而是 Y」 | **只说后半句** | 「⛔ 不是历史会话列表」→「按线分工」 |
| **金句**(听着能被引用) | 说具体事实 | 「一眼看得出来」→「看格子颜色就知道」 |
| **`⇒` 满屏**(每段好几个) | 逗号 / 句号 | 「两段都在线 ⇒ 端到端未验证」→「两个组件都在线,链路还没通」 |
| **`≠` 符号** | 写成句子 | 「在线 ≠ 通过」→「都在线,链路也可能是断的」 |
| **加粗滥用** | 只留关键词 | 每段加粗 ≤2 处 |
| **emoji 装饰** | 只在真警示处留一个 | 图例里的 `⚠` 删掉(色块已表意) |
| **三段式凑数** | 有几项写几项 | 「最近任务 / 承接会话 / 件进度」→「最近做完的件、现在谁在做、还剩几件没完」 |
| **一条信息说两次** | 只留一处 | chip 只给状态(「已停」),后果写在正文段落里 |
| **解释性前后缀** | 直接给值 | 「延迟 = 你看到的时间 − 07:13:12」→「07:13:12 / 延迟 4 秒」 |
| 🔴 **`- ` 列表碎片 + `;`/`⇒` 串联短句** | **段落排版**:一行 header + 每行「标签:一段话」 | 「`- 🔴 **中断/脱节** —— 没有会话在跑,且 0 分钟 无成果;⛔ 未来 1 小时内零排期 ⇒ 不等人发消息就是**确定性静默**`」→「`状态:没有任何会话在跑,而且已经 3 分钟没有出新成果。未来 1 小时内也没有任何排期,只要没人主动开口,这里就会一直静默下去。`」 |
🔴 **判据(写文案时的唯一一问)**:**这是给人读的,还是给程序读的?**
给人读 ⇒ **段落**(完整的句子、标签打头、一行一件事);给程序读(注释 / 日志 / 解析用字段)⇒ 随便。
⚠️ 现有实现里有反面先例:`verdicts()` 里的 `V["verdict"]` 就是「`;` + `⇒` 串联」形态 ——
它**保留**给 `realtime.md` 与通知用,但**摘要⛔ 不许直接复用它**(要按段落重讲,见 `digest_text()`),
⛔ 也⛔ **不许对它做字符串反解**(反解会随措辞改动**静默失效**)⇒ 事实要由 `verdicts()` **另带原始键**(如 `zero_sched` / `probe`)。
**自检(改完文案后跑一遍)**:搜 `⇒` `≠` `一眼看得出来` `⛔ 不是` 在**非注释行**里的出现,应为 0。
⚠️ 判据要**排除注释**(`//` 与 `/* */`、Python 的 `#`)—— 注释是给未来的我看的,可以带标记;**给人看的才是受众**。
### 0.5.3 🔴 看板风格系统(**右上角切换**)
用户 2026-09-30:「画架构图**参考 archify 的样式**(<https://github.com/tt-a1i/archify>),
**右上角加个风格切换(保留当前风格)**」。
| 风格 | `data-style` | 说明 |
|---|---|---|
| **当前风格**(默认·⛔ 不许删) | *(无属性)* / `base` | 原风格,**跟随系统深浅色** |
| **Archify 暗** | `archify-dark` | 令牌**逐字取自** archify `assets/template.html` 的 dark 主题(MIT) |
| **Archify 亮** | `archify-light` | 同上,light 主题 |
🔴 **实现铁律(违反就会"换风格换出花")**:
- **切换只改 `<html data-style="…">`** —— 颜色/字体/网格**全走 CSS 令牌**(`--color-*` / `--font-ui` / `--canvas-dot` / `--node-stroke-w`)
⇒ **换风格不需要重画 SVG**。
- ⛔ **组件与 SVG 里一律不许出现硬编码颜色**(`#hex` / `rgba(...)`)—— 只许出现在令牌块里。
**已有回归用例守着**:`selftest.py::看板:风格系统`(三风格令牌在位 + 组件零硬编码色)。
- 选 `base` 时**要把 `data-style` 属性摘掉**(不是设成 `"base"`),好让它继续吃
`@media (prefers-color-scheme: dark)`。
- 选择存 `localStorage['dsh-board-style']`,且**在 `<head>` 里尽早套用**(⛔ 别等 `load`,否则先闪一下原风格)。
- archify 的关键令牌备忘:画布 `#020617`(暗)/`#f4f5f7`(亮);点阵 `rgba(148,163,184,.16)`/`#d9dee5`;
强调色 `#34d399`/`#059669`;语义色 backend `#34d399`、cloud `#fbbf24`、security `#fb7185`、external `#94a3b8`;
字体 **JetBrains Mono(拉丁)+ 系统 CJK 回退** ⇒ 本地用 `ui-monospace, Consolas, "Microsoft YaHei"` 近似。
⚠️ **等宽字体的字宽与比例字体不同** ⇒ 改风格后**必须重跑"文字越界 + 压框"检查**(本轮三风格均 0 溢出 / 0 压框)。
- ⚠️ **图上文案宽度**:中日韩字符 ≈ 1 em(13 px 字号 ⇒ 约 13 px/字),ASCII ≈ 7 px。
**⛔ 别用"每字符 8.4 px"那种估法**(会把标题算长一倍 ⇒ **文字溢出框外**,第一版就栽在这)。
用 `fitText()` 逐字符累加宽度来截断。**验收**:跑一遍"逐个 `text` 量 `getBBox()` 是否越界"的检查,必须为空数组。
### 0.5.4 🔴 改完看板怎么验(⛔ 别只跑自测 —— 2026-09-30 收尾棒踩出来的)
**问题**:本机(Windows 客户端)**没有独立的浏览器实例**给这个技能用,而本机**明令禁止自起
headless chrome /附着用户 Chrome** ⇒ **不许擅自开浏览器**。但"改了看板没验"=没交付。
⇒ 三步取证的组合拳(都在 `tmp/` 现做,不用常驻件):
| 步 | 做什么 | 判据 |
|---|---|---|
| ① **语法** | 抽出 `board.html` 的主脚本 `node --check` | 通过 |
| ② **渲染** | 极简 **DOM 桩**跑真 `render()`:喂一份真实快照,断言**渲染出来的 DOM 里该有的都有、该没的都没** | 断言全绿 |
| ③ **几何** | 解析渲染出的 SVG:**所有 rect/text 在下界内**、**行带互不重叠**、**格间零重叠**、右沿对齐 | 全部 0 越界 |
🔴🔴 **两条最容易踩的坑**:
1. **桩必须"当场从 `board.html` 现取"主脚本** —— ⛔ **不许读上一轮导出的副本**。
副本可能是**带补丁的形态**(本轮实测:旧副本是"内联快照"版 ⇒ 换新脚本后 10/12 全 ✗,
一查不是代码坏了、是**副本形态不对**)⇒ 读副本 = **改动越多、假绿越稳**。
2. **离线预览页是给人眼复核的,不是替代品**:把快照**内联**进 `board.html`(并让 `load()` 优先吃内联件)
⇒ `file://` 双击即看,⛔ 不依赖本地服务、⛔ 不依赖网络。**桩验"内容与几何",人眼验"观感"**,两件都要做。
🔴🔴 **改之前必须知道的两件位置事实**(2026-10-01 实测,⛔ 不知道就会白改):
- **`board.html` 从「包内」读**(`board.py` 用 `HERE.parent/assets/board.html`)⇒ 改包内**即生效**。
- **`board_ext.py` 从「使用方工作区」读**(配置项 `board_ext`,默认 `<工作区>/.workbuddy/collab/board_ext.py`)
⇒ **只改包内那份等于没改** —— 必须 `cp` 同步过去,并**核 md5 一致**。
⚠️ 同理还有 `goalctl.py` / `stop-collab.py` / `deliver-gateway-token.py` 等**工作区副本**。
⇒ **改任何一件前先 `diff` 两份**:若差异只有你这次改动 ⇒ 直接 `cp`;否则先弄清谁是谁。
⚠️ **回归工具当前住在 `tmp/`**(`tmp/render-check.mjs` + `tmp/arch-geom-check.mjs` + 快照 `tmp/board-verify*.json`)
—— 而 `tmp/` 按规矩是**要清的草稿区** ⇒ **清掉后"改完看板怎么验"这条路就断了**(`selftest.py` 盖不住渲染与几何)。
⇒ 要用之前**先确认它们还在**;⛔ 也别把它们当长期资产(**待迁移进包**,登记为结构性问题)。
⚠️ 两个脚本的路径常量是**写死**的:`render-check.mjs` 的 `BOARD` 曾长期指向**已退役**的
`skills/multi-session-collab/`(合并后没人改)⇒ 一跑就崩。**换机器/换技能名后必查这一行。**
⚠️ **同理适用于"台账/验收"这类状态**:本轮两个真 bug 都属「**不崩溃,只是少说一句话**」
(见 `references/architecture.md`),**"跑一遍没报错"永远验不出来** —— 只能用**两处独立读数对账**。
### 0.5.5 🔴 架构图的**形状语义**:这一格**是一条会话**,不是"调度配置"(2026-10-01 · **二次改形状**)
**形状改过两次(⛔ 别照旧文档做事)**:
| 时点 | 画成什么 | 为什么 |
|---|---|---|
| 2026-10-01 上午 | **六边形 + 外圈虚线**(`<path>`) | 用户当时说「**给新建的唤醒定时任务换个样式,和协作会话区分开**」⇒ 用**异形**表达"它不是会话" |
| **2026-10-01 晚间(现行)** | **圆角矩形 `<rect rx=14>` + 外圈虚线** | 用户随后定性:「**唤醒脉冲会话,本质还是会话**」(**现名:唤醒会话** —— 2026-10-01 用户再改名,原话「**唤醒主会话 改为 唤醒会话**」)⇒ 与主会话/执行会话**同族** |
🔴 **结论:形状不再是区分手段,位置才是** —— 它在**主会话下面单独一行(`RW` 行)的左格**,
与右格「**跟进会话**」**等宽并列**(用户 2026-10-01:「**唤醒会话和 跟进会话 单独放一行**」+
「**唤醒会话 和 跟进会话 框一样大小**」),⛔ **不进下方那一排**。全图**没有六边形**。
⚠️ **位置改过一次**:上一版是"贴在**主会话左边**";拆行后主会话那一行**只剩主会话独占**
(="主会话只能是用户触发"在图上成立:指进它的线只有上面"用户"那一条)。
🔴 **三条硬定性**(2026-10-01 用户原话:「**唤醒会话 是独立会话,不要在主会话上处理,是随着需求确定时创建的**」):
① **独立会话**(自己一条,⛔ 不是主会话的职能);② ⛔ **不在主会话上处理**(叫醒的执行体是它自己);
③ **随需求确定时创建**(⇒ 看板上「**还没建**」是**正常态**,⛔ 不是故障)。详见 **§1.4a ⑤**。
| 形状 | 含义 | 出现在哪 |
|---|---|---|
| **圆角矩形** `<rect rx=14>` | **会话/程序/地基**(一直在那儿的角色) | 主会话 · 主会话**下面那一行左格**的唤醒会话 · 同行右格的跟进会话 · 下方那排任务会话 · 目标检查 · 上报 · Hook进程 · WorkBuddy |
| **外圈虚线** `.trig-halo` | 叠加在唤醒会话上 ⇒ "它靠**脉冲**被叫醒,不是一直在跑"(⚠️ 「**脉冲**」是 **2026-10-01 及更早**的说法;现行口径里**负责叫醒的是常驻投递**。⛔ 但**图形保留** —— 这一格确实**不是一直在跑**,虚线仍然如实) | 只有这一格有 |
| 🆕 **大圆角 + 细虚线 + 不填色** `.grp` | **分组**(⛔ 不是节点、也⛔ 不是"新一层") | 上框=主会话+唤醒+跟进;下框=任务会话那一排(见 §0.5.7) |
- 🔴 **状态用"颜色 + 徽章"两重表达**,⛔ 不能只靠颜色(色盲/黑白打印会丢信息)。
`front.triggers[].state` 三态:`running` **绿 ●运行中** / `paused` **黄 ‖已暂停** / `none` **灰 ○未登记**;
⚠️ **缺 `state` 时回落 `up`**(向后兼容只给 `up` 的老使用方)。
- 🔴 **徽章用 SVG 图形,⛔ 不用 `●‖○` 这类字符** —— 字符依赖字体,缺 CJK 字体的环境会渲染成方框。
- ⛔ **技能里不写死这一格叫什么** —— 名字由使用方经 `front.triggers[].name` 给。
🔴 **用户 2026-10-01 定的名(现行)=「唤醒会话」**;同日更早叫过「唤醒主会话」(**已作废**,用户原话「**唤醒主会话 改为 唤醒会话**」),再早叫「唤醒脉冲会话」「唤醒定时任务」(均**已作废**)。
属**项目侧**,`board.html` 里⛔ 不写死这个名字 —— 由使用方经 `front.triggers[].name` 给。
**判据:`assets/board.html` 的渲染逻辑里不该出现具体项目名。**
- 🔴 **2026-10-01 23:1x 图上改「简称」(用户第六条口径)**:`唤醒会话 →` **唤醒**、`队列上报的跟进会话 →` **跟进**
(⛔ 用户明令**两者字号一致**,`n-title-sm` 13px)、`目标检查 →` **协作**(`n-title` 16px)。
⚠️ **简称 ≠ 改角色** —— 正文/代码里的**全称不变**;图里 `?`/`aria-label` **必须带简称对照**
(否则「**协作**」会被误读成「**协作会话**」)。⚠️ 探针别拿 `唤醒` 当子串判据(见 `architecture.md §2.3.0c-2`)。
项目侧 `front.triggers[].name` 也随之给了 `唤醒`(`board_ext.py`)⇒ 图格标题取的是**简称**。
- 🔴 **读数来源随定性一起改了**:它**是一条会话** ⇒ 状态读 **`sessions` 表**
(`title` / `custom_title` 前缀 `[唤醒]`,且须排掉 `deleted_at`),**⛔ 不再读 `automations`**。
⚠️ 旧口径(读 `automations.status`/`next_run_at`,并限定 `schedule_type='recurring'`)**已作废** ——
但它作为**教训**留着:一次性派棒(如「接续 · …(唤醒回路实战)」)名字里也带「唤醒」二字,
混进来会把状态判成 `running` = **假绿**(2026-10-01 实测:捞出的 11 条里 8 条是一次性)。
- 🔴 **2026-10-02 订正**:原文写「**下次几点到点**」程序**读不到**(理由=脉冲是会话自己起的一次性后台任务)—— **两半都不对**:① 定案的时钟是**常驻投递**(⛔ 不是「会话自己起后台任务」——那条是**明令禁区**,见 §4.0 末);② 「下次到点」**读得到** —— 代偿形态下它就在排期表 `automations.next_run_at`。
⚠️ 但**看板那一格目前还没读它**(=已知缺口)⇒ 眼下仍只给「**上次真的响过是什么时候**」;⛔ 不许编一个「下次到点」出来。
- **回归**:`selftest.py` 覆盖不到图形 ⇒ 靠 `tmp/render-check.mjs`(形状/徽章/图外说明/帮助文本,**8 条断言**)
+ `tmp/arch-geom-check.mjs`(唤醒会话**必须贴在「唤醒+跟进」那一行(`RW`)** —— 它**已不再贴主会话那一行**;
⚠️ 这条"贴在哪一行"的判据 2026-10-01 拆行时**必须跟着改**:不改 ⇒ 拿 R2 顶去比 ⇒ **假红**)。
🔴🔴 **改形状必须同步改断言**:否则过时断言 = **假红**;
⛔ 更阴的是**恒真断言 = 假绿**(本轮实测:形状改回 `<rect>` 后,几何自检里那条查
`<path class="trig-hex">` 的断言**恒不命中** ⇒ 恒真 ⇒ 假绿,改了形状却"看起来还是绿的")。
**断言里的数字一律从快照现算**,⛔ 不写死;**新增断言要做一次反向对照**(故意造错,确认它真会红)。
### 0.5.6 🔴 第三层 = **任务会话排**;任务类别搬进主会话框(2026-10-01 用户定 · 这一排第三次换语义)
**来历(三次换语义,⛔ 别照旧文档做事)**:
| 版本 | 那一排画什么 | 为什么换 |
|---|---|---|
| ≤ 2026-09-30 上午 | **按线**(`goal.lines`/台账 `line`) | 跨工作区时代的维度 |
| 2026-09-30 晚 | **按任务类别**(`goal.topics`) | 为治"同一工作区多类别塌成一行"——⚠️ **当时不是错的**;但类别**本质是主会话自己的事**,占着"任务会话"的位置会误导 |
| **2026-10-01(现行)** | **按任务会话**(`sessions` 里 `role !== '主会话'`) | 用户:「**那一排只放协作会话**,横着排 有几个放几个,当前没有对应协作会话 就空着」 |
**触发这次改动的三连问**:「唤醒定时任务 和 主会话下方的 唤醒机制是不是 重复了」→
「你说的唤醒机制 是不是 主会话根据目标创建的 协作会话」→「**那你的唤醒机制 是啥意思嘛
跟主会话都一个 ID,难道是主会话?**」⇒ 病根 = **那一排把"类别"和"会话"混着画**,读者分不清谁是谁。
#### 三条硬规矩
| # | 规矩 | 为什么 |
|---|---|---|
| ① | 那一排**只放执行会话**,⛔ 不塞类别、⛔ 不塞台账旧线 | 用户原话。混着画就分不清"谁在干活"与"分了几类活" |
| ② | **一条都没有时也要画空框**(虚线框 +「暂无协作会话」+一句说明) | 用户追加:「**放一个空的框 说明 暂无协作会话**」。⛔ 不许因为"没内容"就把这层省掉 —— 读者会以为图缺了一块 |
| ③ | 🔴 **2026-10-01 同日已删** —— 原「任务类别搬进主会话框」(一行「负责类别:…」,按 `main_by_topic` **反查**)**不复存在**。任务类别现在**只在两处**说:顶部「本项目」卡的**任务类别 chips**(每个 chip 带「主会话 `<id8>`」/「⚠️ 无主会话」)+ 图外说明「当前」块第 1 条 | 先有用户:「**唤醒机制如果是主会话的 事就放到主会话框里去**」⇒ 搬进去;随后用户指着那行问:「**这个是什么意思,如果没用就删掉**」⇒ 删。**两条删的理由**(实测,⛔ 不是口味):① **它会说出假话** —— `topics_source.kind='declared'`(类别在对话里说明过)而 `main_by_topic` 为空时,它渲染成「未在对话里说明」,**把"这条主会话没对应到任何类别"说成了"类别没声明过"**(两件事被一个判据混在一起);② 同一事实**版面上已说过两遍**,且方向更对(类别→谁负责),这行是第三遍、还是反方向。⚠️ 顺带:`R2.h` 由 112 **回落到 84**,`tmp/arch-geom-check.mjs` 的 `rows.R2` 必须同步 |
🔵 **「自动化排期」是主会话的\*\*动作\*\*,⛔ 不是独立角色** —— 用户 2026-10-01 点破:
「**自动化排期开新会话:这个不就是主会话创建和派活吗 也是自动任务的方式**」⇒ **对**。
⇒ 它画在**连线标签**里(`① 派活 · 自动化排期`)就够,⛔ **别单开节点**。
⚠️ 但"它是**开新会话的唯一通道**(钩子开不了会话)"这条硬约束**光看线看不出来**
⇒ 在**图外说明**里点一句即可,⛔ 不动图结构。
(**这条踩过**:2026-10-01 我先把它当成"图上缺了一环"报上去,被用户当场纠正 ——
判据:**角色才配节点,动作只配标签/说明**。)
🔵 **空态时 ①② 两个标签都要画** —— 原来只画 ①,② 的标签在 `else` 分支里
⇒ **一条任务会话都没有时,图上只剩 ①**,看不出还有 ② 上报这条回程。
⛔ 空态 ≠ 这两条通道不存在,只是当前没有会话可连(2026-10-01 修)。
🔴 **「未归类」也⛔ 不占这一排** —— 台账里不在当前类别清单的旧线,**汇总改由图外说明**说清
(「N 条线 · 共 M 件 · 已完成 K」)⇒ ⛔ 不许因为"没地方画"就静默丢(本线红线:读到了就要说出口)。
🔴 **任务会话格画什么**(2026-10-03 定稿):**只画三行** —— 会话名 / 状态·`id8` / 多久没活动。
⛔ **不画「所属类别」那一行**(原本有 `类别:xxx`,无类别时又加了一串提示)——
**这行已被用户指示删除**(逐字:「越写越看不懂 还是删除了把」)。
留档(三次踩坑,别再犯):① 原文案「(标题里没读类别前缀)」**只说现象、没信息量**;
② 我改成「⚠ 无类别 ⇒ 不会被派活」——🔴 **那个后果是我编的**(取证:派活读 `tasks.json`,
`topic` 在派活链路上**根本没出现**;而那排格子**本身就是正在执行的任务会话**);
③ ⇒ 结论:**这行信息量不足,⛔ 不值得占版面、更不值得编文案**。
📌 **两条通用红线**:⚠️ **写「会怎样」前必须先取证那个后果真的存在**(否则=编);
⚠️ **改文案改到第三轮还说不清 ⇒ 该问的是「这行要不要留」,⛔ 不是继续润色**。
#### 前一个版本留下的两处真缺陷(代码仍在,⛔ 别照抄回去)
- 🔴 **`board.py::_labor()` 的回落分支 ⛔ 不许再排除主会话**:旧判据
`… and str(s.get("role")) != "主会话"` 本意是"最近在做的=承接会话",但**该类只有主会话自己在跑**时
会把候选**整类判空** ⇒ 同一格里 ③ 说「无可归到它的会话」、④ 说「承接 `<同一条>`」= **自相矛盾**
(用户看到的正是这个画面)。⇒ 候选**不排除任何角色**,`working` 排最前。
- ⚠️ **`fmtAge()` 的返回值自带"前"字**(`4 分钟前`)⇒ ⛔ 别再加一个 —— 2026-10-01 实测拼出过
「4 分钟前**前**有活动」。**这是造正例快照时才暴露的**(见下)。
#### 回归
- `tmp/render-check.mjs`(数字一律从快照现算):
① 🔴 **主会话框⛔ 不许再写「负责类别」**(2026-10-01 删了那行 —— 它会说出假话,见 §0.5.6 表 ⓷)
+ 架构图⛔ 不许再出现「未在对话里说明」(同族假话:类别声明过却说没说过);
② 第三层**只**出现任务会话的 `id8`(0 条时必须是「暂无协作会话」+虚线框);
③ ⛔ 不许再有「收件主会话」这类旧类别格残留(防回潮);
④ 🆕 「当前」块必须按 **`topics_source.kind`** 说类别(`declared` ⇒ 点类别名 + 对应主会话;
否则 ⇒ 明说「还没在对话里说明过」)—— ⛔ 不许拿**回落值**当已声明的类别报出去。
- 🔴 **断言必须"改前能报红"**(⛔ 不许写成恒真 —— 那是假绿)。**做法:拿改前的备份跑同一套桩**,
同一份快照下"改前红、改后绿"才算真判定。2026-10-01 删那行时就是这么验的:
线上真实快照 **改前 6 红 → 改后 4 红**、合成 `kind='fallback'` 样本 **改前 7 红 → 改后 4 红**
(残留那 4 条是**写死给样本快照的内容型断言** ⇒ 拿**线上快照**跑就假红,属**桩的已知弱点**,登记为 P2)。
- 🔴 **正例快照必须造**:真实工作区经常 0 条任务会话 ⇒ **只跑默认快照,"有几个放几个"那条分支
从没被执行过**(=未验证)。做法:把 `sessions` 追加几条 `role='任务会话'`(含一条 `topic` 为空的),
再 `node render-check.mjs <该快照>` 跑一遍。**2026-10-01 就是用这招抓出「前前」那个拼接 bug 的。**
⚠️ 同理:`topics_source.kind='fallback'` 也要**合成**一份样本 —— 现成两份快照都是 `declared`
⇒ 不合成就**验不到**「没声明过」那条分支(2026-10-01 实测踩着)。
- `tmp/arch-geom-check.mjs`:内置层高表 `rows` **必须与 `board.html` 同步**
(R2 84→112→**84** —— 2026-10-01 那天来回过,⛔ 改 `R2.h` 必须同时改这里)—— ⛔ 不同步 ⇒ 几何自检是**假绿**。
---
## 1 🔴 四条通道(**哪件事走哪条 —— 走错就是今天最大的浪费来源**)
| 要做的事 | 走哪条 | 成本 | **为什么必须是它** |
|---|---|---|---|
| **派活 / 唤醒会话的首次创建** | **自动化**(宿主排期) | 一个会话 | 🔴 **钩子开不了新会话** —— 自动化是**唯一**通道(宿主硬边界);⚠️ 派活**首次创建**会话走这条是对的,⚠️ 但「**周期性叫醒**」⛔ **不该**走这条(那是常驻投递的活,见下行) |
| 🔴 **收结果 / 检测** | **直读宿主库**(`automation_runs`,只读 SQL) | **0 token** | 宿主**每次跑完就把它自己的收官结论落库** ⇒ ⛔ 不必轮询、⛔ 不必扫文件、⛔ 不必叫 AI |
| ~~🔴 **通知主会话(投递)**~~ | ~~**宿主钩子唤起投递**(`collabd.py --tick`)~~ <br>🔴 **2026-10-02 标注(⛔ 不是口径变更)**:**本行已作废** —— 用户 **2026-10-01**:「**协作与投递一直运行(常驻)**」+「定时任务的方案已经废弃了」⇒ 定案=**常驻投递**(0 token · 0 会话 · 1 常驻),钩子只作**补充**(§5-1) | ~~0 常驻~~ | 钩子是**宿主子进程** ⇒ 自带口令 + 不占会话 + 跑完即退;而"需要投递的时刻"**全都伴随会话在动** ⇒ 那一刻钩子必然响(`architecture.md §4.1`) |
| **机械判定**(文件在不在 / rc / 哈希 / 锁 / 端口) | **本地脚本**(**由钩子按需唤起**) | **0 token** | 这些判断几毫秒可做;塞进"定时叫 AI"是又慢又贵 |
| **人看进度** | **一个看板**(每轮覆写) | — | 用户只该看这一处 |
⚠️ **推论(最容易搞错)**:**"发消息给另一个会话"和"读它的结果"不是一回事** —— 前者要**网关投递**,后者**只要读库**。
⚠️ **判据**:任何"我需要(或用户需要)持续监控"的诉求 ⇒ **第一反应是「钩子按需唤起本地脚本」**,⛔ 不是自动化、⛔ 也不是常驻。
---
### 1.1 🔴 与主会话的**双向同步**(⛔ **不需要"监管棒"这个角色**)
| 方向 | 怎么做 | 要点 |
|---|---|---|
| **主会话 → 程序** | **无需同步**:改 `任务图.json` / 看板 / 产出文件即可,程序下一轮自然读到 | **文件即接口** |
| **程序 → 主会话** | ① **钩子注入**:`UserPromptSubmit` 时把程序摘要作为 `additionalContext` 注入 ⇒ **用户每次发话就顺手带上最新状态**(**零自动化**)② 需要时主会话主动读实时状态 / `digest.md` | 程序**开不了会话**,但**能把状态送进会话** |
| **派活** | **仍由会话做**(白名单内) | 只有会话能创建自动化 |
🔴 **结论:不需要独立的「监管棒」角色** —— 判断 + 派活**归主会话**;程序负责"**把状态摆到主会话眼前**"。
⇒ 若发现自己在建"另一个会话来监管",**先问三句**:① 机械判定能否下沉到程序?② 状态能否用钩子注入?③ 派活能否由主会话顺手做?
**注入实现要点(照抄)**:
1. 钩子脚本对 `UserPromptSubmit` **允许写 stdout**(stdout 正是钩子协议通道):输出
`{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"…"}}`;
**其他事件仍保持零输出**(⛔ 别破坏原有纪律)。
2. 顺带把**信号文件**(`VACUUM.md` / `READY.md` / `STALL.md`)的存在也拼进上下文 ⇒ 主会话一眼看到"有东西待处理"。
3. 注册:`settings.json` 的 `hooks.UserPromptSubmit` **只能追加条目**(⛔ 禁整段覆盖 —— 会抹掉别人的钩子)。
4. ⚠️ **钩子=会话启动时快照** ⇒ 改完必须**完全重启宿主**才生效(关窗 ≠ 退出);⛔ 别默认它已生效。
---
### 1.2 🔴 严格队列(**避免打架 · 处理好一个再处理下一个**)
> 用户原话:「**严格用队列的方式处理,避免打架,处理好一个,再处理下一个**」
**队列与域锁分工不同(两者都要)**:
- **队列** 管「**谁做下一件**」—— 调度串行
- **域锁** 管「**能不能动这个资源**」—— 资源互斥
| 规矩 | 做法 |
|---|---|
| **① 一次一件(调度串行)** | 队列**只呈现一个"队首"**;⛔ 不许一次派多件 |
| **② 原子取件** | 取件动作 = `mkdir <inbox>/claims/<id>` ⇒ **建不成 ⇒ 别人取走了** ⇒ 换队首/等待(这就是"不打架"的硬保证) |
| **③ "谁在做"唯一权威** | 取到后写 `claims/<id>/holder`(内容 `<会话名>@<线>`) |
| **④ 出队=删 claim** | **做完必须删 `claims/<id>`** ⇒ 下一个才可能成为队首(⛔ 不删 ⇒ 该线一直被占 ⇒ 队列卡住) |
| **⑤ 同线互斥、跨线并行** | 同一条线同时只允许一件;**不同线可并行**(队列按 `line` 判)⇒ 既"不打架"又**不干等** |
| **⑥ 卡死回退** | claim 超 **20 分钟**未续 ⇒ 程序自动移到 `claims-stale/`(**可追溯,⛔ 不删**)⇒ 队列自动放行 |
| **⑦ 优先级** | **"在关键路径上游闭包内" ⇒ 优先**(做它能解锁关键路径);⛔ **不能只看"是不是关键路径节点本身"** —— 那会漏掉它的前置,跑去干无关的活(实测踩过:队首一度推荐非关键路径的 N11) |
**产出**:程序写 `queue.md`(人看:队首/在做/待办+冲突提示)+ `queue.json`(机读)。
⚠️ 命令里**别用 ASCII 双引号**写中文文案(会 `SyntaxError`):一律用 `「」`(见 P11)。
---
### 1.3 🔴 拉取模型(pull):**队列是"拉"的接口 · ⛔ 不要后台任务**
> 用户原话:「**不是程序直接调用你,只产生待处理队列,你开个后台任务去获取然后处理**」
> ⇒ 方向对(把"推"改成"拉"),但**"开后台任务"这一步可以去掉** —— 见下。
**为什么这里不需要"后台任务"**:它会**产生任务状态通知/失败通知**,每次都是一次**打断**;叠加"改代码→重启"就变成 P12 那种反复折腾。
⇒ **正解(本机制):不需要任何后台任务,程序也不调用任何人 —— 只写文件。**
🔴 **⚠️ 两处口径必须分清(2026-09-30 用户反证后定稿)**:
1. ⛔ **"卡消息输出"的真因⛔ 不是"任务挂在会话名下"** —— 真因=**会话日志撞 ~10 MiB 被 `dropped`**(实物读数见 `references/pitfalls.md P0-2`)。
用户反证:挂着后台任务的会话照样能随便发消息。
2. ⛔ **"后台任务不能用来投递"也⛔ 不成立** —— 它**有口令**、能长期活,是**合法次选**(见 `references/architecture.md §4.1.1`)。
本机制选"钩子"的理由只是三条**次要优势**:不需容器会话 · **必须活着的进程数=0** · 重启后自动生效。
| 角色 | 做什么 | ⛔ 不做什么 |
|---|---|---|
| **程序**(**钩子按需唤起**·只读投影) | 只**维护队列**(`queue.md`/`queue.json`)+ 信号文件(`VACUUM`/`READY`/`STALL`)+ 视图 | ⛔ 不派活、⛔ 不起后台任务;⚠️ **投递另归投递(`--tick`)** |
| **会话** | 在**三个时机主动拉**(见下)⇒ 取队首一件 ⇒ 原子 `claim` ⇒ 派活或自己做 | ⛔ 不轮询(拉的是"被唤醒的时机",不是定时器) |
**三个"拉"的时机(够用,且零通知)**:
1. **用户发话时** —— 钩子把摘要 + **队首** 注入 `additionalContext`(钩子在**会话侧**发生 ⇒ 本质就是"拉")
2. **每个棒收尾时** —— 顺手读队首 ⇒ 取一件接着做(**收尾自判**,零额外会话)
3. **需要时** —— 主会话主动读 `queue.md`
⇒ **闭环**:程序只"摆件";会话只"取件";**没有任何一方被对方打断**。
⚠️ 代价(如实说):**没有会话活跃时,队列里的件不会被自动取走** ⇒ 这时才需要"叫一次"(那属于白名单的**接续/派活**,⛔ 不要为此建轮询式自动化)。
### 1.4 🔴 主会话必须"**用户不发消息也能自己处理**"(2026-09-29 用户定案)
> 用户原话:「**主会话就是要用户不发消息自己能处理,用户发消息是需求上的事**」
> ⇒ **用户的消息 = 需求输入(新增/变更需求),⛔ 不是推进的动力源。**
🔴 **硬边界**:会话被唤醒只有两条路 —— ① 用户发消息 ② **自动化(宿主排期)**;钩子**开不了会话**。
⇒ 所以"主会话自己会动"的**唯一合规形态 = 主会话自己续自己**:
```
主会话跑完一轮(判断 → 取队首 → 派活/自己做 → 更新任务图)
│
└─ 自续期:再排一条一次性自动化,scheduledAt = now + 40 分钟,prompt = 本段原文(逐字复制)
⇒ 形成「自续链」;⛔ 全节点 done(或用户要求停)⇒ 不再自续
```
🔑 **关键判据**:**"续主会话"属于白名单里的「接续会话」⇒ 免确认** ✅
(这正是"接续会话"这一类的本义 —— **把链条接上**,包括把主会话这条链接上。)
**纪律**:
0. 🔴 **间隔必须动态**(2026-09-29 用户点破"整套机制都是短的,都要我来说一句你才执行一下"):
**队列里有可派件 ⇒ `now + 5 分钟`**(有人在等 ⇒ 快叫醒);**无 ⇒ `now + 30 分钟`**。
⛔ **固定长间隔(如 40 分钟)= 出现"可派未派"窗口**(实测:42~43 分钟毫无变化,用户以为"只有我说话才动")。
1. 频率上限 5 分钟(别更密 —— 更密就是"轮询式自动化",属非白名单)
2. **每轮只做一件**(严格队列:取队首一件)⇒ 跑完即退,**⛔ 不留常驻**
3. **终止条件必须写进 prompt**(全节点 done ⇒ 停),否则会永远续下去
4. ⛔ **不为"推进"另建轮询式自动化** —— 自续链就是那个机制
⇒ **与 §1.3 的分工**:**自续链负责"醒来"**(没人发消息也会醒来);**队列负责"做什么"**(醒来后取队首一件);**钩子注入负责"用户发话时顺带汇报"**(不是动力源)。
### 1.4a 🔴 **唤醒回路的判据 + 投递路径**(2026-10-01 用户两条批注 · 现行口径)
**① 判据 = 三条件合取**(⛔ 不是计时器):
| # | 条件 | 实现现状 |
|---|---|---|
| (a) | **主会话 和 任务会话都空闲** | ✅ **已按此收窄**(`_main_state()` ∨ `_anyone_busy()`,排除规则见 ④) |
| (b) | **队列没有待反馈**(无 `awaiting` ∧ 无 `pending`) | ✅ 已实现(`no_fb`) |
| (c) | **需求仍未完成** | ✅ 已实现(`goals_open()`) |
**② 需求三态 → 唤醒任务**(用户原话:未完成=继续/有阻碍=暂停/已完成=暂停):
| 需求状态 | 唤醒任务 |
|---|---|
| 未完成 | **继续**(照三条件判) |
| 🔴 **有阻碍** | **暂停** |
| 已完成 | **暂停** |
✅ 已实现(`goal_stuck` = 只剩 `blocked` 项、没有 `pending`/`running` ⇒ **不叫**)。
**③ 投递路径**(用户第二条批注):
- ✅ **应**:提交到**目标检查队列** ⇒ 走既有的「**上报 → 主会话处理**」流程(见 §1.2/§1.3)。
- ⛔ **现状**:唤醒**绕开队列**,直接 `_deliver_str(txt, "上报·唤醒", st)` 投一句给主会话
⇒ 主会话收到后**无处标完成**(它不在队列里)⇒ **没有闭环**。
> ⚠️ ③ 会改**队列语义**(唤醒件算不算一件待办、谁给它标 `done`、会不会淹没队列)
> ⇒ ⛔ 未擅自改,先把口径钉在这里。
**④ 🔴 自指死结 + 解法**(2026-10-01 用户点破 · **本节最要紧**)
**死结**:唤醒的就是**这条会话自己** ⇒ 它醒来那一刻,**自己正在跑**
⇒ 条件①「都空闲」被**它自己**否掉 ⇒ **永远不叫**
(=醒一次、白判一次、接着睡;用户原话追问「**谁把他顶掉了**」)。
用户给的解法(原话):
> 「**把自己排除不就行了,通过会话名称前缀区分**,还有**也不用他自己判断,可以通过代码判断**」
⇒ 落地两条硬规矩:
| # | 规矩 | 实现 |
|---|---|---|
| A | **判据在代码里**,⛔ 不让会话自查 | 在 `supervise()` 里算;会话只负责被叫醒后干活 |
| B | **排除自己** —— 两道**互补,都要** | ① `SELF_SID`(`CODEBUDDY_SESSION_ID`)**精确**:程序跑在会话里时=自己<br>② **名字前缀 `[唤醒]`**:程序在会话外常驻时拿不到 `SELF_SID`,只能认名字 |
🔴 **新增第三类名字前缀**:`[唤醒]-[<类别>]-<具体>` ⇒ `parse_session_name()` 返回 `role="waker"`
(原只有 `[主]` / `[协作]` 两类)。
⚠️ **2026-10-01 用户口径已扩到「四类」**(新增 **④ 队列上报的跟进会话** ⇒ 第 1 级 `[跟进]`)——
但**这一类的解析与路由尚未落地**(全库零命中)⇒ ⛔ 本节的 A/B 两条规矩**只覆盖已落地的前三类**,
第 ④ 类的落地清单一律以 `references/architecture.md §2.3.0c` 为准。
🔴 **第四形态:接续会话**(2026-10-01 落 · 详见 `references/architecture.md §2.3.0b`)
🔴 **定性:它是「形态」,⛔ 不是第 5 类会话** —— 用户原话:「**接续会话 不是单独的一类会话,是这几类会话到达阈值时 创建的接续会话**」
⇒ **角色继承被接续的那条**(主会话的接续**仍是主会话候选**),⛔ 不要把它当成一种并列的类别。
—— **由会话自己建出来的下一棒**,标题由**建它的那条会话**写 ⇒ 常写成
`[<类别>] 接续 · …` 或 `接续棒:…`(**没有角色方括号**)。判据:**含"接续"二字** ⇒ 判 **worker**
(⛔ **不是主会话**);另把 `主控 · …`(中点分隔、无括号)判 **main**。
⚠️ **不改会怎样**:旧判据只排 `[协作]` ⇒ 接续棒**进主会话候选**、又因标题含 `[<类别>]` 被认成
"该类别的主会话" ⇒ **投递把通知投给它自己**(2026-10-01 实测)。
⇒ **主会话候选的排除判据=「角色不是 worker/waker」**(⛔ 不再写 `[协作] not in t`);
实现处**两处必须同款**:`collabd.py::parse_session_name()`(权威)/`board.py::_role_of_title()`(镜像),
已有**真对账用例**在 `selftest.py` 守着。
🔴 **建接续棒时必须按形态命名**(否则新会话要么冒充主会话、要么在看板上归不了类别):
`[协作]-[<类别>]-<具体>` / `主控 · <类别> · <具体>` / `[唤醒]-[类别]-<具体>`。
⚠️ 已知边界:`[<类别>] <具体>` **不带"接续"二字**的仍判**角色未知**(⛔ 不猜),但会被看板点名。
⚠️ **实测暴露的另一件事**(2026-10-01):现役会话名实际写成 `主控 · 唤醒机制线 · 棒:…`
(**中点分隔、无方括号**)⇒ `parse_session_name()` **解析不出来** ⇒ 光靠名字判会漏。
**这正是 `SELF_SID` 那道排除不能省的原因**(该会话的 id 与 `SELF_SID` 一致)。
**⑤ 🔴 唤醒会话的三条硬定性**(2026-10-01 用户**第三次**定义 · **覆盖前两次**)
> 用户原话:「**唤醒会话 是独立会话,不要在主会话上处理,是随着需求确定时创建的**」
| # | 定性 | 意思(⛔ 别读错) |
|---|---|---|
| (a) | **独立会话** | 它**自己一条会话**,⛔ **不是主会话的一个"职能"/一种模式**。⇒ 看板上它是独立一格;判"都空闲"时它**也必须被排除**(见 ④) |
| (b) | ⛔ **不在主会话上处理** | 叫醒这件事的**执行体是它自己** ⇒ ⛔ 不许把"该它干的活"挂到主会话身上(主会话只管**判断与派活**)。⇒ 那个脉冲任务起在**它**里面,⛔ 不是起在主会话里 |
| (c) | **随需求确定时创建** | 创建时机 = **需求确定的那一刻**(不是常设件;⛔ 不提前建,没需求不建)。⇒ 看板上「**还没建**」是**正常态**,⛔ 不是故障;此时链路缺口敞开,**如实说** |
🔴 **`board_ext.py` 落地**:`_waker_session()` 读 `sessions`(前缀 `[唤醒]`);`_triggers()` 的三态 + tips
把这三条**写进图例**("还没建"那一支要说明**这是按定性的正常态**)。回归断言:`render-check.mjs`
两条(「独立会话/⛔ 不在主会话上处理」+「随需求确定时才创建」)。
### 1.4b 🔴 **换主会话(交接)的确切触发条件**(2026-09-30 实测,⛔ 别再靠猜)
**换不换,只看一件事:`roles` 里登记为 main 的那条,此刻**在不在活会话集合里**。**
| 登记那条 | 结果 | 依据 |
|---|---|---|
| **还在**活会话里(桌面还开着) | 🔴 **钉死在它身上** —— 新建的主控会话**完全被无视** | `roles:main` |
| 不在(已关/已切走) | ✅ 立刻按"显式前缀 → 最近活动"换过去 | `prefix:主控` / `workspace` |
⚠️ **另一条老会话开着不影响**(只要它不是登记的那条)—— 别把"旧会话还开着"当成不换的原因。
**交接的正确姿势(三步,缺一不可)**:① 新标题以 `主控` 开头 ② **先把旧主会话从桌面切走**(⛔ 不是删会话、⛔ 不是杀进程)③ **同时把新主会话打开**(它必须出现在某个网关口口的 `live=` 上,否则投递被拒 `main-not-live`)。
⛔ **两条主会话同时开着 ⇒ 一定被钉回旧的那条。**
🔴 **两个已知坑(待修,改前先看 §9)**:
- **前缀判据不认方括号**:代码认 `startswith("主控")`,而实际标题常写成 **`[主控]-…`** ⇒ 判 False ⇒ 那条主会话**只能靠"最近活动"兜底**,**本工作区任何一条会话活动更新就会静默把它顶掉**。⇒ 命名一律用 `主控 · …`(⛔ 别加方括号)。
- **"登记还活着"压过"显式前缀"**:短路顺序是「登记」在「前缀」**之前** ⇒ 旧窗口还开着时新主会话**永远接管不了**。正确优先级应是 **显式前缀 > 登记 > 最近活动**。
⚠️ 活会话集合为**空**时(枚举失败)会**反而信任登记**(解析出一条早关掉的会话)—— 不误投(后续仍拒投),但**归因会误导**。
⚠️ **一条正在跑的会话会自带一个网关口**(`live=它自己`),**跑完口就消失** ⇒ 别把"某个口曾经 live 过它"当成"现在投得进去"。
---
## 2 🔴 主会话只做两件(其余下沉)
- **判断**:按 `任务图.json` 的判据判"节点是否真的完成"(⛔ 不看自述,**看可核对产物**)
- **派活**:只派**依赖已满足**的节点,**优先关键路径**
**⛔ 主会话不该做的**(做了就是走错通道):持续监控(⇒ 常驻程序)/轮询别的会话干完没(⇒ 读库)/机械核对(⇒ 常驻程序)。
---
## 3 🧭 防干等 = 显式任务图 + 关键路径(**总工期问题的唯一正解**)
`任务图.json`:每节点写 `id / title / line / deps / status / evidence|blocker`,并给 `critical_path`。四条派活规则:
1. **只派「依赖已满足」的节点** ⇒ 能并行的**立刻并行**
2. **优先关键路径**(它决定总工期;非关键路径押后不拖工期)
3. **一条线同时只挂一个**,**多条线可同时挂**
4. **收口即派(+3~4 分钟)**,⛔ 不要 +5~8 分钟空窗(🔴 2026-10-01 用户口径:基准 = 收口 + **3~4 分钟**)
🔴 **两个必检的浪费信号**:
- **可派未派**:可派集合非空 ∧ 没有任何棒在跑 ⇒ **有活没人干** ⇒ 立即派
- **关键路径单线化**:关键路径上只有一个执行主体 ⇒ 把它**拆成更小的可验步骤**,能交给别的线的部分**并行出去**
📄 规范 + 模板 ⇒ **`references/taskgraph.md`**(含节点写法与"可派/等待"算法)
---
## 4 🔴 证据分级:**真成果 ≠ 接续任务**(不区分就会被"空转链条"骗)
| 类 | 判据 | 算不算进展 |
|---|---|---|
| **✅ 真成果** | 运行记录为**完成且有结论**,**最好有可核对产物/读数**(文件 / rc / 端口 / 数据对照) | ✅ 算 |
| **🟡 接续任务** | 只是**新增了一条排期**(执行方自己排的下一棒) | ⛔ **不算** —— 只证明「有下一步」,不证明「这步干成了」 |
| **🏃 刚开始跑** | 有运行记录但**尚无结论** | ⛔ 还不算 |
| **⛔ 哑火** | 到点却没产生运行记录 | 🔴 **当异常**(链条断了) |
⚠️ **"在跑的棒 N 个" 是排期数,⛔ 不是成果数。**
---
## 5 🔴 新建自动化 = 白名单 + 确认制(否则会退化到"啥都用自动化")
**只有两类可不经确认直接建**:
1. **接续会话** —— 把某条链/某一线的**下一棒接上**
2. **给其他会话安排任务** —— 派活
⇒ **其余一切用途**(监管轮/巡检/检查点/体检/观测/清理/日报…)**必须先取得用户确认**。
**执行**:建前自问这两问;**答不上 ⇒ 报给用户等确认**(⛔ 不许先建后报)。✅ 删冗余**不属新建** ⇒ 可直接做但须报告。
🔴 **配额**:一个需求线的常驻自动化 **≤ 2**(1 唤醒 + 必要时 1 截止类)。
⚠️ 🔴 **2026-10-02 标注(⛔ 不是口径变更)**:这里那个「**1 唤醒**」**只在代偿形态下成立**(常驻停机 ⇒ 用排期顶替时钟);定案的唤醒时钟是**常驻投递**(⛔ 不占排期名额)。
⚠️ **但"连通知主会话都要靠自动化"是错的** —— 投递走**宿主钩子**(`--tick`),⛔ 不加排期(`pitfalls.md P0-3`)。
---
## 6 ⚠️ 自愈/守护的硬纪律(都是踩出来的)
1. **自愈必须带去抖**:**连续 N 次(≥3)失败才动手** + **冷却**。否则"自愈"会**反复杀掉正在服务的好实例** —— **比故障本身更伤**(实测)。
2. **⛔ 不用会话后台任务跑长跑**:它每轮输出会**唤醒宿主会话** ⇒ 会话永不空闲 ⇒ 用户看到「卡死」。
✅ **可行形态**:由**用户在自己的独立窗口**起(`cmd start` / `wmic` / `Start-Process` 常被安全策略拦,⛔ 别在这上面耗时间)+ **完全静默**(只写自己的日志)。
🔴 **实测补充(2026-09-29)**:**从会话里起的常驻活不长** —— 它会随**其宿主会话结束被回收**(实测:`13:09` 起的 pid 在会话收尾后消失,`13:12` 后再无一轮,单例端口变 `Connection refused`)。
⇒ **两条一起用**:常驻只当"锦上添花";**机制必须有一条「钩子事件驱动」的腿**(每个会话收尾跑一轮),否则常驻一死,监控/队列/唤醒**全部静默停摆**(而没有人会发现)。
3. **单例**:常驻程序独占一个本地端口 ⇒ 防重复实例双写。
4. **fail-open 会掩盖字段错误**:异常全吞 ⇒ 查询写错也"rc=0" ⇒ **必须核对输出非空**,⛔ 不能只看退出码。⚠️ **视图文件要定期 diff 一眼**。
5. **域锁要切细**:按**节点涉及的文件/子目录**声明;⛔ **禁止整工作区域粗域** —— 那是**自造串行瓶颈**(实测造成多次「抢锁失败 ⇒ 整轮白开」的纯浪费)。
6. 🔴 **别"改一行重启一次"常驻程序**:用**会话后台任务**起常驻**本身已被禁**(见第 2 条);而每次 kill/launch 都会留下 `failed` 通知 ⇒ **反复打断主会话,把自己弄卡**(实测一个阶段重启 7~8 次)。⇒ **攒批重启**(≥3 处改动一次)+ **规则/配置做成热加载**(每轮读文件)+ **长任务交棒换会话**。详见 **`references/pitfalls.md` P12**。
📄 完整踩坑清单(含每条的现象/根因/修法)⇒ **`references/pitfalls.md`**
---
## 7 落地:三个件(部署顺序)
| # | 件 | 作用 |
|---|---|---|
| 1 | **常驻程序** `scripts/collabd.py --once`(**投影轮**) | 判定(证据分级/推进/真空/哑火)+ 任务图「可派未派」+ 体检 + 视图 + 需求台账。**⛔ 不派活、⛔ 不投递、⛔ 不推进队列** |
| 2 | **投递** 同文件 `--tick`(**投递轮**) | **逐条读队列 ⇒ 投给主会话**(单条+握手/三条件唤醒)⇒ **唯一投递方+唯一队列推进方** |
| 3 | **任务图** `任务图.json`(模板见 `references/taskgraph.md`) | 显式依赖 + 关键路径 ⇒ 防干等 |
| 4 | 🔴 **钩子接线**:宿主在 **`UserPromptSubmit`** + **`PreToolUse`(matcher `^Bash$`)** 上调用本地脚本 ⇒ 脚本内部再跑 `--once` 与 `--tick` | **这是"投递"的全部** —— ⛔ **不需要排期、⛔ 不需要常驻**。钩子是宿主子进程 ⇒ 自带网关口令(能投递)、⛔ 不占会话、零 token。⚠️ **接线必修**:工作区若另存**旧副本**,钩子指向它 ⇒ **回路等于没接**;⚠️ **新加的钩子事件需宿主重启后才生效**(`UserPromptSubmit` 那条立即生效) |
**配置**:`scripts/collabd.config.example.json`(workspace / 行名 / 目标件 / 任务图 / 看板 …)⇒ 复制成 `collabd.config.json` 改路径即可复用。
**用法**:`--once`(目标检查·投影轮)|`--tick`(投递·**投递轮**)|`--report`(任务会话上报)|`--reconcile`(启动对账)|`--where`(路径自证)。
🔴 **投递真绿的判据**:⛔ 不是 `rc=0`,而是 **`wakeups.jsonl` 新增了一行 `ok:true`**。
---
## 8 自检(每次派活前问这四句)
1. 这件事**非得开新会话**吗?(机械 ⇒ 常驻程序;判断 ⇒ 已有唤醒)
2. 我要派/建的东西**在白名单**里吗?不在 ⇒ **先问用户**。
3. **依赖满足了吗**?它**在关键路径上**吗?不在 ⇒ 它能不能押后?
4. 我是靠**可核对产物**判"完成",还是靠**自述/排期**?(后者 ⇒ 那是"接续任务",⛔ 不算成果)
---
## 9 🔧 维护 `scripts/collabd.py` 的硬要点(2026-09-29 实测,⛔ 别再踩)
**两条实测事实(推翻了原设计的隐含假设)**
- `automation_runs.status` 实测**全是 `ACCEPTED`,从来没有 `IN_PROGRESS`** ⇒ ⛔ 不能当「有人在跑」信号。
- 一次性棒跑完 `next_run_at` **归零**(实测全 `0/None`)⇒ ⛔ 也不能当信号。
- ⇒ **唯一可得的「有人在跑」= `sessions.status='working'`**(取值域:`archived/completed/error/working`)。
⚠️ 用它判「有没有人在推进」时**必须排除观察者自己**(否则主会话自己跑着 ⇒ 永远报"有人在跑",用户问的"是不是又发呆了"就永远答不出来)。
**改本程序的标准手法(⛔ 别手改单点)**
1. 先备份到工作区 `tmp/bak-collabd-<日期>/`(⛔ 不在技能目录里留副本)。
2. 用**断言式补丁脚本**:`old/new` 成对替换,**任一锚点 `count != 1` ⇒ 整份不写盘**;写完 `ast.parse` 门禁 ⇒ 杜绝"半改状态"。
3. ⚠️ **锚点必须先用 `repr()` 核过** —— Read 工具的行号切分与本文件真实字节**不一致**(长行会被折),照抄 Read 的显示会静默失配。
4. ⚠️ `Path.write_text()` 在 Windows **静默把 `\n` 转成 `\r\n`** ⇒ 写完必比对 `md5sum` 与内存 md5;不一致就按 `newline="\n"` 归一化回 LF。
5. 🔴 **它不是常驻进程**(由 `UserPromptSubmit` 钩子按需 `--once` 调起)⇒ **改完下一轮钩子即生效,⛔ 无需重启**。
6. 验证:连跑 `--once` 看 `rc` + `queue.json`/`digest.md`/信号文件(`NEXT`/`STALL`/`VACUUM`/`READY`)是否与预期一致。
**队列的四个部件(各有唯一权威)**
- `claims/<id>/holder` = `<会话名>@<线>@<完整会话id>` —— 「谁在做」的唯一权威;**第 3 段别省**(有它才能判持有者是否已终结 ⇒ **立即出队**;没有就只能等 20 分钟超时,期间队首会僵尸化并**反复复活**)。
- `blocked.json` = `{"<节点id>": "<原因·谁在等>"}` —— 受阻件 ⛔ 不当队首(否则每次唤醒都白跑一遍同一个受阻件)。**解除后记得摘掉**。
- `gate-done.stamp` + `claims` 缺失即 `gate=free`:闸门靠**钩子事件**驱动,⛔ 不靠轮询。
- 🔴 **「同线互斥」取 `holder` 的第 2 段**(线名)** —— 取第 1 段(会话名)会恒不相等 ⇒ 该机制**静默失效**(实测:同一条线可被同时派多件)。
- 🔴🔴 **「算不算做完」以需求台账(`tmp/supervise-inbox/tasks.json`)为准,⛔ 不是以任务图节点为准**(**2026-10-01 加**)。
病根:**任务图是纯手维护件 —— 全仓无任何代码回写它**(实测 `TG` 在 `collabd.py` 里只于 3 处被**读**,无一处写),
而会话「上报完成」只写台账 ⇒ **两处必然漂**,且症状极隐蔽:`claims/` 是空的、没人占线,
但**队首出不来** ⇒ 它被反复当"可派未派"喊,`READY.md`/`STALL.md` 每小时喊一遍同一件,控制台判「目标未完成」。
实测(M5):台账 `done`、上报单 `done`、产物已核对(`wakeups.jsonl` 第 82 行 `http:200 ok:true`),
任务图仍留 `running` ⇒ **整条链被一个早已做完的件卡死**,`goalctl` 只会说「目标状态 = **未完成**(任务图 M5)」,⛔ 不告诉你是"早就做完了"。
⇒ **现行口径**:`图 done ∪ 台账 done` 都算 done(`deps` 解析同理,前置满足即算满足)。
⚠️ 实现落在**两处、必须同口径**:`collabd.taskgraph()` 与 `goalctl.goals_open()`(⛔ 只改一处 ⇒ 控制台与看板两边打架)。
⚠️ **不改任务图本身** —— 它仍是「分解件」,人可手改;只让"算不算做完"以台账为准。
- 🔴 **目标「三路全过」⇒ 终态:⛔ 不产任何告警**(**2026-10-01 加**)。原实现只认 `goal.json.run != active`,
目标是全过了、`run` 却仍是 `active` ⇒ 照产 `STALL`/`VACUUM`/`READY` ⇒ **把「做完了」读成「坏了」**
(实测:M5 解卡后立刻喊「卡住:有会话在跑但 52 分钟无成果」)。
⇒ 现口径:`--once` / `--tick` 在 `goal_paused() or not goals_open()` 时走 `paused_round()` ——
只留一行**终态说明**、清掉四个信号投影、⛔ 不投递;**看板也一并收口**(否则它会永久停在最后一张"还在跑"的快照)。
**🔴 它到底什么时候才会跑(最容易误解的一条 · 2026-09-30 更新)**
- `--once`(投影)挂在 **`UserPromptSubmit`**(节流 180 s)+ **`PreToolUse ^Bash$`** 上 ⇒ **有人发话、或任何会话跑一次 Bash** 就会被调起。
- `--tick`(**投递**)挂同样两个事件(节流 120 s)⇒ **投递与"有事发生"同刻**。
- ⇒ **不再需要靠宿主排期"让机制活着"**(旧结论「没有宿主排期 = 确定性静默」**已作废**)。
⚠️ **剩下的唯一真相**:**"全员静止"期间**(没人发话、任何会话都不跑 Bash)**不会有通知飞出去** —— 那是**为摆脱排期而明确接受的代价**;
停滞类信号由**会话外**的守护落 `STALL.md`/`NEED-USER.md`,**下一次任意宿主事件时补投**。
**`idle`("多久没成果")只认「真成果」**:服务探针的状态变化属**服务层** —— 既不重置 `idle`、也不进 `🟢`。
(实测教训:4 小时静默里程序那唯一一轮恰好赶上探针「不通→通」⇒ 摘要写「1 分钟无成果」而不是「4.5 小时」⇒ 连一声告警都没有。)
**受阻 ⇒ ⛔ 不能静默**:选不出队首但 `blocked.json` 非空时**仍写 `NEXT.md`**(受阻通报)—— 否则就是「有活 ∧ 谁都动不了 ∧ 程序一声不响」。⚠️ 写 `NEXT.md` 时**内容不得含时间戳**,否则唤醒回路的**内容哈希去重**失效 ⇒ 每轮都投一遍(刷屏)。
**唤醒回路的两个触发面**:① `NEXT.md` 存在且 `gate=free`;② `NEXT.md` 不存在但**停滞 / 可派未派**(哈希须用**粗粒度键**:种类 + 空闲按 30 分钟取整,⛔ 不能用带时间戳的正文)。
**🔴 链条不能只靠「上一棒派下一棒」**:棒中途死掉(实测发生过:探针回环打到自己会话)⇒ 链条**静默断掉**,只能等下一次有人发消息。
⇒ ⛔ **不要靠发明新件来兜底**(我发明过"+30 分钟看门狗",已废弃)。**正解在架构原文里**:**收尾自判**(在**已存在**的会话里判本线缺口并写下一行 `automations`)+ **每小时兜底唤醒**(定稿 §3②,专防链条断掉)。改前先过 §10。
---
## 10 🔴 改本机制前必须过的「防打转判据」(架构原文 · 顶层设计 §7)
> 原文原话:**"以后凡是给这套系统加东西,先过这张表 —— 只要让『必须存在的东西』或『需要记得清理的东西』变多,就要停下来重新想,而不是继续补。"**
| 判据 | 目标 |
|---|---|
| 自造件数 | **≤3**(机械判定脚本 / 看板 / 规则文档) |
| 🔴 **自造协议数**("需要记得清理"的) | **0** —— ⛔ 不自造 `mkdir` / flag 去抖:**域锁就是单例** |
| 必须活着的**进程**数 | **0**(⛔ 不用会话后台任务) |
| 必须存在的**会话**数 | **0** |
| **额外会话 / 棒** | **0**(收尾自判在**已存在**的会话里完成) |
**四条地基(顶层设计 §1/§2)**:① 状态**只读宿主库**(`mode=ro`)② 调度**只靠 `automations` 一行**(⛔ 不自建调度器)③ 互斥**只用既有域锁**(⛔ 不自造第二把锁/第二套去抖)④ 执行体**无状态**(会话死了状态不能跟着死)。
**正确形态 = 2 个声明式操作 + 1 份规则**:**派活** = 往 `automations` 写一行(不"叫会话");**收结果** = 读 `automation_runs`(⛔ 不轮询、不催、不问)。⇒ **「监管者」这个角色不存在**,只有**两个动作**:**收尾自判**(在已跑的会话里)+ **兜底唤醒**(⚠️ 原文写「宿主排期」—— 🔴 **2026-10-02 标注(⛔ 不是口径变更)**:定案的兜底唤醒走**常驻投递**,排期只是**代偿**,见 §5-1)。
**🔴 权威清单(改任何判定前先看它 —— 权威只在这四处)**
| 要判什么 | 权威(唯一) | ⛔ 不许 |
|---|---|---|
| 谁在跑 / 哪条线被占 | `sessions.status='working'` + 其 `cwd`(末段=线名) | 自己写 `holder` 文本再解析 |
| **多久没成果** | `automation_runs.updated_at`(毫秒 epoch) | 自己维护 `last_progress_at` 时钟 |
| 有没有排期(会不会有人自动跑) | `automations.next_run_at` **未来 1 小时内** | 看"全库有没有排期" |
| 一棒做完没有 / 结论是什么 | `automation_runs`(`thread_title`;⚠️ `thread_id` 形如 `<aid>:1`,**不是**会话 id) | 轮询别的会话 |
⇒ **自己写的文件只有两种合法身份**:**投影**(可删可重建:看板 / `NEXT.md` / `queue.json`)或**人工输入**(如 `blocked.json`)。⚠️ 凡"需要记得清理"的一律不许(`claims`/TTL/僵尸目录都属此类)。
---
## 11 🔴 术语表(**说话/写文档一律用「正式名」这一列**)
> 2026-09-29 用户指出"宿主是什么/机械程序又是什么,之前只提过**投递**和**常驻程序**" —— 属实:**同一个程序在文件里有 4 个名字**,图当然读不懂。**以本表为准。**
| 正式名 | 实体(在哪) | 干什么 | ⛔ 别再叫它 |
|---|---|---|---|
| **宿主** | WorkBuddy 本体 | 提供五样:状态(只读 3 表)/**调度**(自动化排期)/**事件**(钩子)/执行体(会话)/互斥(域锁) | 「平台」「服务端」 |
| **投递** | `advance-watch.py`(`.workbuddy/tools/`) | 零 token 机械判定:直读 `automation_runs`、判靶点/锁,写 `advance.md` + `needs-ai.json`。⛔ 不派活/不开会话/不联网/不读口令 | 「机械层程序」「机械程序」「看门狗」 |
| **常驻程序** | `collabd.py`(自述"协作守护程序") | 队列闸门(`NEXT.md`)+ **唤醒投递** + 证据分级/真空/断链告警/体检/看板覆写。**⛔ 不做派活** | 「判定层」「常驻程序」「机械层」 |
| **会话** | `sessions` 表 | 执行体:「读—判—写」;**一次性**、可替换 | —— |
| **棒** | 一次派活 = 一个会话 | 同一件事的执行单位(「一棒一线」) | —— |
| **看板** | `scripts/board.py --serve` → `assets/board.html` | **用户唯一入口**(只读旁路 · 异步快照) | ⛔ 别把 `digest.md`(机械摘要)当用户入口;⛔ 也别再提已退役的 `交付物/手机接入-实时状态.md` |
| **设备接入 · 本地反代** | `dsh-client` · 真身 `@dsh-client/device-shim`(`127.0.0.1:20090`) | 只绑回环的 HTTP+WS 反代,**在进程内注入本机 DSH 的会话凭据**,供覆盖网络中继从设备入口访问桌面 DSH。⚠️ 该包已标「已退役」,计划并入 `@dsh-local/ai1net` 的第 6 块模块**M6「设备接入」**,但**尚未落地**(仓库里还没有 `ai1net` 包) | ⛔ 别再叫它「**垫片**」「shim」「薄垫片」 |
| **覆盖网络节点 · 中继客户端** | `dsh-client` · `<Repo>/lib/net/relay/main.js --client`,由 `overlay-node-daemon.ps1` 监护 | 本机作为**节点**接入覆盖网络:向中继注册(`registered host=… accepted=[20090]`)、维护长连、转发流 | ⛔ 别再叫它「**设备侧 worker**」「worker」「节点进程」 |
### 11.1 🔴 命名铁律(用户 2026-09-30 明令)
> 用户原话:「垫片 20090 在线,设备侧 worker 在线:**禁止用这么抽象的词**,
> 用 **系统-模块-功能名**(系统-功能名)」
- 🔴 **凡涉及外部系统/组件,一律写成「`系统` · `模块` · `功能名`」** ——
系统=它属于哪个系统/仓库(如 `dsh-client`、`ai1net`);模块=在这个系统里算哪一块(如 **设备接入**、**覆盖网络节点**);
功能=它干的活(如 **本地反代**、**中继客户端**)。简写时至少保留 **`系统 · 功能名`**。
- ⛔ **不许自造简称**("垫片"/"worker"/"两条腿"/"有腿掉了" 这种),**⛔ 也别只报端口或只报英文包名**当名字。
- ⚠️ **报"在线"必须说清是哪一段的什么状态**,且 **⛔ 不许由"两段都在线"推出"链路通过"** ——
实测教训:中继客户端 `state=up` 而 `streams=0`(**从未有请求真正被转发**)⇒ 那时写"这条链是通的"=**过度断言**。
⇒ 判「端到端通过」的**唯一判据**是**中继真转发过流**(`streams>0`);否则一律写「**未验证**」。
- 📌 落地处:`board.py` 顶部有本铁律的注释块 + `front` 字段全部改用真名;看板「链路前置」卡按真名渲染。
> ⚠️ **以下为 2026-09-29 的史实快照**(当时的"现状打架"),**⛔ 不是现状**;现状以 `architecture.md` 头部「当前结论」为准。
1. ~~`advance-watch.py` 死活之争~~ —— `协同监管棒-SOP.md §276` 说它「已吸收退役」,而 `顶层设计`/`定稿`/`实施方案`/`CODEBUDDY.md` 仍把它当活件。**已随退役件一并作废。**
2. **同一程序多个名字**:任务图叫「协作程序」、它自述「协作守护程序」、原技能叫「机械层/常驻程序」、实施方案叫「机械层脚本」⇒ 统一口径见 `architecture.md §7 术语`。
3. ~~同一程序 2 个路径~~ —— 旧副本 `~/.workbuddy/skills/multi-session-collab/` **已于 2026-10-01 归档退役**,现只剩一份:本包 `scripts/collabd.py`。
---
## 12 文档权威(治"架构都是乱的" —— 原技能时代的问题)
> 🔴 **本节原声明「本技能 = 协作机制的唯一权威」—— 已作废**。并入本包后,**唯一权威是 `references/architecture.md`**(见本件头部)。
**当时的病**:曾有 **5 份"架构"并存且互不认** —— `交付物/多会话协同机制-定稿`/`…顶层设计`/`…实施方案`/`协同监管棒-SOP`/原技能本身;
实测出 **5 处互相矛盾**(一棒做完怎么办 / 「监管棒」算不算角色 / 投递的死活 / 钩子生效了没 / 钩子调的是哪一份程序)。
⇒ **现已收口**:架构**只认 `architecture.md`**;那 4 份业务件 **2026-09-30 已加退役标注**,可作背景,⛔ 不作用判据。
+175
View File
@@ -0,0 +1,175 @@
# 多会话执行 · 一页纸(怎么派活、怎么收活、怎么不干等)
> 🔴🔴 **2026-10-03 07:2x 状态块(本条为准,排在下面所有小节之上)——「上报/投递」整套退役**
>
> · ⛔ **投递已真删**(`supervise()` 203→71 行、`wake_enable` true→false)⇒ **§3 里"唯一投递方"那半条作废**、
> §4 的**唤醒会话/跟进会话两类已整套退役**(⇒ **§4 现只剩两类:主会话 + 任务会话**)。
> · ✅ **现行机制只有两条腿**:**接收**=`--report` 写 `tasks.json` 四态台账(唯一权威);
> **处理**=建 `[检查]-…` 排期(`maybe_spawn_check_agent()`,唯一载体=常驻 `--supervise`)。⛔ **没有"推送"这一环**。
> · 🔴 **「常驻投递」这个角色名也别再用** —— 角色本来就叫「**协作程序**」,"常驻"是**运行形态、不是角色名**。
> · ⇒ 逐条改未做(散在 4 份约 200 处);**冲突时以本块 + `architecture.md` 当前结论节为准**。
> 机制细节 ⇒ `SKILL.md` 文首 10-03 状态块;坑 ⇒ `pitfalls.md` P0-35 ~ P0-38。
> ⚠️ **深度以 `references/architecture.md` 为唯一权威**(本文件是**入口与判据**,⛔ 不另立第二份架构)。
> 踩过的坑以 `references/pitfalls.md` 为准;任务图细则见 `references/taskgraph.md`。
## 1 一句话
**派活用自动化(唯一能开新会话的通道)|收结果直读宿主库(0 token)|机械判定下沉到本地只读脚本|AI 只在需要判断时被叫起|用任务图 + 关键路径防干等。**
## 2 四条通道(哪件事走哪条 —— 走错就是最大的浪费来源)
1. **派活 ⇒ 只能走自动化**。宿主钩子**开不了新会话**,这是硬事实;所以「叫起下一个干活的人」只有自动化这一条路。
2. **收结果 ⇒ 直读宿主库**(`automation_runs` 等,宿主已把各线结论写好)。⛔ 不要为了"问一句进展"去叫一个会话 —— 那是 0 收益的开销。
3. **机械判定 ⇒ 下沉到本地只读脚本**(`collabd.py --once` 等)。这类事不需要 AI 判断,⛔ 不要占用会话。
4. **人只看一个看板** ⇒ 只读旁路观测,异步、可延迟,**⛔ 不许影响程序执行**。
## 3 节奏:常驻为主,钩子作补充
- 🔴 **触发源 = 常驻的目标检查 + 投递**(09-29 定案,**2026-10-01 用户再确认**:「**协作与投递一直运行(常驻)**」)。
理由:**⛔ 不是所有队列都由钩子产生** ⇒ 纯事件驱动**会漏**。
- ⛔ **自动任务("当闹钟")方案已废弃**(用户 2026-10-01:「**定时任务的方案已经废弃了**」)⇒ ⛔ 不要再建。
- `collabd.py --once` = **投影轮**(维护队列,**不投递、不推进**)。
- `collabd.py --tick` = **投递轮**,🔴 **唯一投递方 / 唯一推进方** ⇒ **由常驻投递按周期跑**。
- 🟡 **宿主钩子只作补充**(事件驱动的即时性),⛔ **不是主路径,⛔ 不得用它取代常驻**。
## 4 四个"会话类别",别混(🔴 2026-10-01 用户口径 · 由「三个角色」改)
> 🔴 **四类都在同一个工作区**(`config.workspace` 那一个目录),**靠标题两级前缀区分**(第 1 级=角色、第 2 级=任务类别),⛔ **不按 `cwd`、⛔ 不按 `workspaces` 表**。
- **主会话**:🔴 **只管目标和方向**(判断与派活)。它是**解析出来的、不是登记出来的** —— 三层规则全在 `resolve_main()` 一处(见 `architecture.md §7`)。🔴 **只由用户触发**,⛔ 不接机制推送。
- **任务会话**:承接具体活。看板上=**下面那个虚线大框**里那一排(有几个排几个;一个都没有时画**虚线空框**,⛔ 不许省略整层)。
- **唤醒会话**:🔴 三条硬定性 —— **本质还是会话** / **独立会话(⛔ 不在主会话上处理)** / **随需求确定时才创建**。看板上它在**主会话下面那一行(`RW`)左格**、与右格**跟进会话等宽**,靠**位置**区分、位置不变;「还没建」是**正常态**、⛔ 不是故障。
- 🆕 **队列上报的跟进会话**:🔴 用户 2026-10-01 口径 —— **主会话只管目标和方向**,「**队列上报的会话 让专门的 跟进会话处理**」。✅ **投递路由已落地**:按件的类别选「**跟进会话**」(`follow_for_topic()`),**⛔ 绝不投主会话**;连一条跟进都解析不出/已有但不活 ⇒ **喊用户**(⛔ **不降级投主会话**)⇒ 详见 `architecture.md §2.3.0c`。
🔴 **看板上的两个「虚线大框」= 分组,⛔ 不是节点、也⛔ 不是新一层**:
上面那个框住「**主会话 + 唤醒会话 + 跟进会话**」(=**用户那一面 + 机制那一面**),下面那个框住**任务会话**;
**两个大框之间只有一条线 = ① 派活**(⛔ 不再从主会话画多条扇形线出去)。⚠️ 改框宽/改格宽必须**重算框**(框由该排布局现推,⛔ 不写死)。
⇒ 位置、形状、命名沿革与两道自检:`collab-detail.md §0.5.5`;布局常量与两道自检:`architecture.md §2.3.0c-2`。
### 4.1 🔴🔴 **开工清单:主会话开工时**必须**把三类会话建齐**(用户 2026-10-01 明令)
> 用户原话:「**开始会话完成需求的时候,主会话需要创建 唤醒会话 以及根据分工类别 创建 协作会话
> 和 跟进会话呢 不然整个机制跑不起来**」
| 该建什么 | 标题形态 | 少建 ⇒ 断在哪 |
|---|---|---|
| **唤醒会话** | `[唤醒]-<类别>-<具体>` | 没人推 → 需求停在原地(实测 3.5 h 零成果) |
| **任务会话**(**每个分工类别一条**) | `[协作]-<类别>-<具体>` | 没人干 → 该类别解析不出承接者 |
| **跟进会话** | `[跟进]-<类别>-<具体>` | 没人收 → 队列上报全挤回主会话 |
**三条硬约束**:① **只有自动化能开新会话** ⇒ "建会话"=**登记一条自动化**,⛔ 不是自己 spawn;
② 标题**第 2 级必须带方括号**、值取 `goal.json` 的 `topics`(⛔ 用 `short` ⇒ 静默漏管);
③ **"还没建"是正常态**,但**开工就必须建**,看板上如实显示成灰 ○。
⇒ 细则与踩坑:`architecture.md §2.3.0d`。
### 4.1b 🔴🔴 **缺会话 ⇒ 自动拉起**(用户 2026-10-02 口径 · **运行期也要做**,⛔ 不只是开工那一刻)
> 用户原话(触发面,逐字):「**是用户说 使用协作会话方式 完成目标 或 继续完成目标**」
- **触发**:用户说这两句(等价说法「继续执行」也认),**或**队列堵住(投递报 `follow-not-live` / `no-follow-session`)。
- **动作**:**先查齐备度** ⇒ **缺 ⇒ 自动拉起**。
⛔ **不许**写 `NEED-USER.md` 请用户自己去开一条会话 —— 那是把机制该干的活推给人。
- 🔴 **齐备度怎么算(2026-10-02 订正)**:**协作/唤醒按类别**各算一条;**跟进是全类别唯一席位**——
`collabd.py --gap` 对它**不按类别分桶**(桶键恒 `(follow, "")`),一个工作区从头到尾只应有**一条**活的
跟进会话。⛔ 别按类别建出 N 条 `[跟进]-[<类别>]-…`(本棒初版就建错成 2 条,已订正)。
- **判据 + 现成参数**:`python scripts/collabd.py --gap`(只读)⇒ 列出缺口 + 每条该建的排期
(name / prompt / scheduleType / scheduledAt / cwds)。硬缺=**跟进/协作**会话没有活着的;
**唤醒**会话算"软缺"(用户定性「随需求确定时才创建」)。
- **谁执行**:**会话**(`automation_update`)。⛔ 脚本**不许**写 `automations` 表(双红线)
⇒ 这是唯一通路,也是"自动"的全部含义。
- **怎么送到会话**:钩子 `wb-result-hook.py` 在 `UserPromptSubmit` 把缺口**注入当前会话上下文**
(零 token;触发词命中即查,否则 120 s 节流查一次)⇒ 用户照常说那句话即可,**什么都不用做**。
🔴 **「接续会话」⛔ 不是第 5 类,是「形态」**:上面**任一类**撞到阈值(上下文/日志)时,**由它自己**建出下一棒 —— **角色继承被接续的那条**(主会话的接续仍是主会话候选)。详见 `architecture.md §2.3.0b`。
🔴 **接续棒必须带类别**(`接续 · [<类别>] · <具体>` 或标题里出现类别名)——
否则归属判据(角色+**类别子串**)认不出它,它会被**静默漏管**(不进看板、`--ready-next` 也不算它在跑)。
⚠️ 主会话的续棒要**保住 `主控` 前缀**(`主控 · <类别> · 接续自 <id8>`)—— 否则它被判成干活的棒,
主会话候选会**变空**(P0-11 实测踩过)。
### 4.2 🔴🔴 **接续退位:建出下一棒之后,把前棒**退场**(用户 2026-10-01 立)
> 用户原话:「**通过接续会话的时候,如何处理过期的会话,避免越堆越多**」
**为什么必需**:接续=会话自己建下一棒,而**前棒不会自动消失** —— 它在宿主库里永远是
一条 `completed` 记录、标题同族。实测本工作区**已堆 5 条**「接续 · 会话机制合并…」⇒
看板与状态稿每轮都为旧棒占位,**真在跑的棒被埋在里面**(越用越糊)。
**两条退场路径**(⛔ 一条都不许少):
| 路径 | 怎么做 | 什么时候生效 |
|---|---|---|
| **显式**(准确) | 建出下一棒后立刻 `collabd.py --retire self --by <接棒会话id> --why 接续` | **立刻** |
| **窗口兜底** | 自动:`completed` 且超 `session_live_min`(默认 90)分钟未动 | 等窗口到 |
⛔ **两条闸门都绕开 `working`**(在跑的棒一定要摊在版面上)+ **主会话是版面锚点**(⛔ 不按窗口抽掉)。
⛔ **退场 ≠ 删除**:宿主库原样在、`retired.json` 全留痕(**可逆、可追溯**)⇒ 但**必须报数**
(看板/状态稿都会写「已收起 N 条」—— 收起而不说=读者把"收起了"读成"本来就没有",同族红线)。
⚠️ **前棒中途死掉时显式那步不会发生** ⇒ 靠窗口兜底收(同"每小时兜底唤醒"的设计口径)。
### 4.3 🔴 主会话生病了就**换一条**,别指望它自愈
- **哑会话**(诊断日志撞 ~10 MiB ⇒ 宿主拒写 ⇒ **界面永远刷不出内容**)**照样在"活会话"列表里**
⇒ 判据必须是「**活着 ∧ 不哑**」(`_pick_live()` 同时管这两条,`resolve_main` 的**每条路**都得走它)。
- **处置(标准 runbook)**:把 `<sid>.log`(连同 `.log.1`)**改名挪开**(⛔ 不删,用 Python `os.rename`,
⛔ 别用 `mv`)⇒ 宿主数十秒内重建并恢复写入 ⇒ 详见 `references/forensics.md §4`。
⚠️ **它必然复发**(每个会话涨到上限都会重演)⇒ 靠扫描器 + 钩子兜底。
- **换主会话的逃生口**:`collabd.py --declare --role main` —— 🔴 它的语义是「**换**」:
会把**其它登记为 main 的一律降级为 worker**(一个工作区一条主会话)+ **明确说出口**。
⛔ 旧语义是"加",等于**什么也没换**(`_main_sid()` 取第一条 main,而 Python dict
对已存在的键赋值**不改位置** ⇒ 老那条永远排前面,逃生口形同虚设 —— 2026-10-01 实测踩到)。
🔴 **两个极易踩的读数坑**:
1. **唤醒会话的读数源是 `sessions` 表**(标题前缀 `[唤醒]`),⛔ 不是 `automations` 表。
2. 🔴 **自动唤醒任务 / 协作轮的排期名必须带 `[协作]` 前缀** —— 否则会被 `resolve_main` 认成"主会话",**通知投给它自己**。
⚠️ 🔴 **2026-10-02 标注(⛔ 不是口径变更)**:本规则**只在「唤醒用排期」这个代偿形态下才有对象** —— 定案的唤醒时钟是**常驻投递**(⛔ 不开会话、没有排期名);⛔ 别因为这条规则在,就反过来认定「唤醒本该用排期」。
## 5 一次派活要带什么(模板要点)
1. **开工第 0 步:抢域锁**(域按**显式 domain** 切,⛔ 不按 cwd、⛔ 不按工作区)。
2. **本轮只做一件事,做完即停**(无人值守必写;否则它会一路做下去把预算烧光)。
3. **收尾自判**:还有缺口 ⇒ 接下一棒;`blocked` ⇒ ⛔ 不接,改喊用户。
4. 任务细节⛔ **不要抄进 prompt**(prompt 只写"去哪读",⛔ 不复制正文)—— 抄进去等于每轮都全量重发。
## 5.5 🔴 **任务会话的标准动作:六阶段**(用户 2026-10-04 定案)
> 用户原话:「这个是**会话解决问题的步骤**(需求识别、需求调研、方案规划、任务执行、结果验证、归档清理)这几个步骤 **正式执行会话执行任务的流程**」。
> 📌 **来源**:这套六阶段原是 `dsh-workflow §1`「平台改造六阶段」(DSH 平台改造流程);
> 用户把它**收进会话机制** ⇒ 它现在是**任务会话的正式执行流程**,⛔ 不再只是平台改造专用。
**六阶段**(任务会话从接手到收口的标准动作,⛔ 跳步是本机制最常见的失手):
| # | 阶段 | 这一阶段要做什么 | 缺了会怎样(⛔ 后果) |
|---|---|---|---|
| **0** | **需求识别** | 先盘点现状再动手:派活说清「要做什么 / 怎样算成功」,能落到**功能卡 4 问**就落到 4 问 | ⛔ 直接开工 ⇒ 做的是"我以为的",不是"要的" |
| **1** | **需求调研** | **源码级实证优先** ⛔ 禁止只靠文档推断;查清现状与既有约束 | ⛔ 照着过时文档改 ⇒ 改错对象、返工 |
| **2** | **方案规划** | 文档先行;方案定完再动工;🔴 判"该不该问用户"就在此刻(见下) | ⛔ 边做边想 ⇒ 方向错了已写了一半代码 |
| **3** | **任务执行** | 小步推进 + 每步可验;按 §4 派活通道执行,⛔ 不越界抢别的域 | ⛔ 一口气做完 ⇒ 中途错了不知道错在哪 |
| **4** | **结果验证** | **看产出物** ⛔ 不看任务图 `done`(那是两件事,见 §6);能用浏览器验就浏览器验 | ⛔ 拿"标了完成"当"真完成" ⇒ 假收口 |
| **5** | **归档清理** | 文档同步 + 产物落到**本目标的交付物** ⛔ 不散在工作区根 | ⛔ 下棒找不到上棒产物 ⇒ 链条静默断掉 |
🔴 **每个阶段都能撞上"该自己定还是该问用户"** ⇒ 那一刻的判据读
**`references/02-功能优先协作协议.md`**(9 类自决策白名单 / 只准提报用户 3 类 / 语言转换表 / 拆包提报用户)。
⚠️ **最常提报用户的是阶段 2(方案规划)**:功能语义分叉 ⇒ 必须问;技术选型 ⇒ ⛔ 不许问,自己定并记一句"我选了什么(可推翻)"。
📌 **与 `dsh-workflow` 的分工**(⛔ 别把两处当同一件事):
本节 = **任务会话在会话机制里干活的标准动作**(六阶段,落地口径在本包);
`dsh-workflow §1` = **DSH 平台改造的完整手册**(六阶段之外还有红线 R1–R11、并行调度三把锁、档案模板、浏览器验证栈)
⇒ 需要红线 / 锁 / 模板时 ⇒ 读 `dsh-workflow`;⛔ **只做会话内的活,本节足够**。
## 6 反馈协议与唤醒(防"干等"与"空转")
- **反馈协议 = 单条 + 握手**(⛔ 不是群发、⛔ 不是广播)。
- **唤醒 = 三条件合取**才触发(条件见 `architecture.md §2.2`)。
- **需求内闭环**:一个需求内的活由该需求自己的会话闭环收口(定义见 `architecture.md`「需求内闭环」节)。
- 🔴 **防"空转链条"**:**「只是排了下一棒」≠「有成果」**。判完成必须看**产出物**,⛔ 不看任务图节点标了 `done`(那是两件事)。
## 7 任务图(防干等的核心件)
细则 ⇒ `references/taskgraph.md`(文件格式、`collabd.py taskgraph()` 算法、四条派活规则、维护纪律)。
一句话:**关键路径单线化 ⇒ 必须拆**,否则整条链被一个慢节点卡住,其他人全在干等。
## 8 加东西之前必须过的判据
`architecture.md §6`「加东西前必须过的判据(防打转)」。⛔ 没过的就⛔ 不要加。
⚠️ 但**「协作与投递的常驻」是已定案项**,⛔ **不得拿"进程数=0"去否它**(2026-09-30 踩过:丢掉唤醒时钟 ⇒ 用户点破"成摆设")。
+212
View File
@@ -0,0 +1,212 @@
# 换一台机器怎么用(部署手册)
> ## 🔴 当前结论(**先读这里** · 最后更新 2026-09-30 05:55)
> | 项 | **当前结论** |
> |---|---|
> | **本机起法** | 唯一可行 = **宿主后台任务机制 + `stdout` 全重定向到文件**(完全静默)。⛔ `detached` 活不过工具调用边界;⛔ `schtasks` 被**内置程序黑名单**硬拦。 |
> | **投递** | 🔴 **定案:协作与投递「一直运行」(常驻)** —— 09-29 定案,**2026-10-01 用户再确认**(原话「**协作与投递一直运行(常驻)**」;理由「**可能不是所有队列都是钩子产生的**」)。⚠️ **现状:守护处于停机**(2026-09-29 23:59 止损;其归因已于 09-30 审计修正 —— 真因是**日志撞 ~10 MiB 被丢写**,⛔ 不是"挂在会话名下")⇒ **拉起常驻见 §5**。⛔ 「用自动任务当闹钟」**2026-10-01 已废弃**。 |
> | **前置(覆盖网络节点中继客户端 + 设备接入本地反代)** | **会周期性掉**(实测:设备接入本地反代约 1h 被 `reg.exe` 拦死)⇒ **探测与自起见 §5b**;⚠️ **AI 侧无法自动重起**,只能报警 + 人工/会话拉起。 ⚠️ **两段都在线 ≠ 链路通过**(判据=中继真转发过流 `streams>0`)。 |
> | **三条死路(都实测过,⛔ 别再试)** | `detached` spawn · `schtasks` · 从 bash 调 `powershell.exe` |
> ⚠️ 本文件为**追加式** ⇒ 与本节冲突时**以本节 + `architecture.md` 的「当前结论」节为准**。
> 目标:**整套「多会话协作机制」只在这一个 skill 里**(代码 + 架构 + 规范 + 手册),换机器=拷这个目录 + 改一份配置 + 接一次钩子。
> ⛔ 换机器**不需要**拷贝任何工作区里的脚本 —— 若发现机制代码出现在工作区,那就是旧副本,按 §4 退役。
---
## 0 这个 skill 里有什么(机制本体)
| 文件 | 是什么 |
|---|---|
| `SKILL.md` | 操作入口:怎么派活、怎么收尾、怎么排坑 |
| `references/architecture.md` | 🔴 **唯一架构文档**(五主体/需求台账四态/单条握手/三条件唤醒/红线/判据/术语) |
| `references/deploy.md` | 本文件(换机器) |
| `references/pitfalls.md` | 踩坑清单(每条都真实发生过) |
| `references/taskgraph.md` | 任务图规范 |
| `scripts/collabd.py` | **常驻程序**(`--once` 投影轮)+ **投递**(`--tick` 投递轮)+ 🔴 **常驻形态**(`--supervise`,**定案=一直运行**,见 §5)+ 上报 / 台账 / 看板 |
| `scripts/guard.py` | 🟡 **可选**:守护(只做**发现+落盘**,⛔ 不承担投递)。⛔ 不是投递的前置 |
| `scripts/selftest.py` | **回归自测**(20 条用例 · 不碰生产):**改完必跑,全绿才算改完** |
| `scripts/collabd.config.example.json` | 配置模板(拷成 `collabd.config.json` 改路径) |
**⛔ 不进 skill 的**:运行态产物(台账/看板/通知/日志)—— 它们属于**每个工作区自己**,路径由配置的 `workspace` + `inbox` 决定。
---
## 1 前置(一次性)
| 项 | 要求 |
|---|---|
| Python | 3.11+(实测 3.13);`ast.parse` 可用的标准库即可,**⛔ 无第三方依赖** |
| 宿主 | WorkBuddy 桌面版(需要它的三张只读表 + 自动化排期 + 钩子) |
| 目录 | 把本 skill 放到 `<配置目录>/skills/session-mechanism/`(如 `E:/ProgramData/.workbuddy/skills/`) |
---
## 2 三分钟部署
```
⓪ 声明**任务目标**(🔴 没有目标 ⇒ guard **拒绝启动**,rc=3):
<python> <skill>/scripts/guard.py --goal "<一句话任务目标>"
也支持手写 <workspace>/<inbox>/goal.json(title 必需;可附 acceptance_doc / taskgraph / lines)
① 拷 skill 目录到新机器的 skills/ 下
② 在 scripts/ 下:cp collabd.config.example.json collabd.config.json
改 4 处必填:workspace / inbox / live / taskgraph (其余按需,见 §3)
③ 🔴 **拉起常驻(投递本体)**:`<python> <skill>/scripts/collabd.py --supervise` —— **它同时就是唤醒时钟**。
⚠️ 起法约束见 §5;⛔ 「用自动任务当闹钟」已于 2026-10-01 废弃。
④ 🔴 **接钩子(只作"即时性"补充,⛔ 不是主路径)**
让宿主在**两个**事件上调用一个**本地脚本**(脚本内部再去调 collabd):
事件 A:`UserPromptSubmit` → <python> .../wb-result-hook.py
事件 B:`PreToolUse`(matcher `^Bash$`) → <python> .../wb-result-hook.py
脚本内部做两件事(都**节流**、都**隐窗**):
· `collabd.py --once` ⇒ 常驻程序跑一轮**投影**(⛔ 不投递)
· `collabd.py --tick` ⇒ 投递跑一轮 ⇒ **投递 + 推进队列**
🔴 **它的定位 = 补充**:钩子是**宿主起的子进程** ⇒ 自带网关口令 + ⛔ 不占会话 + 零 token ⇒ 有事件时**立刻**投一版;
但 ⛔ **不能靠它兜底** —— 「需要投递的时刻必然伴随会话在动」**是错假设**(队列可能由**非钩子来源**产生,那时没有事件 ⇒ 漏)。
⚠️ **新加的钩子事件要宿主重启后才生效**(`UserPromptSubmit` 那条**立即生效**);
⚠️ 钩子**只指向 skill/工作区里那一份**脚本,⛔ 不许有第二份(见 §4)。
⑤ 🟡 **守护:可选**(⛔ 不是投递前置;**投递本体= ③ 的常驻**)。
它的职责只剩**发现**:长时间没人动 ⇒ 落 `STALL.md`/`NEED-USER.md`。
· ⛔ 别在会话里起(会被回收);要起 ⇒ 独立窗口或「启动」文件夹 ⇒ 但它**拿不到口令 ⇒ 投递不了**(设计使然)。
```
**验收(逐条可测)**
1. `<python> collabd.py --where` ⇒ 打印出正确的 workspace / inbox / 任务图路径;
2. `<python> collabd.py --once` ⇒ `rc=0`,`inbox/` 下出现看板与机械摘要,**无异常栈**;
3. 造一条上报:`collabd.py --report T1 --state running --line <线> --by <会话名>` ⇒
`collabd.py --reqs` 能看到 `T1 执行中`;
4. `<python> collabd.py --tick` ⇒ `rc=0`,打印一行 `tick: … deliver=…`;
🔴 **真投递的判据 ⛔ 不是 rc=0**:看 `wakeups.jsonl` **有没有新增一行 `ok:true`**;
打印 `no-token` ⇒ 说明这个进程**不在宿主进程树内**(手工跑属正常;由钩子唤起时出现 ⇒ **异常**)。
5. 让一个会话声明角色:`collabd.py --declare --role main` ⇒ 输出 `已声明角色:<sid> = main`;
6. `<python> selftest.py` ⇒ **PASS 39 / FAIL 0**。
7. 🔴 **常驻投递在跑**(定案项,见 §5):进程活着 + `collabd.py --where` 解析到正确 workspace;
判「真投递」⛔ **不看 rc**,看 `wakeups.jsonl` **有没有新增 `ok:true`**。
---
## 3 配置项(`collabd.config.json`)
| 键 | 必填 | 说明 |
|---|---|---|
| `workspace` | ✅ | 你的工作区绝对路径(正斜杠);台账/看板/通知都相对于它 |
| `inbox` | ✅ | 运行态目录(相对 `workspace`),建议 `tmp/supervise-inbox` |
| `live` / `taskgraph` | ✅ | 看板文件 / 任务图 JSON(相对 `workspace`) |
| `lines` | 建议 | 线名 → 中文名 |
| `host_db` | 否 | 宿主库路径;空 ⇒ 用 `CODEBUDDY_CONFIG_DIR` 推导 |
| `shim_port` / `client_entry` / `client_runner` | 否 | 服务自愈用;**不做自愈就留空** |
| `wake_enable` / `wake_min_gap` / `wake_text` | 否 | 唤醒投递(需要网关口令在环境里) |
⚠️ **本机配置(`collabd.config.json`)不要提交到公共仓库** —— 它含本机绝对路径。
---
## 4 旧副本退役(⛔ 否则"代码在 A、钩子在 B")
症状:改了 skill 里的程序却没生效;或 `advance.md` 等产物由**另一个副本**写出(内容与你预期不同)。
```
① 工作区里若有 collabd.py / advance-watch.py / keepalive.py / thin-consumer.py
⇒ 全是旧副本(职责已被本 skill 吸收)⇒ 改名加 `.retired-<日期>`,⛔ 不删(留回滚)
② 确认钩子指向的是 **skill 里的** collabd.py(`--where` 自证路径)
③ 重启后看 `inbox/` 产物的 mtime 是否随会话事件刷新 ⇒ 是则接线正确
```
🔴 **同名的两份实现是"最难查的故障"**(实测踩过:两份同时跑,同一秒给出互相矛盾的读数)。
---
## 5 投递的边界(🔴 一句话:**投递 = 一个常驻进程**(它同时就是唤醒时钟);钩子只作补充,⛔ 不排期、⛔ 不用自动任务)
| | 有口令(能投递) | 需要容器会话 | 必须活着的进程数 | 跨重启自动生效 | 投递延迟 | 零 token |
|---|---|---|---|---|---|---|
| 🔴 **宿主后台任务(现定案的载体)** | ✅ | ✅ 需 1 个 | ≥1 | ⛔ 需再拉起 | ✅ **可调(轮询间隔)** | ✅ |
| 宿主钩子唤起的一次性进程(**降为补充**) | ✅ | ⛔ 不需要 | **0** | ✅ | ⛔ **依赖事件 ⇒ 会漏** | ✅ |
| 会话外常驻(启动文件夹/独立窗口) | ⛔ **否** | ⛔ 不需要 | ≥1 | ✅ | ✅ 可调 | ✅ |
⇒ 🔴 **载体 = 宿主后台任务**(本机实测:`detached` **活不过工具调用边界**、`schtasks` **被内置程序黑名单硬拦**)
⇒ ✅ **唯一可行 = 宿主后台任务机制 + `stdout` 全重定向到文件(完全静默)**。
⚠️ 「必须活着的进程数 = 0」**⛔ 不得再拿来否掉常驻**(2026-09-30 踩过:丢掉时钟 ⇒ 用户点破"成摆设")。
**三条硬约束**:
1. 🔴 **必须完全静默** —— `stdout` 重定向到文件。⚠️ 真风险是**输出/事件量把会话日志推过 ~10 MiB 上限 ⇒ 宿主 `diagnostic-log:dropped` ⇒ 界面不再显示**(⇒ `pitfalls.md P0-2`);
⛔ **不是"任务挂在会话名下"**(该归因已作废)。
2. ⚠️ **别从会话/工具调用里"直接"起**(`detached spawn`)—— 调用一结束就被回收(实测:唤醒停在调用结束那一秒);
要用**宿主后台任务机制**起,`stdout` 重定向到文件。
3. ⛔ **计划任务(`schtasks`)在部分机器被安全策略硬拦** ⇒ 兜底走「启动」文件夹(⚠️ 但那**拿不到口令 ⇒ 投递不了**,只能做发现)。
⚠️ **口令边界的正确表述**:由会话之外起的常驻**拿不到网关口令** ⇒ 它只**发现 + 落盘**(`STALL.md`/`NEED-USER.md`/看板)——
**这不是故障,是设计**:`collabd.py` 用 `FROM_HOOK` 分辨两种"没口令"(钩子唤起却没口令 ⇒ 异常 ⇒ 落 `NEED-USER.md`;常驻没口令 ⇒ 只记日志)。
---
## 5b 🔴 「覆盖网络节点 · 中继客户端 + 设备接入 · 本地反代」的**探测与自起**(2026-09-30 实测定型 · 任何棒都要会)
> 背景:手机接入链路的**最前一格**是"设备接入本地反代 + 覆盖网络节点中继客户端 都在线"。它不在 ⇒ relay 无通道 ⇒ 入口④闸 `503 device-unreachable` ⇒ 整条链断。
> ⚠️ 这两个进程**不跨 WorkBuddy 重启**,且**本机无法做成计划任务**(`schtasks` 被内置程序黑名单硬拦)。
**① 先探测(⛔ 在跑就别重复起 —— 两份会抢同一 host 注册/同一端口)**
```bash
netstat -ano | grep ":20090" # 设备接入 · 本地反代在听否
tail -1 E:/dsh-worker-dev/logs/overlay-bg-*.out.log # 最近一行 state=up(for …) ⇒ 覆盖网络节点中继客户端在跑
curl -s --noproxy '*' -o /dev/null -w "%{http_code}\n" http://127.0.0.1:20090/ # 期望 200
```
**② 不在才起 —— 🔴 必须"宿主后台任务机制 + stdout 全重定向到文件"(完全静默)**
| 件 | 命令 |
|---|---|
| **覆盖网络节点 · 中继客户端** | `node D:/github/dsh_shenxian/lib/net/relay/main.js --client --url wss://ai1net.com/dshs-relay --host <hostId> --network <network> --keys-file E:/dsh-worker-dev/overlay/relay-keys.local.json --ports 20090`(env:`DSHS_OVERLAY_NODE_KEY_FILE` / `DSHS_OVERLAY_NODE_GRANT_FILE`;`<hostId>/<network>` 从 `E:/dsh-worker-dev/overlay/overlay-node.local.json` 读) |
| **设备接入 · 本地反代** | `node E:/github/dsh-desktop-0.1.7rc2/node_modules/tsx/dist/cli.mjs E:/ProgramData/AIProject/ai1net-dsh-desktop/.workbuddy/_devkit/launch-desktop-dev-017.mts`(cwd=`E:/github/dsh-desktop-0.1.7rc2`;⚠️ **必须先 `unset ELECTRON_RUN_AS_NODE`**,否则 Electron 退化成纯 Node) |
**成功判据(两条都要)**:覆盖网络节点中继客户端 ⇒ 日志出现**本轮新增**的 `registered host=… accepted=[20090]` 且 `state=up`;设备接入本地反代 ⇒ `20090 LISTENING` + `curl` **200**。
**⛔ 三条死路,别再试**(都实测过)
1. **`detached` spawn** —— 活不过工具调用边界(日志 0 字节即死);`wb-overlay-node-launch.mjs --detached` 注释里"本机实测不可用"是真的。
2. **`schtasks`** —— 内置程序黑名单硬拦(见 `pitfalls` 里那六项),⛔ 命令内不可放行。
3. **从 bash 调 `powershell.exe`** —— 被策略拦("绕过 PowerShell 安全检查");而 `overlay-node-daemon.ps1 -Action run` 是 **powershell 前台长跑**,也活不下来。
**⚠️ 遗留单点(如实登记)**:这两条后台任务**挂在"起它的那个会话"名下** ⇒ **起它的会话被回收 / WorkBuddy 退出 ⇒ 链路断**。
⇒ 起它们的会话**在那段时间内不要关**;断了就按上面"② 不在才起"重起一次。
## 6 换机器后必查的 5 件事
1. `--where` 的三个路径是否都对;
2. 钩子接线是否指向 skill(§4);
3. 宿主库能否只读打开(`sessions` / `automation_runs` / `automations` 三张表读得到);
4. 有没有**别的实现**在抢同一个 inbox(§4);
5. `guard.py` 是否真的起来了(`--status`)+ 它是否会随开机自启。
---
## 5d 🔴 「会话整体停止」后怎么恢复(2026-09-30 立 · 用户追问逼出来的)
> 用户原话:「所以现在的问题就是 **会话整体停止了怎么办** 的问题」。
**先分清三档 —— 只有第三档才需要动手:**
| 档 | 场景 | 会怎样 | 怎么办 |
|---|---|---|---|
| ① | **只有某个会话停了**(WorkBuddy 还开着) | 机制照跑 —— 钩子是**全局**注册的,**任何**会话跑 Bash/发消息都会唤起它 | **不用办** |
| ② | **所有会话都 idle**(WorkBuddy 还开着) | 钩子不被唤起 ⇒ 不投递 | **不用办** —— 那时没人在看;一有动作,积压立刻补投 |
| ③ | **WorkBuddy 退出 / 机器休眠** | **整条协作链停摆** —— 唯一能"开新会话"的通道就是宿主自动化 | **按下面清单恢复** |
### 恢复清单(重开 WorkBuddy 后按序跑)
1. **看前置两段**:`netstat -ano | grep ":20090"`(判据**只认 LISTENING 行**,⛔ 别用 curl/connect —— 本机 Proxifier 会代理回环,返回是假的)。
缺了 ⇒ 按 **§5b** 拉。⚠️ **中继客户端那半边已计划任务化**,通常自己会回来。
2. **查过期未跑的一次性排期**(`status='ACTIVE' and schedule_type='once'` 且 `next_run_at < 现在`):
有 ⇒ **新建一条**(⛔ **改时间不会触发**)。
3. **跑一轮投影** `collabd.py --once`,让队列/看板/`NEXT.md` 刷新。
4. **看 `NEED-USER.md`**:有没有等你拍板的事。
5. **看 `blocked.json`**:受阻件是否已随状态更新解除(⛔ 别让它一直当队首、每次唤醒白跑)。
### 🔴 为什么"不需要防"(实测结论,⛔ 别再想做常驻去扛)
- **实测(2026-09-30 08:2x)**:11 条一次性排期里 **9 条准点跑**;唯一 0 次那条的排期在**创建时就已过期** ——
⇒ **宿主在跑,排期就准点**;**宿主不在,排期不会自己跑**(**调度器就是宿主本身**)。
- **能扛过 WorkBuddy 退出的**:只有**不需要口令**的长跑 —— 实测就是**覆盖网络节点中继客户端**(已由计划任务持有,
父链 `powershell ← svchost ← services ← wininit`,⛔ 无 bash/无 WorkBuddy)。
- **扛不过的**:任何**要投递**的东西 —— 投递要**网关口令**,而**口令的唯一合法来源是 WorkBuddy 进程 env**
(08:06:31 实测:计划任务上下文 `envPresent=false / envLen=0`)⇒ **「不占会话」与「有口令」二选一**。
- ⇒ **结论**:**「会话整体停止」不需要"防",只需要"恢复流程"**。
⛔ 不要再设计"常驻监督进程"去扛 —— 那条路已被实测堵死(要么没口令、要么拖住会话)。
+73
View File
@@ -0,0 +1,73 @@
# 会话复盘 · 取证手册("某个会话当时到底干了啥 / 为什么卡住")
> 取证脚本:`scripts/forensics/proc-parent.py`(纯 ctypes 查进程父链,⛔ 不依赖 psutil)。
> 深度内容(每条的前因后果、实测读数)⇒ `references/pitfalls.md`。
## 1 先定位会话
- 会话表:`sessions`(`title` / `custom_title` / `created_at` / `last_activity_at` / `cwd` / `status`)。
- 用户说「找**最新的**那个」时,**两个指标都算、取更大者**:`createdAtMs`(创建)与 `lastActivityAtMs`(最后活动)。
⚠️ **两个指标打架 ⇒ 两条都列出来问用户,⛔ 别猜**。
- ⚠️ **同名多会话**在这一层很常见(标题会被复用)⇒ 必须靠 id / 时间 / cwd 三者交叉确认。
## 2 读转录(原文在哪、怎么抽)
- 转录是 JSON 行文件(`.jsonl`)。抽四类东西:**用户原话** / **AI 正文** / **reasoning 思考** / **工具调用**。
- ⚠️ **转录会被清理**(保留窗口有限)⇒ 过期后只能走 §6 的复原路径。
- 🔴 **别拿转录 mtime 当"有产出"**:agent 在跑时转录正文可能**滞后甚至不落盘**;反之活干完了也可能因推送失败而看起来没动。
## 3 🔴 四种"卡住"必须分开判(这是本节的核心)
| 型 | 症状 | 决定性判据 |
|---|---|---|
| **中断** | 那一轮从中间断了 | 转录尾部有**宿主注入的错误**(如 `## Model error retry: Tool Not Found`)⇒ 中断点;其后长时间零 `reasoning` / `function_call`。**瞬时工具不可用 ≠ 会话来死** |
| **工具循环不收敛** | 消息派发了、agent 真在跑,但**永不回话** | 工作区日志状态机:`finish_reason=` **反复是 `tool_calls`,从不出现 `stop`**;`busy=true` 恒不回 idle |
| **干完了推不出去** | 界面显示"一直运行",但看不到任何执行 | 日志里 `sendToClient: … SSE is closed` 大量堆积 + 该轮 `finish_reason="stop"` ⇒ **活干完了,结果送不出去** |
| **投递悬挂** | 消息收到了、也入队了,但**没有任何东西去排空队列** | 🔴 **`route=parkInQueue` + `hasWaiter=false`**(正常是 `route=resolveWaiter` + `hasWaiter=true`) |
🔴 **投递悬挂的定量指纹**(把"症状"变"确诊"):`hasWaiter=false` 只会在出事那段出现、`queueLen` **只进不出**(每再投一次就再压一条)、同一小时 `No state found for connectionId` **飙升**。
✅ **处置=让客户端重新挂上该会话**(切走再切回 / 重开该会话窗口);⛔ **别反复发消息试探**(只往队尾再堆一条)。
⚠️ **两条别混**:同一次事故里常常**同时**出现"日志层异常"与"投递异常" ⇒ **分别归因、分别取证**,⛔ 不要合成一个因。
⚠️ **别拿会话状态字段当判据**:客户端会把 `working` **基于错误假设主动写进库**,而宿主那边根本 idle;`last_activity_at` 的刷新也可能只是"UI 打开了这个会话"。
## 4 日志撞上限(10 MiB)⇒ 整批丢写
**三条同时成立即可确诊**:
1. 守护日志反复出现 `diagnostic log write failed … EPERM`,且 `droppedLines` **持续增长**;
2. 该会话的 `<sid>.log` **大小卡在 ≈10 MiB 且 mtime 冻结**,而同期**其他会话的同名日志正常在长**(对照排除"全盘坏了");
3. 同目录 `<sid>.log.1` 也是满的 ⇒ **轮转无空位**。
**修法(非破坏、可逆)**:把卡死的那个 `<sid>.log` 与 `<sid>.log.1` **改名挪开**(⛔ 不要删)。
🔴 用 Python `os.rename`,⛔ **不要用 `mv`**(MSYS 的 `mv` 在长路径/中文名下更易踩坑)。
**验收必做**:① 数十秒内被宿主**自动重建** ② 它**再次长大** ③ 告警**不再新增**。
⚠️ **它必然复发**(每个会话涨到上限都会重演)⇒ 需要**持续兜底**(本工作区有扫描器 + 钩子兜底)。
⚠️ 这两个文件**并没有被锁**(可正常打开)⇒ ⛔ 别再往"查谁占用文件"上耗时间。
## 5 进程定性("它是不是某个会话的后台任务")
```
netstat -ano | grep ":<端口>" # 只认 LISTENING 那行 ⇒ 拿 pid
python <本包>/scripts/forensics/proc-parent.py <pid> # pid ⇒ 父链
```
- 父链出现 `… ← sandbox-cli.exe ← WorkBuddy.exe` ⇒ ✅ **是会话/宿主托管的后台任务**。
- 父链是 `services.exe` / `svchost.exe` / `explorer.exe` ⇒ ⛔ 不是后台任务(独立进程)。
- 更硬的证据在宿主日志里搜 `executeInBackground` / `background task created` / `processExit`。
- ⚠️ 日志里的中文常是「UTF-8 字节被按 GBK 解」的乱码,还原一行:`s.encode("gbk","replace").decode("utf-8","replace")`。
## 6 转录已被清理时的复原路径(四条腿)
1. **全工作区 memory 日志批量搜"行为的独有名词"**(工具名 / 产物名 / 作品名)⇒ 一次命中就给出「工作区 + 日期」,比逐会话读 jsonl 快一个量级。
2. 读命中的那天日志(它通常写了工具链、命令要点、产出**绝对路径**)。
3. 按**独有产物名**跨盘搜文件系统(⚠️ 必须剪枝:跳过 `node_modules/.git/Windows/Program Files/AppData`,限制下钻深度;⛔ 别用全盘递归 `glob`,实测直接超时被杀)。
4. 与规范化文档互证(文档里常写「基于 <日期> 实测沉淀」)。
⚠️ **交付时必须同时说明「原产出目录是否已失效」与「现存可核验的替代物」**,别让用户去翻一个不存在的路径。
## 7 ⛔ 别做
- ⛔ 别因为 `status=working` 就断定卡死(当前正在跑的会话也是它)。
- ⛔ 别因为"抢不到锁"就去**删锁 / 接管**(红线,只能持有者释放)。
- ⛔ 别"再发一条试试"救投递悬挂的会话(会重开一轮同样的循环 / 只往队尾堆一条)。
- ⛔ 别在自动化会话里手动续聊当正式驱动。
- ⛔ 别为"恢复它"随手重启应用(会打断其他线正在跑的棒)。
+134
View File
@@ -0,0 +1,134 @@
# manifest · 包内文件清单
> 生成方式:逐文件 `compile()` / `json.loads` + md5 | **最近一次全量重算:2026-10-04 17:4x (2026-10-04 单包权威:02 档改为「本档即权威」(⛔ 不再指向外部技能当权威),保留来路说明;SKILL.md 撤回『必需外部依赖』声明改为自包含)**
> ⛔ 本表**不含** `install.log`(运行日志)与 `references/manifest.md`(自引用,写完即失真)。
> ⚠️ **provenance 列里的 `skills/multi-session-collab/…`、`skills/workbuddy-session-forensics/…` 已是历史路径**
> —— 那两个目录 2026-10-01 已移到 `<工作区>/归档/技能-退役-20261001/`(⛔ 不在技能根了)。
文件总数:**49** | 语法 / 结构检查失败:**0**
## ✅ 已完成 · 2026-10-05 三代术语收敛(本节替代原「待办」)
- **已执行**:包内 + 两个工作区副本三方同步,md5 一致(`collabd.py`/`goalctl.py`/`board.py`/`board_ext.py`/
`selftest.py`/`collabctl.py`/`guard.py`/`init_workspace.py`/`session-rules-check.py`/`board.html` 全部 OK)。
`selftest.py` 基线 **PASS 97 / FAIL 0**(报告型 1 条不计入);改名的 5 条 `@case` 均能按新名字 `-k` 找到。
- **⛔ 三类不动,是判据不是遗漏**(下一次改名前照抄):
① **「」引述内容逐字不动**(含跨行引述块)—— 引用用户口径只写原话;
② **历史段整段不动** —— `SKILL.md` 19–27 行代际表、含 `旧称/旧词/一代/二代/第三代` 的行、`<hN> 历史…` 段落
(要讲清三代分别叫什么,就必须留着旧名);
③ **`.py` 里 `目标检查` 一律不动** —— 它是**检查会话类别名**(`_CHECK_TAGS`/`_CHECK_TOPICS`、
`[检查]-[结果检查/目标检查]`),并散在字面断言里(`CHECK_KINDS["queue-empty"][1] == "目标检查"`、
`rows.append(...)`、`【目标检查】` prompt 头)⇒ 改了直接断判据。
- ⚠️ **一处同改对**:`board.py::_ROLE_LABEL["worker"]` 与 `board.html` 的 `role==='…'` 是同一处判据的两侧。
- ⚠️ **本条暴露的存量缺陷(已一并修)**:`selftest.py` 的期望串原写 `才建立一轮任务会话`,而 `SKILL.md`
引述里逐字是 `才建立一轮执行会话` ⇒ 该子项**改前就是红的**(被基线里的 97/0 掩盖)。
- 工具:`tmp/term-apply-v3-20261005.py`(dry-run/`--apply`,写盘前自动备份)+ `tmp/term-sync-20261005.py`。
## 2026-10-04 新增文件(⛔ 由 manifest 重算登记)
- `references/01-文档索引.md` — 3604 B |md5 `825999a411e2a4f6b740063f81c01b03`
- `scripts/judge_audit.py` — 13562 B |md5 `8f14a973fa5709a3b9a60b065dc22d7d`
- `scripts/workspace_mirror.py` — 17263 B |md5 `60f104b37df3133785abb13f686eea4f`
- `assets/start-supervise.ps1.tpl` — 5360 B |md5 `3805f1a82a1747269212287e9dfde369`
- `assets/design-tokens.css` — 6364 B |md5 `0668feb3a4729f1022e579ac0c5d2569`
## 最近改动(⛔ 只记「什么时候改了什么」,不写流水账)
- **2026-10-02 07:0x · 开工第 0 步改名:会话「规划」→ 会话「规则机制」(本轮)**:
① **用户纠正原话**:「**就应该是检查清楚 所有会话规则机制 是否配置完整且生效, 不是规划 是 规则**」
⇒ 首版体检脚本**只查排期那一面**,当天实测出的三类失效(钩子注入指向已退役技能名 / 常驻快照写进幽灵目录 /
每轮注入的记忆指针悬空)**一条都没覆盖** ⇒ 名字还叫「规划」=第二次「在册 ≠ 生效」。
② **收敛成一个入口**:新增 `scripts/session-rules-check.py`(查 **三类十二项**:A 钩子在册·路径存在·
注入技能名存在·闸门日志新鲜 | B 每轮注入记忆的技能指针·常驻快照不比权威旧 | C 周期钟·模型可用性·
`cwds` 同形·投递心跳·活会话·`once` 从未运行的死排期);旧 `scripts/session-plan-check.py` **退役**
到 `<WS>/归档/技能包-旧件-20261002/`,**能力不回退**。
③ **状态标记改名**:`session-plan.json` → **`session-rules.json`**(旧的属过期件,⛔ 别拿来对照)。
④ **判据收紧**:`cwds` 近失配原写成「同父目录 + 字面不同」⇒ 在 `AIProject/` 这种**多业务线平级目录**下
把**别的线**报成失配(实跑当场 2 条假红)⇒ 改为「同父目录 + **名字去 `-`/`_` 后仍相同**」/「同名不同父目录」。
⑤ **判据自验**:⑨⑫ 抽成**纯函数** + 合成样本夹具(`<WS>/tmp/rules-check-mutate.py`)—— 原版 10/0 绿、
**四个变异体**逐一按预期报红。⚠️ 夹具**自己先红两次**(期望值写死:漏算样本 + 有序列表比中文名)。
- **2026-10-01 18:5x · 外置根贯通 + 两处真缺陷(本轮)**:
① `roots.env` 引导块从 9 份扩到 **16 份**(补齐 7 个非钩子脚本:`board` / `board_ext` / `collabd` /
`deliver-gateway-token` / `goalctl` / `selftest` / `stop-collab`);
② **盘符字面量清零**:配置目录兜底改 `~/.workbuddy`,工作区兜底改 `DSH_WS_ROOT`/cwd,
覆盖网日志 glob 改 `DSH_OVERLAY_LOG_GLOB`(原为死路径 —— 本机 `E:/dsh-worker-dev` 不存在);
③ 7 份加**输出编码兜底**;④ `roots.env` 增写 `CODEBUDDY_CONFIG_DIR`;
⑤ 🔴 **修「钩子把技能包当工作区」**:`wb-result-hook.py` 的 `_WS_ROOT` 原按 `3×dirname(__file__)` 推,
包内这份推出来=技能包自己 ⇒ 包里长出 `tmp/supervise-inbox/`;改为复用 `resolve_ws()`;
⑥ 🔴 **修「重定向 + GBK ⇒ print 抛异常 ⇒ 顶层记 fatal、整轮失败」**(本机实测 4 次;
这正是**常驻**跑不起来的拦路石 —— 常驻必须重定向 stdout);
⑦ `install.py` 工作区模板路径纠错(原指 `scripts/collab/`,实际在 `scripts/`);
⑧ `collabd.py` docstring + 两处活注释改**常驻定案**(上一轮漏传导);
⑨ `lock/preflight-lock.sh` + `hooks/lock-guard-hook.py` 的盘符字面量清零;
⑩ 新增 `install.py --manifest`(见下条);
⑪ 🔴 **`scripts/selftest.py` 去掉最后一处项目绝对路径**:`t_tick_wired` 原先写死
`<某工作区>/.workbuddy/tools/wb-result-hook.py` ⇒ 两处都不对:既违反「技能里只用相对路径」,
又因宿主接线已改指**包内**那份而**恒走"文件不在,跳过"** ⇒ 看着绿、其实什么都没验(静默假绿)。
现改按包内相对路径取 `hooks/wb-result-hook.py`,该用例从"跳过"变**真检**(4 项实跑通过)。
- **2026-10-01 19:0x · `install.py --manifest`**:把「重算本表」固化成子命令
(包一改表就过期 ⇒ 以前每轮临时手搓脚本、跑完即删,已因此误判两次)。
只重写**表 + 计数行 + 重算时间**,⛔ 不碰上方散文;**行尾随原文件**(本表是 CRLF,写成 LF 会造成一次无声全文件 diff)。
- **🔴 两份同源脚本「有意不一致」(⛔ 别当 bug 去修平)**:`preflight-lock.sh` 与 `lock-guard-hook.py` 在
**技能包**与**文档库 `07-scripts/`** 各有一份。包内那份住在技能包里、**推不出文档库根** ⇒ 只能走
`roots.env` +(`lock-guard-hook.py` 的)末位字面量兜底;库内那份住在 `<文档库>/07-scripts/` ⇒
**按 `__file__` 往上两级即文档库根**。⇒ 包内有引导块、库内没有,是**位置决定的**,不是漏改。
- **2026-10-01 18:2x · 瘦身(本轮)**:`references/collab-detail.md` 删去**原件 YAML 变更流水**(约 9.4 KB)+ 加**读法索引**+ 压缩 §12;
`references/{manifest,taskgraph,pitfalls}.md` 去掉过期条目与旧抬头;包内 `__pycache__/`、`hooks/bak-*/`、`tmp/` 已清。
**⛔ 本轮无机制代码改动。**
- **2026-10-01 16:5x**:`scripts/hooks/session-log-guard.py` 新增「**硬档就地回收**」—— 到 8 MiB 先试**一次**把本会话
`logs/<日期>/sdk/conversations/<sid>.log` 改名(后缀 `.recycled-<时间戳>`,⛔ 不删、⛔ 不以 `.log` 结尾故与清扫器不重叠)
⇒ 宿主立刻重建并继续写 ⇒ **会话不必再因撞 10 MiB 而停手**;失败则**逐字退回**原「停手+建接续」口径。
开关 `DSH_SLG_NO_RECYCLE=1`(只关回收、保留叫停)。⚠️ 文档库原件 `07-scripts/session-log-guard.py` **已同语义同步**。
- **2026-10-01 16:0x**:原 `multi-session-collab` 的 `SKILL.md` 全文并入 `references/collab-detail.md`(治 8 处悬空引用:
`§0.05` `§0.5.2` `§0.5.4` `§0.5.5` `§1.3` `§1.4a` `§3` `§6`);清掉 9 处旧技能路径/名的活文本;
两个原技能目录移入 `<工作区>/归档/技能-退役-20261001/`。
## 逐文件清单
| 包内路径 | 字节 | md5 | 语法检查 |
|---|---|---|---|
| `SKILL.md` | 52451 | `c304346227e3d2b217e442a471b6c10d` | ok |
| `assets/board.html` | 136715 | `bf0e560a0420c833e94f329958e64a3c` | ok |
| `assets/design-tokens.css` | 6364 | `0668feb3a4729f1022e579ac0c5d2569` | ok |
| `install.py` | 26132 | `9522ec34773385a638efdeb3df0090d2` | ok |
| `references/00-动手前必过.md` | 3923 | `33bc86e442ccd4fcbb930c79c4ad0641` | ok |
| `references/02-功能优先协作协议.md` | 30502 | `eeeab3dcad47ad01380c8b05fee6fb3f` | ok |
| `references/03-回复排版-核心块.md` | 2187 | `201e7f112ee432b89e38f82f49301966` | ok |
| `references/99-速查清单.md` | 9832 | `17cc8717649164fa3b9e26da3be9405b` | ok |
| `references/architecture.md` | 119291 | `c78f5cba060efc79db5d160f7df0cca6` | ok |
| `references/collab-detail.md` | 98760 | `6bd9cc7e28596789d55dcd3da22548fe` | ok |
| `references/collab.md` | 16560 | `e7024a823d14c5aa808c7be3a7b00eda` | ok |
| `references/deploy.md` | 16754 | `66342ae0e32ecb499b4c72e4ccfabb9b` | ok |
| `references/forensics.md` | 6067 | `1fb6deca9bb05bebe950d4f4e3adf043` | ok |
| `references/pitfalls.md` | 132523 | `9bd6509df320833a41313848009aeb9a` | ok |
| `references/rules.md` | 6931 | `d654e9dac68a6f44b93898ba98e49221` | ok |
| `references/supervise-persistence.md` | 19245 | `1d4bbb08f02a661124886578cb700af5` | ok |
| `references/taskgraph.md` | 3521 | `be6c6540be86475bb3430688c2987afc` | ok |
| `roots.env` | 624 | `ec38f15f9a4101d69c815aa2cdd7b477` | ok |
| `scripts/board.py` | 123843 | `339d38fd579c602c20ce59f4aa106408` | ok |
| `scripts/board_ext.py` | 45655 | `f138d944145b276e65f6aa86bd8baa07` | ok |
| `scripts/collabd.config.example.json` | 1158 | `6655c15c411a84e3ca12fd136dbe2c60` | ok |
| `scripts/collabd.py` | 355568 | `58ffc3e52a52445e5b0e782fc99d9323` | ok |
| `scripts/deliver-gateway-token.py` | 5685 | `cbba9648d404ecd2683061e19dbdb24a` | ok |
| `scripts/deploy_code.py` | 4391 | `c22969124dd937ddaa66db09738df4a4` | ok |
| `scripts/forensics/proc-parent.py` | 2347 | `8cdfff2dbb88303298752c3777da7b20` | ok |
| `scripts/goalctl.py` | 39314 | `b3c4428b3aa7e4e3093e96105d1bc4dd` | ok |
| `scripts/guard.py` | 13653 | `378e2a388aa14bc55f83e17b5ea61053` | ok |
| `scripts/hooks/_env.py` | 11082 | `d3a557f22286bffb62ff29ee85c71799` | ok |
| `scripts/hooks/bash-output-guard.py` | 17741 | `3f2fdfa17f66f7fcac42e06853433a26` | ok |
| `scripts/hooks/lock-guard-hook.py` | 20363 | `641457de0297ee7a812ccde57f67dc2b` | ok |
| `scripts/hooks/reply-style-guard.py` | 10779 | `a05ca0d6ebef2be55a3d6f96a4c712da` | ok |
| `scripts/hooks/session-log-guard.py` | 24636 | `0bd6865e1a3ab97315c64631f37963a3` | ok |
| `scripts/hooks/skill-load-guard.py` | 19380 | `682019c18af80991b14c3f3505ecab26` | ok |
| `scripts/hooks/stop-dialog-guard.py` | 40247 | `4e5f9889717d4ed37a6384c643b4d05e` | ok |
| `scripts/hooks/wb-result-hook.py` | 65810 | `94bc3c781b43be6c2a4da645fe008639` | ok |
| `scripts/init_workspace.py` | 8840 | `0581b3864d35461f2fd8f76ea4c66d4d` | ok |
| `scripts/lock/handoff-guard.sh` | 34616 | `f38b2ec92fdc1dab9abdaca0e0bb001a` | ok |
| `scripts/lock/handoff-status.py` | 3923 | `3edb20f9c4021ebe2323478860bf8f6f` | ok |
| `scripts/lock/op-lock.sh` | 4690 | `1a31eda3642e23693a65e1519860c744` | ok |
| `scripts/lock/preflight-lock.sh` | 8673 | `e985ec1cb853fae4c351cd03b171355d` | ok |
| `scripts/selftest.py` | 281959 | `f68dcb48173f2321527d54bc30703bc5` | ok |
| `scripts/session-rules-check.py` | 28650 | `484b4e7370ff7f8377e725471bdfd88a` | ok |
| `scripts/stop-collab.py` | 10462 | `d8043bedd23b52b6642aaf7ec8b8c980` | ok |
| `scripts/wake-session.py` | 11011 | `32c6038b541249730f322f933ee287c1` | ok |
File diff suppressed because it is too large. Load diff
+78
View File
@@ -0,0 +1,78 @@
# 会话机制 · 规矩与判据(照它做,⛔ 别自己另发明)
> ⚠️ 本文件是**作业规矩**;机制全貌 ⇒ `references/architecture.md`;坑 ⇒ `references/pitfalls.md`。
> 🔴 与项目层 `CODEBUDDY.md §1` 冲突时**以项目层为准**(那里有裁决顺序)。
## 1 提问判据(唯一一条)
> **只问「超过现有判断方法边界」的问题。**
- **边界内 ⇒ 自决策,不要问**:技术选型 / 实现路径 / 命名与数据结构 / 调参 / 部署与同步 / 排查方法 / 版本依赖 / 兼容降级 / 方案取舍 / 文档技术内容。 **「部署上线」属此项 ⇒ 做完即上线,不要问**;生产变更**直接做**,只需**动手前一句话说明**。
- **边界外 ⇒ 必须问**:① 业务目标与优先级 ② 花钱与资源承诺 ③ 对外承诺 ④ 需用户提供的凭据或审批 ⑤ 无客观优劣的偏好 ⑥ 影响面超出本平台 ⑦ 红线门禁 ⑧ 判不准。
- **判据**:有没有**客观可判的优劣**?有 ⇒ 自决策;没有 ⇒ 问。
- **提报用户标准 = 存在真取舍**:候选**只有优点或只有缺点 ⇒ 自己拍掉**;各有优劣才提报用户,且**逐项写优点 + 缺点**。
- ⛔ **不许捆包**:要问红线**只问那一句**;技术方案自己定好、当**已定项**陈述。
- ⛔ **禁用征询句收尾**(「要我…吗 / 请确认 / 你看怎么办」)⇒ 按三问重判,没命中就**删掉、自己做完**。
- 🔴 **提报用户内容必须自包含**:① 一句话说清要决定什么(⛔ 不用指代)② 为什么要你定(影响谁 / 断多久 / 花多少钱)③ 每候选写优点 + 缺点,末行给倾向 ④ 一轮一问 ⑤ ⛔ 不出现包名 / 路径 / 变量名。
## 2 回复排版(🔴 用户 2026-10-01 定稿,照抄即可)
- **骨架**:`# 大类`(**已完成** / **待处理任务**)→ `## 任务名` → 每件事两段:**当前状态** + **待处理事项**。
- 🔴 **大类标题必须比任务名大一号**(大类用 `#`、任务名用 `##`;⛔ 不许同号,否则层级压平)。
- **当前状态**:每条一个**圆点**,**一句陈述句说重点**;复杂情况放**句末圆括号**。
- **待处理事项**:用**序号**(`1、2、`,⛔ 不是 `1.`);每条写完整句子。
- **附件**:写在该板块**最末**一行(`附件:<路径>`)。
- **大类顺序**:**已完成的放最前**,`待处理任务`放**最后**。
- **三禁**:⛔ 表格(=省略讲理)|⛔ 长散文(不是写小说)|⛔ 碎标签堆叠。
- ⚠️ 本条只管**给人读的回复**;注释 / 日志 / 解析字段不受限。
## 3 锁
- **开工先抢锁**(是"抢"不是"看");**抢不到 ⇒ 停手 + 报告**(红线 R9:⛔ 不删锁、⛔ 不接管)。
- 🔴 **默认一律带域**(`--domains <本工作区域>/`);⛔ 别省略 —— 省略=**全局独占**,别人连域锁都抢不了。
- 🔴 **只有真正"全平台共用"的改动**(配置 / 加解密 / 隔离 / 锁与钩子**本身**、技能文件)才用**独占**。
- 🔴 **释放必须反序且带会话名**:`--release` → `--release-exec "<会话名>"`;⛔ 不带名 ⇒ 拒释放。
- 🔴 **锁的生命周期 = 任务的生命周期**:⛔ 禁抢锁做一半、不解锁就结束回合(带锁结束=把所有人挡在门外);中途要停 ⇒ 先释放。
- 🔴 **域键算法 shell/hook 必须逐字一致**,否则**静默失效**。
## 4 日志闸(会话"活多久"的物理边界)
- 本工作区闸值:文件**软 5 MiB / 硬 8 MiB**;工具调用**软 200 / 硬 250**。命中 ⇒ 开接续会话。
- 🔴 **宿主侧还有一个 10 MiB 硬上限**:撞顶即**拒写** ⇒ **界面静默哑掉、用户零感知**,而投递方仍记 `ok:true`(**假绿**)。
- 🔴 **病根是"调用次数多",不是"每次调用贵"**;增长只与"工具在跑"成正比(实测:活跃 ≈630 KB/分 ↔ 空闲 34 分钟 0 行)。
- **压增长三条硬纪律**:① 大输出先落盘、只读关键行 ② 命令层限流(`| head -30` / `grep -c` 代替裸 `grep`)③ 让脚本内部聚合、只 print 摘要。⛔ 禁 `cat` 大文件、无 `head` 的 `grep -r`。
- ⛔ **不要在会话里起常驻后台长跑任务**:它每次输出都把会话反复唤醒 ⇒ 永远回不到 idle ⇒ 用户看到"卡死"(已复现多次)。✅ 正确起法=**会话后台任务 + stdout 全重定向到文件**;或干脆做成**独立进程**(见 §6)。
## 5 钩子与脚本的写法约束
- 🔴 **hook 脚本必须走 `sys.stdout.buffer.write(bytes)`** 输出 —— 否则文案含非 ASCII 符号时抛 `UnicodeEncodeError` ⇒ stdout 为空 ⇒ **静默放行**。
- 🔴 **不要按位置推导根目录**:钩子脚本被搬一次就会**静默指错**(历史事故:台账写到别处、测试却全绿)⇒ 用外置的 `roots.env`(本包由 `install.py` 生成)。
- ⚠️ `settings.json` 的 hook 条目**没有 `env` 字段** ⇒ ⛔ 别指望用 env 给钩子传参。
- 🔴 **作用域⛔ 不许静默排除**:被跳过必须**留下痕迹**(写跳过清单 + stderr),否则"没动作"与"坏了"长得一模一样。
- ⚠️ **钩子脚本内容改动即时生效**;但 `settings.json` 的 hooks 条目是**应用启动时快照** ⇒ 改条目要**完全重启**(关窗 ≠ 退出)。
## 6 常驻的精确边界
- 🔴 **「协作与投递一直运行(常驻)」是已定案项**(09-29 定案,2026-10-01 用户再确认)⇒ ⛔ **不得拿"进程数=0"去否它**。
- 🔴 **⛔ 禁的是"在会干活的会话里起"**:会话自己的后台任务会**压制该会话的 idle 钩子**(实测被僵尸任务压 **6h20m**)
⇒ 载体应用**专用容器会话**;且**输出必须完全静默**(`stdout` 全重定向到文件,⛔ 否则反复唤醒宿主 ⇒ 看着像卡死)。
- ✅ **独立进程不在禁令内**(输出不接回任何会话 ⇒ 不唤醒宿主);但它**读不到网关口令 ⇒ 不能投递**,只能做只读判定 / 告警。
- **存活边界一句话**:「**后台任务跟着应用活,不跟着会话活**」⇒ 要"关了应用也还在"必须做成独立进程(⚠️ 但那就没口令了)。
## 7 其他硬约束
- 🔴 **技术讨论不谈法规**:⛔ 不引条文当论据、⛔ 不主动提合规、⛔ 不前置提报用户;只在你问起、或对象就是"对外承诺 / 资质 / 合同"时才谈。
- 🔴 **红线 R5 / R7 / R9 / R10 永远硬约束**(内容见项目层 `CODEBUDDY.md §3` 红线全表)。
- 🎯 **要解决问题,不将就妥协**:降级 / 延期 / 静默兜底**都不算解决**;🟢 **只做正向迭代**。
- 🔴 **接手前人结论先做最小取证**,⛔ 别拿旧结论当既成事实;**取现状一律按 mtime 取最新那份**。
## 8 🔴 验证通过的机制 ⇒ **必须写进技能**(2026-10-05 用户明令)
> 用户原话:「**这个也是会话规则,验证通过的机制要写进技能中**」。
- 🔴 **一条机制实测跑通之后,落点只能是技能**(`SKILL.md` 或 `references/`),⛔ **不许只留在工作区日志或当次对话里**。
- **判据**:换一个新会话、**不读任何历史**,只看技能能不能照着做出来。做不到 ⇒ 等于没立。
- **⚠️ 写的位置决定会不会被读到**:`SKILL.md` 第一屏/「必读三篇」/索引表 ⇒ **每次都会读到**;长文档正文底部 ⇒ 基本没人会翻到(等于没写)。
- ⛔ **反例(10-05,同一件事栽到第四次)**:常驻的起法只写在当天日志里 ⇒ 之后每一轮都重新折腾一遍、每一轮都得出同样的结论。
✅ 正解:结论**同时**落三处 —— `SKILL.md` 第一屏(会被读)+ `pitfalls.md` 一条(有来龙去脉)+ 本档一条规矩(约束以后怎么写)。
- 🔴 **配套**:凡"换会话容易忘、忘了就会重来"的结论,**优先放第一屏**,⛔ 不要塞进长文档。
@@ -0,0 +1,399 @@
# 常驻程序长期在线(Windows)
> 📌 **本档=「想让目标检查程序长期在线」时的可选做法**(2026-10-03 夜里整理)。
> 🔴 **注意:常态用不着它** —— 常态是「调用技能完成目标时起后台任务 + 检查程序」,
> 那套一直好好在跑(见下面那段口径)。
> 证据源=测试3 的复盘 `目标-会话协作测试3-7cd276/S3_常驻启动成功复盘_20261003.md`
> + `S4_常驻自愈闭环复盘_20261003.md` + `S5_A6自指修复与目标收口_20261004.md`。
> 🔴🔴 **口径(2026-10-04 用户定案,当场订正)**
>
> 用户原话:「**从来没说过什么开机自启,只有调用技能完成目标时启动 后台任务和检查程序**」。
>
> ⇒ **常态就是:调用技能完成目标时,起后台任务 + 检查程序。** 这套**一直好好在跑**
> (实测本区 pid 19424 从 10-03 17:05 活到现在 **24.7 h**)。
> ⇒ ⛔ **「开机自启 / 计划任务 / 载体脚本」不是需求,是本文件后半段自己推演出来的可选项**。
> ⛔ **不许**把它当"欠项 / 待办 / 风险"往用户面前摊(本轮就是这么摊错的)。
>
> 📌 **本文档怎么读**:
> · 只想**把技能用起来** ⇒ **读到「〇」就够了**,后面的装法与本场景无关。
> · 只在**明确要"目标做完了程序还得继续跑"**时 ⇒ 才看后面那几节。
## 〇、🔴🔴 先分场景:两种"长期"不是一回事
**两种都要长期在线,但它们要的东西不同** —— ⛔ 别混用,混用就会出现
"看起来在跑、其实随时会死"或"该停时还在跑"。
| | **场景 A · 用户手动创建主会话** | **场景 B · 定时任务创建主会话** |
|---|---|---|
| 典型形态 | 用户在 WorkBuddy 里自己开一条 `[主]-…` 会话,盯着它推进 | 排期按点拉起 `[主]-…`(`recurring`,如 `FREQ=HOURLY;INTERVAL=1`) |
| 会话寿命 | **随用户何时收工**(可能几分钟,也可能几小时) | 跑完即 `completed`,**与下一跳之间有静默窗** |
| 🔴 **对"常驻"的硬要求** | ⛔ **不能靠会话活着** —— 一收工就没人拉 ⇒ **必须脱离会话** | ✅ 会话自己周期性回来 ⇒ 但**每跳之间仍有空窗** ⇒ **同样要脱离会话** |
| 唯一合格载体 | **计划任务 + 守护循环**(本文档第三~五节) | 同左(⛔ **排期代替不了载体** —— 排期只是"谁按点喊一次",喊完会话就结束) |
| 怎么验它真活着 | 心跳 `pid` 活 ∧ `ts` 距今 < 90 s + `round` 递增 | 同左,⚠️ **且要静置 ≥ 12 分钟**(⛔ 短观察证明不了事) |
| 谁负责"目标完成后关它" | 守护循环见 `guard.stop` 即 `exit 0`(第五节之二) | 同左 —— ⛔ **排期不会帮你关** |
### 🔴 两个都踩过的坑(**与场景无关**,因为根子在载体)
1. **"一次性工具调用起的子进程"在两种场景下都不合格** ——
实测测试3 开工时 pid 69080 已死;本区两个后台任务(`collabd --supervise` 活 842 分钟、
`board.py --serve` 活 634 分钟)父链都是
`bash → bash → bash → sandbox-cli → WorkBuddy.exe`(**会话树内**),
⛔ `.workbuddy/collab/` 里**既无 `.ps1` 也无 keeper** ⇒ **没走本文档这套**。
⚠️ **它们现在活着,只因为发起它们的那条会话进程树至今没被回收**
⇒ 「**存活时长 = 发起会话的存活时长**」(测试3 `S4` 第六节原话),
**⛔ 不能用它论证"这种起法也能长期"**(该文明确写「这条『反例』**不成立**」)。
2. **"排期是 `once` 且已过期"=哑排期** —— 实测 `[主]-会话协作测试3-主会话` 是
`schedule_type=once`/`scheduled_at=2026-10-03T20:33`/`next_run_at=None`/
`last_run_at=None` ⇒ **它一次都没被触发过**,而界面上 `status=ACTIVE` **看起来像在跑**。
⚠️ **`status=ACTIVE` ≠ "在跑"**;判"真会触发"要看 `schedule_type='recurring'` ∧ `next_run_at` 非空。
## 一、为什么必须换载体(⛔ 别再自己发明)
> 🔴 **「载体」= 那个"负责把常驻程序拉起来、并盯着它别死"的启动脚本。**
> 说白了就一件事:**谁来开机就把它叫起来。**
> 本文档里它指两样:① 批处理 `start-supervise.ps1` ② 把它挂上开机自启的**计划任务**。
> (⚠️ 这个词是内部简写,不在别处用;本节表格用列名「**启动方式**」更直白。)
| 启动方式 | 能不能长期 | 实证 |
|---|---|---|
| 工具调用起的子进程 | ❌ | 父进程一退出就被回收(活不过当轮/当会话) |
| 宿主排期拉的会话起 | ❌ | 测试3 开工时 pid 69080 已死(心跳停在 22:55:31) |
| `CREATE_BREAKAWAY_FROM_JOB` | ❌ | **`PermissionError(13,'拒绝访问。')`** —— 本机被作业对象拒绝,**这条路封死** |
| `cmd /c start /B` | ❌ | 本机沙箱拦截 |
| 排期到点起一次会话 | ❌ | **跑完即退** ⇒ 静默窗内无人(`once` 更糟:可能**永不触发**) |
| **计划任务 + 守护循环** | ✅ | **实测 23:16:37 起活到 23:29 仍在跑**,`round` 1→74;死亡后 **5–6 秒**自动恢复 |
⚠️ **本表判据是"父链根在谁",⛔ 不是 `in_job` 标志**(见下方 2026-10-04 深夜实测更正:
本机**所有**被起的进程 `in_job` 都是 `Y`,含计划任务起的那个 ⇒ 标志位判不出死活)。
**关键差别=父链根在谁**:计划任务创建的进程**根在 `svchost -s Schedule`(任务计划服务)**
⇒ 与 WorkBuddy 会话树**无关** ⇒ 不会被会话回收。
> 🔴🔴 **2026-10-04 深夜实测更正:⛔ 别再用 `IsProcessInJob` 判死活 —— 本机它对所有进程都返回 `Y`。**
>
> 起因:vibe 会话用 `proc_chain.py` 看到常驻 `in_job=True` ⇒ 判"它在会话的作业对象里、所以会被回收"。
> **结论(「会话树内的会死」)是对的,但引用的判据是错的** —— 我做了三组对照:
>
> | 进程 | `in_job` | 实际命运 |
> |---|---|---|
> | 工具调用直接起(`DETACHED_PROCESS`) | **Y** | 会话边界即死 |
> | **计划任务起**(`svchost→cmd→pythonw`) | **Y** | ✅ **长期存活**(看板实测 634+ 分钟) |
> | 零创建 flags(仅 `STARTUPINFO`) | **Y** | 死 |
> | `svchost -s Schedule`(服务本体) | N | — |
> | 系统进程(System / explorer) | N | — |
>
> ⇒ 🔴 **`in_job=Y` 在本机是"全局容器",⛔ 不区分"会话专属"与"服务创建"** ⇒ **标志位判不出死活**。
> ⭐ 这与 `workbuddy-resident-service/scripts/job_lifetime_probe.py` 开头那句自述完全一致:
> 「**让行为说话,而不是只看 `IsProcessInJob` 的标志位**」。
>
> ✅ **正解判据=看父链里有没有 `svchost -s Schedule`**:
> 有 ⇒ 服务创建 ⇒ 长活;根在会话树(`WorkBuddy.exe`/`bash`/已退出的会话进程)⇒ 会话收回时一起死。
> 实测:看板 62992 父链 `pythonw→cmd.exe→svchost(3924)→services.exe→wininit.exe` ⇒ 活;
> 常驻 63628 父链 `pythonw→71800(已退出)→父已退出` ⇒ 会话回收即死。
> 🔴🔴 **2026-10-04 补一条实测更正(⛔ 我自己违反过,判据已落 `t_supervise_lifespan_not_invented`)**
> ⛔ **别把「现在活着」当成"这种起法也能长期"**。实测:pid 19424 从 10-03 17:05 活到 10-04 17:15
> = **24.2 h / 8695 轮**,而它的**父进程早已退出** ⇒ 它是**孤儿进程**,孤儿化后**不受会话结束影响**。
> ⇒ ⚠️ **「会话/工具调用起的活一定活不过当轮」是错的说法**:能不能活取决于**父链还在不在**,
> ⛔ **不许凭这个推断编造后果**。要判"还能活多久"=**先验父链 + 读心跳 `started_ts`**。
> ✅ 但**开机自启这一层仍然缺**:本机 5 个区都**没有** `start-supervise.ps1`,
> 全机只有 1 条计划任务(`collabd-supervise-ws3`)而它指向**不存在的文件** ⇒ 每次触发都失败。
## 二、装法(照抄,⛔ 别自创参数)
### ① 包装脚本 `<工作区>\.workbuddy\collab\start-supervise.ps1`
⚠️ **必须是「永不返回的守护循环」版**。⛔ **别写成"跑一次就退"** —— 那样常驻死掉时脚本
**正常返回 0** ⇒ 任务被判「成功」⇒ `Restart*` 永不触发(测试3 实测:活 12 分钟后再不复活)。
完整可用版见第五节(含 `-Wait`/心跳判据/退避/**停止标志**四项要点)。
```powershell
$ErrorActionPreference = "Continue"
# 🔴 坑①:必须设,否则 _wb_db() 退回读 0 字节空库 ⇒ 闸二永久失效
$env:CODEBUDDY_CONFIG_DIR = "E:\ProgramData\.workbuddy"
$env:COLLABD_CONFIG = "<工作区>\.workbuddy\collab\collabd.config.json" # 🔴 坑②
$env:PYTHONIOENCODING = "utf-8" # 中文日志不乱码
$env:PYTHONUNBUFFERED = "1" # ⛔ 否则重定向到文件时 Python 块缓冲、诊断不可见
$root = "<工作区>"; Set-Location $root
$hbPath = "<工作区>\.workbuddy\collab\logs\supervise-heartbeat.json"
$stopFlag = "<工作区>\tmp\supervise-inbox\guard.stop" # 🔴 目标完成信号(第五节之二)
$logPath = "<工作区>\tmp\keeper.log"
$staleSec = 90 # 与 collabd.supervise_alive() 同口径
function Say([string]$m) { Add-Content -LiteralPath $logPath `
-Value ("[" + (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") + "] " + $m) -Encoding UTF8 }
function Get-HbAge { try { $o = (Get-Content -LiteralPath $hbPath -Raw -Encoding UTF8) | ConvertFrom-Json
$ts = [double]$o.ts; if ($ts -le 0) { return -1 }; return [int]((Get-Date).ToString("U") - $ts)
} catch { return -1 } }
$backoff = 5
while ($true) {
if (Test-Path -LiteralPath $stopFlag) { Say "guard.stop present => stands down"; exit 0 } # 🔴
$age = Get-HbAge
if ($age -ge 0 -and $age -lt $staleSec) { Start-Sleep -Seconds 5; continue } # 别人在跑 ⇒ 让位
Say ("spawn collabd --supervise (backoff=" + $backoff + "s)")
# 🔴 -Wait 必须:& 对 pythonw.exe(GUI 子系统)**不阻塞** ⇒ 会叠出多个常驻
Start-Process -FilePath "<pythonw.exe>" `
-ArgumentList @("-u", "<工作区>\.workbuddy\collab\collabd.py", "--supervise") `
-WorkingDirectory $root -WindowStyle Hidden `
-RedirectStandardOutput "<工作区>\.workbuddy\collab\logs\supervise.out.log" `
-RedirectStandardError "<工作区>\.workbuddy\collab\logs\supervise.err.log" `
-PassThru -Wait | Out-Null
Say ("collabd exited (heartbeat age=" + (Get-HbAge) + "s)")
$w = $backoff
while ($w -gt 0) { if (Test-Path -LiteralPath $stopFlag) { Say "stop flag during backoff"; exit 0 }
Start-Sleep -Seconds 5; $w -= 5 }
if ($backoff -lt 60) { $backoff = [Math]::Min(60, $backoff * 2) }
}
```
⚠️ **落盘必须带 BOM**(坑③):`[System.IO.File]::WriteAllText($p,$text,[System.Text.UTF8Encoding]::new($true))`。
### ② 建任务(⚠️ 一律 PowerShell 的 `ScheduledTasks` 模块)
⛔ **`schtasks.exe` 在本机沙箱被黑名单拦截**(Security Center → Command Security)
⇒ **建/查/改/停计划任务全部走 `Get-/New-/Set-/Start-ScheduledTask` + `Out-File` 落盘再读**
(⛔ 别指望命令回显)。
```powershell
$a = New-ScheduledTaskAction -Execute "C:\windows\System32\WindowsPowerShell\v1.0\powershell.exe" `
-Argument '-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "<工作区>\.workbuddy\collab\start-supervise.ps1"'
$t = New-ScheduledTaskTrigger -AtLogOn -User "Administrator"
# 🔴 三个设置缺一不可(少一个就前功尽弃):
$s = New-ScheduledTaskSettingsSet -ExecutionTimeLimit ([TimeSpan]::Zero) `
-MultipleInstances IgnoreNew -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1)
New-ScheduledTask -TaskName "collabd-supervise-<区名>" -Action $a -Trigger $t -Settings $s -Principal $p
# 改动作不影响已配的触发器 ⇒ 更新时只传 -Action
Set-ScheduledTask -TaskName "..." -Action $a
```
| 设置 | 值 | 漏了会怎样 |
|---|---|---|
| `ExecutionTimeLimit` | `0`(无时限) | 默认 72 h ⇒ 到点被杀 |
| `MultipleInstances` | `IgnoreNew` | 重复起 ⇒ 双写台账 |
| `RestartCount`/`RestartInterval` | `999`/`1 min` | 死了不重拉 |
| Trigger | `AtLogOn`(Interactive・最高权限) | 开机不启动 |
⚠️ **动作一律用 `powershell.exe`(不是 `pythonw.exe`)** —— 动作里直接跑解释器没有"设环境变量"这一步。
(用户长期记忆里那条"`python.exe` 会闪黑窗"针对的是**交互式起进程**;计划任务走 `Hidden`+无控制台,不闪。)
## 三、🔴 三个坑(都可复现,逐个都是"秒退"或"看着在其实没跑")
### 坑① 🔴 不设 `CODEBUDDY_CONFIG_DIR` ⇒ 读到 **0 字节空库**
- 计划任务**不继承**会话的环境变量 ⇒ `_wb_db()` 退回 `Path.home()/.workbuddy/workbuddy.db`
(本机那个是 **0 字节、无 `sessions` 表**)。
- 症状:每 20 秒刷一次 `all_sessions_idle 读库失败 no such table: sessions`
⇒ 闸二(所有会话都结束)**永久失效** ⇒ fail-safe 判"有会话在跑" ⇒ **检查会话永远建不出来**。
- ✅ **正解=用机制自带的配置项 `host_db`**(`collabd.py` 里它本就**优先于**环境变量),
指向真库 `E:/ProgramData/.workbuddy/workbuddy.db`。
⇒ **这比设环境变量更可靠**(配置跟着工作区走,不依赖谁起的它)。
### 坑② 🔴 配置查找靠 `cwd` ⇒ 传错 cwd 当场拒跑
`_cfg_candidates()` = `COLLABD_CONFIG` → `<cwd>/.workbuddy/collab/collabd.config.json`。
⛔ **不要把 `cwd` 设成 `collab/` 目录** —— 那会拼出多一级 `.workbuddy` ⇒ 当场拒跑。
✅ 设成**工作区根** + 同时显式给 `COLLABD_CONFIG`(双保险)。
(`ensure_supervise()` 内部原先传 `cwd=脚本所在目录` —— 已由测试3 主会话修成 `cwd=str(WS)`。)
### 坑③ 🔴 PowerShell 5.1 读**无 BOM** 的 UTF-8 `.ps1` ⇒ 中文路径全毁 ⇒ **秒退**
- 症状:计划任务 `LastTaskResult=1` **秒退**,心跳毫无动静。
- 定位:`Get-Content -Encoding Byte -TotalCount 3` ⇒ `36 69 114`(ASCII 的 `$Er`)⇒ **无 BOM**。
PowerShell 5.1 对无 BOM 文件按 **ANSI(GBK)** 解码 ⇒ 脚本里 `会话协作测试3` 变乱码 ⇒ 路径不存在。
- ✅ 落盘**必须带 BOM**:
`[System.IO.File]::WriteAllText($p,$text,[System.Text.UTF8Encoding]::new($true))`(`$true` = 带 BOM)。
- 📌 **本机所有含中文路径的 `.ps1` 一律带 BOM**,否则一律秒退。
- ⚠️ 判据:**`LastTaskResult`** —— `0` = 成功;`1` = 秒退(八成是 BOM)。
## 四、验收(三样都齐才算成,⛔ 打印"已启动"不算)
```powershell
Get-ScheduledTask -TaskName "collabd-supervise-<区名>" | Select-Object TaskName,State
Get-ScheduledTaskInfo -TaskName "collabd-supervise-<区名>" | Select-Object LastRunTime,LastTaskResult
```
🔴 `LastTaskResult` **必须 0**。
**存活唯一机读判据**(=机制里那条,⛔ 别用"日志在更新"代替):
`.workbuddy/collab/logs/supervise-heartbeat.json` 里 **`pid` 活着 ∧ `ts` 距今 < 90 秒**。
```powershell
# pid 是否真在进程表(⛔ tasklist 走 bash 会被当路径;用 PowerShell)
$p = Get-Process -Id <pid> -ErrorAction SilentlyContinue # $null = 已死
```
**静置观察**(≥ 4 分钟):`round` 持续递增、`argv0` 逐字等于本区 `collabd.py`、
`已有活的常驻 ⇒ 本实例退出(幂等)` 出现一次(证明没双写)。
## 五、🔴 载体是**一个**永不返回的守护循环(⛔ 不是"两层")
⚠️ **旧版这里写的是"必须两层=计划任务 + 周期性复活排期",🔴 已被实测推翻** ——
真正常态兜底**就在这个计划的守护循环里**,⛔ 不需要另一条排期。
**实测 2026-10-03**:单纯"计划任务跑一次脚本"**救不了"被外部收走"** ——
测试3 常驻 `23:16:37` 起、`23:28:47` 停(活 12 分钟),而计划任务
`LastRunTime` 仍 `23:17:03`、`LastTaskResult=0`、`State=Ready` ⇒ **它没重拉**。
**根因**:`RestartCount/RestartInterval` **只在本任务自己非零退出时生效**。
而脚本里若是**前台阻塞**调用常驻,常驻死掉 ⇒ 脚本**正常返回 0** ⇒
任务调度器判定「成功」⇒ **`Restart*` 永不触发**(测试3 复盘原话:「自愈机制被**成功退出**这三个字废掉了」)。
⇒ ✅ **正解=守护循环版脚本**(⛔ 别用"跑一次就退"):
```
while ($true) {
if (Test-Path $stopFlag) { Say "…stands down"; exit 0 } # 🔴 见第五节之二
if (心跳新鲜(<90s)) { 让位等待; continue } # 活性判据=心跳,⛔ 不是退出码
Start-Process -Wait -PassThru pythonw collabd.py --supervise # 🔴 -Wait 必须
# 退避 5→10→20→40→60s 封顶;每轮开头都再查一次 $stopFlag
}
```
| 要点 | 为什么 |
|---|---|
| `while ($true)` 包住 | 任务永远 `Running` ⇒ 调度器**不介入**、自己管复活 |
| 🔴 **`Start-Process -Wait`** | ⛔ **`&` 对 `pythonw.exe`(GUI 子系统)不阻塞**、立即返回 ⇒ 循环误判"刚起的已退出"⇒ **叠出多个常驻**(测试3 实测 5/10/20/40s 连续重拉) |
| 活性判据=**心跳新鲜度**(`<90s`),⛔ 不是退出码 | `collabd` 发现已有活常驻会**幂等 exit 0** ⇒ 按退出码重拉会造出风暴 |
| 退避封顶 60s | 真崩时别刷屏 |
| `.NET Process` + `add_OutputDataReceived` | ⚠️ PowerShell 5.1 **无消息泵 ⇒ 回调不触发**、日志被静默吞掉 ⇒ 用 `Start-Process -RedirectStandardOutput` |
**实测效果**:23:40 第一次死亡 **5 秒恢复**;23:47 单变量 kill 演练 **6 秒恢复**、新 pid、`pythonw` 实例数 `count=1`(无叠加)。
### 之二 🔴🔴 守护循环**必须看停止标志**(2026-10-04 补,本文档初版漏了)
**症状**:目标已完成(`--set-life 已完成` ⇒ `supervise_stop()` 写下 `guard.stop`、常驻已优雅退出),
**但守护循环仍在每 60 秒重拉一次、每次秒退** ⇒ 实测**空转 370 次/6 小时**
(`keeper.log` 连续 `exited (exit=0, heartbeat unreadable)`)。
**根因**:守卫只问「心跳新鲜吗」,**⛔ 不问「目标还活着吗」** ⇒
**"完成即收工"只做到"停掉常驻",没做到"别再拉它"** —— 那是同一件事的两半。
✅ **正解=循环开头(+退避等待中)都先查停止标志**,在则**退出循环**:
```powershell
$stopFlag = "<工作区>\tmp\supervise-inbox\guard.stop"
while ($true) {
if (Test-Path -LiteralPath $stopFlag) { Say "guard.stop present => stands down"; exit 0 }
... # 起常驻
$waitLeft = $backoff
while ($waitLeft -gt 0) { # 🔴 退避期间也可能出现标志
if (Test-Path -LiteralPath $stopFlag) { Say "…stands down early"; exit 0 }
Start-Sleep -Seconds 5; $waitLeft -= 5
}
}
```
📌 退出前**必须 `Say` 一行**(⛔ 否则又变成"静默消失",同族红线:读到了就要说)。
**实测(2026-10-04 07:03,单变量:只换脚本、没动任务)**:
新 keeper 起来 **0 秒**识别标志 ⇒ `keeper stands down` ⇒ 任务 `State=Ready`(⛔ 不是 `Running`)
⇒ 重拉次数 **372 → 372**(一没涨)⇒ **空转彻底止住**。
⚠️ 验「是否被回收」必须**只做单一变量**(⛔ 不许同时停/起任务)—— 否则因果会搞错。
⚠️ **验收要静置 ≥ 12 分钟**(⛔ 短于 10 分钟证明不了任何事 —— 常驻就死在第 12 分钟)。
## 六、⚠️ 还没有它就不算长期(两条,别自欺)
1. **静默基准**:检查会话要 `静默 ≥ 20 分钟` 才建,而基准取台账 `tasks.json` 的 mtime
⇒ **新建/改动任何排期都会把计时归零** ⇒ 刚派完棒必然等满 20 分钟(实测 0.2/4.3/…/12.6 分钟,**从未越过 20**)。
2. **闸二会自锁(正确行为,不是 bug)**:`_all_sessions_idle()` 要求"所有会话都结束",
而**发起这一切的那条会话自己也在里面** ⇒ 只要它活着,检查会话就建不出来。
⇒ **要让检查会话跑起来,必须先结束发起会话**(fail-safe 方向=宁可不建,不误建)。
## 七、还有一条(已由机制收口,⛔ 别再自己写)
- **目标完成 ⇒ 关后台+常驻程序**:`collabd.py --set-life 已完成` 现在**真的会停常驻**
(`supervise_stop()`:先写 `guard.stop` 走优雅退出 → 等 45 s → 不死才 `taskkill /T /F`)。
⛔ **受阻 / 已暂停不关**(口径如此)。
- **"完成"可逆**:`ensure_supervise()` 续命前**清掉 `guard.stop`** ⇒ 新目标/目标调整后能再起来。
⛔ 不清就永远起不来。
- **跨区自愈**:本区常驻每 6 轮顺带扫一遍 `peer_workspaces`,**只补"目标进行中"的区**
(已完成/受阻/有停止标志的 ⛔ 一律不拉)⇒ 但**计划任务是更靠前的第一道**。
## 八、按场景对照(选哪条 / 怎么验 / 常见坑)
| 场景 | 主会话怎么来 | 常驻载体 | 目标完成后谁关 | 常见坑 |
|---|---|---|---|---|
| **A · 用户手动创建主会话** | 用户自己开 `[主]-…`,盯着推进 | 计划任务 + 守护循环 | keeper 见 `guard.stop` → `exit 0` | 🔴 用户一收工就没人拉 ⇒ 若没装载体,常驻会在会话结束时**静默死掉**(看板还显示"在线") |
| **B · 定时任务创建主会话** | 排期 `recurring` 到点拉 `[主]-…` | 同上(⛔ **排期代替不了载体**) | 同上 | 🔴 **`once` 排期过期即哑**(`next_run_at=None`、界面仍显示 ACTIVE)⇒ **静默窗内无人**;🔴 排期跑完即 `completed`,**下一跳之前是空窗** |
**共同判据(两种场景都一样,⛔ 每次改完都验)**:
1. `LastTaskResult` = `0`(`1` ⇒ 秒退,八成是 BOM);
2. 任务 `State` = **`Running`**(⛔ `Ready` 只说明"此刻没在跑",**不代表机制坏**,
但要配合 ③ 才算稳);
3. 心跳 `pid` 活 ∧ `ts` 距今 **< 90 s** ∧ `round` 递增;
4. **静置 ≥ 12 分钟**再看 ③(⛔ 短观察证明不了事 —— 常驻常死在第 12 分钟);
5. 目标完成后:keeper 日志出现 `stands down` + **重拉计数不再增长**(验"真的停了",
⛔ 不许只看"心跳停了"—— 那可能是死了而不是收工)。
## 九、四个区各自的任务名(⛔ 名字别撞)
`collabd-supervise-<工作区名>`,例:`collabd-supervise-ws3`。
⚠️ 一个区一个任务;⛔ 多个区**共用一个**任务名会被 `IgnoreNew` 挡掉第二个。
## 十、🔴🔴 「各区都能常驻」怎么一次做齐(2026-10-04 用户点问)
> 用户原话:「看看是否有**各工作区都能启动常驻程序**的最好办法,
> 而不是现在这种**只能某个工作区才能常驻**的办法」
✅ **答案:能,而且就是第八节那套 —— 每区各建一条任务,同一条命令换个区名即可。**
⛔ **不存在"一个载体管所有区"的写法**,⛔ 也**不需要**:任务天然可以并存,
`IgnoreNew` 只在**同一个任务名**上生效(⛔ 撞名才挡)。
### 一区一条,四步(把 `<区>` 换成工作区绝对路径、`<名>` 换成区名)
1. **铺包装脚本** `<区>\.workbuddy\collab\start-supervise.ps1`
—— 用第二节①的守护循环版,**落盘必须带 BOM**(坑③,否则秒退)。
`collabd.config.json` 里补两项:`host_db`(指真库,⛔ 别依赖环境变量,见坑①)
+ 本区 `workspace`。
2. **建任务**:`collabd-supervise-<名>`,动作=`powershell.exe -File <该区 ps1>`,
触发器 `AtLogOn`,四个设置按第二节②的表配齐(`ExecutionTimeLimit=0`/
`MultipleInstances=IgnoreNew`/`RestartCount=999`+`RestartInterval=1min`)。
3. **立即 `Start-ScheduledTask`**(⛔ 别等下次登录)。
4. **验收**:`LastTaskResult=0` ∧ 心跳 `pid` 活 ∧ `ts<90s` ∧ `round` 递增 ⇒ **静置 ≥ 12 分钟**再看一次。
### 各区之间的隔离(为什么"能各建各的")
- **任务名不同** ⇒ `IgnoreNew` 互不影响;
- **`COLLABD_CONFIG` 指向各区自己的配置** ⇒ 各读各的 `goal.json`/台账;
- **跨区自愈是"顺带",⛔ 不是主靠**:本区常驻每 6 轮扫一遍 `peer_workspaces`,
只补"目标进行中"的区(已完成/受阻/有 `guard.stop` 的 ⛔ 一律不拉)。
⚠️ 所以**别把跨区自愈当载体的替代品** —— 它要求本区常驻先活着,是第二道。
### ⛔ 别犯的三个错
1. **⛔ 用一条任务带多个区**(一个动作只能跑一个区的脚本);
2. **⛔ 一区多条任务**(会双写台账);
3. **⛔ 以为"某区现在活着"就不用建任务** —— 那是孤儿化幸存,
父链一断就没了(第一节末尾那条实测更正)。
### 🔴🔴 两个"看着做好了、其实没生效"的坑(2026-10-04 实测踩到)
**坑 A:从副本发起时,找载体模板不能写死包根。**
`_escalate_to_keeper()` 原来写死 `Path(__file__).resolve().parent.parent / "assets"`:
- 技能目录里对(`<pkg>/scripts/collabd.py` ⇒ `.parent.parent` = `<pkg>`);
- **工作区副本里错**(`.workbuddy/collab/collabd.py` ⇒ `.parent.parent` = `<WS>/.workbuddy`
⇒ 去找 `<WS>/.workbuddy/assets/`,**不存在**)。
⇒ 症状**极具误导性**:返回值写着「⛔ 缺模板 assets/start-supervise.ps1.tpl」,
看着像仓库少了个文件,其实只是**算错了包根**;而模板好好躺在
`<WS>/.workbuddy/skills/session-mechanism/assets/`。
✅ 修法=`_find_keeper_tpl()` 认三处(按优先级):
① `<pkg>/assets/` ② `<WS>/.workbuddy/skills/session-mechanism/assets/`(**副本正解**)
③ `<CFGDIR>/skills/session-mechanism/assets/`(兜底)。
⚠️ **凡"从 `__file__` 往上推包根"的代码,副本换布局后都会错** —— 副本是
`<WS>/.workbuddy/collab/`(**不是** 包树里的 `scripts/`),别套用同一套相对层数。
**坑 B:重铺 `.ps1` 后,`Stop/Start-ScheduledTask` 不足以让它生效。**
keeper 是 `powershell.exe -File <ps1>`:PowerShell **启动时把脚本读进内存**,
之后磁盘上改了、任务重启了,**旧 keeper 进程可能仍在跑老脚本**。
实测:同时存在两个 keeper(57680 旧的 + 20180 新的),**旧的按老路径拉常驻**
⇒ 现象是"我明明改好了,起来还是老样子"。
✅ 做法:重铺后**显式杀 keeper 进程**(`taskkill /F /T /PID <keeper_pid>`)再 `Start-ScheduledTask`;
复核判据=**新常驻的父链根**(`svchost -s Schedule`)+ 心跳 `argv0` 指向**本区副本**。
**坑 C:改完副本必须重启常驻,判据是 `argv0` 而不是文件 md5。**
`deploy_code.py` 覆盖的只是**磁盘上的副本** —— 正在跑的进程仍持有旧代码
(P0-17 同族)。✅ 判据=心跳里的 `argv0` 是否指向本区副本路径,
⛔ 不是"`md5` 一致"(那只证明文件换了,⛔ 不证明进程换了)。
---
📌 **本文档的事实全部来自实测**(计划任务状态/`LastTaskResult=0`/心跳 13 分钟/BOM 字节
`239 187 191`/三条封死路的原始错误)。⛔ 任何"应该行得通但没实测"的写法都别往里加。
+76
View File
@@ -0,0 +1,76 @@
# 任务图规范(防干等的核心件)
> 任务图**细则档**:本档给**写法、算法、模板**。**架构层判据 ⇒ `references/architecture.md`**;原技能存档 ⇒ `references/collab-detail.md §3`。
## 1 为什么需要它
**没有显式依赖图 ⇒ 三个必然浪费**:
1. **该并行的没并行**(不知道谁不依赖谁)
2. **在非关键路径上花时间**(不知道谁决定总工期)
3. **有活没人干、或有人干着等前置**(看不出"可派未派"和"干等")
## 2 文件格式
`任务图.json`(路径在配置 `taskgraph` 指定):
```json
{
"goal": "一句话说清要达成什么",
"updated": "YYYY-MM-DDTHH:MM",
"rules": {
"dispatch": "只派依赖已满足的节点;优先关键路径;一条线只挂一个",
"domain": "域锁按节点涉及的文件/子目录声明;禁止整工作区粗域",
"handoff": "收口即派(+1~2 分钟)",
"waste": "可派集合非空 ∧ 无棒在跑 ⇒ 可派未派 ⇒ 立即派"
},
"critical_path": ["N5", "N8", "N9"],
"nodes": [
{ "id": "N1", "title": "做什么", "line": "line-a", "deps": [],
"status": "done", "evidence": "可核对的产物/读数(⛔ 不写自述)" },
{ "id": "N5", "title": "做什么", "line": "line-a", "deps": ["N1"],
"status": "blocked", "blocker": "卡在哪、缺什么" },
{ "id": "N8", "title": "做什么", "line": "line-b", "deps": ["N5"],
"status": "todo" }
]
}
```
**字段口径**:
| 字段 | 口径 |
|---|---|
| `status` | `todo` / `running` / `blocked`(依赖已满足但技术卡住) / `done` |
| `deps` | **只有前置 `done` 才解除阻塞**;`blocked` **不解除**(它本身要人干) |
| `evidence` | **必须可核对**(文件 / rc / 端口 / 数据对照)—— 这是"真成果"的判据 |
| `critical_path` | 从目标倒推的**最长依赖链**(决定总工期) |
## 3 程序怎么算(`collabd.py` 内 `taskgraph()`)
```
for 每个未 done 节点:
未满足的前置 = [d for d in deps if nodes[d].status != 'done']
未满足为空 ⇒ ready(可派)
未满足非空 ⇒ waiting(并记录"等谁")
waste = ready 非空 ∧ 没有任何棒在跑 ∧ 无锁 ← 🔴 可派未派=浪费
```
⇒ 输出进实时状态;`waste` 时写 `READY.md` 喊出来。
## 4 四条派活规则(写死在 `rules`,程序与人都照它)
1. **只派 `ready` 节点** ⇒ 能并行的**立刻并行**
2. **优先 `critical_path` 上的节点** ⇒ 非关键路径**押后不拖工期**
3. **一条线同时只挂一个**,**多条线可同时挂**
4. **收口即派(+3~4 分钟)** ⇒ ⛔ 不要 +5~8 分钟空窗(🔴 2026-10-01 用户口径:基准 = 收口 + **3~4 分钟**)
## 5 关键路径单线化 ⇒ 必须拆
若关键路径上**只有一个执行主体**,总工期 = 该主体的串行时间之和 ⇒ **必拆**:
- 把节点**拆成更小的可独立验证步骤**
- 把**不涉及该主体特有资源**的部分**并行出去**(给别的线)
- 例(真实):`N5 20090 通路常驻` 曾被拆成「① 能起 ② 能保持 ③ 掉线自愈」,其中"自愈策略"可交别的线并行
## 6 维护纪律
- **状态变了就改**(谁做的谁改;主会话判定后改)—— 任务图过期比没有更糟
- **`evidence` 只写可核对的东西**;⛔ 不写"已完成""跑通了"这类自述
- **节点粒度**:**一个节点 = 一个会话能在一轮里做完并给出读数**的事