- 变更规模:新增 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/ 知识文件,按口径入库)
967 lines
96 KiB
Markdown
967 lines
96 KiB
Markdown
> ## ⚠️ 本件=**原 `multi-session-collab` 技能的 `SKILL.md`**(2026-10-01 并入本包)
|
||
> · 🔴🔴 **2026-10-03 07:2x 新增状态块(本条最优先,⛔ 排在下面 10-01 那条之上)——「上报/投递」整套退役**:
|
||
> ⛔ **队列投递已真删**(`supervise()` 203→71 行、两个 `_deliver_str()` 调用点删除、`wake_enable` true→false)
|
||
> ⇒ 本件里凡讲**投递/上报/唤醒回路/跟进会话收件**的段落,**读到即跳过**(含 §1.4a、§0.5 里的 `wakeups.jsonl` 面板)。
|
||
> ✅ **现行机制只有两条腿**:**接收**=`--report` 写四态台账;**处理**=建 `[检查]-…` 排期。
|
||
> 🔴 **「常驻投递」这个角色名也别再用** —— 角色本来就叫「**协作程序**」,"常驻"是**运行形态、不是角色名**(P0-37)。
|
||
> ⚠️ 本件 §0.5 的**看板版面纪律仍然全部有效**(2026-10-03 两格合一就是按它做的);⛔ 只有"图上画着投递"那部分作废。
|
||
> ⇒ 冲突时以 `architecture.md` 当前结论节 + `SKILL.md` 文首 10-03 状态块为准。
|
||
> · 为什么带进来:本包 `references/{architecture,deploy,pitfalls,taskgraph}.md` 都写着「**主干判据在 `SKILL.md §x`**」
|
||
> —— 指的就是**这一份**。不带进来,包内就有 8 处悬空引用。
|
||
> · 🔴 **权威关系**:`references/architecture.md` 是**唯一权威**(其头部有「当前结论」节)。
|
||
> **凡本件与它冲突,一律以 `architecture.md` 为准**;本件只作**细则补充 + 溯源**。
|
||
> · 🔴🔴 **本件内所有「零常驻/按需唤起/跑完即退/不需要常驻/不用常驻描述主体」的表述,一律已被推翻**
|
||
> (2026-10-01 定案:**协作与投递一直运行(常驻)**;理由「可能不是所有队列都是钩子产生的」+「定时任务的方案已经废弃了」
|
||
> —— ⚠️ **2026-10-03 订正**:"投递"那半边已退役,⛔ 只剩"协作程序一直运行")
|
||
> ⇒ **读到即跳过**,现行口径见 `architecture.md §4/§5-1`。
|
||
> · 原件 mtime `2026-10-01 13:56`;原件全文(含 YAML 头)⇒ `<工作区>/归档/技能-退役-20261001/multi-session-collab/SKILL.md`。
|
||
|
||
### 🔴 读法索引(**先看这张表,⛔ 别从头顺读**)
|
||
|
||
| 章节 | 性质 | 怎么读 |
|
||
|---|---|---|
|
||
| §0.5 · §0.5.0–§0.5.6(看板) | **本件独有 = 活判据** | ✅ 必读:看板总则、版面纪律、文案纪律、形状语义、改完怎么验 |
|
||
| §1.3 拉取模型 · §1.4a 唤醒回路判据 · §6 自愈纪律 · §8 派活前自检 · §9 `collabd.py` 维护要点 | **本件独有 = 活判据** | ✅ 用到时读 |
|
||
| §3 任务图 · §4 证据分级 · §5 自动化白名单 · §2 主会话只做两件 · §7 落地三个件 | **细则** | ✅ 用到时读(§3 亦见 `taskgraph.md`;§5 亦见项目层 `CODEBUDDY.md §F`) |
|
||
| §0 五个主体 · §0.05 目标 · §1 四条通道 · §10 防打转判据 · §11 术语表 | **已被取代** | ⛔ 跳过 ⇒ 读 `architecture.md` 的 §1/§0.1/§3/§6/§7 |
|
||
| §11.1 命名铁律 · §12 文档权威 | 摘要 | ⚠️ 略读即可 |
|
||
|
||
---
|
||
|
||
> ⚠️ **存档说明**:本件正文取自原技能 `SKILL.md` 的**主体**。
|
||
> 原件的 **YAML 头部**(`name` / `description` / `version` / `updated_at` / **逐轮 `last_change` 变更流水** / `agent_created`)**已于 2026-10-01 删除** ——
|
||
> 那是技能加载器的元数据 + 发布流水,放在参考件里对使用零价值、且会随时间腐坏。
|
||
> 原件全文(含该 YAML 头)见 `<工作区>/归档/技能-退役-20261001/multi-session-collab/SKILL.md`。
|
||
# multi-session-collab — 主会话 × 多子会话执行机制
|
||
|
||
> 🔴 **一句话**:**派活用自动化(唯一能开新会话的通道)|收结果直读宿主库(0 token)|机械判定下沉到本地只读脚本|AI 只在需要判断时被叫起|用任务图+关键路径防干等。**
|
||
|
||
> ## 🔴 当前结论(**先读这里** · 最后更新 2026-09-30 12:30)
|
||
> ⚠️ 本文件**通篇是追加式写法** ⇒ 下文可能残留**已被取代**的内容(已就地标注)。
|
||
> **冲突时以「本节 + `references/architecture.md` 的「当前结论」节 + 两文件 §迭代记录最新条」为准**,并请顺手把冲突处就地标注。
|
||
>
|
||
> | 项 | **当前结论** |
|
||
> |---|---|
|
||
> | **唯一权威** | 架构 ⇒ `references/architecture.md`(⛔ 别处⛔ 不再写第二份)。`交付物/` 里 4 份"多会话协同"平行件 + `落地清单-唤醒回路` ⇒ **2026-09-30 已加退役标注**,⛔ 不作用判据。 |
|
||
> | **运行形态** | 🔴 **本条已订正(2026-09-30)· 原 09-29 定案「协作程序与监督程序一直运行」已废弃**。实测判据(用户原话「**你把监督程序停掉关了 整个流程还是照样跑**」):`guard` 自 09-29 23:59 停到现在,**投递一路正常**(当日 8 条 `http=200`)⇒ 停掉常驻,流程照跑。⇒ **现形态 = 两个模式都由宿主钩子按需唤起**:`--once`(协作程序 · 维护队列)/`--tick`(**投递** · 唯一投递方)。⚠️ 「监督程序」**这个名字已退役** —— 它从来不是「一个该常驻的进程」(用户:「**跟监督程序没有半毛钱关系**」)。 |
|
||
> | **本机起法** | 唯一可行 = **宿主后台任务机制 + `stdout` 全重定向到文件**。⚠️ `detached` 活不过工具调用边界;`schtasks`/`reg`/`wmic`/`sc`/`wsl` 被**内置程序黑名单**硬拦。 |
|
||
> | **派活模板必带** | ① **`开工第 0 步:抢域锁`**(域按**显式 domain** 切,⛔ 不按 cwd/⛔ 不按工作区)② **`收尾自判 ⇒ 有缺口就接下一棒;blocked 则⛔ 不接、改喊用户`**。 |
|
||
> | **看板(三层)** | 🔴 **只读旁路观测**(用户 2026-09-30:「看板**不能影响程序执行**,可以**异步**、可以**延迟**」)⇒ `board.py --serve`:**后台线程产快照,请求线程只读内存缓存**(⛔ 不碰 DB/⛔ 不读文件),开几个标签页都不加宿主负载。🔴 架构图**三层**:用户/主会话/**分工板块(按线,不是历史会话列表)**/协作程序+投递 + 宿主地基带;每格带**真状态**(会话取宿主库、程序取状态戳 mtime)。详见 §0.5。 |
|
||
> | 🔴 **看板=单实例**(2026-09-30 加护栏) | Windows 的 `SO_REUSEADDR` 允许**同端口重复绑定且不报错** ⇒ 多个实例**静默并存**、同一 URL 被**随机**应答 ⇒ 快照/代码版本**打架**(实拍:已有看板时第二个照样打印"看板已起")。⇒ `board.py --serve` 起前探 `/healthz` 签名,已有 ⇒ **拒绝启动**;换新代码用 **`--takeover`**。停机 ⇒ `stop-collab.py` ④。详 `references/pitfalls.md` **P0-9**。 |
|
||
> | **已知待修** | ① ✅ `goals_open()` 已补读 `acceptance_state`(2026-09-30)② ⚠️ `wake_round()` 已停用未删 ③ 常驻与钩子的**投递唯一性**未按新方案收口(现靠三重去重兜着)④ ⚠️ `board.py` 改完**必须重启服务**(进程内已 import,不重启就跑旧代码)⑤ ✅ **`--once` 已认「已停」总闸**(2026-10-01 修:`goal_paused()`/`paused_round()` + 自测 6 项)⑥ ⚠️ **"全节点 done 但验收未声明"** ⇒ 已补验收判据(V1–V5)+ 日志节流 600 s;⚠️ **判据仍由对话说明**(`goalctl declare`),⛔ 别拿本次这五条当项目常量 ⑦ ✅ **多目标 tab**(2026-10-01 加:`build()` 出 `goals[]` + 看板 `renderTabs()`/`paintScope()`;**tab 第一行=完整目标名**,⛔ 不许退回"只给简称")。数据源=`<inbox>/goals/*.json`(一个文件一个目标,**活跃排第一**);⛔ **没有 `goals/` 时行为与旧版逐字一致**(`goals[]` 只有 1 项)。⚠️ 台账按 `line ∈ 目标 topics` 切到各目标;**不属任何目标**的件归**活跃目标**且照常显示(⛔ 不静默丢)。改 `board.py` 记得走 ④ 的重启 |
|
||
| 🔴 **2026-10-01 体检新增三修** | ① **`--once`/`--tick` 认总闸**(`run != active` ⇒ 只写一行"已停"、清告警投影、⛔ 不投递)② **主会话解析只认活会话**(`_pick_live`)—— 实测断点:解析出一条**已 `completed`** 的「主控」当投递目标 ⇒ 一路 `main-not-live`(**程序在跑、通知永远发不出去**);修后判读转为 `target-busy` ③ **退役线清队列**:台账/任务图 18 条全是**已退役的手机接入线** ⇒ 队首长期指向退役需求(已归档、按当前类别重建) |
|
||
| 🔴 **唤醒主会话:只画了格,会话没建**(2026-10-01 实测) | 会话表里**零条** `[唤醒]` 前缀会话(带「唤醒」字样的 6 条全是 `completed` 的旧棒)⇒ 技能 §1.4a 那条"**随需求确定时创建**"从未执行 ⇒ 看板上「还没建」虽然按定性是**正常态**,但**那条腿实际是空的**。叠加"网关排期=死能力"(`CODEBUDDY_DISABLE_CRON=1`)⇒ **全员静止时零唤醒**。**处置:本次未擅自建**(属"派活/接续"白名单外 ⇒ 待用户拍板) |
|
||
>
|
||
> 🔴 **⑤ 已停 ≠ 静默**(2026-10-01 实测 · ⛔ 见 `tmp/协作机制体检-2026-10-01.md`):
|
||
> `goal.json.run=paused` 时,`collabd.py` 全文**零处**引用 `run`/`paused`(只有 `wake_enable` 管**投递**)
|
||
> ⇒ `--once` 照跑、照写 `STALL.md`/`NEXT.md`/`digest.md`/`TO-MAIN.md`,而 `digest.md` 的内容
|
||
> **经钩子注入每个会话的每一轮** ⇒ 一个"人为停掉"的需求被读成"机制坏了"。
|
||
> **正解**:`--once` 开头加总闸,`run != active` ⇒ 只写一行"已停",⛔ 不产告警。
|
||
> 🔴 **⑥ 判不出完成 ⇒ 唤醒永不暂停**:任务图 18/18 done、队列 0 待办,但 `acceptance_state` 只有说明行、无判据
|
||
> ⇒ `goal_state` 判「判不出来」⇒ `goals_open()=true` ⇒ §1.4a ②「已完成 ⇒ 唤醒任务暂停」**永不成立**,
|
||
> `TO-MAIN.md` 每轮照发。**正解**:补判据,或改成「全 done 且未声明 ⇒ 喊一次就静默」。
|
||
|
||
> 🔴🔴 **架构正文不在本文件** ⇒ **`references/architecture.md`** =「多会话协作」的**唯一架构文档**(2026-09-29 用户定案)。
|
||
> 🔴🔴 **本机制的一切迭代都在本 skill 内进行**(用户 2026-09-29 明令:「都要在技能中去迭代」)——
|
||
> 架构 ⇒ `references/architecture.md`|部署与起法 ⇒ `references/deploy.md`|踩坑 ⇒ `references/pitfalls.md`|操作 ⇒ 本文件|代码与配置 ⇒ `scripts/`。
|
||
> ⛔ **不在工作区或别处另开平行文档**;⛔ 不把机制文档散进业务件(业务件只留"用了哪条机制"的指针)。
|
||
> 🔴🔴 **改完必跑回归自测**:`python scripts/selftest.py` —— **全绿(rc=0)才算改完**。
|
||
> 它把**所有异常情况**做成了用例(⚠️ **条数别写死** —— 会随迭代增长,`grep -c '^@case(' scripts/selftest.py` 数一下才准:
|
||
> **看板与收尾确认的归属判据必须同款**/投递成对重复/**纯投影不消费队列**/**投不出不消费**/
|
||
> 两级命名/目标闸/台账四态/启动对账/僵尸/不显窗/假绿/**会话哑掉(诊断日志撞上限)**/常量完整/优雅退出 …),
|
||
> **⛔ 测试不碰生产**(用 `tmp/selftest/` 独立工作区)。
|
||
> 🔴 **写用例的两条纪律**(2026-10-01 各踩到一次):
|
||
> ① **"前提不成立就 SKIP"要把前提判「全」,⛔ 别靠副作用反推一半** —— 例:僵尸用例只看 `adopted` 就断言,
|
||
> 漏了「无 IN_PROGRESS 运行」那半 ⇒ **自动化会话跑自测时必然假红**(它自己就是那条运行)。
|
||
> ② **用例自造的假数据每轮清干净**(放 `_prepare()` 里清),⛔ 别让上一轮的留在原地 ——
|
||
> 断言"精确集合"的用例会被跨轮残留多算几条 ⇒ **第二次跑才红**(最难查的一型)。
|
||
> 📌 用例是从**真实事故**里长出来的 ⇒ 每踩一个新坑,**先把它加成一条用例**,再改代码。
|
||
> · **改架构 ⇒ 只改那一份**(五个主体 / 任务队列=**上报制** / 唤醒=**静默判据** / 四条通道 / 三种节奏 / 防打转判据 / 术语表)。
|
||
> · **本文件 = 操作入口**(怎么派活、怎么收尾、怎么排坑);下文出现的架构内容为**就近速查**,**冲突一律以 `architecture.md` 为准**。
|
||
> · ⚠️ **【已作废 · 2026-09-30 就地标注】** 此处原写"⛔ **零常驻** —— 凡见『常驻程序』字样均属旧说法"。
|
||
> **该说法已被用户 2026-09-29 定案推翻**(见上表「运行形态」:协作程序与投递**一直运行**)。
|
||
> ⛔ 保留此行只为拦住"翻到旧文又照做";**判据一律以上表 + `architecture.md §5` 为准**。
|
||
|
||
## 0 第 0 步:先认清**五个主体**(本机制的**唯一主体清单**)
|
||
|
||
> 🔴 **架构里只有这 4 个主体**:**用户 · 主会话 · 协作会话 · 协作程序**。
|
||
> 🔴 **「主会话」是*解析出来的*,不是*登记出来的***(2026-09-30 用户定则:「**以 一个工作区 为主会话的工作区**」):
|
||
> 登记(`roles[sid]=="main"`)只在**该会话确实活着**时才有效;失效则按**工作区**重新认
|
||
> (`cwd == 本工作区` 且标题**不带** `[协作]`),仍认不出 ⇒ **⛔ 拒绝盲投**、报 `no-main-session` 并喊用户。
|
||
> ⛔ 曾有「目标不在活会话里就随手取第一条口」的**盲选回落**(=零判据投递,会投错窗口),**已删除**。
|
||
> 判据唯一实体=`resolve_main()`;完整分析见 `references/architecture.md §7`。
|
||
>
|
||
> 🔴 **命名定则(2026-09-30 用户)**:**主会话 / 接续会话的标题前缀 = `主控`**,形如
|
||
> **`主控 · <任务类别> · <具体>`** —— 例:`主控 · 机制线(查唤醒为什么断)`。
|
||
> · **⛔ 中段写「任务类别」,不是目标名**(用户原话:「而且后面也不叫 手机接入」);
|
||
> · 🔴 **「任务类别」是同一个词的四处**(⛔ 别当成四件事):**`goal.json.topics` 的一项 ≡
|
||
> 会话标题里的 `<任务类别>` ≡ 台账条目的 `line` ≡ 分工板的一行**。⛔ `topics` 缺省才回落 `short`。
|
||
> · 🔴 **同一工作区可有多个任务类别**(2026-09-30 用户:「之前的协作会话是**跨工作区**的,
|
||
> 要能支持**同一个工作区** 多会话协作(…**通过协作会话名称前缀**区分具体任务会话,
|
||
> 所有主会话,协作会话,**自动唤醒任务**,都在**一个工作区**)」)⇒
|
||
> **每个类别各有自己的主会话**;投递**按件的类别**选主会话(`resolve_mains()` + `main_for_topic()`)。
|
||
> ⛔ **自动唤醒任务的排期名也必须带类别前缀**(`[协作]-<类别>-<具体>`)(⚠️ 仅**代偿形态**下适用;定案时钟=常驻投递,不开会话、无排期名)——
|
||
> 实测踩到:`[协作]-唤醒A · <工作区>` 缺第 2 级 ⇒ 解析不出类别。
|
||
> 全文 ⇒ `references/architecture.md §2.3.0`。
|
||
> · **⛔ 主会话的接续会话不许带 `[协作]` 前缀** —— `[协作]` 是**协作棒**的标记,带上它会被本机制
|
||
> 当成棒**排除掉**(我犯过:把接续会话建成 `[协作]-[手机接入]-接续…` ⇒ 自己排除自己);
|
||
> · `主控` 前缀是**显式标记** ⇒ `resolve_main()` **优先认它**(比"最近活动"可信:那是用户自己标的,不是猜的);
|
||
> · ⚠️ 实测坑:网关 `POST /api/v1/sessions/{id}/rename` **回 204 但不落宿主库**(显示名与库标题可能是两套)
|
||
> ⇒ 改名后**必须回读 `sessions.title` 确认**,⛔ 别只看 204。
|
||
> ⚠️ **2026-09-30 订正**:原来的第 5 个主体「投递」**已退役** —— 它其实只是**同一个 `collabd.py` 的 `--tick` 模式**(投递),⛔ **从来不是一个独立进程**。
|
||
> ⚠️ **宿主(WorkBuddy 本体)是地基,不是第 6 个主体**;它只提供四样:状态(只读 3 表)/**调度(自动化排期 = 唯一能开新会话的通道)**/事件(钩子)/互斥(域锁)。
|
||
> 🔴 **⛔ 不用"角色"这个词**(历史文档里"三个角色"的说法已被**本表「主体」**取代,见 §12);也 ⛔ 不用"常驻"描述任何主体 —— 本机制的设计前提是**零常驻**。
|
||
> ⚠️ **但「主体」与「会话类别」是两根不同的轴**,⛔ 别混:
|
||
> · **本表(§12)= 主体** —— 谁负责什么(含**用户**与**两个程序**:协作程序/投递)。
|
||
> · **会话类别 = 四类**(2026-10-01 用户口径)—— **① 主会话 ② 协作会话 ③ 唤醒会话 ④ 队列上报的跟进会话**,
|
||
> 同工作区、靠**标题两级前缀**区分 ⇒ 见 `collab.md §4` = `architecture.md §2.3.0b`(接续=**形态**,⛔ 不是类别)/**§2.3.0c**(第④类,未落地)。
|
||
> ⚠️ **已知缺口**:第 ③④ 类**尚未列进本表**(本表是"主体"视角,唤醒会话/跟进会话都是**会话**,落点在会话类别那根轴上)。
|
||
|
||
| # | 主体 | 只做什么 | ⛔ 不做什么 | 与谁接口 |
|
||
|---|---|---|---|---|
|
||
| 1 | **用户** | 🔴 **职责(2026-09-30 用户原话):制定目标 · 调整方向 · 做决策**。可做的动作:看板 · 放行授权 · 改目标 · 喊停 | ⛔ 不管过程;⛔ 不必催进度 | → 主会话:需求/判据/授权 |
|
||
| 2 | **主会话** | **判断 + 派活**:读库+看板判缺口 ⇒ **写一行排期**;收尾自判 | ⛔ 不替别线干活;⛔ 不做机械判定;⛔ 不搬砖(除非最靠前那步自己就能做) | → 宿主:一行排期;→ 用户:看板/待授权 |
|
||
| 3 | **执行会话** | **一棒一线,一次一件**:做本棒;**收尾两件**=写产出 + 判本线缺口**接下一棒** | ⛔ 不跨线;⛔ 不夹带;⛔ 不常驻 | → 宿主:运行结论(自动落库) |
|
||
| 4 | **执行检查** | 持有**需求台账**;收上报/判定/告警/体检/看板覆写/单例(`--once` **投影轮**) | 🔴 **⛔ 不派活**(不写排期、不开会话);🔴 **⛔ 不投递、⛔ 不推进队列**(`mutate=False`) | → 用户:看板;→ 主会话:机械摘要 |
|
||
| 5 | **投递**(⚠️ 旧名「监督程序」,2026-09-30 退役。**它是同一个 `collabd.py` 的 `--tick` 模式**,⛔ 不是一个独立主体/进程) | **逐条读队列** ⇒ **投给主会话**(单条+握手/三条件唤醒)⇒ **唯一的投递方+唯一的队列推进方** | ⛔ 不开会话;⛔ 不派活;⛔ 口令不落盘/不进日志/不回显 | → 主会话:通知 |
|
||
|
||
**🔴 判定你是哪一个**:你是被派出去干一件具体事的那个 ⇒ **执行会话**;你是"入口"、负责判缺口与派活 ⇒ **主会话**;你是脚本 ⇒ **投递**(只读只判)或**执行检查**(其余全部,但不派活)。
|
||
|
||
---
|
||
|
||
## 0.05 🔴🔴 **目标不是固定配置 —— 它在「调用本技能的那次对话」里说明**(2026-10-01 用户订正)
|
||
|
||
> 用户原话:「**目标是 通过对话在调用 会话协作skill时说明的,不是固定的**」
|
||
|
||
**这条规矩治的是本技能 2026-09-30 犯过的一个错**:把 `goal.json.topics` 按**工作区里现有的
|
||
接续入口/接续包文件名**填了 4 类,还当成"待拍板事项"去问用户要不要**定死**。
|
||
⇒ 那是**猜**,不是**说明**;而且一旦写进 `goal.json`,机制就会照它跑(**判归属、选主会话**)——
|
||
**猜错了不会报错,只会静默漏管/错投**。
|
||
|
||
| 要说明什么 | 落到哪 | 谁来说 |
|
||
|---|---|---|
|
||
| **目标**(一句话说清要做成什么) | `goal.json.title` | 用户(对话里) |
|
||
| **为什么 / 边界** | `goal.json.why` | 用户(对话里) |
|
||
| **任务类别**(同一工作区靠它分前缀) | `goal.json.topics` | 用户说 / 主会话梳理 |
|
||
| **验收判据** | `goal.json.acceptance_state` | 用户说 / 主会话提 |
|
||
|
||
**🔴 三条硬规矩**
|
||
1. **技能侧与脚本 ⛔ 不许预设目标与类别,⛔ 更不许从目录名/文件名/接续入口名去"推"**
|
||
—— 推出来的东西会**静默决定**「哪些会话算本项目、投递往哪条主会话去」。
|
||
2. **登记有唯一落点**(="说明"这个动作的**唯一出口**):
|
||
```
|
||
python .workbuddy/collab/goalctl.py declare --title "…" [--why "…"] [--topics "A,B"] [--kpi "V1=pass"] --yes
|
||
```
|
||
⛔ 默认**干跑**;`--title` **必填**(脚本**不替你编目标**);省略某个参数 ⇒ **不动那一项**
|
||
(⛔ 不拿旧值凑数);`--topics ""` ⇒ 显式清空(回到单类别回落)。
|
||
3. **可以被覆盖** —— 下次调用技能时在对话里再说一遍 ⇒ 用 `declare` 覆盖即可;
|
||
⛔ 别把上一次说明的目标当成**项目常量**(这正是出错的根源)。
|
||
|
||
**⚠️ 未说明时的行为(⛔ 不许静默)**:`topics` 缺省 ⇒ 回落 `goal.short`(单类别,向后兼容),
|
||
但**看板必须显式标出「⚠ 未声明 ⇒ 暂回落目标简称」**,⛔ 不许让人以为"这就是定下来的类别"。
|
||
落点:`board.py::_goal_topics_source()` → `project.topics_source.kind ∈ {declared, fallback, none}`
|
||
(`declared` 时一并显示说明时间/说明人)。回归用例:`selftest.py::看板:任务类别来源必须说得出口`。
|
||
|
||
## 0.5 🔴 看板(`scripts/board.py` + `assets/board.html`)
|
||
|
||
### 0.5.0 🔴🔴 「看板」默认指**那个实时动态看板** —— 它是**要起着的**(2026-10-01 用户点破)
|
||
|
||
> 用户原话:「**我说的看板是 实时动态看板 现在被关闭了**」
|
||
|
||
⚠️ 别用"一份离线快照页"去顶 —— 快照只是**复核手段**(见 §0.5.4),**用户要看的是活着的那一个**。
|
||
|
||
| 项 | 值 |
|
||
|---|---|
|
||
| 地址 | **`http://127.0.0.1:8788/`**(⛔ 只绑回环) |
|
||
| 起法 | **会话后台任务 + stdout 落文件**(本机唯一可行的起法) |
|
||
| 命令 | `COLLABD_CONFIG=<ws>/.workbuddy/collab/collabd.config.json DSH_COLLAB_WS=<ws> "<python>" "<技能>/scripts/board.py" --serve 8788 --takeover > <ws>/tmp/board-serve.out.log 2>&1` |
|
||
| 换代码重起 | 加 **`--takeover`**(先停旧的再接管)。⛔ **别裸起第二个** —— Windows `SO_REUSEADDR` 会让两实例**静默并存**、同一 URL 随机应答("改了、也重启了、却还是旧的"就是这么来的)。单实例护栏:已有看板 ⇒ **拒绝启动** |
|
||
| 改完必须重启 | ⚠️ 服务进程**已 import** 技能里的 `board.py` ⇒ 改快照内容后**不重启就跑旧代码** |
|
||
| 停机出口 | `.workbuddy/collab/stop-collab.py`(dry-run 只报告)|`goalctl stop --yes` 会顺带停它 |
|
||
|
||
**⚠️ 两条如实登记的边界(⛔ 别当没这回事)**
|
||
1. 它是**会话后台任务** ⇒ **关会话/关宿主就停**。本机**没有**真正的常驻手段(detached spawn 活不过
|
||
工具调用边界;`schtasks`/`reg` 等持久化工具在内置程序黑名单里)⇒ **这不是"忘了常驻",是做不到**。
|
||
2. ⚠️ **该会话只要挂着 `pending`/`running` 的后台任务,宿主的 `idle` 钩子会被静默压制**
|
||
(2026-09-30 实测被一个僵尸任务压过 6h20m)⇒ 起看板的那个会话,其**唤醒/投递回路会哑**。
|
||
⇒ 正解=**让"起看板"这个动作落在一个不承担派活职责的会话里**,⛔ 别让主会话干这件事。
|
||
|
||
|
||
### 0.5.1 铁律:**看板不能影响程序执行**(用户 2026-09-30 明令)
|
||
|
||
> 用户原话:「**看板不能影响程序执行,可以异步 可以延迟**」。
|
||
|
||
三条落地(照这个改,⛔ 别把看板做成"第二个执行检查"):
|
||
|
||
| # | 约束 | 做法 |
|
||
|---|---|---|
|
||
| ① | **解耦** | 执行检查/守护程序**一行都不引用** `board.py`(可用 `grep -rn "board" scripts/*.py` 复核);`board.py` **从不写任何账本**(只写 `board.json`,且仅在 CLI 模式)。 |
|
||
| ② | **异步** | `--serve` 起**一个后台线程**按 `--interval`(默认 3 s)产快照 ⇒ **请求线程只吐内存缓存字节**(⛔ 不碰 DB、⛔ 不读文件)⇒ 宿主库查询频率**恒定 1/interval**,与标签页数量无关。页面轮询节奏**跟随** `board.refresh_interval`(⛔ 不自行加频)。 |
|
||
| ③ | **降级不静默** | 宿主库只读连接 + `busy_timeout=300`(撞写锁 **0.3 s 就放弃**,⛔ 不排队);任一块读不到 ⇒ 记进 `board.json.warn` 并在界面顶部黄条显示,⛔ **不伪装成"0 个会话"**(那是假情报)。刷新失败 ⇒ **保留上一份快照**,界面显示「数据延迟 N 秒」。 |
|
||
|
||
⚠️ **改完 `board.py` 必须重启看板服务**(进程内已 import,不重启跑的还是旧代码)。⚠️ 页面每请求重读 `board.html` ⇒ **只改 HTML 不用重启**。
|
||
⚠️ 起服务时把 `stdout` 重定向到文件(`> tmp/board-serve.log 2>&1`)—— 会话后台任务的每轮 stdout 会**唤醒宿主会话**。
|
||
|
||
### 0.5.2 架构图三层布局(用户 2026-09-30 指定)
|
||
|
||
> 🔴🔴 **2026-10-01:第三层语义又变了一次(第三次)⇒ 本节正文里"分工板块(按线)"已作废。**
|
||
> 用户原话:「**主会话 下面 那一排只放协作会话**,横着排 有几个放几个,当前没有对应协作会话 就空着」
|
||
> +「**放一个空的框 说明 暂无协作会话**」+「**唤醒机制如果是主会话的 事就放到主会话框里去**」。
|
||
> ⇒ **以 §0.5.6 为准**(那一排 = 协作会话排;任务类别搬进主会话框)。下文保留作历史。
|
||
|
||
```
|
||
用户
|
||
↓ 发消息 · 看板
|
||
主会话
|
||
↓ ① 派活 · 自动化排期
|
||
分工板块(按线)…每格:最近协作任务 / 承接会话 / 件进度
|
||
↓ ② 上报 · --report
|
||
执行检查 ──队列──▶ 投递 ──③ 投递 · reply/唤醒──▶ (右侧竖井回主会话)
|
||
↑ ④ 收结果 · 直读宿主库
|
||
宿主 WorkBuddy(地基)
|
||
```
|
||
|
||
- 🔴 **第三层是「分工板块」,⛔ 不是历史会话列表**(用户:「协作会话**不是历史记录**,是**展示分工**的板块」)。
|
||
**分工位 = 线**(`goal.json.lines` ∪ 台账里出现过的线);每格展示该线**最近的协作任务**(用户追加要求)、
|
||
当前**承接会话**、**件汇总**。三色:**有会话在跑**(绿)/**⚠ 有件没人在跑 = 缺口**(黄,最有用的信号)/**件已全完**(灰)。
|
||
- 🔴 **承接会话怎么认**:拿"在跑会话标题里的件 id"匹配(长 id 优先,防 `N1` 抢 `N10`)。
|
||
⛔ **不用 cwd 推断** —— §2.3 明令。匹配不上就如实写「当前无会话在跑」,⛔ 不硬凑。
|
||
- 🔴 **会话归属判据(本项目 vs 别的项目)**:`board.py` 与 `collabd.py` 各有一份 `in_project()`/`_in_project()`,
|
||
**必须逐字同款** —— 一旦漂移就会出现「看板说没人跑、`--ready-next` 却被别的项目拦死」。
|
||
已有回归用例 `selftest.py::归属判据:看板 ≡ 收尾确认` 逐例比对(⛔ 别删)。
|
||
- 🔴 **执行检查节点带「队列计数」**(用户 2026-09-30 追加要求):**队列 = 需求台账 `tasks.json`**(不是 `queue.json`,
|
||
`architecture.md` §迭代记录明载)⇒ 显示 `队列 N 件 · 已完成 X · 未完 Y(· 受阻 Z)`,四态 `pending/running/done/blocked`。
|
||
⚠️ **机制里没有「待验收」这个态** —— 用户问「待验收队列数量」时**按四态如实给数,⛔ 不臆造一个数字**;
|
||
要不要真加一道「验收」关口,属**机制变更,待用户拍板**。
|
||
- ⚠️ **【本条已作废 · 2026-09-30】** 原文讲「投递显示『已停』是正常的」—— **前提已不存在**:该名字已退役,看板不再显示「已停」(见 §0.5.2b/§11.1)。以下原文仅作历史:
|
||
- ~~🔴🔴 **投递显示「已停」是正常的、有据的止损,⛔ 别当故障去"修"**(出处 `.workbuddy/memory/2026-09-29.md` 2424–2435 行
|
||
+ `architecture.md §5-1`,代码常量 `board.py::GUARD_STOP_REASON`):
|
||
2026-09-29 23:59 主会话 `guard.py --stop` 主动停 —— 守护当时是用「**会话内后台任务**」起的 ⇒ 任务**归属发起会话**
|
||
⇒ 该会话被判定"一直挂着长跑任务" ⇒ 实测「**一启动会话就卡消息输出**」。
|
||
🔑 **根因 =「有网关口令」与「不占会话」不可兼得**。**正解 = 拆两件**:「发现」走**会话外**常驻(启动文件夹/独立窗口,
|
||
⛔ 但拿不到口令)、「投递」走**宿主起的**通道(钩子 `--tick` / 低频排期 —— 宿主起的子进程**天生有口令**)。
|
||
~~⇒ 现投递**已由钩子 `--tick` 事件驱动**,守护常驻**按设计不再需要**;⚠️ **代价 = 没有独立唤醒时钟**
|
||
(即用户点破的「心跳成摆设」)—— **此点仍待用户拍板**是否恢复常驻。~~
|
||
🔴 **2026-10-02 标注(⛔ 不是口径变更)**:**这段已作废** —— 用户 **2026-10-01 已拍板**:「**协作与投递一直运行(常驻)**」+「**定时任务的方案已经废弃了**」⇒ **常驻必须回来**(见 §5-1)。⛔ 别再把上面那句当现状读。
|
||
### 0.5.2b 🔴 看板版面纪律:**小字描述只进右上角 `?`**
|
||
|
||
用户 2026-09-30:「把**各板块的小字描述**放到各板块对应**右上角 `?` 号图标**中,鼠标移上去显示
|
||
(**只保留标题,主体,类别标签**这类信息)」,并点名**删除**三处。
|
||
|
||
| 版面只留 | 进 `?`(`.qtip`) |
|
||
|---|---|
|
||
| **标题**(`h2`)· **主体**(表格/chips/架构图)· **类别标签**(`h2 .hint`,如 `tasks.json`/`组件状态`) | 该板块的**说明性小字**(为什么这样、判据是什么、口径提醒) |
|
||
|
||
- 实现:`.qh`(右上角圆点 `?`)+ 内嵌 `.qtip`,CSS `:hover / :focus-visible` 才显示 ⇒ **纯 CSS,零 JS**;
|
||
用 `<span class="qh" tabindex="0">` ⇒ **键盘也能看**。
|
||
- 🔴 **动态的长解释也要进来**(如「投递为什么是停的」)—— 做法:给 `.qtip` 一个 id,
|
||
每轮渲染先 `tip.innerHTML = tip.dataset.base`(首轮存静态原文)再按条件追加,⛔ 防重复追加。
|
||
- ⛔ **别再往版面摊解释性小字**;警告/读数这类**主体内容**不算小字,可以留在版面。
|
||
- **回归用例**:`selftest.py::看板:版面纪律`(`?` 数量与 `.qtip` 配对 + 被点名删除的三句不得复活)。
|
||
- 🔴 **「延迟」与「保留的快照时刻」是例外:必须**固定显示**在「协作架构」板块**右上角**,⛔ 不许收进 `?`
|
||
(用户 2026-09-30:「延迟 = 你看到的时间 − `<快照时刻>` **保留时间** 放在**协作架构板块右上角**」)。
|
||
表单:`.herometa` 两行 —— 第 1 行**只放快照时刻**(`<b id="ts2">`),第 2 行放 **`延迟 <span id="lag">`**。
|
||
⚠️ 2026-09-30 用户又划掉一次:**「延迟 = 你看到的时间 −」这句不要**,时间下面直接写「延迟 X 秒」即可
|
||
⇒ ⛔ 不要再加解释性前缀。由 `paintAge()` 每秒刷新,**超过 15 秒整块转警示色**。
|
||
⛔ 别在 header 再放第二份时刻(已去掉)。
|
||
|
||
### 0.5.1b 🔴🔴 技能 / 使用方分离(用户 2026-09-30 定则)
|
||
|
||
> 用户原话:「**技能就是技能 程序就是程序,谁用产生的文件 放在他自己那里**」
|
||
|
||
**技能 = 通用能力**(`SKILL.md` / `references/` / `scripts/` 的代码 / `assets/` / 范例配置)。
|
||
**使用方产生的东西 ⇒ 放使用方自己那里**:部署配置、运行日志、编译产物、看板扩展、部署启动器。
|
||
|
||
| 件 | 归谁 | 落点 |
|
||
|---|---|---|
|
||
| **部署配置** | 使用方 | `<工作区>/.workbuddy/collab/collabd.config.json`(技能里⛔ 不放;只有 `collabd.config.example.json` 范例) |
|
||
| **运行日志** | 使用方 | 配置键 `log`(默认落在工作区内);⛔ `LOG = HERE / …` 是旧写法,**已禁** |
|
||
| **看板「前置」块** | 使用方 | 配置键 `board_ext` → `.workbuddy/collab/board_ext.py`,契约 `build(ws) -> {title,tag,chips,paragraphs,tip}` |
|
||
| **部署启动器** | 使用方 | 工作区自己的 `start-guard.cmd`(里面写死本机 python 路径 ⇒ 属部署件) |
|
||
|
||
🔴 **配置查找链(`collabd.py`)**:① 环境变量 `COLLABD_CONFIG` → ② `<COLLABD_WORKSPACE 或 cwd>/.workbuddy/collab/collabd.config.json` → ③ **都没有 ⇒ 拒跑 rc=2**。
|
||
⚠️ **为什么 ③ 必须拒跑**:不配配置时 `workspace` 会回落 **cwd**,而 cwd 常常就是**技能目录** ⇒ 技能里长出 `tmp/supervise-inbox/` 与 `_collabd.log`。
|
||
⛔ 连"未找到配置"那条告警**都不许写文件**(它自己就会造成同样的污染)⇒ 只打 stderr。
|
||
🔴 **接线方(钩子/启动器)必须显式传 `COLLABD_CONFIG` + `COLLABD_WORKSPACE`**;⛔ **不许做"回落同目录旧副本"的兜底** —— 那是静默换版本的经典来源。
|
||
🔴 **回归用例**:`selftest.py::技能侧零项目串`(技能目录无结构性产物/日志不挂 HERE/`board.py` 代码零项目串/看板不回读废弃项目键)。
|
||
⚠️ **迁移期尾巴**:改完这套后,**迁移前起的旧进程**仍会用旧代码路径写文件(实测 `wb-supervisor-watch.py --interval 10`)⇒ 等它结束才彻底干净。
|
||
|
||
### 0.5.2c 🔴 文案纪律(**去 AI 味**)—— **看板 + 注入给会话的正文**
|
||
|
||
用户 2026-09-30:「**现在很多文案不是抓不住重点,就是描述太AI味**」。
|
||
用户 2026-10-01:「**会话的反馈信息排版 非常不利于阅读,改为段落排版**」+ 给出目标形态:
|
||
**一行 header + 其后每行「标签:一段话」**。
|
||
|
||
🔴 **适用范围(2026-10-01 扩写 · 原来只管看板,是个漏洞)**:本节纪律**同时管**两处 ——
|
||
① **看板板面**;② 🔴 **注入给会话的正文**:`digest.md`(机械摘要)+ 三个信号文件
|
||
(`STALL.md` / `VACUUM.md` / `READY.md`)。为什么必须一致:后者是**每轮钩子注入给每个会话**的,
|
||
排版差 = **每个会话每轮都要多花注意力去拆句**,是**全平台共用的阅读成本**;
|
||
而且用户**根本分不清**哪句来自看板、哪句来自注入 —— 两处形态不一致就等于纪律没落地。
|
||
⚠️ **载体**:注入正文的唯一生成处 = `scripts/collabd.py::digest_text()`(`digest.md`)与 `signals()`(三个信号文件)。
|
||
**改文案=改这两处**,⛔ 别去改生成的 `.md`(下一轮就被覆写)。
|
||
|
||
**规则来源**:技能 `humanizer-zh`(24 类 AI 写作模式)+ `agent-operating-rules §2.5`。两处文案都按这两份过。
|
||
|
||
| ⛔ 别写 | ✅ 改成 | 实例 |
|
||
|---|---|---|
|
||
| **否定式排比**「不是 X,而是 Y」 | **只说后半句** | 「⛔ 不是历史会话列表」→「按线分工」 |
|
||
| **金句**(听着能被引用) | 说具体事实 | 「一眼看得出来」→「看格子颜色就知道」 |
|
||
| **`⇒` 满屏**(每段好几个) | 逗号 / 句号 | 「两段都在线 ⇒ 端到端未验证」→「两个组件都在线,链路还没通」 |
|
||
| **`≠` 符号** | 写成句子 | 「在线 ≠ 通过」→「都在线,链路也可能是断的」 |
|
||
| **加粗滥用** | 只留关键词 | 每段加粗 ≤2 处 |
|
||
| **emoji 装饰** | 只在真警示处留一个 | 图例里的 `⚠` 删掉(色块已表意) |
|
||
| **三段式凑数** | 有几项写几项 | 「最近任务 / 承接会话 / 件进度」→「最近做完的件、现在谁在做、还剩几件没完」 |
|
||
| **一条信息说两次** | 只留一处 | chip 只给状态(「已停」),后果写在正文段落里 |
|
||
| **解释性前后缀** | 直接给值 | 「延迟 = 你看到的时间 − 07:13:12」→「07:13:12 / 延迟 4 秒」 |
|
||
| 🔴 **`- ` 列表碎片 + `;`/`⇒` 串联短句** | **段落排版**:一行 header + 每行「标签:一段话」 | 「`- 🔴 **中断/脱节** —— 没有会话在跑,且 0 分钟 无成果;⛔ 未来 1 小时内零排期 ⇒ 不等人发消息就是**确定性静默**`」→「`状态:没有任何会话在跑,而且已经 3 分钟没有出新成果。未来 1 小时内也没有任何排期,只要没人主动开口,这里就会一直静默下去。`」 |
|
||
|
||
🔴 **判据(写文案时的唯一一问)**:**这是给人读的,还是给程序读的?**
|
||
给人读 ⇒ **段落**(完整的句子、标签打头、一行一件事);给程序读(注释 / 日志 / 解析用字段)⇒ 随便。
|
||
⚠️ 现有实现里有反面先例:`verdicts()` 里的 `V["verdict"]` 就是「`;` + `⇒` 串联」形态 ——
|
||
它**保留**给 `realtime.md` 与通知用,但**摘要⛔ 不许直接复用它**(要按段落重讲,见 `digest_text()`),
|
||
⛔ 也⛔ **不许对它做字符串反解**(反解会随措辞改动**静默失效**)⇒ 事实要由 `verdicts()` **另带原始键**(如 `zero_sched` / `probe`)。
|
||
|
||
**自检(改完文案后跑一遍)**:搜 `⇒` `≠` `一眼看得出来` `⛔ 不是` 在**非注释行**里的出现,应为 0。
|
||
⚠️ 判据要**排除注释**(`//` 与 `/* */`、Python 的 `#`)—— 注释是给未来的我看的,可以带标记;**给人看的才是受众**。
|
||
|
||
### 0.5.3 🔴 看板风格系统(**右上角切换**)
|
||
|
||
用户 2026-09-30:「画架构图**参考 archify 的样式**(<https://github.com/tt-a1i/archify>),
|
||
**右上角加个风格切换(保留当前风格)**」。
|
||
|
||
| 风格 | `data-style` | 说明 |
|
||
|---|---|---|
|
||
| **当前风格**(默认·⛔ 不许删) | *(无属性)* / `base` | 原风格,**跟随系统深浅色** |
|
||
| **Archify 暗** | `archify-dark` | 令牌**逐字取自** archify `assets/template.html` 的 dark 主题(MIT) |
|
||
| **Archify 亮** | `archify-light` | 同上,light 主题 |
|
||
|
||
🔴 **实现铁律(违反就会"换风格换出花")**:
|
||
- **切换只改 `<html data-style="…">`** —— 颜色/字体/网格**全走 CSS 令牌**(`--color-*` / `--font-ui` / `--canvas-dot` / `--node-stroke-w`)
|
||
⇒ **换风格不需要重画 SVG**。
|
||
- ⛔ **组件与 SVG 里一律不许出现硬编码颜色**(`#hex` / `rgba(...)`)—— 只许出现在令牌块里。
|
||
**已有回归用例守着**:`selftest.py::看板:风格系统`(三风格令牌在位 + 组件零硬编码色)。
|
||
- 选 `base` 时**要把 `data-style` 属性摘掉**(不是设成 `"base"`),好让它继续吃
|
||
`@media (prefers-color-scheme: dark)`。
|
||
- 选择存 `localStorage['dsh-board-style']`,且**在 `<head>` 里尽早套用**(⛔ 别等 `load`,否则先闪一下原风格)。
|
||
- archify 的关键令牌备忘:画布 `#020617`(暗)/`#f4f5f7`(亮);点阵 `rgba(148,163,184,.16)`/`#d9dee5`;
|
||
强调色 `#34d399`/`#059669`;语义色 backend `#34d399`、cloud `#fbbf24`、security `#fb7185`、external `#94a3b8`;
|
||
字体 **JetBrains Mono(拉丁)+ 系统 CJK 回退** ⇒ 本地用 `ui-monospace, Consolas, "Microsoft YaHei"` 近似。
|
||
|
||
⚠️ **等宽字体的字宽与比例字体不同** ⇒ 改风格后**必须重跑"文字越界 + 压框"检查**(本轮三风格均 0 溢出 / 0 压框)。
|
||
|
||
- ⚠️ **图上文案宽度**:中日韩字符 ≈ 1 em(13 px 字号 ⇒ 约 13 px/字),ASCII ≈ 7 px。
|
||
**⛔ 别用"每字符 8.4 px"那种估法**(会把标题算长一倍 ⇒ **文字溢出框外**,第一版就栽在这)。
|
||
用 `fitText()` 逐字符累加宽度来截断。**验收**:跑一遍"逐个 `text` 量 `getBBox()` 是否越界"的检查,必须为空数组。
|
||
|
||
### 0.5.4 🔴 改完看板怎么验(⛔ 别只跑自测 —— 2026-09-30 收尾棒踩出来的)
|
||
|
||
**问题**:本机(Windows 客户端)**没有独立的浏览器实例**给这个技能用,而本机**明令禁止自起
|
||
headless chrome /附着用户 Chrome** ⇒ **不许擅自开浏览器**。但"改了看板没验"=没交付。
|
||
⇒ 三步取证的组合拳(都在 `tmp/` 现做,不用常驻件):
|
||
|
||
| 步 | 做什么 | 判据 |
|
||
|---|---|---|
|
||
| ① **语法** | 抽出 `board.html` 的主脚本 `node --check` | 通过 |
|
||
| ② **渲染** | 极简 **DOM 桩**跑真 `render()`:喂一份真实快照,断言**渲染出来的 DOM 里该有的都有、该没的都没** | 断言全绿 |
|
||
| ③ **几何** | 解析渲染出的 SVG:**所有 rect/text 在下界内**、**行带互不重叠**、**格间零重叠**、右沿对齐 | 全部 0 越界 |
|
||
|
||
🔴🔴 **两条最容易踩的坑**:
|
||
1. **桩必须"当场从 `board.html` 现取"主脚本** —— ⛔ **不许读上一轮导出的副本**。
|
||
副本可能是**带补丁的形态**(本轮实测:旧副本是"内联快照"版 ⇒ 换新脚本后 10/12 全 ✗,
|
||
一查不是代码坏了、是**副本形态不对**)⇒ 读副本 = **改动越多、假绿越稳**。
|
||
2. **离线预览页是给人眼复核的,不是替代品**:把快照**内联**进 `board.html`(并让 `load()` 优先吃内联件)
|
||
⇒ `file://` 双击即看,⛔ 不依赖本地服务、⛔ 不依赖网络。**桩验"内容与几何",人眼验"观感"**,两件都要做。
|
||
|
||
🔴🔴 **改之前必须知道的两件位置事实**(2026-10-01 实测,⛔ 不知道就会白改):
|
||
- **`board.html` 从「包内」读**(`board.py` 用 `HERE.parent/assets/board.html`)⇒ 改包内**即生效**。
|
||
- **`board_ext.py` 从「使用方工作区」读**(配置项 `board_ext`,默认 `<工作区>/.workbuddy/collab/board_ext.py`)
|
||
⇒ **只改包内那份等于没改** —— 必须 `cp` 同步过去,并**核 md5 一致**。
|
||
⚠️ 同理还有 `goalctl.py` / `stop-collab.py` / `deliver-gateway-token.py` 等**工作区副本**。
|
||
⇒ **改任何一件前先 `diff` 两份**:若差异只有你这次改动 ⇒ 直接 `cp`;否则先弄清谁是谁。
|
||
|
||
⚠️ **回归工具当前住在 `tmp/`**(`tmp/render-check.mjs` + `tmp/arch-geom-check.mjs` + 快照 `tmp/board-verify*.json`)
|
||
—— 而 `tmp/` 按规矩是**要清的草稿区** ⇒ **清掉后"改完看板怎么验"这条路就断了**(`selftest.py` 盖不住渲染与几何)。
|
||
⇒ 要用之前**先确认它们还在**;⛔ 也别把它们当长期资产(**待迁移进包**,登记为结构性问题)。
|
||
⚠️ 两个脚本的路径常量是**写死**的:`render-check.mjs` 的 `BOARD` 曾长期指向**已退役**的
|
||
`skills/multi-session-collab/`(合并后没人改)⇒ 一跑就崩。**换机器/换技能名后必查这一行。**
|
||
|
||
⚠️ **同理适用于"台账/验收"这类状态**:本轮两个真 bug 都属「**不崩溃,只是少说一句话**」
|
||
(见 `references/architecture.md`),**"跑一遍没报错"永远验不出来** —— 只能用**两处独立读数对账**。
|
||
|
||
### 0.5.5 🔴 架构图的**形状语义**:这一格**是一条会话**,不是"调度配置"(2026-10-01 · **二次改形状**)
|
||
|
||
**形状改过两次(⛔ 别照旧文档做事)**:
|
||
|
||
| 时点 | 画成什么 | 为什么 |
|
||
|---|---|---|
|
||
| 2026-10-01 上午 | **六边形 + 外圈虚线**(`<path>`) | 用户当时说「**给新建的唤醒定时任务换个样式,和协作会话区分开**」⇒ 用**异形**表达"它不是会话" |
|
||
| **2026-10-01 晚间(现行)** | **圆角矩形 `<rect rx=14>` + 外圈虚线** | 用户随后定性:「**唤醒脉冲会话,本质还是会话**」(**现名:唤醒会话** —— 2026-10-01 用户再改名,原话「**唤醒主会话 改为 唤醒会话**」)⇒ 与主会话/执行会话**同族** |
|
||
|
||
🔴 **结论:形状不再是区分手段,位置才是** —— 它在**主会话下面单独一行(`RW` 行)的左格**,
|
||
与右格「**跟进会话**」**等宽并列**(用户 2026-10-01:「**唤醒会话和 跟进会话 单独放一行**」+
|
||
「**唤醒会话 和 跟进会话 框一样大小**」),⛔ **不进下方那一排**。全图**没有六边形**。
|
||
⚠️ **位置改过一次**:上一版是"贴在**主会话左边**";拆行后主会话那一行**只剩主会话独占**
|
||
(="主会话只能是用户触发"在图上成立:指进它的线只有上面"用户"那一条)。
|
||
|
||
🔴 **三条硬定性**(2026-10-01 用户原话:「**唤醒会话 是独立会话,不要在主会话上处理,是随着需求确定时创建的**」):
|
||
① **独立会话**(自己一条,⛔ 不是主会话的职能);② ⛔ **不在主会话上处理**(叫醒的执行体是它自己);
|
||
③ **随需求确定时创建**(⇒ 看板上「**还没建**」是**正常态**,⛔ 不是故障)。详见 **§1.4a ⑤**。
|
||
|
||
| 形状 | 含义 | 出现在哪 |
|
||
|---|---|---|
|
||
| **圆角矩形** `<rect rx=14>` | **会话/程序/地基**(一直在那儿的角色) | 主会话 · 主会话**下面那一行左格**的唤醒会话 · 同行右格的跟进会话 · 下方那排执行会话 · 执行检查 · 上报 · Hook进程 · WorkBuddy |
|
||
| **外圈虚线** `.trig-halo` | 叠加在唤醒会话上 ⇒ "它靠**脉冲**被叫醒,不是一直在跑"(⚠️ 「**脉冲**」是 **2026-10-01 及更早**的说法;现行口径里**负责叫醒的是常驻投递**。⛔ 但**图形保留** —— 这一格确实**不是一直在跑**,虚线仍然如实) | 只有这一格有 |
|
||
| 🆕 **大圆角 + 细虚线 + 不填色** `.grp` | **分组**(⛔ 不是节点、也⛔ 不是"新一层") | 上框=主会话+唤醒+跟进;下框=执行会话那一排(见 §0.5.7) |
|
||
|
||
- 🔴 **状态用"颜色 + 徽章"两重表达**,⛔ 不能只靠颜色(色盲/黑白打印会丢信息)。
|
||
`front.triggers[].state` 三态:`running` **绿 ●运行中** / `paused` **黄 ‖已暂停** / `none` **灰 ○未登记**;
|
||
⚠️ **缺 `state` 时回落 `up`**(向后兼容只给 `up` 的老使用方)。
|
||
- 🔴 **徽章用 SVG 图形,⛔ 不用 `●‖○` 这类字符** —— 字符依赖字体,缺 CJK 字体的环境会渲染成方框。
|
||
- ⛔ **技能里不写死这一格叫什么** —— 名字由使用方经 `front.triggers[].name` 给。
|
||
🔴 **用户 2026-10-01 定的名(现行)=「唤醒会话」**;同日更早叫过「唤醒主会话」(**已作废**,用户原话「**唤醒主会话 改为 唤醒会话**」),再早叫「唤醒脉冲会话」「唤醒定时任务」(均**已作废**)。
|
||
属**项目侧**,`board.html` 里⛔ 不写死这个名字 —— 由使用方经 `front.triggers[].name` 给。
|
||
**判据:`assets/board.html` 的渲染逻辑里不该出现具体项目名。**
|
||
- 🔴 **2026-10-01 23:1x 图上改「简称」(用户第六条口径)**:`唤醒会话 →` **唤醒**、`队列上报的跟进会话 →` **跟进**
|
||
(⛔ 用户明令**两者字号一致**,`n-title-sm` 13px)、`执行检查 →` **协作**(`n-title` 16px)。
|
||
⚠️ **简称 ≠ 改角色** —— 正文/代码里的**全称不变**;图里 `?`/`aria-label` **必须带简称对照**
|
||
(否则「**协作**」会被误读成「**协作会话**」)。⚠️ 探针别拿 `唤醒` 当子串判据(见 `architecture.md §2.3.0c-2`)。
|
||
项目侧 `front.triggers[].name` 也随之给了 `唤醒`(`board_ext.py`)⇒ 图格标题取的是**简称**。
|
||
- 🔴 **读数来源随定性一起改了**:它**是一条会话** ⇒ 状态读 **`sessions` 表**
|
||
(`title` / `custom_title` 前缀 `[唤醒]`,且须排掉 `deleted_at`),**⛔ 不再读 `automations`**。
|
||
⚠️ 旧口径(读 `automations.status`/`next_run_at`,并限定 `schedule_type='recurring'`)**已作废** ——
|
||
但它作为**教训**留着:一次性派棒(如「接续 · …(唤醒回路实战)」)名字里也带「唤醒」二字,
|
||
混进来会把状态判成 `running` = **假绿**(2026-10-01 实测:捞出的 11 条里 8 条是一次性)。
|
||
- 🔴 **2026-10-02 订正**:原文写「**下次几点到点**」程序**读不到**(理由=脉冲是会话自己起的一次性后台任务)—— **两半都不对**:① 定案的时钟是**常驻投递**(⛔ 不是「会话自己起后台任务」——那条是**明令禁区**,见 §4.0 末);② 「下次到点」**读得到** —— 代偿形态下它就在排期表 `automations.next_run_at`。
|
||
⚠️ 但**看板那一格目前还没读它**(=已知缺口)⇒ 眼下仍只给「**上次真的响过是什么时候**」;⛔ 不许编一个「下次到点」出来。
|
||
- **回归**:`selftest.py` 覆盖不到图形 ⇒ 靠 `tmp/render-check.mjs`(形状/徽章/图外说明/帮助文本,**8 条断言**)
|
||
+ `tmp/arch-geom-check.mjs`(唤醒会话**必须贴在「唤醒+跟进」那一行(`RW`)** —— 它**已不再贴主会话那一行**;
|
||
⚠️ 这条"贴在哪一行"的判据 2026-10-01 拆行时**必须跟着改**:不改 ⇒ 拿 R2 顶去比 ⇒ **假红**)。
|
||
🔴🔴 **改形状必须同步改断言**:否则过时断言 = **假红**;
|
||
⛔ 更阴的是**恒真断言 = 假绿**(本轮实测:形状改回 `<rect>` 后,几何自检里那条查
|
||
`<path class="trig-hex">` 的断言**恒不命中** ⇒ 恒真 ⇒ 假绿,改了形状却"看起来还是绿的")。
|
||
**断言里的数字一律从快照现算**,⛔ 不写死;**新增断言要做一次反向对照**(故意造错,确认它真会红)。
|
||
|
||
### 0.5.6 🔴 第三层 = **执行会话排**;任务类别搬进主会话框(2026-10-01 用户定 · 这一排第三次换语义)
|
||
|
||
**来历(三次换语义,⛔ 别照旧文档做事)**:
|
||
|
||
| 版本 | 那一排画什么 | 为什么换 |
|
||
|---|---|---|
|
||
| ≤ 2026-09-30 上午 | **按线**(`goal.lines`/台账 `line`) | 跨工作区时代的维度 |
|
||
| 2026-09-30 晚 | **按任务类别**(`goal.topics`) | 为治"同一工作区多类别塌成一行"——⚠️ **当时不是错的**;但类别**本质是主会话自己的事**,占着"执行会话"的位置会误导 |
|
||
| **2026-10-01(现行)** | **按执行会话**(`sessions` 里 `role !== '主会话'`) | 用户:「**那一排只放协作会话**,横着排 有几个放几个,当前没有对应协作会话 就空着」 |
|
||
|
||
**触发这次改动的三连问**:「唤醒定时任务 和 主会话下方的 唤醒机制是不是 重复了」→
|
||
「你说的唤醒机制 是不是 主会话根据目标创建的 协作会话」→「**那你的唤醒机制 是啥意思嘛
|
||
跟主会话都一个 ID,难道是主会话?**」⇒ 病根 = **那一排把"类别"和"会话"混着画**,读者分不清谁是谁。
|
||
|
||
#### 三条硬规矩
|
||
|
||
| # | 规矩 | 为什么 |
|
||
|---|---|---|
|
||
| ① | 那一排**只放执行会话**,⛔ 不塞类别、⛔ 不塞台账旧线 | 用户原话。混着画就分不清"谁在干活"与"分了几类活" |
|
||
| ② | **一条都没有时也要画空框**(虚线框 +「暂无协作会话」+一句说明) | 用户追加:「**放一个空的框 说明 暂无协作会话**」。⛔ 不许因为"没内容"就把这层省掉 —— 读者会以为图缺了一块 |
|
||
| ③ | 🔴 **2026-10-01 同日已删** —— 原「任务类别搬进主会话框」(一行「负责类别:…」,按 `main_by_topic` **反查**)**不复存在**。任务类别现在**只在两处**说:顶部「本项目」卡的**任务类别 chips**(每个 chip 带「主会话 `<id8>`」/「⚠️ 无主会话」)+ 图外说明「当前」块第 1 条 | 先有用户:「**唤醒机制如果是主会话的 事就放到主会话框里去**」⇒ 搬进去;随后用户指着那行问:「**这个是什么意思,如果没用就删掉**」⇒ 删。**两条删的理由**(实测,⛔ 不是口味):① **它会说出假话** —— `topics_source.kind='declared'`(类别在对话里说明过)而 `main_by_topic` 为空时,它渲染成「未在对话里说明」,**把"这条主会话没对应到任何类别"说成了"类别没声明过"**(两件事被一个判据混在一起);② 同一事实**版面上已说过两遍**,且方向更对(类别→谁负责),这行是第三遍、还是反方向。⚠️ 顺带:`R2.h` 由 112 **回落到 84**,`tmp/arch-geom-check.mjs` 的 `rows.R2` 必须同步 |
|
||
|
||
🔵 **「自动化排期」是主会话的\*\*动作\*\*,⛔ 不是独立角色** —— 用户 2026-10-01 点破:
|
||
「**自动化排期开新会话:这个不就是主会话创建和派活吗 也是自动任务的方式**」⇒ **对**。
|
||
⇒ 它画在**连线标签**里(`① 派活 · 自动化排期`)就够,⛔ **别单开节点**。
|
||
⚠️ 但"它是**开新会话的唯一通道**(钩子开不了会话)"这条硬约束**光看线看不出来**
|
||
⇒ 在**图外说明**里点一句即可,⛔ 不动图结构。
|
||
(**这条踩过**:2026-10-01 我先把它当成"图上缺了一环"报上去,被用户当场纠正 ——
|
||
判据:**角色才配节点,动作只配标签/说明**。)
|
||
|
||
🔵 **空态时 ①② 两个标签都要画** —— 原来只画 ①,② 的标签在 `else` 分支里
|
||
⇒ **一条执行会话都没有时,图上只剩 ①**,看不出还有 ② 上报这条回程。
|
||
⛔ 空态 ≠ 这两条通道不存在,只是当前没有会话可连(2026-10-01 修)。
|
||
|
||
🔴 **「未归类」也⛔ 不占这一排** —— 台账里不在当前类别清单的旧线,**汇总改由图外说明**说清
|
||
(「N 条线 · 共 M 件 · 已完成 K」)⇒ ⛔ 不许因为"没地方画"就静默丢(本线红线:读到了就要说出口)。
|
||
|
||
🔴 **执行会话格画什么**(2026-10-03 定稿):**只画三行** —— 会话名 / 状态·`id8` / 多久没活动。
|
||
⛔ **不画「所属类别」那一行**(原本有 `类别:xxx`,无类别时又加了一串提示)——
|
||
**这行已被用户指示删除**(逐字:「越写越看不懂 还是删除了把」)。
|
||
留档(三次踩坑,别再犯):① 原文案「(标题里没读类别前缀)」**只说现象、没信息量**;
|
||
② 我改成「⚠ 无类别 ⇒ 不会被派活」——🔴 **那个后果是我编的**(取证:派活读 `tasks.json`,
|
||
`topic` 在派活链路上**根本没出现**;而那排格子**本身就是正在执行的执行会话**);
|
||
③ ⇒ 结论:**这行信息量不足,⛔ 不值得占版面、更不值得编文案**。
|
||
📌 **两条通用红线**:⚠️ **写「会怎样」前必须先取证那个后果真的存在**(否则=编);
|
||
⚠️ **改文案改到第三轮还说不清 ⇒ 该问的是「这行要不要留」,⛔ 不是继续润色**。
|
||
|
||
#### 前一个版本留下的两处真缺陷(代码仍在,⛔ 别照抄回去)
|
||
|
||
- 🔴 **`board.py::_labor()` 的回落分支 ⛔ 不许再排除主会话**:旧判据
|
||
`… and str(s.get("role")) != "主会话"` 本意是"最近在做的=承接会话",但**该类只有主会话自己在跑**时
|
||
会把候选**整类判空** ⇒ 同一格里 ③ 说「无可归到它的会话」、④ 说「承接 `<同一条>`」= **自相矛盾**
|
||
(用户看到的正是这个画面)。⇒ 候选**不排除任何角色**,`working` 排最前。
|
||
- ⚠️ **`fmtAge()` 的返回值自带"前"字**(`4 分钟前`)⇒ ⛔ 别再加一个 —— 2026-10-01 实测拼出过
|
||
「4 分钟前**前**有活动」。**这是造正例快照时才暴露的**(见下)。
|
||
|
||
#### 回归
|
||
|
||
- `tmp/render-check.mjs`(数字一律从快照现算):
|
||
① 🔴 **主会话框⛔ 不许再写「负责类别」**(2026-10-01 删了那行 —— 它会说出假话,见 §0.5.6 表 ⓷)
|
||
+ 架构图⛔ 不许再出现「未在对话里说明」(同族假话:类别声明过却说没说过);
|
||
② 第三层**只**出现执行会话的 `id8`(0 条时必须是「暂无协作会话」+虚线框);
|
||
③ ⛔ 不许再有「收件主会话」这类旧类别格残留(防回潮);
|
||
④ 🆕 「当前」块必须按 **`topics_source.kind`** 说类别(`declared` ⇒ 点类别名 + 对应主会话;
|
||
否则 ⇒ 明说「还没在对话里说明过」)—— ⛔ 不许拿**回落值**当已声明的类别报出去。
|
||
- 🔴 **断言必须"改前能报红"**(⛔ 不许写成恒真 —— 那是假绿)。**做法:拿改前的备份跑同一套桩**,
|
||
同一份快照下"改前红、改后绿"才算真判定。2026-10-01 删那行时就是这么验的:
|
||
线上真实快照 **改前 6 红 → 改后 4 红**、合成 `kind='fallback'` 样本 **改前 7 红 → 改后 4 红**
|
||
(残留那 4 条是**写死给样本快照的内容型断言** ⇒ 拿**线上快照**跑就假红,属**桩的已知弱点**,登记为 P2)。
|
||
- 🔴 **正例快照必须造**:真实工作区经常 0 条执行会话 ⇒ **只跑默认快照,"有几个放几个"那条分支
|
||
从没被执行过**(=未验证)。做法:把 `sessions` 追加几条 `role='执行会话'`(含一条 `topic` 为空的),
|
||
再 `node render-check.mjs <该快照>` 跑一遍。**2026-10-01 就是用这招抓出「前前」那个拼接 bug 的。**
|
||
⚠️ 同理:`topics_source.kind='fallback'` 也要**合成**一份样本 —— 现成两份快照都是 `declared`
|
||
⇒ 不合成就**验不到**「没声明过」那条分支(2026-10-01 实测踩着)。
|
||
- `tmp/arch-geom-check.mjs`:内置层高表 `rows` **必须与 `board.html` 同步**
|
||
(R2 84→112→**84** —— 2026-10-01 那天来回过,⛔ 改 `R2.h` 必须同时改这里)—— ⛔ 不同步 ⇒ 几何自检是**假绿**。
|
||
|
||
---
|
||
|
||
## 1 🔴 四条通道(**哪件事走哪条 —— 走错就是今天最大的浪费来源**)
|
||
|
||
| 要做的事 | 走哪条 | 成本 | **为什么必须是它** |
|
||
|---|---|---|---|
|
||
| **派活 / 唤醒会话的首次创建** | **自动化**(宿主排期) | 一个会话 | 🔴 **钩子开不了新会话** —— 自动化是**唯一**通道(宿主硬边界);⚠️ 派活**首次创建**会话走这条是对的,⚠️ 但「**周期性叫醒**」⛔ **不该**走这条(那是常驻投递的活,见下行) |
|
||
| 🔴 **收结果 / 检测** | **直读宿主库**(`automation_runs`,只读 SQL) | **0 token** | 宿主**每次跑完就把它自己的收官结论落库** ⇒ ⛔ 不必轮询、⛔ 不必扫文件、⛔ 不必叫 AI |
|
||
| ~~🔴 **通知主会话(投递)**~~ | ~~**宿主钩子唤起投递**(`collabd.py --tick`)~~ <br>🔴 **2026-10-02 标注(⛔ 不是口径变更)**:**本行已作废** —— 用户 **2026-10-01**:「**协作与投递一直运行(常驻)**」+「定时任务的方案已经废弃了」⇒ 定案=**常驻投递**(0 token · 0 会话 · 1 常驻),钩子只作**补充**(§5-1) | ~~0 常驻~~ | 钩子是**宿主子进程** ⇒ 自带口令 + 不占会话 + 跑完即退;而"需要投递的时刻"**全都伴随会话在动** ⇒ 那一刻钩子必然响(`architecture.md §4.1`) |
|
||
| **机械判定**(文件在不在 / rc / 哈希 / 锁 / 端口) | **本地脚本**(**由钩子按需唤起**) | **0 token** | 这些判断几毫秒可做;塞进"定时叫 AI"是又慢又贵 |
|
||
| **人看进度** | **一个看板**(每轮覆写) | — | 用户只该看这一处 |
|
||
|
||
⚠️ **推论(最容易搞错)**:**"发消息给另一个会话"和"读它的结果"不是一回事** —— 前者要**网关投递**,后者**只要读库**。
|
||
⚠️ **判据**:任何"我需要(或用户需要)持续监控"的诉求 ⇒ **第一反应是「钩子按需唤起本地脚本」**,⛔ 不是自动化、⛔ 也不是常驻。
|
||
|
||
---
|
||
|
||
### 1.1 🔴 与主会话的**双向同步**(⛔ **不需要"监管棒"这个角色**)
|
||
|
||
| 方向 | 怎么做 | 要点 |
|
||
|---|---|---|
|
||
| **主会话 → 程序** | **无需同步**:改 `任务图.json` / 看板 / 产出文件即可,程序下一轮自然读到 | **文件即接口** |
|
||
| **程序 → 主会话** | ① **钩子注入**:`UserPromptSubmit` 时把程序摘要作为 `additionalContext` 注入 ⇒ **用户每次发话就顺手带上最新状态**(**零自动化**)② 需要时主会话主动读实时状态 / `digest.md` | 程序**开不了会话**,但**能把状态送进会话** |
|
||
| **派活** | **仍由会话做**(白名单内) | 只有会话能创建自动化 |
|
||
|
||
🔴 **结论:不需要独立的「监管棒」角色** —— 判断 + 派活**归主会话**;程序负责"**把状态摆到主会话眼前**"。
|
||
⇒ 若发现自己在建"另一个会话来监管",**先问三句**:① 机械判定能否下沉到程序?② 状态能否用钩子注入?③ 派活能否由主会话顺手做?
|
||
|
||
**注入实现要点(照抄)**:
|
||
1. 钩子脚本对 `UserPromptSubmit` **允许写 stdout**(stdout 正是钩子协议通道):输出
|
||
`{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"…"}}`;
|
||
**其他事件仍保持零输出**(⛔ 别破坏原有纪律)。
|
||
2. 顺带把**信号文件**(`VACUUM.md` / `READY.md` / `STALL.md`)的存在也拼进上下文 ⇒ 主会话一眼看到"有东西待处理"。
|
||
3. 注册:`settings.json` 的 `hooks.UserPromptSubmit` **只能追加条目**(⛔ 禁整段覆盖 —— 会抹掉别人的钩子)。
|
||
4. ⚠️ **钩子=会话启动时快照** ⇒ 改完必须**完全重启宿主**才生效(关窗 ≠ 退出);⛔ 别默认它已生效。
|
||
|
||
---
|
||
|
||
### 1.2 🔴 严格队列(**避免打架 · 处理好一个再处理下一个**)
|
||
|
||
> 用户原话:「**严格用队列的方式处理,避免打架,处理好一个,再处理下一个**」
|
||
|
||
**队列与域锁分工不同(两者都要)**:
|
||
- **队列** 管「**谁做下一件**」—— 调度串行
|
||
- **域锁** 管「**能不能动这个资源**」—— 资源互斥
|
||
|
||
| 规矩 | 做法 |
|
||
|---|---|
|
||
| **① 一次一件(调度串行)** | 队列**只呈现一个"队首"**;⛔ 不许一次派多件 |
|
||
| **② 原子取件** | 取件动作 = `mkdir <inbox>/claims/<id>` ⇒ **建不成 ⇒ 别人取走了** ⇒ 换队首/等待(这就是"不打架"的硬保证) |
|
||
| **③ "谁在做"唯一权威** | 取到后写 `claims/<id>/holder`(内容 `<会话名>@<线>`) |
|
||
| **④ 出队=删 claim** | **做完必须删 `claims/<id>`** ⇒ 下一个才可能成为队首(⛔ 不删 ⇒ 该线一直被占 ⇒ 队列卡住) |
|
||
| **⑤ 同线互斥、跨线并行** | 同一条线同时只允许一件;**不同线可并行**(队列按 `line` 判)⇒ 既"不打架"又**不干等** |
|
||
| **⑥ 卡死回退** | claim 超 **20 分钟**未续 ⇒ 程序自动移到 `claims-stale/`(**可追溯,⛔ 不删**)⇒ 队列自动放行 |
|
||
| **⑦ 优先级** | **"在关键路径上游闭包内" ⇒ 优先**(做它能解锁关键路径);⛔ **不能只看"是不是关键路径节点本身"** —— 那会漏掉它的前置,跑去干无关的活(实测踩过:队首一度推荐非关键路径的 N11) |
|
||
|
||
**产出**:程序写 `queue.md`(人看:队首/在做/待办+冲突提示)+ `queue.json`(机读)。
|
||
⚠️ 命令里**别用 ASCII 双引号**写中文文案(会 `SyntaxError`):一律用 `「」`(见 P11)。
|
||
|
||
---
|
||
|
||
### 1.3 🔴 拉取模型(pull):**队列是"拉"的接口 · ⛔ 不要后台任务**
|
||
|
||
> 用户原话:「**不是程序直接调用你,只产生待处理队列,你开个后台任务去获取然后处理**」
|
||
> ⇒ 方向对(把"推"改成"拉"),但**"开后台任务"这一步可以去掉** —— 见下。
|
||
|
||
**为什么这里不需要"后台任务"**:它会**产生任务状态通知/失败通知**,每次都是一次**打断**;叠加"改代码→重启"就变成 P12 那种反复折腾。
|
||
⇒ **正解(本机制):不需要任何后台任务,程序也不调用任何人 —— 只写文件。**
|
||
🔴 **⚠️ 两处口径必须分清(2026-09-30 用户反证后定稿)**:
|
||
1. ⛔ **"卡消息输出"的真因⛔ 不是"任务挂在会话名下"** —— 真因=**会话日志撞 ~10 MiB 被 `dropped`**(实物读数见 `references/pitfalls.md P0-2`)。
|
||
用户反证:挂着后台任务的会话照样能随便发消息。
|
||
2. ⛔ **"后台任务不能用来投递"也⛔ 不成立** —— 它**有口令**、能长期活,是**合法次选**(见 `references/architecture.md §4.1.1`)。
|
||
本机制选"钩子"的理由只是三条**次要优势**:不需容器会话 · **必须活着的进程数=0** · 重启后自动生效。
|
||
|
||
| 角色 | 做什么 | ⛔ 不做什么 |
|
||
|---|---|---|
|
||
| **程序**(**钩子按需唤起**·只读投影) | 只**维护队列**(`queue.md`/`queue.json`)+ 信号文件(`VACUUM`/`READY`/`STALL`)+ 视图 | ⛔ 不派活、⛔ 不起后台任务;⚠️ **投递另归投递(`--tick`)** |
|
||
| **会话** | 在**三个时机主动拉**(见下)⇒ 取队首一件 ⇒ 原子 `claim` ⇒ 派活或自己做 | ⛔ 不轮询(拉的是"被唤醒的时机",不是定时器) |
|
||
|
||
**三个"拉"的时机(够用,且零通知)**:
|
||
1. **用户发话时** —— 钩子把摘要 + **队首** 注入 `additionalContext`(钩子在**会话侧**发生 ⇒ 本质就是"拉")
|
||
2. **每个棒收尾时** —— 顺手读队首 ⇒ 取一件接着做(**收尾自判**,零额外会话)
|
||
3. **需要时** —— 主会话主动读 `queue.md`
|
||
|
||
⇒ **闭环**:程序只"摆件";会话只"取件";**没有任何一方被对方打断**。
|
||
⚠️ 代价(如实说):**没有会话活跃时,队列里的件不会被自动取走** ⇒ 这时才需要"叫一次"(那属于白名单的**接续/派活**,⛔ 不要为此建轮询式自动化)。
|
||
|
||
### 1.4 🔴 主会话必须"**用户不发消息也能自己处理**"(2026-09-29 用户定案)
|
||
|
||
> 用户原话:「**主会话就是要用户不发消息自己能处理,用户发消息是需求上的事**」
|
||
> ⇒ **用户的消息 = 需求输入(新增/变更需求),⛔ 不是推进的动力源。**
|
||
|
||
🔴 **硬边界**:会话被唤醒只有两条路 —— ① 用户发消息 ② **自动化(宿主排期)**;钩子**开不了会话**。
|
||
⇒ 所以"主会话自己会动"的**唯一合规形态 = 主会话自己续自己**:
|
||
|
||
```
|
||
主会话跑完一轮(判断 → 取队首 → 派活/自己做 → 更新任务图)
|
||
│
|
||
└─ 自续期:再排一条一次性自动化,scheduledAt = now + 40 分钟,prompt = 本段原文(逐字复制)
|
||
⇒ 形成「自续链」;⛔ 全节点 done(或用户要求停)⇒ 不再自续
|
||
```
|
||
|
||
🔑 **关键判据**:**"续主会话"属于白名单里的「接续会话」⇒ 免确认** ✅
|
||
(这正是"接续会话"这一类的本义 —— **把链条接上**,包括把主会话这条链接上。)
|
||
|
||
**纪律**:
|
||
0. 🔴 **间隔必须动态**(2026-09-29 用户点破"整套机制都是短的,都要我来说一句你才执行一下"):
|
||
**队列里有可派件 ⇒ `now + 5 分钟`**(有人在等 ⇒ 快叫醒);**无 ⇒ `now + 30 分钟`**。
|
||
⛔ **固定长间隔(如 40 分钟)= 出现"可派未派"窗口**(实测:42~43 分钟毫无变化,用户以为"只有我说话才动")。
|
||
1. 频率上限 5 分钟(别更密 —— 更密就是"轮询式自动化",属非白名单)
|
||
2. **每轮只做一件**(严格队列:取队首一件)⇒ 跑完即退,**⛔ 不留常驻**
|
||
3. **终止条件必须写进 prompt**(全节点 done ⇒ 停),否则会永远续下去
|
||
4. ⛔ **不为"推进"另建轮询式自动化** —— 自续链就是那个机制
|
||
|
||
⇒ **与 §1.3 的分工**:**自续链负责"醒来"**(没人发消息也会醒来);**队列负责"做什么"**(醒来后取队首一件);**钩子注入负责"用户发话时顺带汇报"**(不是动力源)。
|
||
|
||
### 1.4a 🔴 **唤醒回路的判据 + 投递路径**(2026-10-01 用户两条批注 · 现行口径)
|
||
|
||
**① 判据 = 三条件合取**(⛔ 不是计时器):
|
||
|
||
| # | 条件 | 实现现状 |
|
||
|---|---|---|
|
||
| (a) | **主会话 和 执行会话都空闲** | ✅ **已按此收窄**(`_main_state()` ∨ `_anyone_busy()`,排除规则见 ④) |
|
||
| (b) | **队列没有待反馈**(无 `awaiting` ∧ 无 `pending`) | ✅ 已实现(`no_fb`) |
|
||
| (c) | **需求仍未完成** | ✅ 已实现(`goals_open()`) |
|
||
|
||
**② 需求三态 → 唤醒任务**(用户原话:未完成=继续/有阻碍=暂停/已完成=暂停):
|
||
|
||
| 需求状态 | 唤醒任务 |
|
||
|---|---|
|
||
| 未完成 | **继续**(照三条件判) |
|
||
| 🔴 **有阻碍** | **暂停** |
|
||
| 已完成 | **暂停** |
|
||
|
||
✅ 已实现(`goal_stuck` = 只剩 `blocked` 项、没有 `pending`/`running` ⇒ **不叫**)。
|
||
|
||
**③ 投递路径**(用户第二条批注):
|
||
|
||
- ✅ **应**:提交到**执行检查队列** ⇒ 走既有的「**上报 → 主会话处理**」流程(见 §1.2/§1.3)。
|
||
- ⛔ **现状**:唤醒**绕开队列**,直接 `_deliver_str(txt, "上报·唤醒", st)` 投一句给主会话
|
||
⇒ 主会话收到后**无处标完成**(它不在队列里)⇒ **没有闭环**。
|
||
|
||
> ⚠️ ③ 会改**队列语义**(唤醒件算不算一件待办、谁给它标 `done`、会不会淹没队列)
|
||
> ⇒ ⛔ 未擅自改,先把口径钉在这里。
|
||
|
||
**④ 🔴 自指死结 + 解法**(2026-10-01 用户点破 · **本节最要紧**)
|
||
|
||
**死结**:唤醒的就是**这条会话自己** ⇒ 它醒来那一刻,**自己正在跑**
|
||
⇒ 条件①「都空闲」被**它自己**否掉 ⇒ **永远不叫**
|
||
(=醒一次、白判一次、接着睡;用户原话追问「**谁把他顶掉了**」)。
|
||
|
||
用户给的解法(原话):
|
||
|
||
> 「**把自己排除不就行了,通过会话名称前缀区分**,还有**也不用他自己判断,可以通过代码判断**」
|
||
|
||
⇒ 落地两条硬规矩:
|
||
|
||
| # | 规矩 | 实现 |
|
||
|---|---|---|
|
||
| A | **判据在代码里**,⛔ 不让会话自查 | 在 `supervise()` 里算;会话只负责被叫醒后干活 |
|
||
| B | **排除自己** —— 两道**互补,都要** | ① `SELF_SID`(`CODEBUDDY_SESSION_ID`)**精确**:程序跑在会话里时=自己<br>② **名字前缀 `[唤醒]`**:程序在会话外常驻时拿不到 `SELF_SID`,只能认名字 |
|
||
|
||
🔴 **新增第三类名字前缀**:`[唤醒]-[<类别>]-<具体>` ⇒ `parse_session_name()` 返回 `role="waker"`
|
||
(原只有 `[主]` / `[协作]` 两类)。
|
||
⚠️ **2026-10-01 用户口径已扩到「四类」**(新增 **④ 队列上报的跟进会话** ⇒ 第 1 级 `[跟进]`)——
|
||
但**这一类的解析与路由尚未落地**(全库零命中)⇒ ⛔ 本节的 A/B 两条规矩**只覆盖已落地的前三类**,
|
||
第 ④ 类的落地清单一律以 `references/architecture.md §2.3.0c` 为准。
|
||
|
||
🔴 **第四形态:接续会话**(2026-10-01 落 · 详见 `references/architecture.md §2.3.0b`)
|
||
🔴 **定性:它是「形态」,⛔ 不是第 5 类会话** —— 用户原话:「**接续会话 不是单独的一类会话,是这几类会话到达阈值时 创建的接续会话**」
|
||
⇒ **角色继承被接续的那条**(主会话的接续**仍是主会话候选**),⛔ 不要把它当成一种并列的类别。
|
||
—— **由会话自己建出来的下一棒**,标题由**建它的那条会话**写 ⇒ 常写成
|
||
`[<类别>] 接续 · …` 或 `接续棒:…`(**没有角色方括号**)。判据:**含"接续"二字** ⇒ 判 **worker**
|
||
(⛔ **不是主会话**);另把 `主控 · …`(中点分隔、无括号)判 **main**。
|
||
⚠️ **不改会怎样**:旧判据只排 `[协作]` ⇒ 接续棒**进主会话候选**、又因标题含 `[<类别>]` 被认成
|
||
"该类别的主会话" ⇒ **投递把通知投给它自己**(2026-10-01 实测)。
|
||
⇒ **主会话候选的排除判据=「角色不是 worker/waker」**(⛔ 不再写 `[协作] not in t`);
|
||
实现处**两处必须同款**:`collabd.py::parse_session_name()`(权威)/`board.py::_role_of_title()`(镜像),
|
||
已有**真对账用例**在 `selftest.py` 守着。
|
||
🔴 **建接续棒时必须按形态命名**(否则新会话要么冒充主会话、要么在看板上归不了类别):
|
||
`[协作]-[<类别>]-<具体>` / `主控 · <类别> · <具体>` / `[唤醒]-[类别]-<具体>`。
|
||
⚠️ 已知边界:`[<类别>] <具体>` **不带"接续"二字**的仍判**角色未知**(⛔ 不猜),但会被看板点名。
|
||
|
||
⚠️ **实测暴露的另一件事**(2026-10-01):现役会话名实际写成 `主控 · 唤醒机制线 · 棒:…`
|
||
(**中点分隔、无方括号**)⇒ `parse_session_name()` **解析不出来** ⇒ 光靠名字判会漏。
|
||
**这正是 `SELF_SID` 那道排除不能省的原因**(该会话的 id 与 `SELF_SID` 一致)。
|
||
|
||
**⑤ 🔴 唤醒会话的三条硬定性**(2026-10-01 用户**第三次**定义 · **覆盖前两次**)
|
||
|
||
> 用户原话:「**唤醒会话 是独立会话,不要在主会话上处理,是随着需求确定时创建的**」
|
||
|
||
| # | 定性 | 意思(⛔ 别读错) |
|
||
|---|---|---|
|
||
| (a) | **独立会话** | 它**自己一条会话**,⛔ **不是主会话的一个"职能"/一种模式**。⇒ 看板上它是独立一格;判"都空闲"时它**也必须被排除**(见 ④) |
|
||
| (b) | ⛔ **不在主会话上处理** | 叫醒这件事的**执行体是它自己** ⇒ ⛔ 不许把"该它干的活"挂到主会话身上(主会话只管**判断与派活**)。⇒ 那个脉冲任务起在**它**里面,⛔ 不是起在主会话里 |
|
||
| (c) | **随需求确定时创建** | 创建时机 = **需求确定的那一刻**(不是常设件;⛔ 不提前建,没需求不建)。⇒ 看板上「**还没建**」是**正常态**,⛔ 不是故障;此时链路缺口敞开,**如实说** |
|
||
|
||
🔴 **`board_ext.py` 落地**:`_waker_session()` 读 `sessions`(前缀 `[唤醒]`);`_triggers()` 的三态 + tips
|
||
把这三条**写进图例**("还没建"那一支要说明**这是按定性的正常态**)。回归断言:`render-check.mjs`
|
||
两条(「独立会话/⛔ 不在主会话上处理」+「随需求确定时才创建」)。
|
||
|
||
### 1.4b 🔴 **换主会话(交接)的确切触发条件**(2026-09-30 实测,⛔ 别再靠猜)
|
||
|
||
**换不换,只看一件事:`roles` 里登记为 main 的那条,此刻**在不在活会话集合里**。**
|
||
|
||
| 登记那条 | 结果 | 依据 |
|
||
|---|---|---|
|
||
| **还在**活会话里(桌面还开着) | 🔴 **钉死在它身上** —— 新建的主控会话**完全被无视** | `roles:main` |
|
||
| 不在(已关/已切走) | ✅ 立刻按"显式前缀 → 最近活动"换过去 | `prefix:主控` / `workspace` |
|
||
|
||
⚠️ **另一条老会话开着不影响**(只要它不是登记的那条)—— 别把"旧会话还开着"当成不换的原因。
|
||
|
||
**交接的正确姿势(三步,缺一不可)**:① 新标题以 `主控` 开头 ② **先把旧主会话从桌面切走**(⛔ 不是删会话、⛔ 不是杀进程)③ **同时把新主会话打开**(它必须出现在某个网关口口的 `live=` 上,否则投递被拒 `main-not-live`)。
|
||
⛔ **两条主会话同时开着 ⇒ 一定被钉回旧的那条。**
|
||
|
||
🔴 **两个已知坑(待修,改前先看 §9)**:
|
||
- **前缀判据不认方括号**:代码认 `startswith("主控")`,而实际标题常写成 **`[主控]-…`** ⇒ 判 False ⇒ 那条主会话**只能靠"最近活动"兜底**,**本工作区任何一条会话活动更新就会静默把它顶掉**。⇒ 命名一律用 `主控 · …`(⛔ 别加方括号)。
|
||
- **"登记还活着"压过"显式前缀"**:短路顺序是「登记」在「前缀」**之前** ⇒ 旧窗口还开着时新主会话**永远接管不了**。正确优先级应是 **显式前缀 > 登记 > 最近活动**。
|
||
|
||
⚠️ 活会话集合为**空**时(枚举失败)会**反而信任登记**(解析出一条早关掉的会话)—— 不误投(后续仍拒投),但**归因会误导**。
|
||
|
||
⚠️ **一条正在跑的会话会自带一个网关口**(`live=它自己`),**跑完口就消失** ⇒ 别把"某个口曾经 live 过它"当成"现在投得进去"。
|
||
|
||
---
|
||
|
||
## 2 🔴 主会话只做两件(其余下沉)
|
||
|
||
- **判断**:按 `任务图.json` 的判据判"节点是否真的完成"(⛔ 不看自述,**看可核对产物**)
|
||
- **派活**:只派**依赖已满足**的节点,**优先关键路径**
|
||
|
||
**⛔ 主会话不该做的**(做了就是走错通道):持续监控(⇒ 常驻程序)/轮询别的会话干完没(⇒ 读库)/机械核对(⇒ 常驻程序)。
|
||
|
||
---
|
||
|
||
## 3 🧭 防干等 = 显式任务图 + 关键路径(**总工期问题的唯一正解**)
|
||
|
||
`任务图.json`:每节点写 `id / title / line / deps / status / evidence|blocker`,并给 `critical_path`。四条派活规则:
|
||
|
||
1. **只派「依赖已满足」的节点** ⇒ 能并行的**立刻并行**
|
||
2. **优先关键路径**(它决定总工期;非关键路径押后不拖工期)
|
||
3. **一条线同时只挂一个**,**多条线可同时挂**
|
||
4. **收口即派(+3~4 分钟)**,⛔ 不要 +5~8 分钟空窗(🔴 2026-10-01 用户口径:基准 = 收口 + **3~4 分钟**)
|
||
|
||
🔴 **两个必检的浪费信号**:
|
||
- **可派未派**:可派集合非空 ∧ 没有任何棒在跑 ⇒ **有活没人干** ⇒ 立即派
|
||
- **关键路径单线化**:关键路径上只有一个执行主体 ⇒ 把它**拆成更小的可验步骤**,能交给别的线的部分**并行出去**
|
||
|
||
📄 规范 + 模板 ⇒ **`references/taskgraph.md`**(含节点写法与"可派/等待"算法)
|
||
|
||
---
|
||
|
||
## 4 🔴 证据分级:**真成果 ≠ 接续任务**(不区分就会被"空转链条"骗)
|
||
|
||
| 类 | 判据 | 算不算进展 |
|
||
|---|---|---|
|
||
| **✅ 真成果** | 运行记录为**完成且有结论**,**最好有可核对产物/读数**(文件 / rc / 端口 / 数据对照) | ✅ 算 |
|
||
| **🟡 接续任务** | 只是**新增了一条排期**(执行方自己排的下一棒) | ⛔ **不算** —— 只证明「有下一步」,不证明「这步干成了」 |
|
||
| **🏃 刚开始跑** | 有运行记录但**尚无结论** | ⛔ 还不算 |
|
||
| **⛔ 哑火** | 到点却没产生运行记录 | 🔴 **当异常**(链条断了) |
|
||
|
||
⚠️ **"在跑的棒 N 个" 是排期数,⛔ 不是成果数。**
|
||
|
||
---
|
||
|
||
## 5 🔴 新建自动化 = 白名单 + 确认制(否则会退化到"啥都用自动化")
|
||
|
||
**只有两类可不经确认直接建**:
|
||
1. **接续会话** —— 把某条链/某一线的**下一棒接上**
|
||
2. **给其他会话安排任务** —— 派活
|
||
|
||
⇒ **其余一切用途**(监管轮/巡检/检查点/体检/观测/清理/日报…)**必须先取得用户确认**。
|
||
**执行**:建前自问这两问;**答不上 ⇒ 报给用户等确认**(⛔ 不许先建后报)。✅ 删冗余**不属新建** ⇒ 可直接做但须报告。
|
||
🔴 **配额**:一个需求线的常驻自动化 **≤ 2**(1 唤醒 + 必要时 1 截止类)。
|
||
⚠️ 🔴 **2026-10-02 标注(⛔ 不是口径变更)**:这里那个「**1 唤醒**」**只在代偿形态下成立**(常驻停机 ⇒ 用排期顶替时钟);定案的唤醒时钟是**常驻投递**(⛔ 不占排期名额)。
|
||
⚠️ **但"连通知主会话都要靠自动化"是错的** —— 投递走**宿主钩子**(`--tick`),⛔ 不加排期(`pitfalls.md P0-3`)。
|
||
|
||
---
|
||
|
||
## 6 ⚠️ 自愈/守护的硬纪律(都是踩出来的)
|
||
|
||
1. **自愈必须带去抖**:**连续 N 次(≥3)失败才动手** + **冷却**。否则"自愈"会**反复杀掉正在服务的好实例** —— **比故障本身更伤**(实测)。
|
||
2. **⛔ 不用会话后台任务跑长跑**:它每轮输出会**唤醒宿主会话** ⇒ 会话永不空闲 ⇒ 用户看到「卡死」。
|
||
✅ **可行形态**:由**用户在自己的独立窗口**起(`cmd start` / `wmic` / `Start-Process` 常被安全策略拦,⛔ 别在这上面耗时间)+ **完全静默**(只写自己的日志)。
|
||
🔴 **实测补充(2026-09-29)**:**从会话里起的常驻活不长** —— 它会随**其宿主会话结束被回收**(实测:`13:09` 起的 pid 在会话收尾后消失,`13:12` 后再无一轮,单例端口变 `Connection refused`)。
|
||
⇒ **两条一起用**:常驻只当"锦上添花";**机制必须有一条「钩子事件驱动」的腿**(每个会话收尾跑一轮),否则常驻一死,监控/队列/唤醒**全部静默停摆**(而没有人会发现)。
|
||
3. **单例**:常驻程序独占一个本地端口 ⇒ 防重复实例双写。
|
||
4. **fail-open 会掩盖字段错误**:异常全吞 ⇒ 查询写错也"rc=0" ⇒ **必须核对输出非空**,⛔ 不能只看退出码。⚠️ **视图文件要定期 diff 一眼**。
|
||
5. **域锁要切细**:按**节点涉及的文件/子目录**声明;⛔ **禁止整工作区域粗域** —— 那是**自造串行瓶颈**(实测造成多次「抢锁失败 ⇒ 整轮白开」的纯浪费)。
|
||
6. 🔴 **别"改一行重启一次"常驻程序**:用**会话后台任务**起常驻**本身已被禁**(见第 2 条);而每次 kill/launch 都会留下 `failed` 通知 ⇒ **反复打断主会话,把自己弄卡**(实测一个阶段重启 7~8 次)。⇒ **攒批重启**(≥3 处改动一次)+ **规则/配置做成热加载**(每轮读文件)+ **长任务交棒换会话**。详见 **`references/pitfalls.md` P12**。
|
||
|
||
📄 完整踩坑清单(含每条的现象/根因/修法)⇒ **`references/pitfalls.md`**
|
||
|
||
---
|
||
|
||
## 7 落地:三个件(部署顺序)
|
||
|
||
| # | 件 | 作用 |
|
||
|---|---|---|
|
||
| 1 | **执行检查** `scripts/collabd.py --once`(**投影轮**) | 判定(证据分级/推进/真空/哑火)+ 任务图「可派未派」+ 体检 + 视图 + 需求台账。**⛔ 不派活、⛔ 不投递、⛔ 不推进队列** |
|
||
| 2 | **投递** 同文件 `--tick`(**投递轮**) | **逐条读队列 ⇒ 投给主会话**(单条+握手/三条件唤醒)⇒ **唯一投递方+唯一队列推进方** |
|
||
| 3 | **任务图** `任务图.json`(模板见 `references/taskgraph.md`) | 显式依赖 + 关键路径 ⇒ 防干等 |
|
||
| 4 | 🔴 **钩子接线**:宿主在 **`UserPromptSubmit`** + **`PreToolUse`(matcher `^Bash$`)** 上调用本地脚本 ⇒ 脚本内部再跑 `--once` 与 `--tick` | **这是"投递"的全部** —— ⛔ **不需要排期、⛔ 不需要常驻**。钩子是宿主子进程 ⇒ 自带网关口令(能投递)、⛔ 不占会话、零 token。⚠️ **接线必修**:工作区若另存**旧副本**,钩子指向它 ⇒ **回路等于没接**;⚠️ **新加的钩子事件需宿主重启后才生效**(`UserPromptSubmit` 那条立即生效) |
|
||
|
||
**配置**:`scripts/collabd.config.example.json`(workspace / 行名 / 目标件 / 任务图 / 看板 …)⇒ 复制成 `collabd.config.json` 改路径即可复用。
|
||
**用法**:`--once`(执行检查·投影轮)|`--tick`(投递·**投递轮**)|`--report`(执行会话上报)|`--reconcile`(启动对账)|`--where`(路径自证)。
|
||
🔴 **投递真绿的判据**:⛔ 不是 `rc=0`,而是 **`wakeups.jsonl` 新增了一行 `ok:true`**。
|
||
|
||
---
|
||
|
||
## 8 自检(每次派活前问这四句)
|
||
|
||
1. 这件事**非得开新会话**吗?(机械 ⇒ 常驻程序;判断 ⇒ 已有唤醒)
|
||
2. 我要派/建的东西**在白名单**里吗?不在 ⇒ **先问用户**。
|
||
3. **依赖满足了吗**?它**在关键路径上**吗?不在 ⇒ 它能不能押后?
|
||
4. 我是靠**可核对产物**判"完成",还是靠**自述/排期**?(后者 ⇒ 那是"接续任务",⛔ 不算成果)
|
||
|
||
---
|
||
|
||
## 9 🔧 维护 `scripts/collabd.py` 的硬要点(2026-09-29 实测,⛔ 别再踩)
|
||
|
||
**两条实测事实(推翻了原设计的隐含假设)**
|
||
- `automation_runs.status` 实测**全是 `ACCEPTED`,从来没有 `IN_PROGRESS`** ⇒ ⛔ 不能当「有人在跑」信号。
|
||
- 一次性棒跑完 `next_run_at` **归零**(实测全 `0/None`)⇒ ⛔ 也不能当信号。
|
||
- ⇒ **唯一可得的「有人在跑」= `sessions.status='working'`**(取值域:`archived/completed/error/working`)。
|
||
⚠️ 用它判「有没有人在推进」时**必须排除观察者自己**(否则主会话自己跑着 ⇒ 永远报"有人在跑",用户问的"是不是又发呆了"就永远答不出来)。
|
||
|
||
**改本程序的标准手法(⛔ 别手改单点)**
|
||
1. 先备份到工作区 `tmp/bak-collabd-<日期>/`(⛔ 不在技能目录里留副本)。
|
||
2. 用**断言式补丁脚本**:`old/new` 成对替换,**任一锚点 `count != 1` ⇒ 整份不写盘**;写完 `ast.parse` 门禁 ⇒ 杜绝"半改状态"。
|
||
3. ⚠️ **锚点必须先用 `repr()` 核过** —— Read 工具的行号切分与本文件真实字节**不一致**(长行会被折),照抄 Read 的显示会静默失配。
|
||
4. ⚠️ `Path.write_text()` 在 Windows **静默把 `\n` 转成 `\r\n`** ⇒ 写完必比对 `md5sum` 与内存 md5;不一致就按 `newline="\n"` 归一化回 LF。
|
||
5. 🔴 **它不是常驻进程**(由 `UserPromptSubmit` 钩子按需 `--once` 调起)⇒ **改完下一轮钩子即生效,⛔ 无需重启**。
|
||
6. 验证:连跑 `--once` 看 `rc` + `queue.json`/`digest.md`/信号文件(`NEXT`/`STALL`/`VACUUM`/`READY`)是否与预期一致。
|
||
|
||
**队列的四个部件(各有唯一权威)**
|
||
- `claims/<id>/holder` = `<会话名>@<线>@<完整会话id>` —— 「谁在做」的唯一权威;**第 3 段别省**(有它才能判持有者是否已终结 ⇒ **立即出队**;没有就只能等 20 分钟超时,期间队首会僵尸化并**反复复活**)。
|
||
- `blocked.json` = `{"<节点id>": "<原因·谁在等>"}` —— 受阻件 ⛔ 不当队首(否则每次唤醒都白跑一遍同一个受阻件)。**解除后记得摘掉**。
|
||
- `gate-done.stamp` + `claims` 缺失即 `gate=free`:闸门靠**钩子事件**驱动,⛔ 不靠轮询。
|
||
- 🔴 **「同线互斥」取 `holder` 的第 2 段**(线名)** —— 取第 1 段(会话名)会恒不相等 ⇒ 该机制**静默失效**(实测:同一条线可被同时派多件)。
|
||
- 🔴🔴 **「算不算做完」以需求台账(`tmp/supervise-inbox/tasks.json`)为准,⛔ 不是以任务图节点为准**(**2026-10-01 加**)。
|
||
病根:**任务图是纯手维护件 —— 全仓无任何代码回写它**(实测 `TG` 在 `collabd.py` 里只于 3 处被**读**,无一处写),
|
||
而会话「上报完成」只写台账 ⇒ **两处必然漂**,且症状极隐蔽:`claims/` 是空的、没人占线,
|
||
但**队首出不来** ⇒ 它被反复当"可派未派"喊,`READY.md`/`STALL.md` 每小时喊一遍同一件,控制台判「目标未完成」。
|
||
实测(M5):台账 `done`、上报单 `done`、产物已核对(`wakeups.jsonl` 第 82 行 `http:200 ok:true`),
|
||
任务图仍留 `running` ⇒ **整条链被一个早已做完的件卡死**,`goalctl` 只会说「目标状态 = **未完成**(任务图 M5)」,⛔ 不告诉你是"早就做完了"。
|
||
⇒ **现行口径**:`图 done ∪ 台账 done` 都算 done(`deps` 解析同理,前置满足即算满足)。
|
||
⚠️ 实现落在**两处、必须同口径**:`collabd.taskgraph()` 与 `goalctl.goals_open()`(⛔ 只改一处 ⇒ 控制台与看板两边打架)。
|
||
⚠️ **不改任务图本身** —— 它仍是「分解件」,人可手改;只让"算不算做完"以台账为准。
|
||
- 🔴 **目标「三路全过」⇒ 终态:⛔ 不产任何告警**(**2026-10-01 加**)。原实现只认 `goal.json.run != active`,
|
||
目标是全过了、`run` 却仍是 `active` ⇒ 照产 `STALL`/`VACUUM`/`READY` ⇒ **把「做完了」读成「坏了」**
|
||
(实测:M5 解卡后立刻喊「卡住:有会话在跑但 52 分钟无成果」)。
|
||
⇒ 现口径:`--once` / `--tick` 在 `goal_paused() or not goals_open()` 时走 `paused_round()` ——
|
||
只留一行**终态说明**、清掉四个信号投影、⛔ 不投递;**看板也一并收口**(否则它会永久停在最后一张"还在跑"的快照)。
|
||
|
||
**🔴 它到底什么时候才会跑(最容易误解的一条 · 2026-09-30 更新)**
|
||
- `--once`(投影)挂在 **`UserPromptSubmit`**(节流 180 s)+ **`PreToolUse ^Bash$`** 上 ⇒ **有人发话、或任何会话跑一次 Bash** 就会被调起。
|
||
- `--tick`(**投递**)挂同样两个事件(节流 120 s)⇒ **投递与"有事发生"同刻**。
|
||
- ⇒ **不再需要靠宿主排期"让机制活着"**(旧结论「没有宿主排期 = 确定性静默」**已作废**)。
|
||
⚠️ **剩下的唯一真相**:**"全员静止"期间**(没人发话、任何会话都不跑 Bash)**不会有通知飞出去** —— 那是**为摆脱排期而明确接受的代价**;
|
||
停滞类信号由**会话外**的守护落 `STALL.md`/`NEED-USER.md`,**下一次任意宿主事件时补投**。
|
||
|
||
**`idle`("多久没成果")只认「真成果」**:服务探针的状态变化属**服务层** —— 既不重置 `idle`、也不进 `🟢`。
|
||
(实测教训:4 小时静默里程序那唯一一轮恰好赶上探针「不通→通」⇒ 摘要写「1 分钟无成果」而不是「4.5 小时」⇒ 连一声告警都没有。)
|
||
|
||
**受阻 ⇒ ⛔ 不能静默**:选不出队首但 `blocked.json` 非空时**仍写 `NEXT.md`**(受阻通报)—— 否则就是「有活 ∧ 谁都动不了 ∧ 程序一声不响」。⚠️ 写 `NEXT.md` 时**内容不得含时间戳**,否则唤醒回路的**内容哈希去重**失效 ⇒ 每轮都投一遍(刷屏)。
|
||
|
||
**唤醒回路的两个触发面**:① `NEXT.md` 存在且 `gate=free`;② `NEXT.md` 不存在但**停滞 / 可派未派**(哈希须用**粗粒度键**:种类 + 空闲按 30 分钟取整,⛔ 不能用带时间戳的正文)。
|
||
|
||
**🔴 链条不能只靠「上一棒派下一棒」**:棒中途死掉(实测发生过:探针回环打到自己会话)⇒ 链条**静默断掉**,只能等下一次有人发消息。
|
||
⇒ ⛔ **不要靠发明新件来兜底**(我发明过"+30 分钟看门狗",已废弃)。**正解在架构原文里**:**收尾自判**(在**已存在**的会话里判本线缺口并写下一行 `automations`)+ **每小时兜底唤醒**(定稿 §3②,专防链条断掉)。改前先过 §10。
|
||
|
||
---
|
||
|
||
## 10 🔴 改本机制前必须过的「防打转判据」(架构原文 · 顶层设计 §7)
|
||
|
||
> 原文原话:**"以后凡是给这套系统加东西,先过这张表 —— 只要让『必须存在的东西』或『需要记得清理的东西』变多,就要停下来重新想,而不是继续补。"**
|
||
|
||
| 判据 | 目标 |
|
||
|---|---|
|
||
| 自造件数 | **≤3**(机械判定脚本 / 看板 / 规则文档) |
|
||
| 🔴 **自造协议数**("需要记得清理"的) | **0** —— ⛔ 不自造 `mkdir` / flag 去抖:**域锁就是单例** |
|
||
| 必须活着的**进程**数 | **0**(⛔ 不用会话后台任务) |
|
||
| 必须存在的**会话**数 | **0** |
|
||
| **额外会话 / 棒** | **0**(收尾自判在**已存在**的会话里完成) |
|
||
|
||
**四条地基(顶层设计 §1/§2)**:① 状态**只读宿主库**(`mode=ro`)② 调度**只靠 `automations` 一行**(⛔ 不自建调度器)③ 互斥**只用既有域锁**(⛔ 不自造第二把锁/第二套去抖)④ 执行体**无状态**(会话死了状态不能跟着死)。
|
||
**正确形态 = 2 个声明式操作 + 1 份规则**:**派活** = 往 `automations` 写一行(不"叫会话");**收结果** = 读 `automation_runs`(⛔ 不轮询、不催、不问)。⇒ **「监管者」这个角色不存在**,只有**两个动作**:**收尾自判**(在已跑的会话里)+ **兜底唤醒**(⚠️ 原文写「宿主排期」—— 🔴 **2026-10-02 标注(⛔ 不是口径变更)**:定案的兜底唤醒走**常驻投递**,排期只是**代偿**,见 §5-1)。
|
||
|
||
**🔴 权威清单(改任何判定前先看它 —— 权威只在这四处)**
|
||
|
||
| 要判什么 | 权威(唯一) | ⛔ 不许 |
|
||
|---|---|---|
|
||
| 谁在跑 / 哪条线被占 | `sessions.status='working'` + 其 `cwd`(末段=线名) | 自己写 `holder` 文本再解析 |
|
||
| **多久没成果** | `automation_runs.updated_at`(毫秒 epoch) | 自己维护 `last_progress_at` 时钟 |
|
||
| 有没有排期(会不会有人自动跑) | `automations.next_run_at` **未来 1 小时内** | 看"全库有没有排期" |
|
||
| 一棒做完没有 / 结论是什么 | `automation_runs`(`thread_title`;⚠️ `thread_id` 形如 `<aid>:1`,**不是**会话 id) | 轮询别的会话 |
|
||
|
||
⇒ **自己写的文件只有两种合法身份**:**投影**(可删可重建:看板 / `NEXT.md` / `queue.json`)或**人工输入**(如 `blocked.json`)。⚠️ 凡"需要记得清理"的一律不许(`claims`/TTL/僵尸目录都属此类)。
|
||
|
||
---
|
||
|
||
## 11 🔴 术语表(**说话/写文档一律用「正式名」这一列**)
|
||
|
||
> 2026-09-29 用户指出"宿主是什么/机械程序又是什么,之前只提过**投递**和**协作程序**" —— 属实:**同一个程序在文件里有 4 个名字**,图当然读不懂。**以本表为准。**
|
||
|
||
| 正式名 | 实体(在哪) | 干什么 | ⛔ 别再叫它 |
|
||
|---|---|---|---|
|
||
| **宿主** | WorkBuddy 本体 | 提供五样:状态(只读 3 表)/**调度**(自动化排期)/**事件**(钩子)/执行体(会话)/互斥(域锁) | 「平台」「服务端」 |
|
||
| **投递** | `advance-watch.py`(`.workbuddy/tools/`) | 零 token 机械判定:直读 `automation_runs`、判靶点/锁,写 `advance.md` + `needs-ai.json`。⛔ 不派活/不开会话/不联网/不读口令 | 「机械层程序」「机械程序」「看门狗」 |
|
||
| **执行检查** | `collabd.py`(自述"协作守护程序") | 队列闸门(`NEXT.md`)+ **唤醒投递** + 证据分级/真空/断链告警/体检/看板覆写。**⛔ 不做派活** | 「判定层」「常驻程序」「机械层」 |
|
||
| **会话** | `sessions` 表 | 执行体:「读—判—写」;**一次性**、可替换 | —— |
|
||
| **棒** | 一次派活 = 一个会话 | 同一件事的执行单位(「一棒一线」) | —— |
|
||
| **看板** | `scripts/board.py --serve` → `assets/board.html` | **用户唯一入口**(只读旁路 · 异步快照) | ⛔ 别把 `digest.md`(机械摘要)当用户入口;⛔ 也别再提已退役的 `交付物/手机接入-实时状态.md` |
|
||
| **设备接入 · 本地反代** | `dsh-client` · 真身 `@dsh-client/device-shim`(`127.0.0.1:20090`) | 只绑回环的 HTTP+WS 反代,**在进程内注入本机 DSH 的会话凭据**,供覆盖网络中继从设备入口访问桌面 DSH。⚠️ 该包已标「已退役」,计划并入 `@dsh-local/ai1net` 的第 6 块模块**M6「设备接入」**,但**尚未落地**(仓库里还没有 `ai1net` 包) | ⛔ 别再叫它「**垫片**」「shim」「薄垫片」 |
|
||
| **覆盖网络节点 · 中继客户端** | `dsh-client` · `<Repo>/lib/net/relay/main.js --client`,由 `overlay-node-daemon.ps1` 监护 | 本机作为**节点**接入覆盖网络:向中继注册(`registered host=… accepted=[20090]`)、维护长连、转发流 | ⛔ 别再叫它「**设备侧 worker**」「worker」「节点进程」 |
|
||
|
||
### 11.1 🔴 命名铁律(用户 2026-09-30 明令)
|
||
|
||
> 用户原话:「垫片 20090 在线,设备侧 worker 在线:**禁止用这么抽象的词**,
|
||
> 用 **系统-模块-功能名**(系统-功能名)」
|
||
|
||
- 🔴 **凡涉及外部系统/组件,一律写成「`系统` · `模块` · `功能名`」** ——
|
||
系统=它属于哪个系统/仓库(如 `dsh-client`、`ai1net`);模块=在这个系统里算哪一块(如 **设备接入**、**覆盖网络节点**);
|
||
功能=它干的活(如 **本地反代**、**中继客户端**)。简写时至少保留 **`系统 · 功能名`**。
|
||
- ⛔ **不许自造简称**("垫片"/"worker"/"两条腿"/"有腿掉了" 这种),**⛔ 也别只报端口或只报英文包名**当名字。
|
||
- ⚠️ **报"在线"必须说清是哪一段的什么状态**,且 **⛔ 不许由"两段都在线"推出"链路通过"** ——
|
||
实测教训:中继客户端 `state=up` 而 `streams=0`(**从未有请求真正被转发**)⇒ 那时写"这条链是通的"=**过度断言**。
|
||
⇒ 判「端到端通过」的**唯一判据**是**中继真转发过流**(`streams>0`);否则一律写「**未验证**」。
|
||
- 📌 落地处:`board.py` 顶部有本铁律的注释块 + `front` 字段全部改用真名;看板「链路前置」卡按真名渲染。
|
||
|
||
> ⚠️ **以下为 2026-09-29 的史实快照**(当时的"现状打架"),**⛔ 不是现状**;现状以 `architecture.md` 头部「当前结论」为准。
|
||
|
||
1. ~~`advance-watch.py` 死活之争~~ —— `协同监管棒-SOP.md §276` 说它「已吸收退役」,而 `顶层设计`/`定稿`/`实施方案`/`CODEBUDDY.md` 仍把它当活件。**已随退役件一并作废。**
|
||
2. **同一程序多个名字**:任务图叫「协作程序」、它自述「协作守护程序」、原技能叫「机械层/常驻程序」、实施方案叫「机械层脚本」⇒ 统一口径见 `architecture.md §7 术语`。
|
||
3. ~~同一程序 2 个路径~~ —— 旧副本 `~/.workbuddy/skills/multi-session-collab/` **已于 2026-10-01 归档退役**,现只剩一份:本包 `scripts/collabd.py`。
|
||
|
||
---
|
||
|
||
## 12 文档权威(治"架构都是乱的" —— 原技能时代的问题)
|
||
|
||
> 🔴 **本节原声明「本技能 = 协作机制的唯一权威」—— 已作废**。并入本包后,**唯一权威是 `references/architecture.md`**(见本件头部)。
|
||
|
||
**当时的病**:曾有 **5 份"架构"并存且互不认** —— `交付物/多会话协同机制-定稿`/`…顶层设计`/`…实施方案`/`协同监管棒-SOP`/原技能本身;
|
||
实测出 **5 处互相矛盾**(一棒做完怎么办 / 「监管棒」算不算角色 / 投递的死活 / 钩子生效了没 / 钩子调的是哪一份程序)。
|
||
|
||
⇒ **现已收口**:架构**只认 `architecture.md`**;那 4 份业务件 **2026-09-30 已加退役标注**,可作背景,⛔ 不作用判据。
|