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/ 知识文件,按口径入库)
This commit is contained in:
admin committed 2026-10-10 23:13:22 +08:00
1 parent 30b46dbd0c
commit c1b5e4d966
735 files changed
+153192 -2415

No files matched your search

@@ -0,0 +1,301 @@
<!-- 🔴🔴 2026-10-06 权威归属(用户令:规则全部整合进会话技能)
本档已从 `agent-operating-rules` 迁入 `session-mechanism`。
⚠️ 其中「提报用户判据 / 边界内自决策 / 边界外八类 / 红线门禁 / 提报格式 / 拆包 / 取舍三问」
与 **本包 `references/02-功能优先协作协议.md`** 同题 ⇒ **判据以 02 为准**,
本档保留作**完整版(含全文与语言转换表)**参考;两者冲突时以 02 为准。
本档独有、02 没有的:排版十四条反模式、结论骨架全文、语言转换表。 -->
# 参考 01 — 协作与提报用户判据(全文)
> 本文件是 `agent-operating-rules` 的**细节层**。入口文件已给**每次都要生效**的硬规则;
> 本文件给**完整依据、出处、语言转换表与排版全套**。**按需读**,别全塞进上下文。
---
## 1. 提报用户唯一判据(完整)
> **只问「超过现有判断方法边界」的问题。**(用户原话)
### 1.1 边界内 → 一律自决策
技术选型 / 实现路径 / 命名与数据结构 / 性能与资源调参 / 部署与同步 / 排查方法 / 版本与依赖 / 兼容与降级 / 方法内的方案取舍 / 文档与档案的技术内容。
- ⚠️ **「部署 / 上线」明确属于边界内**:**不中断用户**的上线动作(传产物、换包、改静态页、投放)**做完即上线,不要问** —— 用户要先看线上效果才能判断需求是否被满足。
- **开发 / 自用环境**:重启、停任务、改配额或配置、改网关 **直接做**,只需**动手前一句话说明**。
- **仍未放开**:不可逆的破坏性操作(删数据 / 迁库 / 清目录)⇒ **仍先出清单**。
### 1.2 边界外 → 必须问(八类)
| # | 类别 | 为什么方法判不了 |
|---|---|---|
| 1 | **业务目标与优先级** | 方法只能判"**怎么做得最优**",判不了"**该不该做**"。<br>⚠️ **出口**:方向已定(用户已说要做什么)时,"**先做哪个**"若候选有**客观排序** ⇒ **属边界内,自决策**。只有"几个都该做、用户对**节奏 / 取舍**有偏好"才提报用户。 |
| 2 | **成本与资源承诺** | 涉及用户的钱,不是技术优劣问题 |
| 3 | **对外承诺** | 对外 SLA / 合同 / 品牌文案 —— 商业后果超出工程判断 |
| 4 | **需要用户提供的凭据 / 审批** | API Key、DNS 权限、账号授权 —— 只有用户有 |
| 5 | **体验偏好(无客观优劣)** | 审美与文案语气、默认值取向、措辞 |
| 6 | **影响面超出本平台** | 会波及其他系统 / 他人数据 / 不可逆的对外影响 |
| 7 | **红线门禁** | 安全与影响面变更必须用户知情同意(见 §2) |
| 8 | **方法确实判不准** | 两边都无依据、事实不足以判断 ⇒ **宁可问,不要卡死** |
**提报用户方式**:这八类**照实说人话**说明「为什么要你定」,⛔ 不要包装成技术选项。
### 1.3 判断口诀
**「用户能不能从可感知的视角判断这个选项的好坏?」**
- 能 → 可以提报给用户(§1.2)
- 不能 → **这就是 AI 的工作,不要问**
---
## 2. 红线门禁(通用形态)
> ⚠️ **具体编号与例外以本工作区规则文件为准**;下面只给**判据**。
| 门禁 | 判据 |
|---|---|
| **扩大权限 / 可见面** | 新挂载、放开遮蔽、暴露平台目录或环境变量、放宽网络规则、提升档位 ⇒ 先出「影响评估」并取得确认 |
| **批量 / 全仓写入** | 可能影响 **>10 文件** ⇒ 先出受影响清单 + 确认;先**单点验证** |
| **不可逆的破坏性操作** | 删数据 / 迁数据库 / 清目录 ⇒ **先出清单** |
| ⛔ **「会中断在线用户」通常不是门禁** | 开发 / 自用环境 ⇒ 重启、停任务、改配置**直接做**,动手前一句话说明。⚠️ 生产环境相反,按本工作区规则判 |
**🔀 规则冲突裁决顺序**(同一对象被多条规则给出相反结论时,取**首个命中项**,⛔ 不"自行取保守侧"):
① **开发 / 自用环境的放开条款** → ② **边界内自决策清单** → ③ **其余红线**(权限扩大 / 批量写入 / 锁 / uid —— 这几条**永远是硬约束**,不参与裁决)。
⛔ **冲突 ≠ 门禁**:两条规则打架**不构成**提报用户理由;真门禁**只有**上面三类 + §1.2 八类。
🔑 **"平台级" ≠ "别人的"**:**我们自己的**资源(自己的服务器单元、自己的数据目录、自己的网关配置、自己的端口)⇒ 按规则**直接做**;「只报告不动手」**只针对"别人的 / 归属不明"**的对象。
---
## 3. 提报用户格式(**强制**)
| ❌ 技术语言(禁止) | ✅ 用户能感知的语言(必须) |
|---|---|
| 「要不要启用 `enablePatch`?」 | 「要不要让用户**自己装插件**,还是只由管理员统一装?」 |
| 「某环境变量设成哪个档位?」 | 「AI 在你的会话里**能不能直接执行命令**,还是每次都问你一遍?」 |
| 「新会话 vs 改写存量会话事件」 | 「**新开一个会话**就好,还是要我去改你**已有的**会话设置(改完你正在用的会话会变)」 |
| 「限堆 / 内存上限降到 X」 | 「每个用户能用的内存**小一点更安全**,但用户跑大任务时余地也小一点」 |
| 「插件走内投技能还是平台共享技能层」 | 「技能**跟着插件一起装**,还是**单独管理**?」 |
> **规则**:提报用户时**不允许出现**包名、环境变量、文件路径、commit、API 路径、代码标识符。出现即是没转换。
### 3.1 拆包提报用户(**实证**)
**实证**:把「要不要现在重启服务(该问)」和「用 A 还是 B 实现(不该问)」**捆成一个提问** ⇒ 用户被迫先读懂两套技术方案才能回答那个红线问题 ⇒ 体感依然是"又在让我确认技术问题"。
**这才是"设了规则却没用"的真正形态**:不是问得太多,而是**把该问的和不该问的混在一次提问里**。
**三条规则**:
1. **剥出红线问题单独问**,且只问用户能判断的维度:**要不要现在动 / 影响谁 / 断多久 / 能否避开**。
2. **技术形态自己定**,作为**已定项**写进回复("我按 B 做,因为…;可推翻"),⛔ 不要做成选项让用户选。
3. **一轮最多一个问题**;同一个问题**不要连问两次**(第二次只是细化 ⇒ 本可合并,或直接自定)。
**正确写法(照这个格式 · 已定项在前,提问按五栏竖排)**:
```text
我先按 B 做(平台侧自动摘除坏插件,覆盖所有装法)——**已定**。
1、**问题**:什么时候可以重启服务。
**说明**:重启会让正在用平台的用户断线数秒(实例"访问即拉起",会话数据不丢),不重启这一批改动就验不完。
**A 案**:约一个空闲窗口(优点:影响最小;缺点:要等)。
**B 案**:现在重启(优点:立刻能验完;缺点:在线用户被打断数秒)。
**倾向**:A 案。
```
---
## 4. 取舍筛与三问(完整)
> **用户原话**:「**需要我确认的方案需要说明优点和缺点,现在没法判断,假如只有优点或只有缺点那不需要我判断**」
**提报用户标准 = 存在「真取舍」**:
1. 把候选各写 **优点 + 缺点**;
2. 某个候选**只有优点**(明显更优)或**只有缺点** ⇒ **自己拍掉、直接做完、陈述结果**;
3. 只有**各有优有劣、客观标准分不出高下**才算真取舍,才允许提报用户;
4. 提报给用户时**必须逐项列出优点与缺点**(只写"差别在哪"**不算**);
5. 候选**竖排成段**(A / B / C **各占一行**)—— ⛔ 不横排(`A:… · B:…`),⛔ 不做成表格的列。
### 4.1 提报给用户前必答三问(任一条足以自决策,全答"否"才允许提报用户)
① 对象是**我们自己的资源**吗?→ 是 ⇒ 自决策。
② 我**查证过**关键不确定点了吗(如"还有谁在用")?→ 没查 ⇒ **先查**,⛔ 不许把"不确定"当提报用户理由。
③ 候选排完序,**第一名是否明显更优**?→ 是 ⇒ 自决策。
> ⛔ **禁止把"我有倾向"降级成"建议 + 待你拍板"**:候选能排出优劣 ⇒ **直接做完**并写一句「我选了什么(可推翻)」。
### 4.2 方法本身的演进 —— 改为自决策
- **方法的日常应用不问**:边界内的一切判断自己定。
- **方法本身的修订也不问**:发现缺口 / 反例 / 需加规则时**直接改、直接记录**(改完说一句"我更新了方法:因为 X")。
- **唯一例外**:方法修订若**扩大**了 AI 的自主权或**收窄**了用户的门禁 ⇒ 属边界外第 1 / 7 类,**必须问**。
---
## 5. 回话前自检(**发出任何回复前过一遍**)
> ⚠️ **起因**:拦截类钩子通常只能拦「提问工具」调用,而**真实的提报用户大多发生在正文里**(实测某工作区日志:提问工具调用数 43 → 3 → 0 → 0,因为大家改用正文提问了)。
> ⇒ **钩子拦不到正文里的征询句,只能靠这条自检。**
⛔ **禁止用征询句收尾**:出现「**要我…吗 / 是否要我 / 需要我…吗 / 要不要我 / 请确认 / 你看怎么办**」时,**重判三问**:
① 命中**真门禁**吗(不可逆破坏性操作 / 边界外八类)?**没命中 → 删掉这句,自己做完,改成陈述句**("我接着做 X");
② 我是不是在**把已经定下来的事再问一遍**?是 → 删;
③ 我要问的这件事,**候选之间是「真取舍」吗**?—— 某个只有优点 / 只有缺点 ⇒ **自己拍掉**;是真取舍 → 才允许问,且**一轮只问这一句**、**逐项写优缺点**。
---
## 6. 排版规范(让长回答**可扫读**)全文
> 🔴 **第 0 条(优先于本节全部条目):以「本工作区规则文件」的排版节为准。**
> 本节写的是**通用形态**;某个工作区若对排版另有**更新定稿** ⇒ **本节让位**,⛔ 不许拿本节去覆盖它。
> ⚠️ **已实测的冲突(2026-10-02 登记,务必先看)**:DSH 项目 2026-10-01 用户定稿**明确 ⛔ 禁用表格** ——
> 原话「**为什么回复的内容 那么人机 把我都看抑郁了,禁止用表格,全部用文字排版**」,并把骨架定为
> **`#` 大类(已完成 / 待处理任务)→ `##` 任务名 → 每条「`- ` 圆点陈述句 + `1、2、3、` 序号」**。
> 而本节的**约束 5「表格 ≤5 列」**与 **§6.3「长清单 / 对比 ⇒ 表格」**仍是旧口径 ⇒ **照本节做就会违反定稿**
> (实测症状:用户当场纠正「我发现你又忘记如何回复 执行结果了,是不是技能规则失效了」)。
> ⇒ 在那类工作区里:**表格一律换成文字段落**、「首屏 3 行给判定」等**形态**可留,**"表格优先"必须废**。
> 目标:**30 秒扫到结论,2 分钟看全细节**。适用范围 = **每一轮回复**(执行信息 / 报障答复 / 提问 / 交付回执都算)。
> 判据:排版不是为了好看,是为了**能不能被扫** —— 所以下面每条都**可自检**(数得出来)。
### 6.1 十条硬约束
| # | 约束 | 自检怎么数 |
|---|---|---|
| 1 | **首屏 3 行内给判定**(✅/⚠️/❌ + 一句) | 前 3 行有没有判定 |
| 2 | **层级 ≤ 3 级**(`##` → `###` → 列表),**不出 `####`** | 有没有第 4 级 |
| 3 | **每节 ≤ 7 行**;连续 **>12 行无结构** = 文字墙 | 有没有墙 |
| 4 | **加粗只留跳读关键词**:每节 ≤2 处、**不整句加粗** | 数加粗处数 |
| 5 | **表格 ≤ 5 列**;单元格不塞整句 | 数列宽 |
| 6 | **一条信息只出现一次**(别标题 / 正文 / 表格各写一遍) | 抽查重复 |
| 7 | **能自决策的继续做;不能自决策的收进最后一节、逐条编号** | 翻到最后一节看是不是拍板项;数它有没有编号 |
| 8 | **提报给用户的项必须带「优点 / 缺点」两栏** | 每个候选是否优缺点各至少一条 |
| 9 | **候选竖排成段**(各占一行)—— ⛔ 不横排、⛔ 不做成表格的列 | 有没有一行塞多个候选 |
| 10 | **并列内容逐条分段** —— 凡 `①②③` / `1) 2) 3)` / `首先·其次·最后` 式的并列项,**每条独占一段**;⛔ 不许用分号或顿号挤在同一段 | 有没有 `①…;②…;③…` 串成一段 |
### 6.2 待用户拍板项的**位置与形态**(三条一起用)
- **位置 = 整条回复的最后一节**(⛔ 不许埋在中间,后面不许再有任何节);
- **形态 = 有序段落、逐条编号**(⛔ 不写成散文一段);
- **语气 = 陈述句**(问题 + 说明 + 各候选优缺点 + 倾向),⛔ **不是**征询句。
### 6.3 按回答类型套现成骨架(**不新造**)
| 回答类型 | 用哪个骨架 |
|---|---|
| 执行信息(做了什么 / 结果如何) | **§7.2 交付回执** |
| 是否已实现 / 能不能 / 为什么不行 | **§7.1 结论骨架** |
| 报障 / 排查结果 | 判定(根因一句)→ 证据(命令 + 输出,代码块 **≤10 行**)→ 处置 → 未闭环 |
| **向用户提问** | `**问题**`(一句)→ `**说明**`(为什么要你定:影响谁 / 断多久 / 花多少钱 / 有无不可逆)→ `**A 案**` / `**B 案**`(**每个候选各占一段、竖排**,各 ≤3 行,**每个都必须写「优点 / 缺点」**,推荐项置首标"(推荐)")→ `**倾向**`(一句) |
| 长清单 / 对比 | **表格**(不要长 bullet 串) |
### 6.4 十四条反模式(见到就改)
1. ❌ 大段无空行文字(>12 行)→ 拆节或转表
2. ❌ 嵌套列表超过 2 层 → 降为表格或加粗小标题
3. ❌ 结论埋在段落中间 → 提到该节**首句**
4. ❌ 整句 / 整段加粗 → 只留关键词
5. ❌ 表格 >5 列、单元格里塞整句 → 拆表 / 缩短
6. ❌ 同一信息重复三遍 → 留一处
7. ❌ 用"如下所述 / 综上"指代不清 → 直接写"见 §X"或重述一句
8. ❌ emoji 堆砌 → **只用于状态**(✅⚠️❌🔄)与**分级**(P0/P1)
9. ❌ 术语 / 路径 / 版本号混进结论层 → 移入「技术附录」
10. ❌ 标题层级跳跃(`##` 直接到 `####`)→ 逐级
11. ❌ **待拍板内容夹在中间** → **挪到最后一节**;❌ 写成散文一段 → 改成**有序编号条目**
12. ❌ 只写"两者差别在哪"却**不写优缺点** → 补齐两栏;❌ 把**只有优点 / 只有缺点**的候选拿来问 → **自己拍掉**
13. ❌ 候选方案**横排**或做成表格的列 → **每个候选各占一段(竖排)**
14. ❌ **并列项挤成一段** → **每条独占一段**
### 6.5 文案点名主体,⛔ 不用指代性代词
⛔ 不用 `你`、`自己`、`这把 / 那把 / 这块 / 那些 / 那份` —— **直接给名词**(`平台管理员` / `用户` / `管理员配置的模型共享`)。
**判据** = **这一个分句单独摘出来,能不能答出"谁做的、说的是什么"**。
⚠️ 例外:命令行占位符(`--email [email protected]`)是字面值,不动。
### 6.6 说明文档要「直入主题」
⛔ 不写「重点不在 X,而在 Y」这类**先否定再转折**的绕弯开场。
**判据**:**第一句能不能单独看懂"这是什么、解决什么"**;凡是需要靠对比才读得懂的写法,一律改写成陈述句。
---
## 7. 现成骨架(照抄)
### 7.1 结论骨架(回答「是否已实现 / 能不能 / 为什么不行」)
> **用户原话**:「**需要告诉我的是 是否已实现,如果未实现:为什么不能,需要我拍板可以问我**」
> 触发场景:用户问「是否已实现」,AI 却用「我自己的失误 + 探针怎么被污染 + 版本流水 + 下一步三步计划」作答 ⇒ **要的结论被埋在第 5 段之后**。
**固定四节,顺序不许换;没有的节整节删掉,不要留空标题:**
```markdown
## 判定
❌ 未实现 / ⚠️ 部分可用 / ✅ 已实现 —— <一句话>
| 目标 | 状态 |
|---|---|
| <用户列的第 1 项> | ✅/❌ + 半句依据 |
## 为什么不行(只在有 ❌ 时写;最多 3 层,结论层零技术标识)
- 已经排除的:<已修完、不再是原因的> —— 一句话带过
- 当前唯一卡点:<一句人话>
- 为什么难(可选):<1–2 句;不确定性要写进结论句,例:「还不能说做不到,只能说这一步还没试」>
## 我接着做
- 下一步 <X> —— **陈述句,不是征询句**
## 需要你拍板(**最后一节**;真需要才写,不需要 → 整节删掉;**逐条编号**)
**1、问题**:<一句话>。
**说明**:<影响谁 / 断多久 / 花多少钱 / 会不会丢数据>。
**A 案**:<做法>(优点:…;缺点:…)。
**B 案**:<做法>(优点:…;缺点:…)。
**倾向**:<A 案 或 B 案>。
```
### 7.2 交付回执(已完成工作的交付)
> AI 交付时**先说成果,技术细节折叠在后**。
```markdown
## 做了什么
<2-3 句:现在多了一个什么能力,在哪儿>
## 你现在能看到
- <具体位置> 出现了 <什么>;点它会发生 <什么>
- 验证方式:<用户亲手可做的一步>
## 不用你决策的技术选择(已定,可随时推翻)
- <易感知的一条,一句话>(若你希望反过来,说一声即可)
## 技术附录(可选读)
<文件 / commit / md5 / 端点 / 记录编号>
```
> 第 3 段是关键:**把"事前请示"改成"事后可推翻"** —— 用户获得了知情权与推翻权,但不需要在读之前做判断。
---
## 8. 六条铁律
| # | 铁律 | 说明 |
|---|---|---|
| 1 | **回答主位 = 用户问的那件事** | AI 的进度 / 失误 / 计划**不得占前两节** |
| 2 | **结论层零技术标识** | 版本号 / commit / 包名 / 内部函数名 / 路径 / 探针名 —— 最多放「技术附录」 |
| 3 | **禁止征询式收尾** | 「要我接着做吗 / 说一声即可 / 你看怎么弄」—— 下一步**已定**且不命中门禁 ⇒ **直接做**,用陈述句交代。⚠️ 本条禁的是**形态**;**位置**见铁律 4 |
| 4 | **能自决策的继续做;不能自决策的收到最后一节** | 能自决策 ⇒ **自决策 + 继续处理**(不为"要不要继续"而停);不能自决策 ⇒ **收进最后一节、逐条编号**,⛔ 不许夹在中间,也不许散在正文里问 |
| 5 | **提报给用户前先过取舍筛**(§4) | 只有优点 / 只有缺点 ⇒ 自己拍掉;真取舍才提报用户,且**必须写优缺点、候选竖排** |
| 6 | **要解决问题,不是将就妥协** | 面对风险 / 缺陷**默认目标是解决**;**降级目标 / 延期 / 静默兜底**三种**都不算解决**。只有**客观不可逾越**(技术不可行 / 上游未支持 / 需用户给凭据或窗口)才允许"暂时接受",且必须写明 ① 卡在哪(证据)② 已做到哪一步 ③ **什么条件一出现必须回头解决**。⚠️ 与"最小代价路径"不矛盾:**目标不打折,路径取最小代价**。 |
### 8.1 只做正向迭代(硬红线)
**判据**:这个改动是否让项目**任一维度净变差** —— **目标 / 方向 / 架构 / 功能 / 性能 / 安全 / 交互 / UI / 便利性 / 扩展性**?
**命中 ⇒ 立即停下复盘**(⛔ 不许"先做着看"、不许将就):
① 写清**劣化在哪一维、代价多大**(证据 / 量级);
② 找出**能保住正向收益的做法**(改小范围 / 换实现 / 分阶段);
③ **拿不出正向做法 ⇒ 立即停止、不再执行,只报告**。
⛔ **三种伪装禁止**:把劣化说成"必要代价"/用"后续再优化"掩盖已知劣化/把劣化项藏进交付不写。
> **自己失误的交代**:只在两种情况下写 —— ① 它**改变了结论**;② 用户**问根因**。否则放最后一节一行,或先不提。
@@ -0,0 +1,132 @@
# collabd supervisor keeper -- NEVER returns on purpose.
#
# 🔴🔴 2026-10-04 修:首行原本多打了三个双引号(即「三连双引号 + #」开头,
# 把整份 `.ps1` 变成了「Python 看着合法、PowerShell 看着语法错」的畸形文件
# ⇒ 生成物被 PowerShell 判定 9 处语法错、**一秒内秒退** ⇒ 常驻彻底没人续命。
# ⛔ 这个文件是 **PowerShell**(不是 Python)⇒ 注释一律用 `#`,⛔ 不要三引号。
#
# ⚙️ 本文件由 `init_workspace.py` **按工作区变量自动生成**(⛔ 请勿手改:要改改模板 + 重跑脚本)。
# 生成时间:__STAMP__
# 适用工作区:__WS__
#
# Why this file exists(⛔ 下面几条是踩出来的,⛔ 别"简化"掉):
# ① 计划任务**只在任务失败(非零退出)时重启**。早前版本末尾直接前台调 pythonw,
# pythonw 死掉后脚本返回 0 ⇒ 任务被判"成功"⇒ RestartCount=999 永不触发
# ⇒ **常驻死了就永远死着**。⇒ **重启逻辑必须住在脚本里**。
# ② ⛔ **绝不能用 `& $pyw ...`** 起常驻:pythonw 是 GUI 子系统程序,PowerShell 的
# 调用运算符**不阻塞**它 ⇒ 立刻返回 ⇒ 脚本以为"collabd 退出了"⇒ 每几秒就重拉
# ⇒ **叠出一堆重复常驻**。⇒ 用 `Start-Process -Wait`(真阻塞)。
# ③ **存活判据=心跳,不是退出码**:collabd 发现有活的常驻时会**故意 exit 0**(幂等),
# 那个 0 退出**绝不能**触发重拉风暴。
# ④ UTF-8 **带 BOM**(⛔ 不带 ⇒ PowerShell 5.1 按 ANSI/GBK 解码 ⇒ CJK 路径被毁
# ⇒ 任务一秒内 Result=1 退出)。
$ErrorActionPreference = "Continue"
$env:CODEBUDDY_CONFIG_DIR = "__CFGDIR__"
$env:COLLABD_CONFIG = "__COLLABDCFG__"
$env:PYTHONIOENCODING = "utf-8"
$env:PYTHONUNBUFFERED = "1" # stdout 重定向到文件,Python 会块缓冲 ⇒ 不设它诊断全压在 4KB 之后
$root = "__WS__"
$pyw = "__PYW__"
$script = "__SCRIPT__"
$hbPath = "__HBPATH__"
$logPath = "__LOG__"
$stampPath = "__STAMP__"
# 🔴 停止标志:机制侧 `--set-life 已完成` ⇒ 写它 ⇒ 常驻优雅退出。
# ⚠️ 改前缺陷:keeper 不知道它 ⇒ 目标收口后**每 60 秒重拉一次、每次秒退**
# (实测**空转 370 次/6 小时**)⇒ "完成即收工"只做到"停掉常驻",
# ⛔ **没做到"别再拉它起来"** —— 那是同一件事的两半。
$stopFlag = "__STOPFLAG__"
$staleSec = 90 # 与 collabd.supervise_alive() 同一陈旧阈值
Set-Location $root
function Say([string]$m) {
$ts = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
Add-Content -LiteralPath $logPath -Value ("[" + $ts + "] " + $m) -Encoding UTF8
}
function Get-HbAge {
try {
if (-not (Test-Path -LiteralPath $hbPath)) { return -1 }
$raw = Get-Content -LiteralPath $hbPath -Raw -Encoding UTF8
if (-not $raw) { return -1 }
$ts = [double](($raw | ConvertFrom-Json).ts)
if ($ts -le 0) { return -1 }
return [int]((Get-Date).ToString("U") - $ts)
} catch { return -1 }
}
function Wait-HeartbeatFresh([int]$maxWait) {
$waited = 0
while ($waited -lt $maxWait) {
$age = Get-HbAge
if ($age -ge 0 -and $age -lt $staleSec) {
Say ("another supervisor is alive (heartbeat " + $age + "s old) => standing down")
return $true
}
Start-Sleep -Seconds 5
$waited += 5
}
return $false
}
Say ("keeper up. keeper_pid=" + $PID + " script=" + $script)
$backoff = 5
while ($true) {
# ① 循环开头:目标已完成 ⇒ 收工,不再重拉
if (Test-Path -LiteralPath $stopFlag) {
Say "guard.stop present => target finished; keeper stands down (no more respawn)"
exit 0
}
$ageBefore = Get-HbAge
if ($ageBefore -ge 0 -and $ageBefore -lt $staleSec) {
if (Wait-HeartbeatFresh 300) { continue }
}
Say ("spawn collabd --supervise (backoff=" + $backoff + "s)")
$outLog = "__OUTLOG__"
$errLog = "__ERRLOG__"
try {
# -Wait 给的是**真阻塞**(`&` 给不了 GUI 子系统程序);-Redirect* 让证据落盘
$proc = Start-Process -FilePath $pyw `
-ArgumentList @("-u", $script, "--supervise") `
-WorkingDirectory $root `
-WindowStyle Hidden `
-RedirectStandardOutput $outLog `
-RedirectStandardError $errLog `
-PassThru -Wait
$code = $proc.ExitCode
} catch {
Say ("spawn threw: " + $_.Exception.Message + " => retry in " + $backoff + "s")
Start-Sleep -Seconds $backoff
if ($backoff -lt 60) { $backoff = [Math]::Min(60, $backoff * 2) }
continue
}
$ageAfter = Get-HbAge
if ($ageAfter -ge 0 -and $ageAfter -lt $staleSec) {
$verdict = "heartbeat still fresh (" + $ageAfter + "s) => idle exit, no respawn yet"
} elseif ($ageAfter -ge 0) {
$verdict = "heartbeat stale (" + $ageAfter + "s) => supervisor really died"
} else {
$verdict = "heartbeat unreadable"
}
Say ("collabd exited (exit=" + $code + ", " + $verdict + ")")
Set-Content -LiteralPath $stampPath -Value ((Get-Date).ToString("yyyy-MM-dd HH:mm:ss")) -Encoding UTF8
# ② 退避**期间**标志可能出现 ⇒ 别再睡满一轮(⛔ 漏这里 ⇒ 收工后还会多拉一次)
$waitLeft = $backoff
while ($waitLeft -gt 0) {
if (Test-Path -LiteralPath $stopFlag) {
Say "guard.stop appeared during backoff => keeper stands down early"
exit 0
}
Start-Sleep -Seconds 5
$waitLeft -= 5
}
if ($backoff -lt 60) { $backoff = [Math]::Min(60, $backoff * 2) }
}
@@ -0,0 +1,410 @@
# 常驻程序长期在线(Windows)
> 🔴🔴 **2026-10-06 过期警告 · 读之前必看**
> 本档 §一(包装脚本)、§三(铺脚本步骤)等节描述的 **`start-supervise.ps1` 形态已整套废弃**。
> 现行形态见 `SKILL.md`「两个启动器」表 + `pitfalls.md` **P0-74**:
> **计划任务 → `pythonw.exe` → `supervise-launch.py`**(GUI 子系统 ⇒ 零控制台 ⇒ **不闪窗**)。
> ⛔ 凡本档出现 `start-supervise.ps1`、`assets/start-supervise.ps1.tpl`(**该模板已删除**)、
> 或 `New-ScheduledTaskAction ... -File` 的段落,**一律按历史留痕读,⛔ 不许照着做**。
> ⚠️ 另:本档所说"载体是可选需求、常态用不着计划任务"的**2026-10-04 口径已被推翻** ——
> 2026-10-05 用户拍板「**常驻崩了,拉起来这个事儿,你不能也用计划任务起个程序吗?**」
> ⇒ 计划任务现在是**既定形态**(每 5 分钟兜底),开关是 `supervise.switch`(`collabctl.py on|off`)。
> 📌 **整份重写待办**(本轮只加了本警告块,未重写正文)。
> 📌 **本档=「想让目标检查程序长期在线」时的可选做法**(2026-10-03 夜里整理)。
> 🔴 **注意:常态用不着它** —— 常态是「调用技能完成目标时起后台任务 + 检查程序」,
> 那套一直好好在跑(见下面那段口径)。
> 证据源=测试3 的复盘 `目标-会话协作测试3-7cd276/S3_常驻启动成功复盘_20261003.md`
> + `S4_常驻自愈闭环复盘_20261003.md` + `S5_A6自指修复与目标收口_20261004.md`。
> 🔴🔴 **口径(2026-10-04 用户定案,当场订正)**
>
> 用户原话:「**从来没说过什么开机自启,只有调用技能完成目标时启动 后台任务和检查程序**」。
>
> ⇒ **常态就是:调用技能完成目标时,起后台任务 + 检查程序。** 这套**一直好好在跑**
> (实测本区 pid 19424 从 10-03 17:05 活到现在 **24.7 h**)。
> ⇒ ⛔ **「开机自启 / 计划任务 / 载体脚本」不是需求,是本文件后半段自己推演出来的可选项**。
> ⛔ **不许**把它当"欠项 / 待办 / 风险"往用户面前摊(本轮就是这么摊错的)。
>
> 📌 **本文档怎么读**:
> · 只想**把技能用起来** ⇒ **读到「〇」就够了**,后面的装法与本场景无关。
> · 只在**明确要"目标做完了程序还得继续跑"**时 ⇒ 才看后面那几节。
## 〇、🔴🔴 先分场景:两种"长期"不是一回事
**两种都要长期在线,但它们要的东西不同** —— ⛔ 别混用,混用就会出现
"看起来在跑、其实随时会死"或"该停时还在跑"。
| | **场景 A · 用户手动创建主会话** | **场景 B · 定时任务创建主会话** |
|---|---|---|
| 典型形态 | 用户在 WorkBuddy 里自己开一条 `[主]-…` 会话,盯着它推进 | 排期按点拉起 `[主]-…`(`recurring`,如 `FREQ=HOURLY;INTERVAL=1`) |
| 会话寿命 | **随用户何时收工**(可能几分钟,也可能几小时) | 跑完即 `completed`,**与下一跳之间有静默窗** |
| 🔴 **对"常驻"的硬要求** | ⛔ **不能靠会话活着** —— 一收工就没人拉 ⇒ **必须脱离会话** | ✅ 会话自己周期性回来 ⇒ 但**每跳之间仍有空窗** ⇒ **同样要脱离会话** |
| 唯一合格载体 | **计划任务 + 守护循环**(本文档第三~五节) | 同左(⛔ **排期代替不了载体** —— 排期只是"谁按点喊一次",喊完会话就结束) |
| 怎么验它真活着 | 心跳 `pid` 活 ∧ `ts` 距今 < 90 s + `round` 递增 | 同左,⚠️ **且要静置 ≥ 12 分钟**(⛔ 短观察证明不了事) |
| 谁负责"目标完成后关它" | 守护循环见 `guard.stop` 即 `exit 0`(第五节之二) | 同左 —— ⛔ **排期不会帮你关** |
### 🔴 两个都踩过的坑(**与场景无关**,因为根子在载体)
1. **"一次性工具调用起的子进程"在两种场景下都不合格** ——
实测测试3 开工时 pid 69080 已死;本区两个后台任务(`collabd --supervise` 活 842 分钟、
`board.py --serve` 活 634 分钟)父链都是
`bash → bash → bash → sandbox-cli → WorkBuddy.exe`(**会话树内**),
⛔ `.workbuddy/collab/` 里**既无 `.ps1` 也无 keeper** ⇒ **没走本文档这套**。
⚠️ **它们现在活着,只因为发起它们的那条会话进程树至今没被回收**
⇒ 「**存活时长 = 发起会话的存活时长**」(测试3 `S4` 第六节原话),
**⛔ 不能用它论证"这种起法也能长期"**(该文明确写「这条『反例』**不成立**」)。
2. **"排期是 `once` 且已过期"=哑排期** —— 实测 `[主]-会话协作测试3-主会话` 是
`schedule_type=once`/`scheduled_at=2026-10-03T20:33`/`next_run_at=None`/
`last_run_at=None` ⇒ **它一次都没被触发过**,而界面上 `status=ACTIVE` **看起来像在跑**。
⚠️ **`status=ACTIVE` ≠ "在跑"**;判"真会触发"要看 `schedule_type='recurring'` ∧ `next_run_at` 非空。
## 一、为什么必须换载体(⛔ 别再自己发明)
> 🔴 **「载体」= 那个"负责把常驻程序拉起来、并盯着它别死"的启动脚本。**
> 说白了就一件事:**谁来开机就把它叫起来。**
> 本文档里它指两样:① 批处理 `start-supervise.ps1` ② 把它挂上开机自启的**计划任务**。
> (⚠️ 这个词是内部简写,不在别处用;本节表格用列名「**启动方式**」更直白。)
| 启动方式 | 能不能长期 | 实证 |
|---|---|---|
| 工具调用起的子进程 | ❌ | 父进程一退出就被回收(活不过当轮/当会话) |
| 宿主排期拉的会话起 | ❌ | 测试3 开工时 pid 69080 已死(心跳停在 22:55:31) |
| `CREATE_BREAKAWAY_FROM_JOB` | ❌ | **`PermissionError(13,'拒绝访问。')`** —— 本机被作业对象拒绝,**这条路封死** |
| `cmd /c start /B` | ❌ | 本机沙箱拦截 |
| 排期到点起一次会话 | ❌ | **跑完即退** ⇒ 静默窗内无人(`once` 更糟:可能**永不触发**) |
| **计划任务 + 守护循环** | ✅ | **实测 23:16:37 起活到 23:29 仍在跑**,`round` 1→74;死亡后 **5–6 秒**自动恢复 |
⚠️ **本表判据是"父链根在谁",⛔ 不是 `in_job` 标志**(见下方 2026-10-04 深夜实测更正:
本机**所有**被起的进程 `in_job` 都是 `Y`,含计划任务起的那个 ⇒ 标志位判不出死活)。
**关键差别=父链根在谁**:计划任务创建的进程**根在 `svchost -s Schedule`(任务计划服务)**
⇒ 与 WorkBuddy 会话树**无关** ⇒ 不会被会话回收。
> 🔴🔴 **2026-10-04 深夜实测更正:⛔ 别再用 `IsProcessInJob` 判死活 —— 本机它对所有进程都返回 `Y`。**
>
> 起因:vibe 会话用 `proc_chain.py` 看到常驻 `in_job=True` ⇒ 判"它在会话的作业对象里、所以会被回收"。
> **结论(「会话树内的会死」)是对的,但引用的判据是错的** —— 我做了三组对照:
>
> | 进程 | `in_job` | 实际命运 |
> |---|---|---|
> | 工具调用直接起(`DETACHED_PROCESS`) | **Y** | 会话边界即死 |
> | **计划任务起**(`svchost→cmd→pythonw`) | **Y** | ✅ **长期存活**(看板实测 634+ 分钟) |
> | 零创建 flags(仅 `STARTUPINFO`) | **Y** | 死 |
> | `svchost -s Schedule`(服务本体) | N | — |
> | 系统进程(System / explorer) | N | — |
>
> ⇒ 🔴 **`in_job=Y` 在本机是"全局容器",⛔ 不区分"会话专属"与"服务创建"** ⇒ **标志位判不出死活**。
> ⭐ 这与 `workbuddy-resident-service/scripts/job_lifetime_probe.py` 开头那句自述完全一致:
> 「**让行为说话,而不是只看 `IsProcessInJob` 的标志位**」。
>
> ✅ **正解判据=看父链里有没有 `svchost -s Schedule`**:
> 有 ⇒ 服务创建 ⇒ 长活;根在会话树(`WorkBuddy.exe`/`bash`/已退出的会话进程)⇒ 会话收回时一起死。
> 实测:看板 62992 父链 `pythonw→cmd.exe→svchost(3924)→services.exe→wininit.exe` ⇒ 活;
> 常驻 63628 父链 `pythonw→71800(已退出)→父已退出` ⇒ 会话回收即死。
> 🔴🔴 **2026-10-04 补一条实测更正(⛔ 我自己违反过,判据已落 `t_supervise_lifespan_not_invented`)**
> ⛔ **别把「现在活着」当成"这种起法也能长期"**。实测:pid 19424 从 10-03 17:05 活到 10-04 17:15
> = **24.2 h / 8695 轮**,而它的**父进程早已退出** ⇒ 它是**孤儿进程**,孤儿化后**不受会话结束影响**。
> ⇒ ⚠️ **「会话/工具调用起的活一定活不过当轮」是错的说法**:能不能活取决于**父链还在不在**,
> ⛔ **不许凭这个推断编造后果**。要判"还能活多久"=**先验父链 + 读心跳 `started_ts`**。
> ✅ 但**开机自启这一层仍然缺**:本机 5 个区都**没有** `start-supervise.ps1`,
> 全机只有 1 条计划任务(`collabd-supervise-ws3`)而它指向**不存在的文件** ⇒ 每次触发都失败。
## 二、装法(照抄,⛔ 别自创参数)
### ① 包装脚本 `<工作区>\.workbuddy\collab\start-supervise.ps1`
⚠️ **必须是「永不返回的守护循环」版**。⛔ **别写成"跑一次就退"** —— 那样常驻死掉时脚本
**正常返回 0** ⇒ 任务被判「成功」⇒ `Restart*` 永不触发(测试3 实测:活 12 分钟后再不复活)。
完整可用版见第五节(含 `-Wait`/心跳判据/退避/**停止标志**四项要点)。
```powershell
$ErrorActionPreference = "Continue"
# 🔴 坑①:必须设,否则 _wb_db() 退回读 0 字节空库 ⇒ 闸二永久失效
$env:CODEBUDDY_CONFIG_DIR = "E:\ProgramData\.workbuddy"
$env:COLLABD_CONFIG = "<工作区>\.workbuddy\collab\collabd.config.json" # 🔴 坑②
$env:PYTHONIOENCODING = "utf-8" # 中文日志不乱码
$env:PYTHONUNBUFFERED = "1" # ⛔ 否则重定向到文件时 Python 块缓冲、诊断不可见
$root = "<工作区>"; Set-Location $root
$hbPath = "<工作区>\.workbuddy\collab\logs\supervise-heartbeat.json"
$stopFlag = "<工作区>\tmp\supervise-inbox\guard.stop" # 🔴 目标完成信号(第五节之二)
$logPath = "<工作区>\tmp\keeper.log"
$staleSec = 90 # 与 collabd.supervise_alive() 同口径
function Say([string]$m) { Add-Content -LiteralPath $logPath `
-Value ("[" + (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") + "] " + $m) -Encoding UTF8 }
function Get-HbAge { try { $o = (Get-Content -LiteralPath $hbPath -Raw -Encoding UTF8) | ConvertFrom-Json
$ts = [double]$o.ts; if ($ts -le 0) { return -1 }; return [int]((Get-Date).ToString("U") - $ts)
} catch { return -1 } }
$backoff = 5
while ($true) {
if (Test-Path -LiteralPath $stopFlag) { Say "guard.stop present => stands down"; exit 0 } # 🔴
$age = Get-HbAge
if ($age -ge 0 -and $age -lt $staleSec) { Start-Sleep -Seconds 5; continue } # 别人在跑 ⇒ 让位
Say ("spawn collabd --supervise (backoff=" + $backoff + "s)")
# 🔴 -Wait 必须:& 对 pythonw.exe(GUI 子系统)**不阻塞** ⇒ 会叠出多个常驻
Start-Process -FilePath "<pythonw.exe>" `
-ArgumentList @("-u", "<工作区>\.workbuddy\collab\collabd.py", "--supervise") `
-WorkingDirectory $root -WindowStyle Hidden `
-RedirectStandardOutput "<工作区>\.workbuddy\collab\logs\supervise.out.log" `
-RedirectStandardError "<工作区>\.workbuddy\collab\logs\supervise.err.log" `
-PassThru -Wait | Out-Null
Say ("collabd exited (heartbeat age=" + (Get-HbAge) + "s)")
$w = $backoff
while ($w -gt 0) { if (Test-Path -LiteralPath $stopFlag) { Say "stop flag during backoff"; exit 0 }
Start-Sleep -Seconds 5; $w -= 5 }
if ($backoff -lt 60) { $backoff = [Math]::Min(60, $backoff * 2) }
}
```
⚠️ **落盘必须带 BOM**(坑③):`[System.IO.File]::WriteAllText($p,$text,[System.Text.UTF8Encoding]::new($true))`。
### ② 建任务(⚠️ 一律 PowerShell 的 `ScheduledTasks` 模块)
⛔ **`schtasks.exe` 在本机沙箱被黑名单拦截**(Security Center → Command Security)
⇒ **建/查/改/停计划任务全部走 `Get-/New-/Set-/Start-ScheduledTask` + `Out-File` 落盘再读**
(⛔ 别指望命令回显)。
```powershell
$a = New-ScheduledTaskAction -Execute "C:\windows\System32\WindowsPowerShell\v1.0\powershell.exe" `
-Argument '-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "<工作区>\.workbuddy\collab\start-supervise.ps1"'
$t = New-ScheduledTaskTrigger -AtLogOn -User "Administrator"
# 🔴 三个设置缺一不可(少一个就前功尽弃):
$s = New-ScheduledTaskSettingsSet -ExecutionTimeLimit ([TimeSpan]::Zero) `
-MultipleInstances IgnoreNew -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1)
New-ScheduledTask -TaskName "collabd-supervise-<区名>" -Action $a -Trigger $t -Settings $s -Principal $p
# 改动作不影响已配的触发器 ⇒ 更新时只传 -Action
Set-ScheduledTask -TaskName "..." -Action $a
```
| 设置 | 值 | 漏了会怎样 |
|---|---|---|
| `ExecutionTimeLimit` | `0`(无时限) | 默认 72 h ⇒ 到点被杀 |
| `MultipleInstances` | `IgnoreNew` | 重复起 ⇒ 双写台账 |
| `RestartCount`/`RestartInterval` | `999`/`1 min` | 死了不重拉 |
| Trigger | `AtLogOn`(Interactive・最高权限) | 开机不启动 |
⚠️ **动作一律用 `powershell.exe`(不是 `pythonw.exe`)** —— 动作里直接跑解释器没有"设环境变量"这一步。
(用户长期记忆里那条"`python.exe` 会闪黑窗"针对的是**交互式起进程**;计划任务走 `Hidden`+无控制台,不闪。)
## 三、🔴 三个坑(都可复现,逐个都是"秒退"或"看着在其实没跑")
### 坑① 🔴 不设 `CODEBUDDY_CONFIG_DIR` ⇒ 读到 **0 字节空库**
- 计划任务**不继承**会话的环境变量 ⇒ `_wb_db()` 退回 `Path.home()/.workbuddy/workbuddy.db`
(本机那个是 **0 字节、无 `sessions` 表**)。
- 症状:每 20 秒刷一次 `all_sessions_idle 读库失败 no such table: sessions`
⇒ 闸二(所有会话都结束)**永久失效** ⇒ fail-safe 判"有会话在跑" ⇒ **检查会话永远建不出来**。
- ✅ **正解=用机制自带的配置项 `host_db`**(`collabd.py` 里它本就**优先于**环境变量),
指向真库 `E:/ProgramData/.workbuddy/workbuddy.db`。
⇒ **这比设环境变量更可靠**(配置跟着工作区走,不依赖谁起的它)。
### 坑② 🔴 配置查找靠 `cwd` ⇒ 传错 cwd 当场拒跑
`_cfg_candidates()` = `COLLABD_CONFIG` → `<cwd>/.workbuddy/collab/collabd.config.json`。
⛔ **不要把 `cwd` 设成 `collab/` 目录** —— 那会拼出多一级 `.workbuddy` ⇒ 当场拒跑。
✅ 设成**工作区根** + 同时显式给 `COLLABD_CONFIG`(双保险)。
(`ensure_supervise()` 内部原先传 `cwd=脚本所在目录` —— 已由测试3 主会话修成 `cwd=str(WS)`。)
### 坑③ 🔴 PowerShell 5.1 读**无 BOM** 的 UTF-8 `.ps1` ⇒ 中文路径全毁 ⇒ **秒退**
- 症状:计划任务 `LastTaskResult=1` **秒退**,心跳毫无动静。
- 定位:`Get-Content -Encoding Byte -TotalCount 3` ⇒ `36 69 114`(ASCII 的 `$Er`)⇒ **无 BOM**。
PowerShell 5.1 对无 BOM 文件按 **ANSI(GBK)** 解码 ⇒ 脚本里 `会话协作测试3` 变乱码 ⇒ 路径不存在。
- ✅ 落盘**必须带 BOM**:
`[System.IO.File]::WriteAllText($p,$text,[System.Text.UTF8Encoding]::new($true))`(`$true` = 带 BOM)。
- 📌 **本机所有含中文路径的 `.ps1` 一律带 BOM**,否则一律秒退。
- ⚠️ 判据:**`LastTaskResult`** —— `0` = 成功;`1` = 秒退(八成是 BOM)。
## 四、验收(三样都齐才算成,⛔ 打印"已启动"不算)
```powershell
Get-ScheduledTask -TaskName "collabd-supervise-<区名>" | Select-Object TaskName,State
Get-ScheduledTaskInfo -TaskName "collabd-supervise-<区名>" | Select-Object LastRunTime,LastTaskResult
```
🔴 `LastTaskResult` **必须 0**。
**存活唯一机读判据**(=机制里那条,⛔ 别用"日志在更新"代替):
`.workbuddy/collab/logs/supervise-heartbeat.json` 里 **`pid` 活着 ∧ `ts` 距今 < 90 秒**。
```powershell
# pid 是否真在进程表(⛔ tasklist 走 bash 会被当路径;用 PowerShell)
$p = Get-Process -Id <pid> -ErrorAction SilentlyContinue # $null = 已死
```
**静置观察**(≥ 4 分钟):`round` 持续递增、`argv0` 逐字等于本区 `collabd.py`、
`已有活的常驻 ⇒ 本实例退出(幂等)` 出现一次(证明没双写)。
## 五、🔴 载体是**一个**永不返回的守护循环(⛔ 不是"两层")
⚠️ **旧版这里写的是"必须两层=计划任务 + 周期性复活排期",🔴 已被实测推翻** ——
真正常态兜底**就在这个计划的守护循环里**,⛔ 不需要另一条排期。
**实测 2026-10-03**:单纯"计划任务跑一次脚本"**救不了"被外部收走"** ——
测试3 常驻 `23:16:37` 起、`23:28:47` 停(活 12 分钟),而计划任务
`LastRunTime` 仍 `23:17:03`、`LastTaskResult=0`、`State=Ready` ⇒ **它没重拉**。
**根因**:`RestartCount/RestartInterval` **只在本任务自己非零退出时生效**。
而脚本里若是**前台阻塞**调用常驻,常驻死掉 ⇒ 脚本**正常返回 0** ⇒
任务调度器判定「成功」⇒ **`Restart*` 永不触发**(测试3 复盘原话:「自愈机制被**成功退出**这三个字废掉了」)。
⇒ ✅ **正解=守护循环版脚本**(⛔ 别用"跑一次就退"):
```
while ($true) {
if (Test-Path $stopFlag) { Say "…stands down"; exit 0 } # 🔴 见第五节之二
if (心跳新鲜(<90s)) { 让位等待; continue } # 活性判据=心跳,⛔ 不是退出码
Start-Process -Wait -PassThru pythonw collabd.py --supervise # 🔴 -Wait 必须
# 退避 5→10→20→40→60s 封顶;每轮开头都再查一次 $stopFlag
}
```
| 要点 | 为什么 |
|---|---|
| `while ($true)` 包住 | 任务永远 `Running` ⇒ 调度器**不介入**、自己管复活 |
| 🔴 **`Start-Process -Wait`** | ⛔ **`&` 对 `pythonw.exe`(GUI 子系统)不阻塞**、立即返回 ⇒ 循环误判"刚起的已退出"⇒ **叠出多个常驻**(测试3 实测 5/10/20/40s 连续重拉) |
| 活性判据=**心跳新鲜度**(`<90s`),⛔ 不是退出码 | `collabd` 发现已有活常驻会**幂等 exit 0** ⇒ 按退出码重拉会造出风暴 |
| 退避封顶 60s | 真崩时别刷屏 |
| `.NET Process` + `add_OutputDataReceived` | ⚠️ PowerShell 5.1 **无消息泵 ⇒ 回调不触发**、日志被静默吞掉 ⇒ 用 `Start-Process -RedirectStandardOutput` |
**实测效果**:23:40 第一次死亡 **5 秒恢复**;23:47 单变量 kill 演练 **6 秒恢复**、新 pid、`pythonw` 实例数 `count=1`(无叠加)。
### 之二 🔴🔴 守护循环**必须看停止标志**(2026-10-04 补,本文档初版漏了)
**症状**:目标已完成(`--set-life 已完成` ⇒ `supervise_stop()` 写下 `guard.stop`、常驻已优雅退出),
**但守护循环仍在每 60 秒重拉一次、每次秒退** ⇒ 实测**空转 370 次/6 小时**
(`keeper.log` 连续 `exited (exit=0, heartbeat unreadable)`)。
**根因**:守卫只问「心跳新鲜吗」,**⛔ 不问「目标还活着吗」** ⇒
**"完成即收工"只做到"停掉常驻",没做到"别再拉它"** —— 那是同一件事的两半。
✅ **正解=循环开头(+退避等待中)都先查停止标志**,在则**退出循环**:
```powershell
$stopFlag = "<工作区>\tmp\supervise-inbox\guard.stop"
while ($true) {
if (Test-Path -LiteralPath $stopFlag) { Say "guard.stop present => stands down"; exit 0 }
... # 起常驻
$waitLeft = $backoff
while ($waitLeft -gt 0) { # 🔴 退避期间也可能出现标志
if (Test-Path -LiteralPath $stopFlag) { Say "…stands down early"; exit 0 }
Start-Sleep -Seconds 5; $waitLeft -= 5
}
}
```
📌 退出前**必须 `Say` 一行**(⛔ 否则又变成"静默消失",同族红线:读到了就要说)。
**实测(2026-10-04 07:03,单变量:只换脚本、没动任务)**:
新 keeper 起来 **0 秒**识别标志 ⇒ `keeper stands down` ⇒ 任务 `State=Ready`(⛔ 不是 `Running`)
⇒ 重拉次数 **372 → 372**(一没涨)⇒ **空转彻底止住**。
⚠️ 验「是否被回收」必须**只做单一变量**(⛔ 不许同时停/起任务)—— 否则因果会搞错。
⚠️ **验收要静置 ≥ 12 分钟**(⛔ 短于 10 分钟证明不了任何事 —— 常驻就死在第 12 分钟)。
## 六、⚠️ 还没有它就不算长期(两条,别自欺)
1. **静默基准**:检查会话要 `静默 ≥ 20 分钟` 才建,而基准取台账 `tasks.json` 的 mtime
⇒ **新建/改动任何排期都会把计时归零** ⇒ 刚派完棒必然等满 20 分钟(实测 0.2/4.3/…/12.6 分钟,**从未越过 20**)。
2. **闸二会自锁(正确行为,不是 bug)**:`_all_sessions_idle()` 要求"所有会话都结束",
而**发起这一切的那条会话自己也在里面** ⇒ 只要它活着,检查会话就建不出来。
⇒ **要让检查会话跑起来,必须先结束发起会话**(fail-safe 方向=宁可不建,不误建)。
## 七、还有一条(已由机制收口,⛔ 别再自己写)
- **目标完成 ⇒ 关后台+常驻程序**:`collabd.py --set-life 已完成` 现在**真的会停常驻**
(`supervise_stop()`:先写 `guard.stop` 走优雅退出 → 等 45 s → 不死才 `taskkill /T /F`)。
⛔ **受阻 / 已暂停不关**(口径如此)。
- **"完成"可逆**:`ensure_supervise()` 续命前**清掉 `guard.stop`** ⇒ 新目标/目标调整后能再起来。
⛔ 不清就永远起不来。
- **跨区自愈**:本区常驻每 6 轮顺带扫一遍 `peer_workspaces`,**只补"目标进行中"的区**
(已完成/受阻/有停止标志的 ⛔ 一律不拉)⇒ 但**计划任务是更靠前的第一道**。
## 八、按场景对照(选哪条 / 怎么验 / 常见坑)
| 场景 | 主会话怎么来 | 常驻载体 | 目标完成后谁关 | 常见坑 |
|---|---|---|---|---|
| **A · 用户手动创建主会话** | 用户自己开 `[主]-…`,盯着推进 | 计划任务 + 守护循环 | keeper 见 `guard.stop` → `exit 0` | 🔴 用户一收工就没人拉 ⇒ 若没装载体,常驻会在会话结束时**静默死掉**(看板还显示"在线") |
| **B · 定时任务创建主会话** | 排期 `recurring` 到点拉 `[主]-…` | 同上(⛔ **排期代替不了载体**) | 同上 | 🔴 **`once` 排期过期即哑**(`next_run_at=None`、界面仍显示 ACTIVE)⇒ **静默窗内无人**;🔴 排期跑完即 `completed`,**下一跳之前是空窗** |
**共同判据(两种场景都一样,⛔ 每次改完都验)**:
1. `LastTaskResult` = `0`(`1` ⇒ 秒退,八成是 BOM);
2. 任务 `State` = **`Running`**(⛔ `Ready` 只说明"此刻没在跑",**不代表机制坏**,
但要配合 ③ 才算稳);
3. 心跳 `pid` 活 ∧ `ts` 距今 **< 90 s** ∧ `round` 递增;
4. **静置 ≥ 12 分钟**再看 ③(⛔ 短观察证明不了事 —— 常驻常死在第 12 分钟);
5. 目标完成后:keeper 日志出现 `stands down` + **重拉计数不再增长**(验"真的停了",
⛔ 不许只看"心跳停了"—— 那可能是死了而不是收工)。
## 九、四个区各自的任务名(⛔ 名字别撞)
`collabd-supervise-<工作区名>`,例:`collabd-supervise-ws3`。
⚠️ 一个区一个任务;⛔ 多个区**共用一个**任务名会被 `IgnoreNew` 挡掉第二个。
## 十、🔴🔴 「各区都能常驻」怎么一次做齐(2026-10-04 用户点问)
> 用户原话:「看看是否有**各工作区都能启动常驻程序**的最好办法,
> 而不是现在这种**只能某个工作区才能常驻**的办法」
✅ **答案:能,而且就是第八节那套 —— 每区各建一条任务,同一条命令换个区名即可。**
⛔ **不存在"一个载体管所有区"的写法**,⛔ 也**不需要**:任务天然可以并存,
`IgnoreNew` 只在**同一个任务名**上生效(⛔ 撞名才挡)。
### 一区一条,四步(把 `<区>` 换成工作区绝对路径、`<名>` 换成区名)
1. **铺包装脚本** `<区>\.workbuddy\collab\start-supervise.ps1`
—— 用第二节①的守护循环版,**落盘必须带 BOM**(坑③,否则秒退)。
`collabd.config.json` 里补两项:`host_db`(指真库,⛔ 别依赖环境变量,见坑①)
+ 本区 `workspace`。
2. **建任务**:`collabd-supervise-<名>`,动作=`powershell.exe -File <该区 ps1>`,
触发器 `AtLogOn`,四个设置按第二节②的表配齐(`ExecutionTimeLimit=0`/
`MultipleInstances=IgnoreNew`/`RestartCount=999`+`RestartInterval=1min`)。
3. **立即 `Start-ScheduledTask`**(⛔ 别等下次登录)。
4. **验收**:`LastTaskResult=0` ∧ 心跳 `pid` 活 ∧ `ts<90s` ∧ `round` 递增 ⇒ **静置 ≥ 12 分钟**再看一次。
### 各区之间的隔离(为什么"能各建各的")
- **任务名不同** ⇒ `IgnoreNew` 互不影响;
- **`COLLABD_CONFIG` 指向各区自己的配置** ⇒ 各读各的 `goal.json`/台账;
- **跨区自愈是"顺带",⛔ 不是主靠**:本区常驻每 6 轮扫一遍 `peer_workspaces`,
只补"目标进行中"的区(已完成/受阻/有 `guard.stop` 的 ⛔ 一律不拉)。
⚠️ 所以**别把跨区自愈当载体的替代品** —— 它要求本区常驻先活着,是第二道。
### ⛔ 别犯的三个错
1. **⛔ 用一条任务带多个区**(一个动作只能跑一个区的脚本);
2. **⛔ 一区多条任务**(会双写台账);
3. **⛔ 以为"某区现在活着"就不用建任务** —— 那是孤儿化幸存,
父链一断就没了(第一节末尾那条实测更正)。
### 🔴🔴 两个"看着做好了、其实没生效"的坑(2026-10-04 实测踩到)
**坑 A:从副本发起时,找载体模板不能写死包根。**
`_escalate_to_keeper()` 原来写死 `Path(__file__).resolve().parent.parent / "assets"`:
- 技能目录里对(`<pkg>/scripts/collabd.py` ⇒ `.parent.parent` = `<pkg>`);
- **工作区副本里错**(`.workbuddy/collab/collabd.py` ⇒ `.parent.parent` = `<WS>/.workbuddy`
⇒ 去找 `<WS>/.workbuddy/assets/`,**不存在**)。
⇒ 症状**极具误导性**:返回值写着「⛔ 缺模板 assets/start-supervise.ps1.tpl」,
看着像仓库少了个文件,其实只是**算错了包根**;而模板好好躺在
`<WS>/.workbuddy/skills/session-mechanism/assets/`。
✅ 修法=`_find_keeper_tpl()` 认三处(按优先级):
① `<pkg>/assets/` ② `<WS>/.workbuddy/skills/session-mechanism/assets/`(**副本正解**)
③ `<CFGDIR>/skills/session-mechanism/assets/`(兜底)。
⚠️ **凡"从 `__file__` 往上推包根"的代码,副本换布局后都会错** —— 副本是
`<WS>/.workbuddy/collab/`(**不是** 包树里的 `scripts/`),别套用同一套相对层数。
**坑 B:重铺 `.ps1` 后,`Stop/Start-ScheduledTask` 不足以让它生效。**
keeper 是 `powershell.exe -File <ps1>`:PowerShell **启动时把脚本读进内存**,
之后磁盘上改了、任务重启了,**旧 keeper 进程可能仍在跑老脚本**。
实测:同时存在两个 keeper(57680 旧的 + 20180 新的),**旧的按老路径拉常驻**
⇒ 现象是"我明明改好了,起来还是老样子"。
✅ 做法:重铺后**显式杀 keeper 进程**(`taskkill /F /T /PID <keeper_pid>`)再 `Start-ScheduledTask`;
复核判据=**新常驻的父链根**(`svchost -s Schedule`)+ 心跳 `argv0` 指向**本区副本**。
**坑 C:改完副本必须重启常驻,判据是 `argv0` 而不是文件 md5。**
`deploy_code.py` 覆盖的只是**磁盘上的副本** —— 正在跑的进程仍持有旧代码
(P0-17 同族)。✅ 判据=心跳里的 `argv0` 是否指向本区副本路径,
⛔ 不是"`md5` 一致"(那只证明文件换了,⛔ 不证明进程换了)。
---
📌 **本文档的事实全部来自实测**(计划任务状态/`LastTaskResult=0`/心跳 13 分钟/BOM 字节
`239 187 191`/三条封死路的原始错误)。⛔ 任何"应该行得通但没实测"的写法都别往里加。