Files
admin c1b5e4d966 chore(工作区): 全量入库 + 补齐 .gitignore(以工作区为准)
- 变更规模:新增 514 / 修改 62 / 重命名 155 / 删除 4(归档重组与文档轮次)
- .gitignore 修:`归档/**/db-cwd归一-备份-*/` —— 原规则写绝对层级(归档/db-cwd归一-…),
  目录搬进 归档/配置与备份/ 后**静默失效**,43 MB 的 DB 备份又变成未跟踪
- .gitignore 补:嵌套 git 内部数据(归档/内嵌git-20261008/、归档/skills-git-旧线-20261007/dotgit-原样移出/)
- .gitignore 补:运行态与部署副本(.workbuddy/collab/、.workbuddy/tools/、.workbuddy/.load-pending、.workbuddy/tmp-*)
- .gitignore 补:备份件(*.bak-*)
- 未跟踪文件从 2190 降到 890(其余为 归档/ 归档件与 .workbuddy/memory/ 知识文件,按口径入库)
2026-10-10 23:13:22 +08:00

903 lines
94 KiB
Markdown
Raw Permalink 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.
---
name: multi-session-collab
description: 「**一个主会话管理多个子会话协作完成一个需求**」的总入口 —— 治「多个会话协同推进但总工期被拖长/有会话在干等/链条断了没人接/分不清『真完成』还是『只是排了下一棒』/反复用自动化解决一切」。当用户说「怎么协同多个会话」「别的会话都在干等」「任务没推进」「链条断了」「你监督这些会话」「别老让我发消息催」「定期检查需求完成没有」「怎么安排任务避免拖长工期」「把这个协作机制做成技能」时使用。核心 = **四条通道各走各的**(派活靠自动化/收结果直读宿主库/机械判定下沉到常驻程序/人只看一个看板)+ **主会话只做判断与派活** + **显式任务图 + 关键路径优先**(防干等)+ **证据分级**(真成果 ≠ 接续任务)+ **自愈必须带去抖**(否则自愈害事)+ **新建自动化=白名单+确认制**。判据实体在主干,**全文在 `references/`(3 档)**,可执行件在 `scripts/`。
version: 1.1.9
updated_at: 2026-10-01
last_change: 2026-10-01 02:4x · 🔴 唤醒这一格**从"调度配置"改回"会话"**(**形状二次改** + **三条硬定性**)—— 用户定性:「**唤醒脉冲会话,本质还是会话**」(现名:唤醒主会话)+「**唤醒脉冲会话,位置不变**」(现名:唤醒主会话)+「**唤醒会话 是独立会话,不要在主会话上处理,是随着需求确定时创建的**」。① **形状**:六边形 `<path>` ⇒ **圆角矩形 `<rect rx=14>` + 外圈虚线**(与主会话/协作会话**同族**,靠**位置**区分,⛔ 不进下方那排);**全图再无六边形**。② **名字 + 读数源一起换**:这格由「唤醒定时任务」改为「**唤醒主会话**」;读数据从 `automations` 表**切到 `sessions` 表**(`title`/`custom_title` 前缀 `[唤醒]`,排 `deleted_at`)⇒ 使用方新增 `_waker_session()`、`_triggers()` 整体重写,两态文案改说"会话"(在跑/空闲等脉冲/**还没建这条会话**)。③ **诚实边界写进 tips**:「**下次几点到点**」⛔ 读不到 ⇒ 只给「**上次真的响过**」。④ 重写 §0.5.5(含"形状改过两次"对照表 + 读数据源变更 + 诚实边界)。⑤ 顺手修 `?` 帮助文本(原写「第三层按**任务类别**分工」= 09-30 旧语义)。⚠️ **两个验证坑当场踩到并修掉**:改形状后 `render-check.mjs` **2 条过时断言⇒假红**;`arch-geom-check.mjs` 里查 `<path class="trig-hex">` 那条**恒不命中 ⇒ 恒真 ⇒ 假绿**(形状改了却"看起来还是绿的")。⇒ 已同步断言并把假绿改成**真几何判定**(判"本体顶边 ≡ 主会话那行的顶"="位置不变"的回归闸),**并各做一次反向对照**(故意造错)确认它真会红。⑥ 🔴 **再加一条定性**(用户原话:「**唤醒会话 是独立会话,不要在主会话上处理,是随着需求确定时创建的**」)⇒ 图例 + tips 说清三点(**独立会话**/⛔ **不在主会话上处理**/**随需求确定时才创建**),并把"还没建"那支从含糊的「待创建」改说成**按定性的正常态**(⛔ 不是故障);`board_ext.py` 的 `_waker_session()`/`_triggers()` 两处 docstring + 链路前置文案同步;**新增 2 条断言**钉住(「独立会话/⛔ 不在主会话上处理」+「随需求确定时才创建」)。验证:渲染断言 **30/30**(空态 + 正例各一遍)· 几何 **6/6**(两个快照)。
last_change_prev: 2026-10-01 01:0x · 🔴 **一条被用户当场纠正的判断 + 一处空态遗漏**。① 我上一轮把「① 派活的**手段**(自动化排期)」当成"架构图上缺的一环"报上去 ⇒ 用户点破:「**自动化排期开新会话:这个不就是主会话创建和派活吗 也是自动任务的方式**」= **他对**:那是主会话的**动作**,⛔ 不是角色 ⇒ **角色才配节点,动作只配标签/说明**(已写进 §0.5.6 当判据)。⇒ 图⛔ **不加节点**;只把"它是**开新会话的唯一通道**(钩子做不到)"这句加进**图外说明**(⛔ 不动图结构)。② 顺带补齐**空态遗漏**:`② 上报 · --report` 的标签原来只在 `else` 分支里画 ⇒ **一条协作会话都没有时图上只剩 ①**,看不出还有 ② 这条回程 ⇒ 空态也画上。验证:渲染断言 **26/26**(空态 + 正例各一遍)· 几何 **5/5**。
last_change_prev: 2026-10-01 01:1x · 🔴🔴 **第三层整体换语义:只放「协作会话」** —— 用户:「**主会话 下面 那一排只放协作会话**,横着排 有几个放几个,当前没有对应协作会话 就空着」+「**放一个空的框 说明 暂无协作会话**」+「**唤醒机制如果是主会话的 事就放到主会话框里去**」。① 那一排 `cells` 从"任务类别"改成 **`sessions` 里 `role !== '主会话'`** 的协作会话;宽度 `clamp(150,340,可用/n)` ⇒ 有几个排几个;**0 条时画虚线空框**(⛔ 不许省略整层)。② **任务类别搬进主会话框**(R2 高 84→112,多一行「负责类别:…」,按 `main_by_topic` 反查)。③ 台账「未归类」也⛔ 不再占这一排 ⇒ 汇总改到**图外说明**摊开说(⛔ 不静默丢)。④ 重写 §0.5.6(含"三次换语义"对照表)+给 §0.5.2 加作废状态块。⑤ **新增正例快照法**:真实工作区常常 0 条协作会话 ⇒ 只跑默认快照时"有几个放几个"这条分支**从未被执行** ⇒ 造一份含 3 条协作会话的快照再跑,**当场抓出「4 分钟前<u>前</u>有活动」这个拼接 bug**(`fmtAge()` 返回值自带"前"字)。验证:自测 **32/32** · 渲染断言 **26/26**(空态 + 正例各跑一遍)· 几何 **5/5** · 线上 `/` 200 / 63,900 B。
last_change_prev: 2026-10-01 01:0x · 🔴 **分工格里"同一条会话既收件又承接"要说透** —— 用户连追三问(「唤醒定时任务 和 主会话下方的 唤醒机制是不是 重复了」→「你说的唤醒机制 是不是 主会话根据目标创建的 协作会话」→「**那你的唤醒机制 是啥意思嘛 跟主会话都一个 ID,难道是主会话?**」)。① **答**:「唤醒机制」是**任务类别名**(⛔ 不是会话);同 ID 是因为 `main_by_topic[类别]` 与 `labor[].running[0]` 指向**同一条会话**。② 🔴 **顺带查出一处真缺陷**:`board.py::_labor()` 的"台账无件 ⇒ 回落该类最近会话"分支带着 `and role != "主会话"` ⇒ 该类**只有主会话自己在跑**时候选整类判空 ⇒ 同一格里 ③ 说「(该类还没有记录,也无可归到它的会话)」、④ 说「承接 `<同一条>`」= **自相矛盾**(用户的困惑正源于此)⇒ 去掉该排除、`working` 排前。③ **文案**:② 行改「**收件主会话** `<id8>`」(⛔ 不再光写"主会话");③ 与 ④ 同会话时合成「**本类就这一条会话(兼收件与承接)**」,⛔ 不并列两行同 ID。④ 新增 §0.5.6「分工格四行各是什么角色」。验证:自测 **32/32** · 渲染断言 **25/25**(新增 2 条)· 几何 **5/5** · 实时看板 `/` 200 / 64,255 B。
last_change_prev: 2026-10-01 0x:xx · 🔴 唤醒定时任务**换形状 + 显真状态** —— 用户:「之前 **主会话 左边的唤醒通道**是不是没有了,给**新建的唤醒定时任务 换个样式** 和**协作会话**区分开」+「要**实时展示运行状态**,包括**图框样式要能体现**」。① **形状**:该格由 `<rect rx=14>`(与协作会话同形)改为 **六边形 `<path>` + 外圈虚线 `.trig-halo`** ⇒ 全图唯一一处非矩形,一眼区分「脉冲式触发器」vs「常驻角色」。② **状态**:新增 `front.triggers[].state` 三态(`running` 绿 ●/`paused` 黄 ‖/`none` 灰 ○),徽章用 **SVG 图形不用字符**;缺 `state` 回落 `up`。③ **图外说明**+`aria-label` 同步。④ 新增 §0.5.5「形状语义」。⑤ **使用方**(`board_ext.py`)读数改走 `automations` 表(原只读已废弃的网关册子 ⇒ 永远"通道已停用"):⚠️ 判据加 `schedule_type='recurring'`(不加会把 8 条同名一次性派棒算进去 = **假绿**)。验证:渲染断言 **23/23** · 几何 **5/5** · 实时看板 `/` 200。
last_change_prev: 2026-10-01 00:3x · 🔴🔴 两条用户订正落地 —— ① **目标不是固定配置**:「**目标是 通过对话在调用 会话协作skill时说明的,不是固定的**」⇒ 新增 §0.05(说明什么/落到哪/谁来说 + 三条硬规矩:⛔ 技能侧不许预设、⛔ 不许从文件名猜);给"说明"补上**唯一落点**(使用方 `goalctl.py declare`);`goal.json.topics` 里我 09-30 **猜的 3 类移入 `_topics候选`**;看板新增 `project.topics_source`(`declared/fallback/none`)⇒ **没说明过时显式标「⚠ 未声明 ⇒ 暂回落目标简称」**。② **「看板」=实时动态看板**:「**我说的看板是 实时动态看板 现在被关闭了**」⇒ 新增 §0.5.0(地址/起法/`--takeover`/停机出口 + 两条如实边界:它是会话后台任务、且会压制该会话的 idle 钩子)。自测 **32/32**。
last_change_prev: 2026-09-30 21:2x · 🔴 看板收尾:看板全面改「任务类别」;修掉两个同族红线(`acceptance_state` 只有说明行被当「全过」→ `goal_state()` 三态;台账旧线在图上整块不见 → `labor[].kind` + `orphan` 折叠格);新增 §0.5.4「改完看板怎么验」。自测 31/31。
agent_created: true
---
# 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);也 ⛔ 不用"常驻"描述任何主体 —— 本机制的设计前提是**零常驻**。
| # | 主体 | 只做什么 | ⛔ 不做什么 | 与谁接口 |
|---|---|---|---|---|
| 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` 事件驱动**,守护常驻**按设计不再需要**;⚠️ **代价 = 没有独立心跳时钟**
(即用户点破的「心跳成摆设」)—— **此点仍待用户拍板**是否恢复常驻。
### 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://` 双击即看,⛔ 不依赖本地服务、⛔ 不依赖网络。**桩验"内容与几何",人眼验"观感"**,两件都要做。
⚠️ **同理适用于"台账/验收"这类状态**:本轮两个真 bug 都属「**不崩溃,只是少说一句话**」
(见 `references/architecture.md`),**"跑一遍没报错"永远验不出来** —— 只能用**两处独立读数对账**。
### 0.5.5 🔴 架构图的**形状语义**:这一格**是一条会话**,不是"调度配置"(2026-10-01 · **二次改形状**)
**形状改过两次(⛔ 别照旧文档做事)**:
| 时点 | 画成什么 | 为什么 |
|---|---|---|
| 2026-10-01 上午 | **六边形 + 外圈虚线**(`<path>`) | 用户当时说「**给新建的唤醒定时任务换个样式,和协作会话区分开**」⇒ 用**异形**表达"它不是会话" |
| **2026-10-01 晚间(现行)** | **圆角矩形 `<rect rx=14>` + 外圈虚线** | 用户随后定性:「**唤醒脉冲会话,本质还是会话**」(现名:唤醒主会话)⇒ 与主会话/协作会话**同族**;「**位置不变**」 |
🔴 **结论:形状不再是区分手段,位置才是** —— 它贴在**主会话左边**,⛔ **不进下方那一排**。全图**没有六边形**。
🔴 **三条硬定性**(2026-10-01 用户原话:「**唤醒会话 是独立会话,不要在主会话上处理,是随着需求确定时创建的**」):
① **独立会话**(自己一条,⛔ 不是主会话的职能);② ⛔ **不在主会话上处理**(叫醒的执行体是它自己);
③ **随需求确定时创建**(⇒ 看板上「**还没建**」是**正常态**,⛔ 不是故障)。详见 **§1.4a ⑤**。
| 形状 | 含义 | 出现在哪 |
|---|---|---|
| **圆角矩形** `<rect rx=14>` | **会话/程序/地基**(一直在那儿的角色) | 主会话 · 主会话**左边**的唤醒主会话 · 下方那排协作会话 · 协作程序 · 上报 · Hook进程 · WorkBuddy |
| **外圈虚线** `.trig-halo` | 叠加在唤醒主会话上 ⇒ "它靠**脉冲**被叫醒,不是一直在跑" | 只有这一格有 |
- 🔴 **状态用"颜色 + 徽章"两重表达**,⛔ 不能只靠颜色(色盲/黑白打印会丢信息)。
`front.triggers[].state` 三态:`running` **绿 ●运行中** / `paused` **黄 ‖已暂停** / `none` **灰 ○未登记**;
⚠️ **缺 `state` 时回落 `up`**(向后兼容只给 `up` 的老使用方)。
- 🔴 **徽章用 SVG 图形,⛔ 不用 `●‖○` 这类字符** —— 字符依赖字体,缺 CJK 字体的环境会渲染成方框。
- ⛔ **技能里不写死这一格叫什么** —— 名字由使用方经 `front.triggers[].name` 给。
🔴 **用户 2026-10-01 定的名=「唤醒主会话」**(2026-10-01 晚再改,原名「唤醒脉冲会话」作废;属**项目侧**,技能侧⛔ 不写死);此前叫过「唤醒定时任务」,**该名已作废**。
**判据:`assets/board.html` 的渲染逻辑里不该出现具体项目名。**
- 🔴 **读数来源随定性一起改了**:它**是一条会话** ⇒ 状态读 **`sessions` 表**
(`title` / `custom_title` 前缀 `[唤醒]`,且须排掉 `deleted_at`),**⛔ 不再读 `automations`**。
⚠️ 旧口径(读 `automations.status`/`next_run_at`,并限定 `schedule_type='recurring'`)**已作废** ——
但它作为**教训**留着:一次性派棒(如「接续 · …(唤醒回路实战)」)名字里也带「唤醒」二字,
混进来会把状态判成 `running` = **假绿**(2026-10-01 实测:捞出的 11 条里 8 条是一次性)。
- ⚠️ **诚实边界**:「**下次几点到点**」程序**读不到**(脉冲是会话自己起的一次性后台任务,没有公开接口)
⇒ 看板只能给「**上次真的响过是什么时候**」;⛔ 不许编一个"下次到点"出来。
- **回归**:`selftest.py` 覆盖不到图形 ⇒ 靠 `tmp/render-check.mjs`(形状/徽章/图外说明/帮助文本,**8 条断言**)
+ `tmp/arch-geom-check.mjs`(唤醒主会话**必须贴在主会话那一行** —— "位置不变"的回归闸 —— 且⛔ 不压上下两层)。
🔴🔴 **改形状必须同步改断言**:否则过时断言 = **假红**;
⛔ 更阴的是**恒真断言 = 假绿**(本轮实测:形状改回 `<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,难道是主会话?**」⇒ 病根 = **那一排把"类别"和"会话"混着画**,读者分不清谁是谁。
#### 三条硬规矩
| # | 规矩 | 为什么 |
|---|---|---|
| ① | 那一排**只放协作会话**,⛔ 不塞类别、⛔ 不塞台账旧线 | 用户原话。混着画就分不清"谁在干活"与"分了几类活" |
| ② | **一条都没有时也要画空框**(虚线框 +「暂无协作会话」+一句说明) | 用户追加:「**放一个空的框 说明 暂无协作会话**」。⛔ 不许因为"没内容"就把这层省掉 —— 读者会以为图缺了一块 |
| ③ | **任务类别搬进主会话框**(一行「负责类别:…」,按 `main_by_topic` **反查**) | 用户:「**唤醒机制如果是主会话的 事就放到主会话框里去**」。⚠️ 查不到时**明说**「未在对话里说明」,⛔ 不闭嘴 |
🔵 **「自动化排期」是主会话的\*\*动作\*\*,⛔ 不是独立角色** —— 用户 2026-10-01 点破:
「**自动化排期开新会话:这个不就是主会话创建和派活吗 也是自动任务的方式**」⇒ **对**。
⇒ 它画在**连线标签**里(`① 派活 · 自动化排期`)就够,⛔ **别单开节点**。
⚠️ 但"它是**开新会话的唯一通道**(钩子开不了会话)"这条硬约束**光看线看不出来**
⇒ 在**图外说明**里点一句即可,⛔ 不动图结构。
(**这条踩过**:2026-10-01 我先把它当成"图上缺了一环"报上去,被用户当场纠正 ——
判据:**角色才配节点,动作只配标签/说明**。)
🔵 **空态时 ①② 两个标签都要画** —— 原来只画 ①,② 的标签在 `else` 分支里
⇒ **一条协作会话都没有时,图上只剩 ①**,看不出还有 ② 上报这条回程。
⛔ 空态 ≠ 这两条通道不存在,只是当前没有会话可连(2026-10-01 修)。
🔴 **「未归类」也⛔ 不占这一排** —— 台账里不在当前类别清单的旧线,**汇总改由图外说明**说清
(「N 条线 · 共 M 件 · 已完成 K」)⇒ ⛔ 不许因为"没地方画"就静默丢(本线红线:读到了就要说出口)。
🔴 **协作会话格画什么**:会话名 / 状态·`id8` / **所属类别**(读 `session.topic`;读不到就写
「(标题里没读类别前缀)」,⛔ 不许让格子看着正常却其实没类别) / 多久没活动。
宽度 = `clamp(150, 340, 可用宽 / n)` ⇒ **有几个排几个**;超 8 条才截断并在图外点名。
#### 前一个版本留下的两处真缺陷(代码仍在,⛔ 别照抄回去)
- 🔴 **`board.py::_labor()` 的回落分支 ⛔ 不许再排除主会话**:旧判据
`… and str(s.get("role")) != "主会话"` 本意是"最近在做的=承接会话",但**该类只有主会话自己在跑**时
会把候选**整类判空** ⇒ 同一格里 ③ 说「无可归到它的会话」、④ 说「承接 `<同一条>`」= **自相矛盾**
(用户看到的正是这个画面)。⇒ 候选**不排除任何角色**,`working` 排最前。
- ⚠️ **`fmtAge()` 的返回值自带"前"字**(`4 分钟前`)⇒ ⛔ 别再加一个 —— 2026-10-01 实测拼出过
「4 分钟前**前**有活动」。**这是造正例快照时才暴露的**(见下)。
#### 回归
- `tmp/render-check.mjs`(数字一律从快照现算):
① 主会话框含「负责类别」;② 第三层**只**出现协作会话的 `id8`(0 条时必须是「暂无协作会话」+虚线框);
③ ⛔ 不许再有「收件主会话」这类旧类别格残留(防回潮)。
- 🔴 **正例快照必须造**:真实工作区经常 0 条协作会话 ⇒ **只跑默认快照,"有几个放几个"那条分支
从没被执行过**(=未验证)。做法:把 `sessions` 追加几条 `role='协作会话'`(含一条 `topic` 为空的),
再 `node render-check.mjs <该快照>` 跑一遍。**2026-10-01 就是用这招抓出「前前」那个拼接 bug 的。**
- `tmp/arch-geom-check.mjs`:内置层高表 `rows` **必须与 `board.html` 同步**
(R2 84→112 就是这轮改的)—— ⛔ 不同步 ⇒ 几何自检是**假绿**。
---
## 1 🔴 四条通道(**哪件事走哪条 —— 走错就是今天最大的浪费来源**)
| 要做的事 | 走哪条 | 成本 | **为什么必须是它** |
|---|---|---|---|
| **派活 / 唤醒会话** | **自动化**(宿主排期) | 一个会话 | 🔴 **钩子开不了新会话** —— 自动化是**唯一**通道(宿主硬边界) |
| 🔴 **收结果 / 检测** | **直读宿主库**(`automation_runs`,只读 SQL) | **0 token** | 宿主**每次跑完就把它自己的收官结论落库** ⇒ ⛔ 不必轮询、⛔ 不必扫文件、⛔ 不必叫 AI |
| 🔴 **通知主会话(投递)** | **宿主钩子唤起投递**(`collabd.py --tick`) | **0 token · 0 会话 · 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 落 · 详见 `references/architecture.md §2.3.0b`)
—— **由会话自己建出来的下一棒**,标题由**建它的那条会话**写 ⇒ 常写成
`[<类别>] 接续 · …` 或 `接续棒:…`(**没有角色方括号**)。判据:**含"接续"二字** ⇒ 判 **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 截止类)。
⚠️ **但"连通知主会话都要靠自动化"是错的** —— 投递走**宿主钩子**(`--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`(⛔ 不轮询、不催、不问)。⇒ **「监管者」这个角色不存在**,只有**两个动作**:**收尾自判**(在已跑的会话里)+ **兜底心跳**(宿主排期)。
**🔴 权威清单(改任何判定前先看它 —— 权威只在这四处)**
| 要判什么 | 权威(唯一) | ⛔ 不许 |
|---|---|---|
| 谁在跑 / 哪条线被占 | `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` 字段全部改用真名;看板「链路前置」卡按真名渲染。
🔴 **三条现状打架(照原文列,⛔ 别照猜)**
1. `协同监管棒-SOP.md §276` 写「`advance-watch.py` **已吸收退役**(三件合并为本件;⛔ 不要再单独启动)」;但 `顶层设计 §3`/`定稿 §2·§3`/`实施方案 §一①`/`CODEBUDDY.md §1.5 D⑤` **仍把它当活件**("留"/"已就绪"/"读 `advance.md`")⇒ **投递到底还在不在跑,须以实测接线为准**。
2. **同一程序 4 个名字**:任务图里叫「协作程序」、它自述「协作守护程序」、本技能叫「机械层/常驻程序」、实施方案叫「机械层脚本」。
3. **同一程序 2 个路径**:`SOP` 写 `.workbuddy/tools/collabd.py`;实际在跑的是**技能版** `~/.workbuddy/skills/multi-session-collab/scripts/collabd.py` —— 本技能自己都警告「指向旧副本 ⇒ 唤醒回路等于没接」。
---
## 12 🔴 **唯一权威声明**(治"架构都是乱的" —— 2026-09-29 用户点破)
**病**:B(协作机制)曾有 **5 份"架构"并存且互相不认** —— `交付物/多会话协同机制-定稿`/`…顶层设计`/`…实施方案`/`协同监管棒-SOP`/**本技能**。
**实测出的五处互相矛盾(全有原文/实测支撑)**
| # | 议题 | 两套说法 |
|---|---|---|
| 1 | 一棒做完后怎么办 | `定稿 §3①`=**立即建一次性自动化「叫监管棒」** / `顶层设计 §4`=**收尾自判,零额外会话** |
| 2 | 「监管棒」是不是角色 | `定稿`/`SOP` 用它 / `顶层设计 §3` 判它**退役**(只剩两个动作) |
| 3 | 投递的死活 | `顶层设计 §3`="留"/`实施方案 §一①`="已就绪" / `SOP §276`="已吸收退役" |
| 4 | 钩子生效了吗 | `实施方案 §一⑦·§五-1`="未落地,须**完全重启宿主**" / **实测已生效**(换了触发锚点) |
| 5 | 钩子调哪一份程序 | **本技能**写"跑技能版" / **实测**钩子调的是**工作区版**(它再 fork 技能版) |
⇒ 🔴 **本技能 = B(协作机制)的唯一权威**。依据:`交付物/架构总览-20260929.md` 已定「B 见技能 `multi-session-collab`,业务文档 ⛔ 不抄正文」。
⇒ **其余四份自本行起降级为「历史/过程」**:可读作背景,⛔ **不作为判据**;冲突时**一律以本技能为准**。
**待办**:给那四份加头部状态块(指向本技能,⛔ 不改正文)—— `交付物/` 当前被域锁占用,等锁释放再做。
⚠️ 本技能自身也只保留"当下正确的说法":已被取代的旧说法一律在原处标注废弃(如 §9 的「+30 分钟看门狗」)。