项目管理界面:工作区侧改成薄转发(真身已入会话技能 boards/)

承接 52566c7(技能仓):两个看板整包搬进 session-mechanism/boards/。

工作区侧不留第二份实现(同族 P0:一份规则两处实现 ⇒ 必然漂):
* project-board.py   38380B 完整实现 → 薄转发(解析技能根 → 设变量 → runpy 真身)
* project-board.html 54102B 真身页面 → 路标页(说明三处位置,⛔ 别再当源文件改)
* project-board-launch.py 上一轮已改薄转发

计划任务不用改:动作指向本目录 project-board-launch.py(薄转发会转到技能包真身)——
留这一层的目的就是路径稳定(技能升级/换机时只改一处)。

验收:
* python 交付物/项目管理界面/project-board.py --once ⇒ 4 个项目全列出(与技能包真身同读数)
* HTTP / 200 · 54102 字节(=技能包真身那份,证明确实是转发)· /api/projects 出数据 · /healthz ok
* 计划任务重起:pid 35080 → 23596

原件备份 ⇒ tmp/pm-board-bak-20261014/
This commit is contained in:
admin committed 2026-10-14 15:40:32 +08:00
1 parent d933181a55
commit 6b783aaf82
4 files changed
+595 -1518

No files matched your search

+432
View File
@@ -0,0 +1,432 @@
# 2026-10-14 工作日志
## 主会话 · 排查「执行/检查会话批量操作不能归档、不能删除」(07:2x~07:4x)
**用户提问**:为什么会话机制创建的执行会话和检查会话,批量操作时不能归档也无法删除。
**取证路径**(全部只读,未改任何代码与数据):
1. 只读打开宿主库 `E:\ProgramData\.workbuddy\workbuddy.db`,查 `sessions` 表结构与取值分布。
2. 读出 asar 头部索引(13910 文件),自建 `tmp/asar-extract.js` / `tmp/asar-grep.js` 做定点抽取与全量检索。
3. 抽取并核实 `main/conversations.js`、`main/handlers.js`、`main/server.js`、
`renderer/assets/adapters-*.js`、`renderer/assets/DataManagementPanel-*.js`、
`renderer/assets/ui-docs-viewer-*.js`。
**结论(产品层设计,非本项目机制缺陷)**:
1. 产品**没有会话的批量归档/批量删除接口** —— `archiveSessions`/`deleteSessions`/`batchArchive` 全量检索 0 命中。
2. 归档可用性判据(`ui-docs-viewer`):
`_n = e => !((e.status??'').toLowerCase()==='archived' || e.projectId || I4n(e, 空间))`
⇒ `archived` / `projectId` 非空 / `I4n` 命中,三者任一即不可归档。
`I4n(e,t) = e.transport==='cloud' ? false : e.spaceType==='assistant' || M4n(e.cwd,t)`。
3. 归档只改 `status`,`deleted_at` 保持 `NULL`;而保留策略自动清理对已归档项是 `archivedSkipped += 1; continue`
⇒ 归档不会让会话消失,只移出默认列表。
4. 「删除」在本地是**软删**(`SET deleted_at = ?`,`transport='local'`),行仍留在 `sessions` 表
⇒ 从库里看「像没删掉」。`deleted_at = -1` 是另一套语义(云端索引 `WHERE deleted_at = -1` 表未删)。
5. `DataManagementPanel` 的批量交互操作的是「归档列表项」,按 `kind` 分流;
`readonly` 项(`origin` 为 `project`/`cloud`)**结构上不渲染操作按钮**。
**现场读数**:`sessions` 412 行,`deleted_at IS NOT NULL` 319 行;
`[执行]`/`[检查]` 会话全部 `is_background_automation=1` + `transport='local'`。
**产出**:`tmp/会话批量归档删除-排查-20261014.md`(完整证据链与可复现取证方法)。
**未做(等拍板)**:机制层加「批量归档/清理已完结执行·检查会话」的动作 —— 属直接改宿主库,
须守活库改动铁律(停 app 或 `backup()` API,⛔ 禁文件级 `cp`)。
**本机坑(再次命中)**:⛔ 别把 MSYS 路径(`/e/…`)交给 Windows 原生 node,会落到 `E:\e\…`;一律传 `E:/…`。
---
## 主会话 · 排查「mcn-aigc-ontology 为什么自己创建了一个主会话」(07:34~07:40)
**用户提问**:mcn-aigc-ontology 为什么这个工作区运行会话项目机制,自己创建了一个主会话。
**现场读数**(全部只读):
1. 该区已装机制:`.workbuddy/collab/collabd.config.json` 在位,`supervise.switch` = `on`。
2. `sessions` 表里该 `cwd` **只有 1 条会话**:`9e069caf`「加载会话技能」,`status=working`,
`is_background_automation=None`(**不是**机制建的),`created_at` 07:12:14 —— **这条就是当前用户在用的主会话本身**。
3. 该区 `automations` 只有 **2 条**:
- `473c13a3`「`[主]-super-i卡片采集-主会话`」`recurring` `FREQ=DAILY;BYHOUR=09;BYMINUTE=0`
`cwds=["E:/ProgramData/AIProject/mcn-aigc-ontology"]`,`created_at` **07:31:11**,`last_run_at=None`
(**下次触发 09:00,从没跑过**)。prompt 逐字:「你是本工作区的主会话……要推进就直接建 `[执行]` 排期拉起任务会话」。
- `5e16f0de`「`[执行]-super-i卡片采集-全量抓取与落盘`」`once`,`scheduled_at=2026-10-14T07:35`,`created_at` **07:31:19**。
4. 常驻:`logs/supervise-heartbeat.json` → `pid=38624`、`started_h=07:30:37`、`round=25`、
`argv0` 指向本区 `.workbuddy/collab/collabd.py`(**argv0 合规**)。
5. 🔴 `logs/_collabd.log` 每 10 s 一条 **`all_sessions_idle 读库失败 no such table: sessions`**
—— **典型「启动器丢了 `CODEBUDDY_CONFIG_DIR`」故障**(读 0 字节空库 ⇒ 检查程序静默失效)。
**结论:机制没有"自己创建主会话"。** 真实发生了四件事,被误读成"机制自创主会话":
> 🔴🔴 **2026-10-14 07:53 订正(用户点破)**:本行结论**只对了一半,且说偏了**。
> 用户原话:「**那就更不对了,技能中没有任何创建主会话的机制,除非是创建接续会话**」。
> 「机制**运行中**没有偷偷建主会话」这半句对;但**技能文档里确实存在"排期创建主会话"这条路**(写成了合法场景),
> 那才是真问题。⇒ **别照本行往下读,改看本文件末节《订正「技能里存在"创建主会话"这条路"》》。**
> (本行保留仅为留痕 —— 「过程 ≠ 成品」同理:错结论⛔ 不删,加状态块指向订正处。)
1. **07:12** 用户自己在该区开了一条会话(标题「加载会话技能」)= 本区**真主会话**。
2. **07:30** 该区常驻被拉起(`pid 38624`),随即把这条 07:12 的主会话**登记进 goal.json**
(日志逐字:`登记主会话 9e069caf(创建目标时顺手登记)`)。
3. **07:31** 该会话(或同轮动作)**新建了一条 recurring 排期** `[主]-super-i卡片采集-主会话`,
`cwds` 指本区、每天 09:00 触发 —— **这条排期到点会真的开出一条新的主会话类型会话**。
4. 同一分钟还建了一条 `[执行]` once 排期(07:35 触发)。
⇒ 「自己创建了一个主会话」的**可见现象**=排期台账里那条 `[主]-…-主会话`;
它**尚未运行**(`last_run_at=None`),**不是已经多出来一个窗口**。
**风险(实测,未处置)**:
- 该 recurring 排期**每天 09:00 会开一条新会话**;若长此以往 ⇒ 每天多一条"主会话"⇒ 与用户实际在用的 `9e069caf` **并存**,
变成"一个工作区两个主会话"。技能口径:**主会话由用户手动开,⛔ 不该由排期生产**。
- `_collabd.log` 的 `no such table: sessions` ⇒ 该区**检查程序(建检查会话那条腿)静默失效**。
与该排期是否清理**是两件独立的事**。
**本机机制层观察**:排期 `scheduled_at` 给了毫秒级时间戳;`rrule` 为空串(once)。
---
## 主会话 · 修复 mcn-aigc-ontology 两项(07:35~07:45)
用户令:「两个问题都按照建议进行修复」⇒ 按倾向 A 执行。
**修复一:撤掉每日主会话排期**
- `[主]-super-i卡片采集-主会话`(id `473c13a3`)已置 **`PAUSED`**,`next_run_at` 清空。
- 用 `automation_update` 改状态(⛔ 未用 rm/sqlite,符合机制铁律);**零删除、可逆**(改回 ACTIVE 即恢复)。
- 那条 `[执行]-super-i卡片采集-全量抓取与落盘`(`5e16f0de`)保持 ACTIVE,07:35:09 已正常触发建出执行会话 `c6226e12`。
**修复二:常驻读库故障(`no such table: sessions`)**
- 真因(证据链):`supervise-launch.py` 的默认值在 **10-11 被改成 `~/.workbuddy`**,
而本机 `C:\Users\Administrator\.workbuddy\workbuddy.db` 是 **0 字节、零张表**(实测);
真库在 `E:\ProgramData\.workbuddy\workbuddy.db`(124 MB)。ai1net 区没炸是因为它 config 里 `host_db` 兜住了。
- 修法(双保险,与技能 P0-73 口径一致):
① `supervise-launch.py`:默认值改回**真库绝对路径** `E:\ProgramData\.workbuddy`(`setdefault` 语义保留,宿主 env 优先);
② `collabd.config.json`:补 `"host_db": "E:/ProgramData/.workbuddy/workbuddy.db"`(**本该有的单区兜底**)。
- 改前备份:`supervise-launch.py.bak-configdir-20261014-073619`、`collabd.config.json.bak-hostdb-20261014-073619`。
- 重启:计划任务 `collabd-keepalive-mcn-aigc-ontology`(`pythonw.exe`,合规)⇒ 旧 `pid 38624` → 新 `pid 28628`。
**验收(三条对齐,非恒绿)**:
1. 启动器解析出的库 = `E:/ProgramData/.workbuddy/workbuddy.db`,`sessions` 413 行读通。
2. 重启后日志:`all_sessions_idle:还有 1 条 working(c6226e12)⇒ 不算全结束` —— **正常读到库**,
本轮 `no such table` **0 条**(重启前是每 20s 一条、连续 25+ 轮)。
3. **变异对照**:旧形态(`~/.workbuddy` + 无 `host_db`)⇒ 复现 `no such table: sessions`;
新形态 ⇒ 读通 413 行。⇒ 判据有效,不是恒绿。
**遗留(技能/其他区层面,本轮未动)**:
- 🔴 **技能源 `E:\ProgramData\.workbuddy\skills\session-mechanism\scripts\supervise-launch.py` 第 30 行仍是
`os.environ.setdefault("CODEBUDDY_CONFIG_DIR", os.path.expanduser("~/.workbuddy"))`** ⇒
**凡是照技能源部署的新区,都会命中同一个空库故障**。本区已单区修好,技能源未动 = 待拍板。
- 各区部署副本形态不一(实测):
`ai1net-dsh-server`/`contentm_agent`/`mcn-aigc-ontology`(已修)= 写死 `E:\ProgramData\.workbuddy` + config 有 `host_db`;
`aigc-idea-impression` = 写死绝对路径但 **config 无 `host_db`**(靠启动器那条腿活着,实测日志正常);
`vibe-product` = 未部署启动器。
- 本机 `~/.workbuddy/workbuddy.db` 那个 0 字节空文件仍在(无害,但会持续误导"以为 `~` 是活动环境")。
**终态复核**:写法定为与 `contentm_agent` 逐行一致(`setdefault` + 绝对路径),语法 OK、重启后 `pid 38428`、读库失败 0 条。
---
## 主会话 · 技能源相对化 + 四区统一部署(07:45~07:52)
用户令:「按照建议处理,技能中 **文件路径 必须用相对路径**」⇒ 该条**推翻了上一轮"给各区写死绝对路径"的做法**。
**动因**:上一轮为救 mcn 区把技能源里的 `~/.workbuddy` 换成写死绝对路径 —— 这本身是**把「一台机器的环境」焊进技能**,
与既有的 `collabctl.py::_resolve_airoot()` 口径(⛔ 不在代码里写死盘符/目录)冲突。用户第四条指令正是纠这个。
**改法:`HERE` 相对基准 + 三级外置取值 + ⛔ 禁回落 `~`**
```
HERE = os.path.dirname(os.path.abspath(__file__)) # 全部路径以 HERE 为基准(相对)
CB = <HERE 推导出的 collabd.py>;CFG = <HERE 推导出的 collabd.config.json>
_resolve_config_dir() 三级:
① os.environ["CODEBUDDY_CONFIG_DIR"] —— 宿主 env 已给 ⇒ 直接用,⛔ 不动
② 本区 collabd.config.json 的 host_db —— 取其父目录
③ 包内 env/roots.env 的 CODEBUDDY_CONFIG_DIR —— 向上最多 7 层找 env/roots.env
三级全空 ⇒ 置空 + stderr 告警,⛔ 绝不回落 ~/.workbuddy(那是 0 字节空库)
```
- 三级取值均**外置**(宿主 env/本区配置/包内 env),代码内**零绝对路径**。
- 保留 `setdefault` 语义的反面:**宿主 env 优先级最高**(命中①时原样不动)。
- docstring 里示例路径改写为正斜杠(`E:/ProgramData/.workbuddy`)—— 顺带解掉 `\U` unicodeescape 报错。
- 新增 `import json`。
**验证(探针,非纸面)**:造临时区跑探针 ⇒
① 宿主 env 命中(`D:/fake/host/env`,原样采用);
② 无宿主 env 但有 `host_db` ⇒ 解析出 `E:\ProgramData\.workbuddy`;
③ 三者皆无 ⇒ `CODEBUDDY_CONFIG_DIR` 为 `None` 并打出告警(**fail-safe 方向正确**:宁可报错也不静默读空库)。
**分发与终态**:
- `deploy_code.py --ws <区>` 分发四区,自动备份旧副本 ⇒ **四区 `supervise-launch.py` md5 统一 `bfa03b2f`**。
- `aigc-idea-impression` 补 `"host_db": "E:/ProgramData/.workbuddy/workbuddy.db"`(JSON 校验通过)——
这是四级兜底里它原本缺的一环。
- 技能包 `env/roots.env` 第 9 行已含 `CODEBUDDY_CONFIG_DIR=E:/ProgramData/.workbuddy` ⇒ 第③级在位,无需补。
**四区常驻重启终验**:
```
ai1net-dsh-server 旧 20680 → 37280 07:50:41 ✅ 读库正常
mcn-aigc-ontology 旧 38428 → 4728 07:50:46 ✅ 读库正常
aigc-idea-impression 旧 29364 → 20304 07:50:52 ✅ 读库正常
contentm_agent 旧 27092 → 29400 07:50:58 ✅ 读库正常
日志实证:all_sessions_idle:还有 1 条 working(f920d73e)⇒ 不算全结束 / no such table 共 0 条
```
**🔴 必须记住的一条口径**:技能源改动**只影响将来部署的新区**;已在跑的四区是靠**部署副本 + 重启**生效的。
⇒ 以后改技能源,动作永远是「改源 → `deploy_code.py` 分发 → 重启常驻 → 验心跳 `started_h` 晚于部署时间」,三步缺一即未生效。
**遗留**:`~/.workbuddy/workbuddy.db`(0 字节)仍在,无害但会持续误导;`vibe-product` 未部署启动器。
---
## 沉淀(跨区通用,供后续同类问题直接引用)
- 🔴 **读写宿主库的脚本,路径一律「外置 + 相对基准」**:代码里只认 `CODEBUDDY_CONFIG_DIR`/本区 `host_db`/包内 `env/roots.env`,
⛔ 不写死盘符、⛔ 不猜 `~`。判据:把区**整个搬到另一台机器**仍能跑通 ⇒ 合格。
- 🔴 **绝对路径修复是「治好了但焊死了」**:同一症状下次换机器/换盘符必复发 ⇒ 只能作**应急**,须在同轮内补上相对/外置改造。
- 🔴 **`~/.workbuddy` 在本机是空壳**(0 字节库),凡代码里出现 `expanduser("~/.workbuddy")` 一律视为**高危默认值**。
---
## 主会话 · 订正「技能里存在"创建主会话"这条路」(07:53~08:00)
**用户订正原话**:「**那就更不对了,技能中没有任何创建主会话的机制,除非是创建接续会话**」。
⇒ 我上轮把那条排期说成"多出来的主会话(只是还没跑)",**说岔了** —— 用户的点不在"它跑没跑",
而在**技能里根本就不该有"排期创建主会话"这条路**。核实结论:**用户判断正确,技能里确实有这么一条(错的)路**。
**病灶(四处,同源)**:`supervise-persistence.md` 早期把「**定时任务创建主会话**」与「用户手动创建主会话」
并列成**两种合法场景**,`SKILL.md` §⑥ 顺着抄了一遍 ⇒ 读到的人(或 AI)会理解成"这条路是允许的",于是照做。
**事故链条(mcn-aigc-ontology,全部实测)**:
1. 07:12 用户手动开 `9e069caf`「加载会话技能」= 该区**真主会话**(`is_background_automation=None`)。
2. 07:30 `init_workspace.py` 装机制(`goal.json` 的 `lifecycle_why` 逐字「init_workspace.py 新建」),
常驻拉起并顺手登记主会话(日志逐字「登记主会话 9e069caf(创建目标时顺手登记)」)。
3. **07:31:11 建出 `recurring` 排期 `[主]-super-i卡片采集-主会话`**(`FREQ=DAILY;BYHOUR=09;BYMINUTE=0`),
**prompt 逐字**:「你是本工作区(…)的主会话,只负责目标与方向,不亲自干活。……职责:判断与派活
(要推进就直接建 `[执行]` 排期拉起任务会话)」
⇒ **这是"主会话"的口径,被套到了一条排期身上** —— 排期开出来的会话恒是任务会话那一档。
4. ⇒ 每天 09:00 会开出一条**顶着主会话名头的任务会话**;长期跑 ⇒ 台账里"主会话"越积越多,用户那边**没多窗口**。
**权威口径(技能里本来就有,只是被上述那处盖住了)**:`SKILL.md`《三类会话》类别表逐字
——「**主会话 | 用户手动开 | 管目标与方向、派活 | 用户关窗**」;
`architecture.md §1` 同款;排期只服务**派活线**(`[执行]-…`)与**检查线**(`[检查]-…`)。
「接续会话」是主会话撞阈值时**由它自己建出来的形态**(角色继承),⛔ 不是第三条建主会话的路。
**修复(三件,已全部落地并验收)**:
**① 改文档(4 处)**:
- `references/supervise-persistence.md §〇`:表格从"场景 A/场景 B 两列"改成**只有一列**(「用户手动打开主会话」),
并新增《为什么⛔ 不能"用排期拉起主会话"》小节 —— 含事故回放 + 明确写「本档早期表格是病灶,2026-10-14 已改掉」。
- 同档 §八 对照表:**删掉「B · 定时任务创建主会话」那一行**,只留有价值的读数(`once` 过期即哑)。
- `SKILL.md §⑥`:那句「用户手动创建主会话 / 定时任务创建主会话」改成
「**形态只有一种:主会话由用户手动开** —— ⛔ 排期拉不出主会话」。
- `references/architecture.md §1`:新增 🔴🔴 段《主会话怎么来:只能由用户手动开》,含判据指针。
**② 加判据(第 ⑬ 条,`session-rules-check.py`)**:
- 扫本区排期,命中 `name` 以 `[主]` 开头或含"主会话" ⇒ `fail`(还在 ACTIVE)/`warn`(已非 ACTIVE 的留痕)/否则 `ok`。
- ⚠️ **扫描面 recurring 与 once 都含**(当年 ⑧ 只扫 recurring 才看不见机制建的 once 排期,同类坑)。
- **变异对照(证明非恒绿)**:喂 mcn 区真数据三种输入 —— 原样 PAUSED ⇒ `warn`;伪造成 ACTIVE ⇒ `fail`;
无此类排期的区 ⇒ `ok`。**三档全判得出**。夹具 `tmp/probe-main-sched-judge.py`。
- `SKILL.md §体检` 清单从「查三类、十二项」改成「**十三项**」;变异对照记录补上 ⑬ 那行。
**③ 分发与验收**:
- `deploy_code.py` 分发四区,`session-rules-check.py` md5 统一 **`4b53c88d`**(技能源同值)。
- mcn 区用部署副本真跑 ⇒ 第 ⑬ 条按预期报 `warn`(那排期现为 PAUSED)。
- **顺带抓出另一条真隐患**:那条排期 `model_id=deepseek-v4.1-flash` + `model_is_thinking=0`
⇒ **每次触发必被服务端拒**(判据 ⑧ 报 `fail`)。⚠️ 该排期现已 PAUSED ⇒ 不触发,但**若哪天改回 ACTIVE 必须同时改模型**。
**沉淀(本机口径)**:
- 🔴 **主会话只能由用户手动开**。排期开出来的会话**恒是任务会话那一档**(`is_background_automation=1`);
名字里带 `[主]`/"主会话"**不改变它的档位**。判据=排期名,见第 ⑬ 条。
- 🔴 **技能文档里"并列两种合法场景"的写法极危险** —— 它会把一条**本该唯一的路**读成"二选一"。
⇒ 写"形态"类内容一律**单列**;要留旧形态只作**事故回放/留痕**,并明确写"⛔ 别照做"。
---
## 主会话 · 三项拍板执行(09:46~09:55)
用户令(逐字):「**1、A / 2、确认调整无误 就更新 / 3、不需要就删除,避免创建无效文件**」。
**① 排期保持 PAUSED(候选 A)**
- mcn 区 `[主]-super-i卡片采集-主会话` **保持 PAUSED 不动**(`next_run_at=None` ⇒ 不触发;零风险、可逆)。
- 终态复核:该区 4 条排期 —— `[主]-…` PAUSED|`[执行]-…第2棒` ACTIVE(`next_run_at` 已排)|`[检查]-第1棒` ACTIVE|
`[执行]-…全量抓取与落盘`(首棒)已跑完。链路正常。
**② 核实无误后更新(推送技能总仓)**
- **入库前必查三件**(全部通过):白名单核对 `git ls-tree -d` = **7 个技能目录**、与 `.gitignore` 放行条逐条对上;
凭据扫描(`password/token/secret/api_key/PRIVATE/x-access-token/ghp_/sk-`)**零命中**;两个 py 语法编译通过。
- **错口径清零复核**:`SKILL.md`/`architecture.md` 里「定时任务创建主会话」「场景 B」**零命中**;
`supervise-persistence.md` 剩 4 处**全是事故回放/留痕**(明写「⛔ 别照做」)。
- **编号冲突复核**:体检清单 ⑬ 唯一;`pitfalls.md` 里那套 ⑦~⑬ 是 `t_srsm_busy` 夹具的独立编号,不冲突。
- **提交**:`053a6a6`「session-mechanism:主会话只能由用户开(撤掉"排期建主会话"那条路)」,5 files / +180 / -26。
- **推送**:`[email protected]:admin/workbuddy_skills.git` ⇒ `20eacef..053a6a6 master -> master`(成功)。
- **终验**:本地 HEAD = 远端 `origin/master` = `053a6a67231fe39bc6fcbcfe983d8ab5b6438196`;`git status` 干净。
**③ 删除 0 字节空壳库(避免误导)**
- 清单:`C:\Users\Administrator\.workbuddy\workbuddy.db` —— **0 字节**、2026-10-03 00:06 建、零张表、**无 WAL/SHM 伴生**
(该目录其余 `MEMORY.md`/`device-id`/`logs`/`observe`/`pending-telemetry` **一律不动**)。
- 做法:**送回收站**(`SHFileOperationW` + `FOF_ALLOWUNDO`,⛔ 不直删)。
- ⚠️ **删前加了一道自检**:脚本先查大小,**非 0 字节就拒绝执行**(防误删真库)。
- 结果:`rc=2`(`SHFileOperation` 的"部分成功"语义),原位已不存在。
- 影响面复核:真库 `E:\ProgramData\.workbuddy\workbuddy.db` 在位(`sessions` 416 行/`automations` 341 行);
四区常驻读库正常 —— `no such table` 最后一次出现分别是 **10-05 12:14**(ai1net)与 **今天 07:37**(mcn),
**均为重启前历史行**,aigc/contentm 为 0 条。本轮删除**未造成任何新故障**。
**临时文件清理**(用户令「避免创建无效文件」)
- 送回收站:`who-created-main-sched.py`/`main-sched-prompt.py`/`trash-empty-db.py`/`cleanup-20261014.py`(清理脚本自身也一并送走)。
- **保留 1 个**:`tmp/probe-main-sched-judge.py` —— 技能文档已把它登记为判据 ⑬ 的**变异对照夹具**,
删了以后 ⑬ 的红绿对照就没法复现(技能纪律:夹具"跑完即弃"指不占常驻位置,⛔ 不等于删掉证据)。
---
## 主会话 · 核实「行尾符与同源冲突」接手单(11:09~11:20)
**来件**:`C:\Users\Administrator\Desktop\workbuddy_skills-行尾符与同源冲突-交仓库会话.md`
(另一个环境留下的接手单;用户令「**确认是否有相关问题**」⇒ 只要核实结论,⛔ 不动手改)。
**⚠️ 来件与实际不符的一处(先说清)**:单子写本机仓是 `D:\.workbuddy\skills`,
实际**该路径不存在**;D 盘那个仓在 `D:/github/_alotbuy-gitea/workbuddy_skills`(remote 指向 `work.alotbuy.com`,与本机用的不是同一个 origin)。
更要紧的是:**单子称该仓「本地领先远端 2 个提交」`1774693` / `886d99a` —— 这两笔提交在 D 盘仓里不存在**
(`git cat-file` 报 `Not a valid object name`),且该仓 `origin/master..HEAD` 与 `HEAD..origin/master` **双向为空**(既不领先也不落后)。
⇒ **单子里第三条("有 2 笔提交停在本地待同步")在本机无法证实**,⛔ 不作为待办。
**核实结论:三条里第一、二条属实,且第一条比单子描述更严重。**
**① 行尾符 —— 属实,实测读数**(仓 `E:/ProgramData/.workbuddy/skills`):
- `.gitattributes` **不存在**;`core.autocrlf = true`;`.gitattributes` 属性列 **全仓为空**(零覆盖)。
- 全仓入库形态统计:`i/lf` **4576** / `i/crlf` **244** / `i/-text` 28 / `i/none` 5 / **`i/mixed` 1**。
- CRLF 244 个的分布:**`product-planning/references/` 占 237 个**(冲突重灾区),`session-mechanism/scripts` 5 个、
`session-mechanism/references` 1 个、`product-planning/SKILL.md` 1 个。
- 🔴 **单子没提到的一个更糟样本**:`product-planning/references/stage-requirements/references/create-prd.md`
是 **`i/lf` + `w/crlf`**(入库 LF、工作副本 CRLF)⇒ 这正是"本地一改就整文件 diff / 合并整块冲突"的最典型形态。
- 另两处同批读数:`stage-delivery/SKILL.md` = `i/crlf w/crlf`;`stage-discovery/.../grill-me.md` = `i/lf w/lf`。
- **本机当前无受害症状**:`git status` 干净(0 行)、无冲突标记残留 ⇒ **隐患在位、尚未发作**。
- ⚠️ 我自己上一笔提交 `053a6a6` 的 5 个文件**全部 `i/lf w/lf`**,不受影响。
**② `session-mechanism/SKILL.md` frontmatter —— 属实**:
- 实况:第 6 行 `last_change: 2026-10-05 10:5x · 🔴🔴 …` 是一行超长标量,
其后**12 行以 `·` 开头且无缩进** ⇒ 顶层出现无键的行,**不是合法 YAML**(单子描述准确)。
- 同文件其它字段(`name`/`description`/`version`/`updated_at`)写法正常,确属孤例。
**③ 同源冲突(两份各写一版)—— 本机无异常**:
- 单子列的 4 个冲突文件**路径写少了一层**(实际都在二级 `references/` 下):
`stage-delivery/SKILL.md`、`stage-delivery/references/execution-runbook.md`、
`stage-discovery/references/grill-me.md`、`stage-requirements/references/create-prd.md`。
- 本机仓里这 4 个文件**无未提交改动、无冲突标记**(`grep '^<<<<<<<'` 零命中)⇒ 当前是合好的状态。
- 单子那句"动手前先 `git fetch`、同一件事只在一处定稿"是**作业纪律**,本机已在守(本轮推送前就跑了 `git fetch`)。
**待办(⛔ 本轮未动,属另一个仓/需出方案)**:见本轮回复末尾的待拍板项。
---
## 主会话 · 执行「行尾符 + frontmatter」三项修复并推送(11:22~11:40)
用户令:「**修改完成后 检查无误 推到技能仓库**」⇒ 按上一轮三项的**倾向 A** 全做。
**① `.gitattributes`(新文件,仓根)**
- 内容:默认 `* text=auto eol=lf`,再对文本类逐类显式声明(md/py/sh/json/css/html/js/mjs/yml/yaml 等);
**`.bat`/`.cmd` 保留 CRLF**(Windows 批处理对 LF 敏感);图片/字体/压缩包/音视频/数据库一律 `binary`。
- 🔴 **`.gitignore` 同步放行 `!/.gitattributes`** —— 本仓是「先 `/*` 全挡再逐个放行」的白名单模式,
不放行 ⇒ 这个文件根本进不了库;而它跟 `.gitignore` 同属**仓级规则**,必须入库,
否则别的机器取仓后仍按各自 `core.autocrlf` 各行其是。
- 提交 `cf0ec41`(2 files, +68)。
**② 本机 `core.autocrlf` 设 false(立刻止血)**
- `true` → `false`(仓级配置,只影响本仓)。
- 验:`git ls-files --eol` 的 `attr` 列从**空**变成 **`attr/text eol=lf`** ⇒ 属性真的生效了(⛔ 不是只写了文件)。
**③ `--renormalize` 重新规范化(单独一笔)**
- 实跑改动 **245 个文件**:`product-planning` 239 / `session-mechanism` 6。
- 🔴 **关键验收:纯行尾符,零内容改动** —— `git diff --cached --numstat --ignore-cr-at-eol` **输出为空**;
且 `insertions 44412 / deletions 44412` **完全对称**(行尾符转换的数学特征)。
- 复核**没碰二进制与资源**(`grep -E '\.(png|jpg|ttf|woff2?|zip|gz)$'` 零命中)。
- 提交 `9e779c4`。**单独一笔**的理由:把「改规则」与「按规则刷数据」分开,
将来撞车时可直接回退这一笔而不牵连 `.gitattributes`。
- **行尾符终态**:`i/crlf` **244 → 0**;`i/lf` 4822;剩 `i/-text` 28(二进制)+ `i/none` 5。
**④ `session-mechanism/SKILL.md` frontmatter 合法化(内容一字不动)**
- 原形态:`last_change:` 一行超长标量 + 其后 **12 行**以 `·` 开头无缩进 ⇒ 顶层无键行 12 处。
- 🔴 **顺带发现接手单没提的第二处**:`agent_created:` 前面还夹着一行游离的
`· **本包自身**:install.py …` —— 同样是顶层无键行(改前共 **12** 处,接手单只指出了前 11 行)。
- 改法:块标量 `last_change: |-` + 续行统一两格缩进(含那行游离内容)。
- **验收四条(改前 vs 改后逐字比对)**:
① 顶层无键行 **12 → 0**;② `last_change` 正文 **12 行逐字一致**;
③ 正文(第二个 `---` 之后)**逐字一致**;④ **PyYAML 解析通过**,
字段=`name/description/version/updated_at/last_change/agent_created`,`last_change` 3358 字符 / 36 行。
- 提交 `5d16c6d`(1 file, +14/-13)。
- ⚠️ 验证脚本**第一版有 bug**(`split("|-")[1]` 把首段读成空串)⇒ 一度误报"内容不守恒";
修掉解析后复验才通过。**教训:验证脚本自身也要先自证**,否则会把"改动正确"误判成"改坏了"。
**⑤ 推送与终验**
- `git push origin master` ⇒ `d9445b5..5d16c6d`(成功)。
- 终验:本地 HEAD = `origin/master` = `5d16c6df706f63543073ff63c50889dd99c55c81`;三笔提交在远端齐全;工作区干净。
**沉淀(跨项目可用)**:
- 🔴 **白名单 `.gitignore` 模式下,新加仓级文件(如 `.gitattributes`)必须同步放行** —— 否则"写了但没入库",
而且 `git status` 里它显示为 `??`(未跟踪),**很容易被误读成"已经生效了"**。
- 🔴 **行尾符规范化的验收判据 = `--ignore-cr-at-eol` 后零差异 + 增删行数对称**。
只看文件数会误判(246 个文件听着很吓人,实际一个字都没改)。
- 🔴 **验证脚本要先自证**:解析器有 bug 时会产出"看起来很严重"的假红(本轮就撞了一次)。
---
## 两个看板整包搬进会话技能 `boards/`(用户令「1 boards 2 A」)
**用户原话**:「把这两个看板都纳入会话技能中 ,分两个看板文件夹 放在assert中(判断是否应该放在这里)」
→「1 肯定是整包啊 2 就两个看板 对应两个文件夹」→「但是当前情况 是两个看板最好代码和页面放一起,
要不单独开个 workshop 文件夹 放两个看板」→「这里面有html代码适合放到 scripts中吗」→「1 boards 2 A」。
**拍板结果**:文件夹名 `boards`(⛔ 不是 workshop);A = 本区部署与计划任务**本轮一起改**。
**新增第三层分层**(判据,已写进 SKILL.md):
- `scripts/` = 可执行程序(被计划任务/hook 调)|`assets/` = 静态素材(样式、模板)
- 🔴 `boards/` = **自带页面的看板程序**(一个看板一个文件夹,页面 + 脚本同处一包)
- ⛔ **HTML 不进 `scripts/`**:那层是可执行程序层(现有全是 `.py`),
`session-rules-check.py`/`install.py` 会按脚本扫(语法检查、副本比对)⇒ HTML 进去会被误判。
**搬了什么**:
- `boards/会话看板/`(6 文件):`board.py`(150141B)/ `board_ext.py` / `board-launch.py` /
`board-launch.py.tpl` / `board.html`(151811B)/ `board-render-probe.js`(端口 20099)
- `boards/项目管理界面/`(3 文件):`project-board.py` / `project-board.html`(54102B)/
`project-board-launch.py`(端口 20100)
**🔴 搬迁带出的真缺陷(最重要的一条)**:
`board.py` 里 `importlib` 按 `Path(__file__).parent / "collabd.py"` 取判据 ——
**「与 collabd.py 同目录」这个前提搬走后不成立** ⇒ 静默落到 **except 分支里本地抄的那份判据**。
危害:判据可能漂(同族"共用同一实现"当场 FAIL),**但看板照跑、肉眼完全看不出来**。
修法:候选表(同目录 → 包内 `scripts/` → `DSH_SKILL_ROOT/scripts/`)+ 兜底分支
**写 stderr 留痕**(⛔ 不许静默降级)。`CFG_P` 同病同治(对齐 `collabd.py::load_cfg()` 查找顺序)。
**selftest.py 的三处假设修正**(都源于"一个路径指两处"):
- `t_scope_parity` 的子进程代码用**一个 `H`** 同时指 board.py 与 collabd.py ⇒ 拆成 `H`/`B` 两参数
- `selftest.py` 里 `HERE / "board.py"` 27 处 ⇒ `board_py()`(带旧位置兜底)
- `HERE.parent / "assets" / "board.html"` 15 处 ⇒ `BOARD_DIR / "board.html"`
**顺带修一条既有 FAIL(与搬迁无关)**:判据锚点没跟文档改 ——
`supervise-persistence.md` 的措辞在 `053a6a6` 已改成「排期按点拉起主会话这条路不存在」,
判据仍锚旧句「排期代替不了载体」(那句现在只在 `SKILL.md` 里)⇒ 恒红。
**验收读数**:
- `--filter 看板`:**PASS 110 / FAIL 0**(另 1 条报告型不计入)。迭代轨迹 92/18 → 104/6 → 105/5 → 109/1 → **110/0**。
- 全量:**PASS 91 / FAIL 1**(同一条报告型「产物落点」,实质 FAIL 0)。
⚠️ 「合计」行缺失的真因=`safe-delete` 安全闸在收尾时拦了 64 个清理目标(> 阈值 50)
⇒ 本机工具层策略,⛔ 与技能无关。
- 会话看板 `board.py --out` ⇒ **50515 字节**快照,**无「回落本地判据」告警**(证明真从 collabd.py 取)。
- 项目管理界面 `--once` ⇒ 4 个项目全列出;HTTP `/` 200 · **54102 字节**
(=技能包真身那份 ⇒ 证明工作区侧确实是转发);`/api/projects` 出数据;`/healthz` ok。
**工作区侧改成薄转发**(⛔ 不留第二份实现):
- `交付物/项目管理界面/project-board.py`:改成薄转发(原 38380B 完整实现 → 转发壳)
- `交付物/项目管理界面/project-board.html`:改成**路标页**(54KB 真身 → 说明指向)
- `交付物/项目管理界面/project-board-launch.py`:上一轮已改薄转发
- 原件备份 ⇒ `tmp/pm-board-bak-20261014/`
- 🔴 **计划任务不用改**:动作指向工作区启动器(薄转发会转到技能包真身)⇒ 稳定路径正是留这一层的目的。
重起后 pid **35080 → 23596**,命令行仍是 `pythonw.exe ".../project-board-launch.py"`。
**入库**:提交 `52566c7`(9 新文件 + 3 改名 + 6 修改),推送 `5d16c6d..52566c7` 成功。
- 🔴 `.gitignore` **无需放行** `boards/` —— 白名单放的是整包 `!/session-mechanism/`,包内新增目录自动被包含。
- 凭据扫描:`token` 命中全是文案里的普通词(「0 token」「SyntaxError: Unexpected token」),**无真凭据**。
- 行尾符:`git ls-files --eol` 全部 `i/lf w/lf attr/text eol=lf` ✅。
**沉淀(跨项目可用)**:
- 🔴 **搬家会静默破坏"同目录"隐式前提** —— 这类 import 都有 `except` 兜底,
于是**行为看着正常、判据却已漂**。判据=「搬迁后必须有一条用例专门验『真的从模块取、不是本地抄』」;
正解 = 兜底分支**必须留痕**(⛔ 宁可见地坏,不可静默降级)。
- 🔴 **一个路径常量指两处 = 下一次搬家的地雷**。判据:凡 `HERE / "x.py"` 里 `HERE` 是"包根/脚本目录"
这类**位置代称**,就该问一句"另一个文件搬家时它还成立吗"。
- 🔴 **判据锚点必须逐字抄原文**(本轮第三次栽):文档改措辞后判据恒红,
⛔ 别把"判据红"直接读成"实现坏了"——先问"是不是锚点过期了"。