# 团队协作的「台账 + 形态」设计 > **状态**:✅ 已拍板(2026-09-26)· ✅ **设计评审 PASS(2026-09-26,零 🔴;7 条建议已按用户裁定全部折入本档)** · ✅ **已全部实施**(步 0–6:第 1 步「台账」+ 第 2 / 4 / 6 步「可观测批(A 批)」+ 第 3 / 5 步「形态批(B 批)」)· 判据 **U1–U18**。**第 0 步(文档与约定)已完成**;§6 是拍板结果与决策留痕,§7 是实施顺序,§9 是第一批的实施级设计。 > **来源**:会诊 #9 裁定层(`docs/consult-minutes/2026-09-26-consult-9-minutes.md` §2–§5)+ 2026-09-26 的用户口径与讨论。 > **读法**:**先看 §1(白话版)**,再看 §6(拍板点);§2–§5 是想细看时的方案本体。 ## 1. 白话版:这个方案在解决什么 把主会话想成一家店的店长,几个 worker 会话是店员。 - **店员干完活会回来报告 —— 报告必须当面说**(进店长的上下文)。只有当面说,店长才能当场反问、核对、抓出「你记错了」。这套「互相不信、互相核对」是防止一个人记岔了、全体跟着错的关键。 **所以报告不能改成「写张便条贴墙上,让店长自己看」** —— 那样店长就不会去核对了,便条上的错会被当成事实继承下去。**这一条是整个方案的红线。** - **但店长的桌子很小。** 十个店员轮流进来讲话,加上老板(你)不断插新指令,桌子很快堆满;更糟的是**店员会定期帮你「清空桌面」**(上下文压缩),而「我们到底在干什么」这份计划,现在只写在店长脑子里 —— 一清空就没了。 - **解法不是让店员少说话,而是把「记账」和「说话」分开**:墙上挂一块**白板**(台账),只记「**谁在什么时候声称了什么**」;真正的结论、证据、反驳,仍然靠**当面说**。店长忘了就去读白板,一读就知道「哦,T-7 是小王在跟,他在等我批」。 - 白板上**永远不写「事实」**,只写「谁声称了什么」。因为白板一旦能写「事实」,店员就会偷懒信白板 —— 核对就死了,而核对是你最不想弄丢的东西。 一句话:**账搬上墙,事留在桌上。** ## 2. 方案本体:五层 + 一条红线 ### 2.0 红线:投递层一个字不改 | 现状 | 为什么不动它 | |---|---| | 点对点推、**原话**进上下文 | 互核的载体。核对的对象是原话,不是二手摘要 | | 双门批准(发送方 + 接收方各一次) | 平级会话之间没有天然可信 | | **未投递即丢**、返回首行 `❌ 未投递` | 「先排队、以后再送」=把信任决策挪到未来;会诊 2/2 反对持久队列 | | 插件层**不合并、不摘要、不拦截** | 合并器就是一层摘要,会造出「无署名的二手声音」——没人可怀疑,互核即死 | | 不做人工分拣台 | 人处理不了这个流量(你的判断,会诊同意) | ### 2.1 第一层:台账(新增一个文件) - **落点**:`team/<团队名>/tasks.md`,与现有 `decisions.md` **同构**:只追加、每行带署名(作者会话 id)、序号由插件单调分配、单行有长度上限。 - **只写三种行**:派活、接活、**声称**(做完/卡住/需要授权)。以及**第四种:存疑**。 - **它不能改** —— 只能追加。有争议就**再加一行**,不是覆盖旧行。 - **「当前状态」是读的时候算出来的**(每个任务取最后一条主张),并且输出里必须标注「这是派生读数,不是事实」。 - **谁有资格说什么**:派活的人记派活;接活的人只能记自己那条的状态;**任何人**都能对任何一行记「存疑」。反驳和记账走**同一条写入路径** —— 不需要任何新机制。 - **状态永不由投递事件自动推进**:消息送到了 ≠ 事做完了。只有人的显式主张能推进。 ### 2.2 第二层:映射(消息 ↔ 台账,用现成的字段) - 派活时给每件事一个编号:`t-<序号>`。 - 每条回报消息带上 `meta.ref = t-<序号>`(这个字段**今天就有**:≤16 字符、已校验、已渲染在消息 banner 首行)。 - 效果:主会话收到一条回报,不必记得全部计划,看 `ref` 就知道**这是哪件事的更新**;反过来按 `t-` 号也能把相关消息归到一起(**读时算,不建队列**)。 - **零新增存储、零新工具**:这是约定 + 一个读法。 ### 2.3 第三层:形态开关(多会话 ↔ agent-team) - 团队的记录里加一个**形态**字段:`多会话`(默认)/ `agent-team`。**由人指定、可来回切**(你的口径)。 - 插件只报**诊断**(「多会话通道不可用,你可以切」),**绝不自动切**。 - agent-team 档下: - **成员名册不由本插件拥有**:只存一个**指针**(Lead 会话 id),成员视图**读时现算**并标注「来源:宿主 Agent Teams」,**一行都不拷贝**进 settings; - **只读不写**:不代理宿主的 spawn / 派活 / 改任务状态(既避开实验 API 的 schema 变更风险,也不在两套权限模型之间发明第三套)。 - **能力矩阵(必须如实写进 README 与界面)**: | 能力 | 多会话档 | agent-team 档 | |---|---|---| | 跨会话投递(`team_link_send`) | ✅ 主用途 | ❌ 成员是子代理,不是可投递目标 | | 双门批准 / 配对 | ✅ | ❌ 用不上(宿主那套没有独立信任模型) | | 换届(`rotate`)/ 恢复(`recover`) | ✅ | ❌ 没有可换届的会话;Lead 会话没了团队就散了 | | 看门狗(`watch`) | ✅ | ❌ teammate 不是根代理,盯不了 | | 黑板(decisions / discipline / tasks) | ✅ | ⚠️ 仍可用,但只有 Lead 一方读写 | | roster 身份 / 版本史 | ✅ | ⚠️ 只记「本团队是 agent-team 档 + Lead 是谁」 | | 团队状态卡(只读) | ✅ | ⚠️ 成员部分读宿主投影(2026-09-27 已核:本部署已挂载该服务 ⇒ 探针 available=true) | | 会话深链 / 导出 | ✅ | ✅ 与形态无关 | 结论:agent-team 档应叫「**单会话兜底档**」,不是「另一种平等的形态」。**多会话不可用**时才切过去,切过去就等于放弃跨会话的全部能力。 - **切回来**:**信任不自动回迁** —— 旧的配对可能早已过期,必须人重新点一次批准(fail-closed)。 - **切换本身**:记一行模式史进 `decisions.md`,走一次人类确认;**不套换届的令牌机制**(切形态没有「继任者」这个角色)。 ### 2.4 第四层:可观测(给人看的一屏) - 现在这些事实**只有模型读得到**。夜里三点团队卡住,人要知道六件事:谁在跑 · 谁静默多久 · 哪个角色空着 · 有没有在飞的换届令牌 · 看门狗在盯谁 · 台账最后几条。 - 做一张**只读**的团队状态卡(零新信任面)。 - 附带一条**反面预警注记**(会诊提出):**消息量塌缩、而台账写入增长** ⇒ 大家开始「只写行、不说话」⇒ 互核正在死亡。这是可观测项,不是机制。 ### 2.5 第五层:约定(不是机制,写进 `discipline.md` 就生效) 1. **派活即落账**:派活的人当场在台账记一行(带 `t-<序号>`)。 2. **回报必带 `ref`**:回报消息必须挂到对应任务号。 3. **记不清就读台账**,不靠回忆。 4. **验货去查文件与证据,不查白板** —— 白板只是路标。 ## 3. 台账长什么样(样例:**拍板前的草案格式**) > ⚠️ 本节样例是讨论期的**草案**(4 列、任务号写在正文里)。**最终行格式以 §9.3.1 为准**(6 列:`seq | ISO时间 | author | kind | task | 正文`)——实现时不要照抄本节。 `team/<团队名>/tasks.md`(只追加,谁都改不了历史行): ```text seq | 时间 | 作者 | 正文 41 | 2026-09-26 21:10:02 | lead@abc123 | 派活 t-7:让 worker-b 复核 §3 的行号(ref=t-7) 42 | 2026-09-26 21:12:44 | worker-b@def456 | t-7 接了,预计 10 分钟 43 | 2026-09-26 21:19:31 | worker-b@def456 | 声称 t-7 完成:核出 3 处错(依据 lib/index.js:3548 / :3550、README §四) 44 | 2026-09-26 21:21:08 | lead@abc123 | 对 43 存疑:第 2 处我读到的行号不同(依据 …);已发消息请 worker-b 复核 45 | 2026-09-26 21:24:55 | worker-b@def456 | 撤回 43 的第 2 处:确认是我读数窗口旧了 ``` 读的时候:`t-7` 的**最后主张**=按 seq 取最后一条主张 ⇒ 显示「最后主张 done(… · 已由 #45 撤回 · 撤回者 …)· 存疑已由 #45 消解」。若存疑没被任何后续主张 / 撤回消解,就在**同一行行尾**印 `⚠ 未消解存疑 N 条`(§9.3.3 的形状)—— **绝不按时间戳或序号自动裁决**。**`conflicted` 标签未采用**,被 §9 的措辞纪律取代(§9.3.4 规则 4:一律「最后主张 / 未消解存疑」,读面不得把主张写成事实)。 这个例子里最关键的是 **44 与 45**:**反驳是新增一行,不是覆盖旧行**;而且反驳**只能靠会话之间发消息来消解**,白板只负责记录结果。 ## 4. 台账 ≠ 内置任务板(为什么不能照抄) | 维度 | 内置 Agent Teams 的任务板 | 本方案台账 | |---|---|---| | 能不能改已写下的行 | 能(CAS 改状态) | **不能**(只追加) | | 谁有资格改 | 有权限的成员 | 谁都能**追加**,没人能**覆盖** | | 冲突怎么表现 | 后写覆盖前写,或过期 revision 被拒 | 未消解存疑**显式**标 `⚠ 未消解存疑 N 条`,摆着不裁决(**`conflicted` 标签未采用**,被 §9 的措辞纪律取代) | | 一个错误写进去的后果 | 板子自认为现实 ⇒ 全员继承 | 台账只是路标 ⇒ 仍要靠消息核对 | | 压缩后能否重建全局 | 能 | 能(这是它唯一要干的事) | 内置的任务板能成立,是因为写它的人**都是 Lead 的孩子**、权限继承、天然可信。平级不信任的世界里**没有仲裁者** —— 谁有资格把 `doing` 改成 `done`?没人有。所以只能「谁声称、谁署名、谁反驳」。 ## 5. 明确的红线(本方案不碰的东西) 1. 不改投递层:不加持久队列、不合并、不摘要、不拦截、不做人工分拣台。 2. 不抄 CAS 看板,不引入任何「可改状态」的共享结构。 3. 不在 agent-team 档下代理宿主工具、不写宿主状态。 4. 不自动切形态、不悄悄降级(不可用就刺眼报错)。 5. 不新增「会误判的持久状态」:台账是**只追加的可读文件**,不是隐藏状态机。 ## 6. 要你拍的三件事(**已拍板**) > **拍板结果(2026-09-26)**:一 = **A**(新增台账文件 `tasks.md`)· 二 = **B**(对话里说一声 → 人类确认框 → 落笔 + 模式史;设置 UI 永远可作兜底)· 三 = **B**(提示式交卷,不阻断切换)。以下保留拍板前的三个选项与理由,作为决策留痕。 ### 拍板一:台账放在哪? | 选项 | 做法 | 代价 | 好处 | |---|---|---|---| | **A(我建议)** | 新增第三个黑板文件 `tasks.md`,与 `decisions.md` 同构 | 要改 README §四与既有设计档的「黑板两件套」声明(一次文档同步) | 写入路径复用现成的;读面单一;**不碰投递层** | | B | 塞进 `decisions.md` | 裁决与进行中的活混在一起,读时要过滤;500 字符/行上限更紧张 | 零新增文件 | | C | 不做台账,只靠约定(worker 自己写文件) | 没有统一读面,压缩后仍无处可查 | 零改动 | **建议 A**,并且**只做两件事**:只追加 + 读时投影;**不做任何状态机、不做写入校验之外的自动化**。 ### 拍板二:形态切换由谁触发? | 选项 | 做法 | 代价 | |---|---|---| | A | 只能在设置 UI 改(人是超级写者) | 半夜要切形态得去开设置 | | **B(我建议)** | 你跟主管说一声 → 模型调用 → **弹一次人类确认框** → 落笔 + 记一行模式史。没有确认服务时 **fail-closed(不切)** | 需要一次确认交互设计 | | C | A + B 都要 | 工作量略大 | **建议 B**(A 作为永远可用的兜底):你要的就是「说一声就切」;确认框是你们**既有**的模式(换届/恢复都这样),**不引入新的授权类型**。 ### 拍板三:teammate 的成果怎么收? 问题:teammate 是子代理,**没有持久身份**。切走形态、解散团队、Lead 会话关闭后,它们做过的事只留在 Lead 会话日志里 —— 那不是本插件的读面。 | 选项 | 做法 | 代价 | |---|---|---| | A | 强制交卷(不交卷不让切) | 可能把切换卡死 | | **B(我建议)** | **提示式**:切换/解散时列出「这些 teammate 的成果还没外化」,由人决定;不阻断 | 不保证成果一定被保留 | | C | 不做,只在文档里写明这是 agent-team 档的固有代价 | 用户可能不知道要丢东西 | **建议 B**:你们的红线是「不能悄悄」,不是「必须做」;提示式符合「失败要可见、判断留给人」。 ## 7. 实施顺序(已拍板,按风险从低到高) > **第 0 步已于 2026-09-26 完成**:四条纪律已进 README §四的「推荐起手的 `discipline.md`」;README 置顶新增「与 DSH Agent Teams 的关系:正交,不是竞争」(并已进目录与设计文档索引);本页与对比页 §7 已回填拍板结论。**第 1 步(台账)已于 2026-09-26 实施**(`lib/index.js` + `host-half.test.mjs`,判据 U1–U18 见 §9;**当次实测** `node host-half.test.mjs` → `1057 (failed: 0)`、`node client-half.test.mjs` → `269 (failed: 0)`,逐轮红相/绿相读数见 `docs/verification-log.md`)。**第 2 / 4 / 6 步已于 2026-09-26 作为「可观测批(A 批)」一并实施**(实施级设计另立一档:[observability-batch-design-2026-09-26.md](observability-batch-design-2026-09-26.md),判据 U1–U14;范围 = 本档 §7 的步 2 + 步 4 + 步 6 两半;**当次实测** `node host-half.test.mjs` → `1117 (failed: 0)`、`node client-half.test.mjs` → `269 (failed: 0)`,红相/绿相读数见 `docs/verification-log.md` 的「可观测批(A 批)」一节;提交哈希 `ac3a831d74fb64ae4f6577b3f79a9cc0cc7fb371`(回填于 2026-09-27))。**第 3 步(形态)与第 5 步(交卷提示)已于 2026-09-27 作为「形态批(B 批)」一并实施**(实施级设计另立一档:[team-mode-batch-design-2026-09-26.md](team-mode-batch-design-2026-09-26.md),判据 U1–U14 + U11b;**当次实测** `node host-half.test.mjs` → `1168 (failed: 0)`、`node client-half.test.mjs` → `269 (failed: 0)`,红相/绿相读数见 [verification-log.md](verification-log.md) 的「形态批(B 批)」一节;提交哈希 **`12f0874`(2026-09-27 由父会话回填)**)。**B 批收尾修复轮(代码评审 R1–R6)已于 2026-09-27 在同一批内闭环**:R1 诊断话术软化(不再把「服务当前不可见」断言成「profile 没装载」)· R2 同一判据只探一次 · R3 镜像与形态史**同源** · R4 幂等语义明示「首次声明也算一行」· R5 无会话身份时形态诊断行**不判** · R6 本档状态行与 §7 步 3 的「唯一的行为改动」收窄为「唯一的**门 / 写路径语义**改动」(R7 仅留档、代码与夹具一字未动);本轮新增 **9** 条宿主断言 + 改写 **1** 条 ⇒ 套件自报 `1193 (failed: 0)` / `269 (failed: 0)`(此后真机冒烟修复轮 L1/L2 升至 `1203 (failed: 0)`,见 [verification-log.md](verification-log.md) 的「真机冒烟修复轮(L1 时基 / L2 镜像)」一节),读数见 [verification-log.md](verification-log.md) 的「B 批收尾修复轮(R1–R6)」一节;提交哈希 **`12f0874`(2026-09-27 由父会话回填)**)。 | 步 | 内容 | 规模 | 前置 | |---|---|---|---| | 0 | 文档与约定:本条四条纪律进 `discipline.md`;README 置顶「与 DSH Agent Teams 的关系」;对比页 §7 回填 | 小(零代码) | 无 | | 1 | ✅ **已实施**(2026-09-26;当次实测 `host 1057 (failed: 0)` / `client 269 (failed: 0)`;提交哈希:`3a9635c7f7248f34ed0d1cd23f95099fa58c1ef0`(回填于 2026-09-26)):台账 `tasks.md`(只追加 + 读时投影)+ `t-<序号>` 约定 | 中 | 拍板一 | | 2 | ✅ **已实施**(2026-09-26,随「可观测批(A 批)」,设计与判据见 [observability-batch-design-2026-09-26.md](observability-batch-design-2026-09-26.md) §4.1;提交哈希 `ac3a831d74fb64ae4f6577b3f79a9cc0cc7fb371`(回填于 2026-09-27)):解读会话的 **12 窗口**(`PREVIEW_SESSIONS`)—— 台账收件视图与派生回执都依赖它;本批把它从「固定 12」扩成「**默认 12 + 点名 `readIds` + 分页 `offset`**」,并把成本上限定成**按工具分账**的不变量 | 中 | 无(可并行) | | 3 | ✅ **已实施**(2026-09-27,随「形态批(B 批)」,设计与判据见 [team-mode-batch-design-2026-09-26.md](team-mode-batch-design-2026-09-26.md) §4.1 / §4.2 / §4.3;**当次实测** `host 1168 (failed: 0)` / `client 269 (failed: 0)`,提交哈希 **`12f0874`(2026-09-27 由父会话回填)**):形态字段 + agent-team 档(只读投影 + 拒绝时指路 + 不悄悄降级)—— 形态记在 settings 的 `mode` / `leadSessionId`;`team_link_roster action=set-mode` 第五个封闭动词;读面形态段(roster get + 状态卡 ⑦ 段);**分歧修复轮 `DIVERGENCE(8)`(2026-09-27)在同批内闭环 8 条分歧**(唯一的**门 / 写路径语义**改动是 D8:幂等放宽为「同档同 Lead + 账上有该行」,让「重发以补记」这条指路可执行;另 D1 按真机实测更正文档、D2/D3/D4/D5/D6/D7 见该档与 [verification-log.md](verification-log.md) 的「分歧审计修复轮 `DIVERGENCE(8)`」一节;本轮新增 16 条宿主断言;**收尾修复轮 R1–R6(2026-09-27)**又在同批内闭环 6 条评审结论(R4 明示「首次声明也算一行」· R6 把本行那句声明收窄为「唯一的门 / 写路径语义改动」· 其余见该轮验证档一节),再新增 9 条断言 + 改写 1 条 ⇒ 当时的套件自报 `1203 (failed: 0)` / `269 (failed: 0)`(**该读数已被后续批次取代,当前为 `1264 / 269`**,见 [verification-log.md](verification-log.md) 末两节)(`1203 = 1193 + 10`:真机冒烟修复轮 L1/L2 于 2026-09-27 再新增 10 条宿主断言,读数见 [verification-log.md](verification-log.md) 的「真机冒烟修复轮(L1 时基 / L2 镜像)」一节)) | 中 | 拍板二 | | 4 | ✅ **已实施**(2026-09-26,随「可观测批(A 批)」:新工具 `team_link_status`,六段 + 判据写死的反面预警注记(**第 ⑦ 段「形态」由 2026-09-27 的「形态批(B 批)」追加**,见 [team-mode-batch-design-2026-09-26.md](team-mode-batch-design-2026-09-26.md) §4.3);见 [observability-batch-design-2026-09-26.md](observability-batch-design-2026-09-26.md) §4.2;提交哈希 `ac3a831d74fb64ae4f6577b3f79a9cc0cc7fb371`(回填于 2026-09-27)):只读团队状态卡 + 反面预警注记 | 中 | 步 2 | | 5 | ✅ **已实施**(2026-09-27,同属「形态批(B 批)」,见 [team-mode-batch-design-2026-09-26.md](team-mode-batch-design-2026-09-26.md) §4.4;提交哈希 **`12f0874`(2026-09-27 由父会话回填)**):teammate 交卷提示 —— 切换确认框正文列出「不会迁移的三件事」(信任 / 成员名册 / 任务归属)+ 当前 teammate(可读则列、不可读如实说)+「请确认它们的结论已落到黑板或文件」;**提示式、不阻断**,不逐成员推断有没有交卷 | 小 | 拍板三 + 步 3 | | 6 | ✅ **已实施**(2026-09-26,随「可观测批(A 批)」;**收件视图 + 派生回执 均 ✅**——两半都在本批,见 [observability-batch-design-2026-09-26.md](observability-batch-design-2026-09-26.md) §4.3 / §4.4;提交哈希 `ac3a831d74fb64ae4f6577b3f79a9cc0cc7fb371`(回填于 2026-09-27)):**台账收件视图与派生回执**(会诊 Q1 采纳项:按 `t-` 把相关消息归到一起,**读时算**、不建队列) | 中 | 步 1 + 步 2 | 第 0 步**零风险且已完成**;**第 1 步(台账本体)不依赖任何其它步**——它读的是 `tasks.md` 而非会话日志(§9 已核);第 2 步是**第 4 步(状态卡)与 §2.2 收件视图**的前置。**第 2 / 4 / 6 步的实施级设计与判据(U1–U14)另立一档**:[observability-batch-design-2026-09-26.md](observability-batch-design-2026-09-26.md)——自此「**每批一档**」是本仓的既定约定(第一批的内嵌式 §9 是特例,不再复制)。 ## 8. 本档不写什么 / 待验 - §1–§7 是**提案与决策留痕**,§9 是**实施级设计**(落点已逐条实读)。两者都**不是**「已实施事实」——落地与否只看 §9.8 的阶段状态与该批的交付报告(**§9 的第一批已于 2026-09-26 实施**:见 §7 第 1 步与 §9 的状态行)。 - 不写任何宿主源码级结论:对比页 §9 的「未复核线索」在独立复核前不进本方案。 - ✅ **已核(2026-09-27,形态批实施期;同日由分歧修复轮 `DIVERGENCE(8)` D1 实证更正)**:agent-team 档下「宿主 `agentTeams` 投影能否被本插件读到」——① **服务名与读面形状已核实**(`TeamService extends Service` → `super(ctx, 'agentTeams')`;读面 `tryMembership(agent)` / `listMembers(agent)`,**以活动代理本人为凭据**);② **本部署已装载该组合包 ⇒ 服务已挂载**(活动 profile 是 desktop,其 `dsh.profile.bundles` 含 `@deepseek-ai/dsh-experimental-agent-team-profile`;本会话系统提示词同时含该工具包的 POLICY 常量与只由它注册的 `spawn_teammate` 声明 ⇒ 它的 `inject` 含 `agentTeams` 已被满足)⇒ 探针 `available:true`、**U11b 的拒绝路径在本部署不会触发、US1 可达**。此前记的「本部署未装载」(证据是便携残留的 `profile/profiles/web/...`)**是错的**,已更正。读面仍按**分支**文案处置(服务缺席 ⇒ 「成员名册不可读(本部署未提供该投影)—— 仍可读 Lead 与形态」;服务在场而读不到 ⇒ 中性首行 + 原因行)。详见 [team-mode-batch-design-2026-09-26.md](team-mode-batch-design-2026-09-26.md) §8.1 A1 与 §4.3(3)。 - ✅ **已核实**:`tasks.md` 的写入路径**可以**与 `decisions.md` 完全同构——见 §9.2 落点表(`appendFile` + 每文件独立 seq + 同样的 500 码点上限)。会诊列为「待核实」的那一项,本会话已读码确认。 ## 9. 实施级设计 · 第一批:台账 `tasks.md` > **状态**:✅ **已实施**(2026-09-26,eng_coder,提交哈希 `3a9635c7f7248f34ed0d1cd23f95099fa58c1ef0`(回填于 2026-09-26);文件 `lib/index.js` + `host-half.test.mjs`;**当次实测** `node host-half.test.mjs` → `1057 (failed: 0)`、`node client-half.test.mjs` → `269 (failed: 0)`,读数账本见 `docs/verification-log.md`)。本批范围**只做台账本体**(同日分歧审计的修复轮 D1–D9 也在本批内:只动台账的渲染 / 分配与描述面,未扩范围);§7 的第 2–5 步(12 窗口 / 形态切换 / 状态卡 / 交卷提示)**不在本批**。 > **落点核对**:§9.2 的行号按 `3eba6c1` 的 `lib/index.js` **逐条实读**,不是估计。 ### 9.1 需求 **用户故事** | # | 角色 | 故事 | | --- | --- | --- | | US1 | 协调者 | 把一件事派出去时,能在台账登记一条「派活」并**拿到插件分配的编号**(`t-`),后续派发消息带上这个号 | | US2 | 工作线 | 接活 / 卡住 / **声称**完成时,能往同一任务**追加**一行**署名**主张——而不是覆盖别人的记录 | | US3 | 任何会话 | 不同意某条主张时,能追加一条「存疑」,**和记账走同一个写入路径**;工具**不替我裁决** | | US4 | 接管者 / 被压缩后的主会话 | 读一次 `team_link_team_read` 就能看到「每个任务的最新主张是什么、有没有未消解的存疑」,不必回读历史消息 | **非功能标准** | # | 标准 | | --- | --- | | N1 | **只追加**:本批新增的任何写入路径都不得重写既有行(`discipline` 的整文件替换语义本批不动) | | N2 | **有界呈现**:读面必须给出界(窗口 + 派生扫描上限),截断时**如实标注** | | N3 | **零新持久状态**:台账是磁盘上的只追加文本(与 `decisions`/`discipline` 同族)——不引入 settings 新键、不引入内存态 | | N4 | **措辞纪律**:读面不得把「主张」写成「事实」(禁止「状态=完成」「已完成」这类措辞) | ### 9.2 现状与落点(as-of `3eba6c1`) | 现有件 | 位置(`lib/index.js`) | 本批动它吗 | | --- | --- | --- | | 文件名常量(roster / decisions / discipline) | :1673–1675 | +1:`TASKS_FILE = "tasks.md"` | | 黑板文件闭集 `BLACKBOARD_FILES = {decisions, discipline}` | :1677 | **扩为三值** | | 单行上限 `BLACKBOARD_LINE_LIMIT = 500`(按码点) | :1680 | 复用,不改 | | decisions 窗口 `DECISIONS_WINDOW = 20` | :1682 | 新增同值的 `TASKS_WINDOW = 20`(独立常量,不共用) | | 派生扫描上限 | — | 新增 `TASKS_PROJECTION_SCAN = 500` | | `blackboardPaths(team)` → `{dir, roster, decisions, discipline}` | :2083–2091 | +1 键:`tasks` | | `blackboardHash` / `readBlackboardFile` / `blackboardLines` / `readBlackboardLine` / `blackboardAuthor` / `decisionRow` | :2172–2230 | **全部复用**;只新增 `tasksRow(...)` 与 `lastSeq(text)` | | `lastDecisionSeq(text)` | :2196–2205 | 抽出公共 `lastSeq(text)`,`lastDecisionSeq` 改为调用它(语义零变化) | | `team_link_team_read` 工具 | :2502–2554 | 追加 tasks 块(原始窗口 + 派生视图) | | `team_link_team_append`:白名单校验 :2575 / decisions 分支 :2579–2603 / discipline 分支 :2605–2627 | :2556–2632 | 白名单三值 + **新增** tasks 分支(两条既有分支一行不动) | | `file` 参数枚举 `["decisions","discipline"]` | :2561 | 扩为三值 | | 黑板测试夹具 `boardEnv` / `boardDir` / `decisionsPath` / `disciplinePath` | `host-half.test.mjs`:1990–2105 | 扩展出 tasks 夹具(同构) | ### 9.3 方案 #### 9.3.1 文件与行长格式 ```text /team//tasks.md seq | ISO时间 | author-session-id | kind | task | 正文 ``` - `kind` 是**闭集**:`plan`(派活)/ `claim`(接活)/ `done`(**声称**完成)/ `block`(卡住 / 需要授权)/ `dispute`(存疑)/ `retract`(撤回自己此前的某条主张)。 - `task` 形如 `t-`(`^t-\d+$`);**`kind=plan` 时由插件分配**(与 seq 同类机械动作),其余 kind 必填。 - 正文:单行、非空、≤500 码点(复用 `readBlackboardLine`)。 - seq:**每文件独立计数**(`lastSeq(tasks) + 1`),与 decisions 的计数器互不影响。 #### 9.3.2 写入面(`team_link_team_append`) 新增两个参数:`kind`(file=tasks 时必填)与 `task`(非 plan 必填)。 | 情形 | 行为 | | --- | --- | | `kind=plan` | 插件**扫整个文件**取已登记的最大号,分配 `t-` 并写行(不可解析行按 U3 的规则忽略、**不重置**计数器);**调用方若同时给了 task 即拒绝**(消除歧义)。并发两个 `plan` 可能拿到同一个号——与「同 seq」同族的**已知最坏情况**,如实写在 §9.4,**不引入锁** | | 非 plan 且 `task` 缺失 | 拒绝:「非 plan 必须给 task(形如 t-7)」 | | `task` 形状非法,或**该号从未在本文件登记过** | 拒绝并提示先 `kind=plan` 登记——这一条正对着「凭记忆转录 id」那类事故 | | `kind` 不在闭集 | 拒绝并列出闭集 | | `kind` / `task` 出现在 `file=decisions` 或 `file=discipline` | **拒绝**(一行错误)——防参数误用静默生效 | | `kind=retract` | 与其它非 plan 相同(必填 `task`);**不校验**撤回者与被撤回主张的作者是否一致(约定级,见 §9.3.4 规则 2) | | 正文含换行 / 空 / 超 500 码点 | 拒绝(沿用既有文案风格) | | 团队不存在 / 无 workspace | 沿用既有文案 | 成功返回值给出 `seq`、`task`(plan 时是新分配的)、`author` 与文件路径——与 decisions 分支的返回形状同族。 #### 9.3.3 读面(`team_link_team_read`) 在 discipline 块之后追加: ```text --- tasks.md(只追加台账;显示末 20 条)--- 共 N 行,显示 20 条 · baseHash=<16hex>(仅供参考/审计:tasks 只追加、不接受 baseHash) (派生读数:扫描最近 500 行;逐任务给「最后主张」与「未消解存疑」——是读数,不是裁决) - t-7 · 4 行 · 最后主张 done(2026-09-26 21:19 · worker-b@def456)· ⚠ 未消解存疑 1 条 - t-8 · 2 行 · 最后主张 claim(2026-09-26 21:10 · lead@abc123) (原始行) 42 | 2026-09-26T...Z | lead@abc123 | plan | t-7 | 让 worker-b 复核 §3 的行号 ``` **时间基(渲染 = 本地时基,原始行 = UTC ISO)**: - 派生视图里的时间戳(上面两行的 `21:19` / `21:10`)是**本地时基** `YYYY-MM-DD HH:mm` —— 与同一屏末尾那句 `(读数 …)` **同一套本地格式化**(`readStamp` / `localStamp`)。理由:这两处时间要被**人一眼对比**(「这条主张是刚发生的吗」),同屏两个时基就是读数失准; - **原始行**(`(原始行)` 之下)的第 2 列是 `toISOString()` 的 **UTC** 值,**逐字保留、永不改写** —— 它才是事实源(可排序、可外部消费); - 因此渲染值与原始行值**相差一个时区偏移**,**不是**同一条串的截断。切 ISO 串前 16 位当渲染值曾是实现里的做法,**2026-09-27 真机冒烟修复轮**已废弃(真机同屏差 8 小时):渲染一律走本地格式化,时间的**那个瞬间**由 `new Date()` 解析(不是字符串操作);时间列解析不出日期的**手写行**单行化后回显该列(`preview()` 会折叠内部连续空白并 trim —— 不是逐字节照抄) —— 既不编造时间,也不打印 `NaN` 串。 边界(逐条如实标注):文件不存在 →「(文件不存在,按空处理:0 行)」;超过扫描上限 →「(派生只扫描最近 500 行;更早的 N 行未参与派生)」;不可解析行 →「(N 行无法解析,未参与派生)」。 #### 9.3.4 派生规则(严格定义,不许自由发挥) 对某个 `task`,取其全部行按 seq 升序: 1. **最后主张** = 该任务中 `kind ∈ {claim, done, block}` 的**最后一条**;一条都没有 →「尚无主张(仅登记)」。**若其后存在 `retract` 行**,该主张**不删除**(只追加台账不抹历史),呈现为「最后主张 done(**已由 #N 撤回** · 撤回者 …)」,并同时保留原主张的作者;**多条撤回时 `#N` 取最后一条撤回行**(与「最后一条主张」同一条取法),撤回**条数不另印**(原始行里本就看得到,§9.3.4 不做自由发挥)。 2. **未消解存疑** = 存在 `kind=dispute` 的行,且在其之后**不存在**该任务的 `claim / done / block / retract` 行 → 标 `⚠`;否则标「存疑已由 # 消解」。**两条子情形在此定死**(实现即此,各由一条断言钉住):① **同一任务同时存在未消解与已消解存疑时印 `⚠`**(`if` 分支优先,「存疑已由 #N 消解」那一支不再出现);② **多条存疑都已消解时,`#N` 取「最后一条被消解存疑的**首个**应答行」的 seq**(不是它之后的那一条应答行,也不是更早那条存疑的应答行)。**作者关系不在派生里强制**(黑板无写权限门,见 §9.4):任何会话都可追加 `retract`,读面如实显示「主张者」与「撤回者」两个 id,由人判断这够不够。 3. **绝不自动裁决**:派生只做「取最后一条 + 看其后有没有新主张」,不比较正文、不按时间戳裁冲突。 4. **措辞**:一律「最后主张 / 未消解存疑」;不得输出「状态=完成」这类把主张写成事实的词。 ### 9.4 边界与红线 - **只追加**:`appendFile`,不重写;并发追加最坏同 seq、**不丢行**(沿用 decisions 的既有论证)。 - **并发分配 `t-` 的已知最坏情况**:两个 `plan` 同时扫到同一个最大号时会撞号。黑板**无锁**(既有纪律),因此这与「同 seq」同族、**如实声明而不消除**;撞号时该号下会多出一行 `plan`,读面照显。 - **闭集**:`file` 白名单三值,绝不接受路径形状;`kind` 闭集;`task` 形状校验;**`kind` / `task` 与 `file≠tasks` 的组合一律拒绝**(防参数误用静默生效)。 - **无写权限门**(与黑板一致);作者身份照记,无会话身份记 `unknown`。 - **派生是读数**:删掉 `tasks.md` 不影响任何写入路径;派生结果随时可从原始行重算。 - **零新持久状态**:不新增 settings 键、不引入内存态。 ### 9.5 验收判据(U1–U18) | # | 判据 | 覆盖 | | --- | --- | --- | | U1 | 白名单:`file=tasks` 接受;路径形状与其它任意值拒绝 | 9.3.2 | | U2 | 追加语义:写后文件 = 旧内容 + 新行(既有行逐字节不变) | N1 | | U3 | seq 独立且单调:连续追加递增;含不可解析行时不重置计数器 | 9.3.1 | | U4 | 单行上限:正文 >500 码点拒绝;=500 接受(按码点计,astral 记 1) | 9.3.1 | | U5 | 换行拒绝:正文含 CR/LF 一律拒绝 | 9.3.1 | | U6 | 署名:author = 调用方会话 id;无身份记 `unknown`;竖线与换行被替换为 `_` | 9.4 | | U7 | kind 闭集:六个合法值之外一律拒绝(含缺失) | 9.3.2 | | U8 | plan 分配:`kind=plan` 由插件分配 `t-`,返回体给出该号 | 9.3.2 | | U9 | plan 禁带 task:`kind=plan` 与 `task` 同时出现 → 拒绝 | 9.3.2 | | U10 | task 校验:非 plan 缺 task → 拒绝;形状非法 → 拒绝;**未登记的号 → 拒绝** | 9.3.2 | | U11 | 读面存在性:有文件时给出「共 N 行 / 显示 M 条 / baseHash」;无文件时如实标注空 | 9.3.3 | | U12 | 派生正确性:「最后主张」取 claim/done/block 的最后一条;无主张标「尚无主张(仅登记)」 | 9.3.4 | | U13 | 未消解存疑:dispute 之后无新主张 → 标 `⚠`;其后有新主张 → 标「已由 # 消解」 | 9.3.4 | | U14 | 回归零变化:decisions / discipline 的行为与文案**逐字不变**(唯一例外:白名单拒绝文案由两值改三值) | 9.6 | | U15 | `retract`:撤回后最后主张**不被抹除**,呈现「最后主张 X(已由 #N 撤回 · 撤回者 …)」;撤回者 ≠ 原主张者时**两个 id 都出现** | 9.3.4 | | U16 | 分配扫**全文件**:掺入不可解析行后,新号仍 = 已登记最大号 + 1(不重置、不倒退) | 9.3.2 | | U17 | 截断标注:超过扫描上限 → 出现「更早的 N 行未参与派生」;存在不可解析行 → 出现「N 行无法解析,未参与派生」 | 9.3.3 | | U18 | 参数误用拒绝:`kind` 或 `task` 与 `file=decisions` / `discipline` 同时出现 → 拒绝,且该次调用**零写入** | 9.3.2 | ### 9.6 明确不做(本批) 1. 不做状态机 / CAS / 自动裁决 / 自动推进(「送达即完成」禁止) 2. 不做写入权限门 3. 不做跨会话物理队列(会诊 Q2 已裁不做) 4. 不做 12 窗口、形态切换、状态卡、交卷提示(§7 第 2–5 步) 5. 不改 decisions / discipline 的任何语义与文案——唯一例外是白名单拒绝文案由「decisions 或 discipline」改为三值(**这一条本身要有断言锁**) ### 9.7 显式假设与待验 | # | 假设 | 不成立的后果 | 验证方式 | | --- | --- | --- | --- | | A1 | 既有 `boardEnv` 夹具可直接扩展出 tasks 夹具 | 测试面要重搭 | 实施期在 `host-half.test.mjs` 内验证 | | A2 | 抽出 `lastSeq` 后既有 995 断言零回归 | 回退到「复制一份实现」 | 阶段 4 跑整套读数 | | A3 | 500 码点上限对「任务正文」够用 | 放宽上限**需另立一批**(本批不得顺手改) | 实施期如实记录是否触顶 | ### 9.8 实施分批(交给 eng_coder 的阶段) | 阶段 | 目标 | 文件 | 自检 | | --- | --- | --- | --- | | 1 | 常量 / `blackboardPaths` / 白名单三值 | `lib/index.js` | 语法检查 + 既有套件全绿(995) | | 2 | 写入分支(kind/task 校验 + plan 分配 + 追加) | `lib/index.js` | U1–U10 + U15 / U16 / U18 转绿 | | 3 | 读面(原始窗口 + 派生视图 + 边界标注) | `lib/index.js` | U11–U13 + U17 转绿 | | 4 | 判据补齐与全量读数 | `host-half.test.mjs` | 新断言**实施前必红**(功能缺失)→ 实施后绿;两套件双绿 | | 5 | 文档同步(§9.9 六条) | README / CHANGELOG / 本档 | 逐条勾 | ### 9.9 跨文档同步清单(与实施**同批**必改) 1. `README.md` §四 黑板目录树:加 `tasks.md` 一行,并把「两件套」措辞改为三件套 2. `README.md` §四 黑板小节:`team_link_team_append` 的参数说明(`file` 三值 + `kind` / `task`) 3. `README.md` §四「推荐起手的 `discipline.md`」旁:补一段「台账推荐用法」(派活落账 / 回报带 `ref`) 4. `README.md` 工具一览表:`team_link_team_append` 行 5. `CHANGELOG.md`:新增条目(按「修了什么 → 为什么 → 怎么验证」体例) 6. 本档 §7 第 1 步:标「✅ 已实施」并附提交哈希