Files
workbuddy_skills/session-mechanism/references/collab-detail.md
T

967 lines
96 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
> ## ⚠️ 本件=**原 `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 写作模式)+ 本包作业规矩 `references/作业规矩/04-去AI味与说话方式.md`。两处文案都按这两份过。
| ⛔ 别写 | ✅ 改成 | 实例 |
|---|---|---|
| **否定式排比**「不是 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 已加退役标注**,可作背景,⛔ 不作用判据。