{ "meta": { "schemaVersion": "0.1", "title": "dsh-web-relay 主 agent 语境能力 lesson 库(机器可读)", "updatedAt": "2026-09-14T18:15:11.414Z", "carrierOf": "docs/main-agent-lesson-schema-v0.1.md", "statusConvention": "proposed(主 agent 产出)→ approved / in-runbook(用户或外部 AI 确认,记 confirmedBy/confirmedAt);in-runbook=要点已并入 main-agent-runbook 正文;superseded=内容已并入他条(扩展值,机器消费时应跳过)", "confirmedAt": "2026-09-04T13:14:35.897Z", "confirmedBy": "user(主对话确认:同意三态分类建议)", "confirmationNote": "2026-09-05:用户确认 025-027 转 approved。" }, "lessons": [ { "id": "L-2026-0903-001", "title": "high+review:true 步骤不得由主 agent 自批 approved", "category": "collaboration-rhythm", "layer": "L3", "trigger": "importance=high 且 review=true 的步骤实施完成,准备置状态时", "decision": "置 review / 提交 auto-review(external→dialog→manual),由外部 AI、dialog 或用户在面板批准;仅 low/review:false 可自批(reviewedBy=mainagent)", "rationale": "审核来源是 v1.9 审计链的一环;自批会伪造 reviewedBy 并破坏版间门与降级链语义", "evidence": "expr-2026-09-03_01-27-21;main-agent-runbook-v0.1.md §2.2", "confidence": "high", "status": "in-runbook", "suggested": "in-runbook", "recordedAt": "2026-09-03T00:00:00.000Z", "confirmedBy": "user", "confirmedAt": "2026-09-04T13:14:35.897Z" }, { "id": "L-2026-0903-002", "title": "被唤起先读 steps.json 顶层 iterations/autoDecision 再表态(叙述式声明可能未解析)", "category": "repository-knowledge", "layer": "L3", "trigger": "收到 dsh-web-relay handoff / 主 agent 对任务表态前", "decision": "先读 expr-.steps.json 顶层(protocolVersion/iterations/autoDecision/currentIteration/status)再表态;叙述式声明(Answer 文本里的 iterations/autoDecision)可能未落盘,以落盘值为准", "rationale": "叙述式'自动迭代N个版本'等声明未落盘时,引擎按普通任务执行(01:27 案例顶层仍 1/false)", "evidence": "expr-2026-09-03_01-27-21;expr-2026-09-02_01-07-59 / 13-07-59 / 02-36-44(叙述式未落盘复现,自 L-2026-0903-007 并入);main-agent-runbook-v0.1.md §3 SOP.1", "confidence": "high", "status": "in-runbook", "suggested": "in-runbook", "crossRef": [], "recordedAt": "2026-09-03T00:00:00.000Z", "confirmedBy": "user", "confirmedAt": "2026-09-04T13:14:35.897Z" }, { "id": "L-2026-0903-003", "title": "tag 版本延续 v3.x,勿用旧 v1.9.x 命名", "category": "repository-knowledge", "layer": "L3", "trigger": "自动迭代发布、决定版本号/打 tag 前", "decision": "版本号延续 package v3.x(v3.3.x/v3.4.x/…)语义化续线;拒绝 v1.9.x/v2.x 旧协议时代命名;bump 用 node 重写 package.json 避免 BOM", "rationale": "仓库 tag 已演进至 v3.2.x~v3.4.0;回退旧命名会造成 tag/审计混乱", "evidence": "expr-2026-09-03_01-27-21(v3.4.x 被采纳);expr-2026-09-02_02-36-44(冲突负例,自 L-2026-0903-011 并入);main-agent-runbook-v0.1.md §4", "confidence": "high", "status": "in-runbook", "suggested": "in-runbook", "crossRef": [], "recordedAt": "2026-09-03T00:00:00.000Z", "confirmedBy": "user", "confirmedAt": "2026-09-04T13:14:35.897Z" }, { "id": "L-2026-0903-004", "title": "PS5.1 Set-Content UTF8 写 BOM 会破坏 JSON.parse,版本 bump 用 node 重写", "category": "tooling-trap", "layer": "L3", "trigger": "Windows 下用 PowerShell 写 package.json / JSON 文件后 node JSON.parse 失败", "decision": "用 node fs.writeFileSync(...,'utf8') 重写(去 BOM);勿用 PS5.1 Set-Content -Encoding UTF8", "rationale": "PS5.1 UTF8 编码写 BOM,BOM 破坏 JSON.parse(9/3 版本 bump 实测)", "evidence": "main-agent-runbook-v0.1.md §6 陷阱表", "confidence": "high", "status": "in-runbook", "suggested": "in-runbook", "recordedAt": "2026-09-03T00:00:00.000Z", "confirmedBy": "user", "confirmedAt": "2026-09-04T13:14:35.897Z" }, { "id": "L-2026-0903-005", "title": "规划-现状冲突:审计行号取证→交外部 AI 评审 restructure,不自裁", "category": "judgment-heuristic", "layer": "L3", "trigger": "外部 AI 规划与仓库现状冲突 / 疑似重造时", "decision": "行号级取证 + 历史 trace 对照 → 差异表经 /ask 交外部 AI 评审 restructure;不擅自替换外部 AI Step List", "rationale": "restructure 规划权在外部 AI/面板/用户;自裁曾导致越权(01-27-21 自我纠正)", "evidence": "expr-2026-09-03_01-27-21;main-agent-runbook-v0.1.md §2.3/§5", "confidence": "high", "status": "in-runbook", "suggested": "in-runbook", "recordedAt": "2026-09-03T00:00:00.000Z", "confirmedBy": "user", "confirmedAt": "2026-09-04T13:14:35.897Z" }, { "id": "L-2026-0903-006", "title": "外部 AI 声称 iterations/autoDecision 未落盘时,先核对 steps.json 顶层再判定模式失效", "category": "repository-knowledge", "layer": "L3", "trigger": "外部 AI 声称已声明自动迭代(iterations/autoDecision)而引擎未按自动模式执行时", "decision": "先核对 steps.json 顶层是否真的落盘,再判定'模式失效/引擎缺陷';勿以 Answer 文本为据", "rationale": "与 002 同源(以落盘为准),面向'对外部声称做复核'的判定场景", "evidence": "expr-2026-09-02_01-07-59(叙述式未落盘) vs expr-2026-09-02_01-33-59(严格 JSON 落盘正例)", "confidence": "medium", "status": "approved", "suggested": "approved", "crossRef": [ "L-2026-0903-002" ], "recordedAt": "2026-09-03T00:00:00.000Z", "confirmedBy": "user", "confirmedAt": "2026-09-04T13:14:35.897Z" }, { "id": "L-2026-0903-007", "title": "自动迭代先核对 steps.json 顶层落盘声明(叙述式 iterations/autoDecision 常未落盘,勿信 Answer 文本)", "category": "repository-knowledge", "layer": "L3", "trigger": "(与 002 重复)收到含'自动迭代N个版本'声明的任务时", "decision": "(同 002)以 steps.json 顶层落盘声明为准;本条目已并入 L-2026-0903-002", "rationale": "考古新增时未发现已存在的 002;2026-09-04 用户确认并入 002", "evidence": "expr-2026-09-02_01-07-59 / 13-07-59 / 02-36-44 / 01-27-21(已并入 002 evidence)", "confidence": "high", "status": "superseded", "suggested": "merge-into:L-2026-0903-002", "supersededBy": "L-2026-0903-002", "crossRef": [ "L-2026-0903-002" ], "recordedAt": "2026-09-04T00:00:00.000Z", "confirmedBy": "user", "confirmedAt": "2026-09-04T13:14:35.897Z" }, { "id": "L-2026-0903-008", "title": "被打回先查审核通道缺陷(摘要截断/dialog 无 JSON/批次连坐)再 reopen,非内容问题不改产物", "category": "judgment-heuristic", "layer": "L3", "trigger": "步骤被打回,准备 reopen / 补证据前", "decision": "先判断打回是否审核通道缺陷(审核摘要静默截断、dialog 无干净 JSON、batch 原子连坐)→ 举证驳回 reopen;确为内容问题才改产物补提", "rationale": "13-36-43 两次打回均为通道缺陷误判(截断/v3.3-1 修复、dialog 无 JSON);18-46-49 批次连坐放大成本", "evidence": "expr-2026-08-31_13-36-43;expr-2026-08-31_18-46-49", "confidence": "high", "status": "approved", "suggested": "approved", "recordedAt": "2026-09-04T00:00:00.000Z", "confirmedBy": "user", "confirmedAt": "2026-09-04T13:14:35.897Z" }, { "id": "L-2026-0903-009", "title": "一步一版时 currentIteration 计数失真,以步级 Vn 标签+done 状态判断真实进度", "category": "repository-knowledge", "layer": "L3", "trigger": "steps.json currentIteration 与步内 Vn 标签 / 实际 done 状态不一致时", "decision": "以步级 Vn 标签 + 步骤终态判断真实进度,不依赖 currentIteration 计数;如引擎发模板化'进入 Vn'消息与事实不符,以文件/事实为准并上报", "rationale": "一步一版(flat steps)模式下 ci 停在 2 而 V5/V3 已完成(14-34-33/15-00-11),计数与事实脱节", "evidence": "expr-2026-08-31_14-34-33;expr-2026-08-31_15-00-11", "confidence": "medium", "status": "approved", "suggested": "approved", "recordedAt": "2026-09-04T00:00:00.000Z", "confirmedBy": "user", "confirmedAt": "2026-09-04T13:14:35.897Z" }, { "id": "L-2026-0903-010", "title": "提审前先落实体产物(artifact_required),text-complete 与 artifact-complete 脱节会招致无效打回", "category": "engineering-action", "layer": "L2", "trigger": "步骤 complete / 提审(置 review)前", "decision": "artifact_required=true(默认)的步骤必须先产出实体产物并挂载 step.artifacts 再提审;纯分析步显式 artifact_required=false", "rationale": "text-complete(只写 notes)与 artifact-complete 脱节是打回主因(15-00-11/18-46-49)", "evidence": "expr-2026-08-31_18-46-49;expr-2026-08-31_15-00-11", "confidence": "high", "status": "approved", "suggested": "approved", "crossRef": [ "L-2026-0903-014" ], "recordedAt": "2026-09-04T00:00:00.000Z", "confirmedBy": "user", "confirmedAt": "2026-09-04T13:14:35.897Z" }, { "id": "L-2026-0903-011", "title": "版本/tag 命名以仓库 package v3.x 现状续线,拒绝回退 v1.9.x/v2.x 规划命名", "category": "repository-knowledge", "layer": "L3", "trigger": "(与 003 重复)外部 AI 规划要求打 v1.9.x/v2.x 等与仓库现状冲突的 tag 时", "decision": "(同 003)以 package v3.x 现状续线;冲突规划先在审计中标注 tag 冲突并纠正;本条目已并入 L-2026-0903-003", "rationale": "考古新增时未发现已存在的 003;2026-09-04 用户确认并入 003", "evidence": "expr-2026-09-03_01-27-21;expr-2026-09-02_02-36-44(负例,已并入 003 evidence)", "confidence": "high", "status": "superseded", "suggested": "merge-into:L-2026-0903-003", "supersededBy": "L-2026-0903-003", "crossRef": [ "L-2026-0903-003" ], "recordedAt": "2026-09-04T00:00:00.000Z", "confirmedBy": "user", "confirmedAt": "2026-09-04T13:14:35.897Z" }, { "id": "L-2026-0903-012", "title": "外部 AI 通道失能时代行其职要明示'代构造'并留独立审方兜底,防自审自产", "category": "collaboration-rhythm", "layer": "L3", "trigger": "外部 AI ask 通道反复中止/429 不可用,需要主 agent 代构造方案/Step List 时", "decision": "代构造的 Vn 要点/Step List 须在产出与 trace 明示'主 agent 代构造',并尽量留独立审方(dialog/manual)兜底审核", "rationale": "自己代构造又自己审核=自审自产(15-00-11 靠 dialog 独立审核兜底);角色混淆风险需透明化", "evidence": "expr-2026-08-31_15-00-11;expr-2026-08-31_13-36-43", "confidence": "medium", "status": "approved", "suggested": "approved", "recordedAt": "2026-09-04T00:00:00.000Z", "confirmedBy": "user", "confirmedAt": "2026-09-04T13:14:35.897Z" }, { "id": "L-2026-0903-013", "title": "自评自证(reviewedBy=mainagent)必须显式披露并留打回重审选项", "category": "collaboration-rhythm", "layer": "L3", "trigger": "主 agent 以 reviewedBy=mainagent 通过(含 high)步骤或收口时", "decision": "显式披露'自评自证,非外部 AI 逐审',并在交付物/档案中留'打回触发 auto-review 重审 / 一键收口'选项", "rationale": "autoReview=true 但审方=执行方是治理风险点(01-27-21 3 步 mainagent 自评);透明披露+重审选项可审计", "evidence": "expr-2026-09-03_01-27-21(交接档案 §3.4)", "confidence": "high", "status": "approved", "suggested": "approved", "recordedAt": "2026-09-04T00:00:00.000Z", "confirmedBy": "user", "confirmedAt": "2026-09-04T13:14:35.897Z" }, { "id": "L-2026-0903-014", "title": "artifacts 必须在提审前实挂载(notes 文本不算产物),否则招致连坐打回甚至熔断", "category": "engineering-action", "layer": "L2", "trigger": "提审前 / 被打回后补产物时", "decision": "产物文件实挂 step.artifacts(路径+内容),notes 文字不作为产物证据;缺失即补文件再提审", "rationale": "15-26-14 产物只写 notes 未挂 artifacts → 三连打回触发真实熔断(paused→resume)", "evidence": "expr-2026-08-27_15-26-14(熔断实战);expr-2026-08-31_18-46-49", "confidence": "high", "status": "approved", "suggested": "approved", "crossRef": [ "L-2026-0903-010" ], "recordedAt": "2026-09-04T00:00:00.000Z", "confirmedBy": "user", "confirmedAt": "2026-09-04T13:14:35.897Z" }, { "id": "L-2026-0903-015", "title": "发布纪律:功能代号/tag/semver 对齐;预发布 vX.Y.Z-iter.N → 正式 tag;避免 tag force 覆盖与跨大版本单跳", "category": "repository-knowledge", "layer": "L3", "trigger": "自动迭代多轮发布、打 tag、bump 版本时", "decision": "功能代号(v2.5/v2.6 等)、tag 与 package semver 三者对齐;预发布用 vX.Y.Z-iter.N,迭代完成升正式 tag;避免 tag force 覆盖与跨大版本单跳", "rationale": "15-42-26 v1.5.0 被 force 覆盖、22-15-02 功能号 v2.5/2.6 配 tag v2.2.0、20-20-48 1.5.2→2.0.0 单跳,均造成审计混乱", "evidence": "expr-2026-08-27_15-42-26;expr-2026-08-27_22-15-02;expr-2026-08-27_20-20-48", "confidence": "medium", "status": "approved", "suggested": "approved", "recordedAt": "2026-09-04T00:00:00.000Z", "confirmedBy": "user", "confirmedAt": "2026-09-04T13:14:35.897Z" }, { "id": "L-2026-0903-016", "title": "restructure 后保持审核上下文(步骤标题/摘要)与执行列表同步,防审核者按旧标题盲审", "category": "judgment-heuristic", "layer": "L3", "trigger": "restructure 变更步骤(id 复用/标题替换)后即将提交审核时", "decision": "restructure 后同步更新任务记录摘要/审核提示,标注被替换步骤的旧标题→新标题映射,防 dialog 等按旧摘要审核", "rationale": "20-20-48 v2.3-1 步骤 id 复用后 dialog 审核者按旧'任务记录摘要'审核并指出矛盾", "evidence": "expr-2026-08-27_20-20-48", "confidence": "medium", "status": "approved", "suggested": "approved", "recordedAt": "2026-09-04T00:00:00.000Z", "confirmedBy": "user", "confirmedAt": "2026-09-04T13:14:35.897Z" }, { "id": "L-2026-0904-017", "title": "能力考古须三源交叉(expr 三件套 + 主会话轨迹解码 + git tag/commit 发布史)并做 tag 覆盖度自检,防机制层能力漏登记", "category": "judgment-heuristic", "layer": "L3", "trigger": "能力考古 / 复盘收口时", "decision": "三源交叉取证:① expr 记录/steps/trace ② 主会话轨迹(如 session-a0a01dfa,需解码)③ git tag/commit 发布史;机制层能力(feature-capability:影子沙盒/Prompt 案例库/Swarm/突破度等引擎交付物)与行为层(A-D 判断模式)并行提炼;收口前逐 tag 核对'每个发布功能在清单是否有对应条目',输出'已知未入库'清单", "rationale": "v1.8→v1.9 考古漏 v3.0~v3.2 三个机制(expr-02-05-00 从未深读、影子沙盒被降格为 B10 证据、Swarm/突破度未登记);根因 R1-R6(本体缺机制层/漏斗缺功能向关键词/漏主会话轨迹数据源/提炼偏 process/收口缺覆盖度自检/时间窗按协议字面收窄)", "evidence": "docs/v3-roadmap-context-2026-08-28.md;expr-2026-08-28_01-29-26、expr-2026-08-28_02-05-00、expr-2026-08-29_00-00-00;git tag v3.0.0~v3.4.0", "confidence": "high", "status": "approved", "suggested": "approved", "recordedAt": "2026-09-04T13:14:35.897Z", "confirmedBy": "user", "confirmedAt": "2026-09-04T15:20:00.000Z" }, { "id": "L-2026-0904-018", "title": "大版本多模块实施用\"文件解耦 + 多子代理并行 + 主 agent 统一验收\"(index/client/docs 分路,每路 node --check+冒烟);仅文件可解耦时才并行,代码同文件场景串行为主", "category": "engineering-action", "layer": "L2", "trigger": "多模块大版本(v0.9.0/v1.0.0 类)需提速实施时", "decision": "按文件边界拆子代理并行(各路只动自己文件),每路完成后 node --check+冒烟,主 agent 最后统一收口;同一文件/仓库写操作场景回到串行(防 git 冲突)", "rationale": "8/25 v0.9.0 三路、v1.0.0 多路子代理并行实测成功;8/28 讨论确认代码场景串行为主(物理 git 冲突+隐式依赖)", "evidence": "session-a0a01dfa [U 08-25 05:42~05:54];[U 08-28 01:35]", "confidence": "high", "status": "approved", "suggested": "approved", "recordedAt": "2026-09-04T00:00:00.000Z", "confirmedBy": "user", "confirmedAt": "2026-09-04T15:20:00.000Z" }, { "id": "L-2026-0904-019", "title": "版本断言:任何重构/改版前先读 package.json 版本与 git HEAD,不符即停,不强行 Patch", "category": "repository-knowledge", "layer": "L3", "trigger": "收到改版/重构任务,动手前", "decision": "先断言 package.json 版本号/HEAD 符合预期再改;不符则停下报告,不强行 patch(防在多版本漂移状态下叠加改动)", "rationale": "用户采纳外部 AI 规约建议(8/24);v1.8 子代理明确\"先断言 1.1.0 后动手\";曾出现 statusHandler 硬编码 1.3.0 漂移与 1.0.1 未入表", "evidence": "session-a0a01dfa [U 08-24 23:15];d925feb1 子代理报告;commit 92b57d6", "confidence": "high", "status": "approved", "suggested": "approved", "recordedAt": "2026-09-04T00:00:00.000Z", "confirmedBy": "user", "confirmedAt": "2026-09-04T15:20:00.000Z" }, { "id": "L-2026-0904-020", "title": "git 原子交付规约:里程碑强制 tag/commit;回退用 checkout/reset 而非跨会话重放历史 Edit;必要时全量快照备份", "category": "engineering-action", "layer": "L2", "trigger": "版本里程碑/需要回退到旧版本时", "decision": "完成即 git tag/commit(原子);回退 git checkout/reset --hard 到 tag;严禁靠会话历史重放 Edit;无 git 环境用全量文件快照", "rationale": "v0.7.0 曾被\"手工重放旧 Patch\"方式搞乱(8/24 用户带回);外部 AI 4 建议被用户采纳", "evidence": "session-a0a01dfa [U 08-24 23:15/23:40]", "confidence": "high", "status": "approved", "suggested": "approved", "recordedAt": "2026-09-04T00:00:00.000Z", "confirmedBy": "user", "confirmedAt": "2026-09-04T15:20:00.000Z" }, { "id": "L-2026-0904-021", "title": "实测数据驱动协议演进:大任务后以真实用量复盘(626 turns/$ 成本)反推协议缺口(→ alternatives/importance;固定成本≈$0.05/turn → 优先减 Turn)", "category": "judgment-heuristic", "layer": "L3", "trigger": "大任务收口后 / 每次协议迭代规划前", "decision": "先盘点真实 turns/成本与返工来源,再决定协议级改进(减 turn/合并审核/权重),而非拍脑袋加功能", "rationale": "626 turns 复盘直接催生 v1.7 alternatives/importance/批量审核;固定成本模型下减 turn 是第一优先级", "evidence": "session-a0a01dfa [U 08-25 21:28~22:14](v1.7 P1-P9)", "confidence": "medium", "status": "approved", "suggested": "approved", "recordedAt": "2026-09-04T00:00:00.000Z", "confirmedBy": "user", "confirmedAt": "2026-09-04T15:20:00.000Z" }, { "id": "L-2026-0904-022", "title": "跨工作区上下文桥接:handoff 注入的 workspacePath ≠ 默认 cwd 时,读/写/回写一律以注入路径为准", "category": "repository-knowledge", "layer": "L2", "trigger": "收到含 workspacePath 的跨工作区 handoff/试验", "decision": "记录、steps、trace 都解析到注入 workspacePath 下(web-relay/…),不落当前 cwd;收口 POST /trace 带 workspacePath", "rationale": "expr-…_02-09-32 跨工作区桥接实测(8/25);多工作区并行时防止写错目录", "evidence": "session-a0a01dfa [U 08-25 10:09~10:12]", "confidence": "medium", "status": "approved", "suggested": "approved", "recordedAt": "2026-09-04T00:00:00.000Z", "confirmedBy": "user", "confirmedAt": "2026-09-04T15:20:00.000Z" }, { "id": "L-2026-0904-023", "title": "web-gemini 渠道问题分层排查:唤醒缺失/steplist 截断/面板无反应 → 扩展↔bridge↔解析三层定位,不单一归因", "category": "judgment-heuristic", "layer": "L3", "trigger": "web-gemini 通道出现任务无唤醒/回复截断/面板无响应", "decision": "按 扩展(轮询/防截断/多Tab)→ bridge(超时/提交)→ 插件解析(prompt 构造/steplist 提取)分层取证定位;修复后留复测", "rationale": "8/27-8/28 多次 web-gemini 渠道问题(唤醒缺失/steplist 截断/面板无反应)逐层定位并修复(v2.1.0 content 双信号防截断等)", "evidence": "session-a0a01dfa [U 08-27 22:31~23:26];commit 2b9d23a", "confidence": "medium", "status": "approved", "suggested": "approved", "recordedAt": "2026-09-04T00:00:00.000Z", "confirmedBy": "user", "confirmedAt": "2026-09-04T15:20:00.000Z" }, { "id": "L-2026-0904-024", "title": "版本号多源一致性:package.json/README/说明书/statusHandler 四处同源,防硬编码漂移", "category": "tooling-trap", "layer": "L3", "trigger": "发布 bump / 文档同步 / status 显示版本时", "decision": "版本号以 package.json 为单一数据源;README/说明书/statusHandler 同步;文档历史表不遗漏已发小版本(曾 1.0.1 未入表)", "rationale": "statusHandler 曾硬编码 1.3.0 漂移(v2.0.0 修复);1.0.1 发布未入 README 历史表(子代理提示)", "evidence": "commit 92b57d6;session-a0a01dfa [U 08-25 22:20]", "confidence": "high", "status": "approved", "suggested": "approved", "recordedAt": "2026-09-04T00:00:00.000Z", "confirmedBy": "user", "confirmedAt": "2026-09-04T15:20:00.000Z" }, { "id": "L-2026-0905-025", "title": "E2E 端到端真实校验是长链深层 bug 的防线:静态预检/单测易漏参数错位与字段丢失,发布前必须做一次真实运行时链路验证", "category": "engineering-action", "layer": "L2", "trigger": "涉及 fs 写参/状态字段持久化/数据透传的改动,在发布前", "decision": "除单测外补一次 E2E(真实 finalize/restructure/complete 触发)验证字段与写参在运行时闭环;失败按错误清单修复后重验", "rationale": "v3.6.0 实测抓到 3 个真 bug(P1 fs.writeText 5 参错位致案例库从不写入;P3 incrementalStreak 未持久化;P3 restructure 审 normalizedNew 丢 breakthrough_type),单测全绿仍漏", "evidence": "expr-2026-09-04_17-45-25 E2E 实证;commit 8535fda", "confidence": "high", "status": "approved", "suggested": "approved", "recordedAt": "2026-09-05T00:00:00.000Z", "confirmedBy": "user", "confirmedAt": "2026-09-05T01:00:00.000Z" }, { "id": "L-2026-0905-026", "title": "\"缺证据打回→补证重提\"三方闭环:complete 必带实体证据(执行日志/测试数/commit),外部 AI 按证据审核", "category": "collaboration-rhythm", "layer": "L2", "trigger": "主 agent complete/提交审核时;外部 AI 审核证据不足时", "decision": "主 agent:complete 前把实施证据写入 notes(文件/命令输出/测试数/commit);外部 AI:证据不足即 rejected 并指明缺什么,避免放水", "rationale": "v3.6.0 5 步外部审核 2 次打回-补证闭环,拦截\"虚假完成\";证据型完成是三方可信的基石", "evidence": "expr-2026-09-04_17-45-25(Step1/5 打回补证)", "confidence": "high", "status": "approved", "suggested": "approved", "recordedAt": "2026-09-05T00:00:00.000Z", "confirmedBy": "user", "confirmedAt": "2026-09-05T01:00:00.000Z" }, { "id": "L-2026-0905-027", "title": "热路径成批变更 + 受控重启纪律:core 加载链改动攒批、一次重启验证多步,减少中间态不一致", "category": "engineering-action", "layer": "L2", "trigger": "需重启才生效的宿主(lib/index.js 等)改动", "decision": "能热(client/纯模块)就不重启;host 改动成批后单次重启收口验证;重启清单随版本记录(E2E/外部审核/版本号显示项)", "rationale": "v3.6.0 以\"client 刷新即生效 + host 改动聚合一次重启\"完成 5 步收口;设计文档 §10 三类支撑(门禁/影子/热路径)", "evidence": "docs/auto-iteration-shadow-support-design.md §10;expr-2026-09-04_17-45-25", "confidence": "medium", "status": "approved", "suggested": "approved", "recordedAt": "2026-09-05T00:00:00.000Z", "confirmedBy": "user", "confirmedAt": "2026-09-05T01:00:00.000Z" }, { "id": "L-2026-0905-028", "title": "宿主级机制开发必须 DRYRUN/模拟先行;禁止对真实宿主直接执行 kill/拉起", "category": "engineering-action", "layer": "L3", "trigger": "实现涉及真实宿主进程生命周期的机制(watchdog 重启/优雅停机/编排)并准备验证时", "decision": "先给实现加 DRYRUN/模拟模式(只打日志不产生真实副作用),用死端口/假目标做失联模拟验证完整触发链(miss→门→拉起流程),再以单测锁门序语义;真实 kill/spawn 链只在用户按部署文档启用时执行,验证期间绝不对运行中的宿主做 kill", "rationale": "watchdog DRYRUN 失联模拟(端口 3999)暴露 stormGate 计数膨胀 bug(先记录后判门→暂停期仍累计),若直接对真实宿主实机验证会同时引入误杀风险与难以复现的偶发;模拟先行把状态机 bug 在零副作用下暴露", "evidence": "expr-2026-09-05_01-53-03(v3.9.0)S3:DRYRUN 模拟两轮(第一轮暴露 bug→attemptRestart 先判门再记录修复→第二轮计数稳定 3 不再膨胀);watchdog.test.js attemptRestart 用例;commit 8b16cfa", "confidence": "high", "status": "approved", "suggested": "in-runbook", "crossRef": [], "recordedAt": "2026-09-05T02:00:00.000Z" }, { "id": "L-2026-0905-029", "title": "Windows 计划任务启动器 .cmd 必须纯 ASCII(cmd.exe 按 ANSI/GBK 解析,UTF-8 中文注释会变成乱码命令导致后续 set 不执行)", "category": "tooling-trap", "layer": "L3", "trigger": "编写/修改供 Windows 计划任务或 cmd 直接执行的 .cmd/.bat 启动器,且内含注释时", "decision": "批处理文件注释与文案一律 ASCII;需要说明性文字放同名 .md 或 watchdog 日志;避免中文 UTF-8 写入 .cmd(LF 或 CRLF 均可,但字节必须 ASCII)。改后必测:cmd /c call 观察无 \"is not recognized\" 报错且环境变量/子进程参数正确", "rationale": "v3.9.3 dsh-web-watchdog.cmd 中文注释(UTF-8)被 cmd.exe 按 GBK 解析出乱码命令(\",随后启动\" is not recognized),goto/set 流程被破坏,DSH_WEB_ARGS 未设置 → watchdog 拉起宿主丢失 --trusted-host;重写纯 ASCII 后 env 传播恢复(宿主命令日志含 trusted-host)", "evidence": "C:\\Users\\Administrator\\dsh-web-watchdog.cmd(v3.9.3 ASCII 版);watchdog 日志 13:25:51 宿主命令含 --trusted-host;commit 25bbadd", "confidence": "high", "status": "approved", "suggested": "in-runbook", "crossRef": [ "L-2026-0903-004" ], "recordedAt": "2026-09-05T13:30:00.000Z" }, { "id": "L-2026-0905-030", "title": "测试文件必须严格 *.test.js 命名(套件 glob 只认 .test.js 结尾,否则静默漏跑)", "category": "tooling-trap", "layer": "L3", "trigger": "新增 node --test 测试文件、确认全量测试计数时", "decision": "测试文件统一 .test.js 命名(如 client-restart.test.js),禁 test-.js 形式;新增后核对全量计数(文件数=23、用例数递增),发现计数未增即查命名/glob", "rationale": "AutoIteration 实验 V2:test-client-restart.js 不以 .test.js 结尾被 test/*.test.js glob 静默跳过——单跑 2/2 通过但全量仍 152,险些漏测;改名 client-restart.test.js 后 154/154", "evidence": "expr-2026-09-05_13-58-07 V2;commit c1bbb1b;docs/AUTO-ITERATION-ANALYSIS.md 观察点 E", "confidence": "high", "status": "approved", "suggested": "in-runbook", "crossRef": [], "recordedAt": "2026-09-05T14:10:00.000Z" }, { "id": "L-2026-0905-031", "title": "AutoIteration 声明必须用机读 JSON 块(叙述式 {\"iterations\":N} 常不被 extractAutoIterDecl 解析)", "category": "collaboration-rhythm", "layer": "L3", "trigger": "向外部 AI 发起多版本(iterations≥2)任务、prompt 声明迭代参数时", "decision": "声明独立成 ```json 块({\"iterations\":N,\"autoDecision\":true,\"finalAcceptance\":\"…\"})而非中文叙述;发起后先读 steps.json 顶层确认 iterations 落盘,未落盘即修正后再推进(lesson 002 同族)", "rationale": "实验 V1:叙述式「声明:{\"iterations\":3,…}」首行未解析 → steps.json iterations=1,版间门无法多版推进;人工修正落盘后才 V1→V2→V3 自动演进", "evidence": "expr-2026-09-05_13-58-07(V1 修正记录);docs/AUTO-ITERATION-ANALYSIS.md 观察点 A;关联 L-2026-0903-002", "confidence": "high", "status": "approved", "suggested": "in-runbook", "crossRef": [ "L-2026-0903-002" ], "recordedAt": "2026-09-05T14:10:00.000Z" }, { "id": "L-2026-0905-032", "title": "宿主重启编排须单步幂等小命令且把树杀作为回合最后动作(多步宿主操作命令易被运行时中断→半状态→用户手动兜底)", "category": "engineering-action", "layer": "L3", "trigger": "由 agent 工具调用编排宿主重启(prepare → 停 watchdog → Start-ScheduledTask → 延迟树杀 host)等涉及杀进程/改守护的多步命令时", "decision": "拆成可验证的小步:① 触发 /admin/prepare-restart 单独一步确认 ready;② watchdog 重启(Start-ScheduledTask)单独一步确认单实例;③ 延迟树杀作为回合最后动作(Start-Process 独立进程),随后立即结束回合让用户/后续回合验证;每步前先只读盘点当前 PID 状态,避免重复执行半状态命令;依赖 watchdog 单例锁 + 首检保证任何手动兜底安全", "rationale": "v4.0 E2E 阶段多次「多步编排命令被运行时中断」(watchdog 已 kill 而 Start-ScheduledTask/树杀未执行)→ 宿主无人托管/未换代 → 用户手动启动 ≥3 次;watchdog 触发链本身可靠(日志实证 ~6 次自主拉起),不可靠的是 agent 单次长命令的原子性", "evidence": "AUTO-ITERATION-ANALYSIS.md §七(重启账目:watchdog 自主 ~6 / 用户手动 ≥3);宿主 PID 18008/18164 PPID=人工 launcher", "confidence": "high", "status": "approved", "suggested": "in-runbook", "crossRef": [ "L-2026-0905-028" ], "recordedAt": "2026-09-05T14:20:00.000Z" }, { "id": "L-2026-0906-033", "title": "宿主重启编排严禁先杀 watchdog:先保守护存活/新守护接管,再动宿主(先杀守护+编排中断=双死 585 分钟)", "category": "engineering-action", "layer": "L3", "trigger": "编排宿主重启/维护 DSH-WEB-Watchdog 计划任务(Stop/Start、taskkill watchdog、restart-now)时", "decision": "顺序铁律:① 先确认 watchdog 在跑(Start-ScheduledTask 幂等,勿 taskkill 运行中的 watchdog);② 触发 restart-now 或独立延迟树杀(不杀 watchdog);③ 操作后立即验证 watchdog 日志出现新行(重启记录)再继续;计划任务配 RestartOnFailure(RestartCount=5/RestartInterval=1m)+ ExecutionTimeLimit=0(防 72h 被杀)+ MultipleInstances=IgnoreNew,确保 watchdog 被强杀/崩溃 ~1min 内复活", "rationale": "2026-09-06 凌晨 585 分钟死局:编排命令(git push + taskkill watchdog + Start-Process restart-now)在 taskkill 之后被 harness 中断(会话解码 interrupted @ 2026-09-05T15:11:21Z turn81)→ watchdog 死、restart-now 未启动(日志 23:1x 零记录)、宿主随后 down → 双死无人拉起,浏览器 502 持续 585 分钟,只能人工重启", "evidence": "会话解码 interrupted-tool-result @ 2026-09-05T15:11:21Z;git 443426f @23:11:21 已推(命令跑到 git 完成)但编排未完成;dsh-web-watchdog.log 无 23:1x 行(restart-now 从未运行);用户浏览器 502/connection lost 持续 + health-check 无法连接", "confidence": "high", "status": "approved", "suggested": "in-runbook", "crossRef": [ "L-2026-0905-028", "L-2026-0905-032" ], "recordedAt": "2026-09-06T02:00:00.000Z" }, { "id": "L-2026-0907-034", "title": "批量并发审核必须先拆层:并发只 obtain 结论(不落盘不改状态),再串行 apply/原子打回统一落盘——共享 state 文件并发 writeStepState 会乱序覆盖丢结果", "category": "engineering-action", "layer": "L2", "trigger": "重构 /steps/auto-review 的 batchStepIds 为并发、或任何对同一 steps.json 状态的并发审核/写入场景(v9_3 并发批)时", "decision": "审核判定拆两层:obtainReviewVerdict(只走通道拿 verdict,零写盘零改状态,可安全并发)→ applyReviewOutcome(状态/notes/轨迹/webhook/落盘,串行)。批量 = 预检(串行,not_found/非 review/锁冲突直接记)→ mapLimit 受控并发 obtain(默认并发 2,env DSH_RELAY_BATCH_CONCURRENCY)→ 判定 anyRejected → 原子打回统一一次 writeStepState 或逐项串行 apply。严禁直接并发调 reviewOneStep(内部 writeStepState 同文件异步写 → 后写覆盖先写丢步审核结果)", "rationale": "v4.9/v2.0 实施:初版方案并发调 reviewOneStep 发现竞态——reviewOneStep 每步落盘同一 state 文件,JS 单线程 await 交错下两协程快照互相覆盖;拆层后并发只省审核请求时间(主耗时),落盘串行毫秒级,无竞态且原子打回语义保持(任一 rejected → 全批 rejected 含刚 approved)", "evidence": "lib/index.js reviewOneStep 拆层 commit(v4.9.0);全量 218/218 含 cc-channel 21 + alternatives 9;dialog-fallback 源码标记测试同步 v2.0 链断言(旧串『v3.0.1: Gemini API 失败 → web-gemini 网页通道(免配额)』已随链注释移除)", "confidence": "high", "status": "approved", "suggested": "in-protocol", "crossRef": [ "L-2026-0905-031", "L-2026-0906-033" ], "recordedAt": "2026-09-07T00:40:00.000Z" }, { "id": "L-2026-0907-035", "title": "审核上下文长文截断必须尾部优先(tailClip):三方轨迹时间正序、最新证据在末尾,头截 4000 会丢打回意见/重提证据/仲裁记录 → 审方基于旧状态误拒", "category": "engineering-action", "layer": "L2", "trigger": "长 trace / 长 notes / 多次打回重提的 expr 审核(buildReviewPrompt trace 段、cc 派发 notesText/traceText、alternatives trace)——凡给审方的上下文含时间序列文本且需截断时", "decision": "时间正序的上下文(三方轨迹、notes 追加日志)一律尾部优先截断:tailClip(s,n) 取末尾 n 字符 + 前置省略说明;头部截断只适用于固定短文本(任务记录、record)。审核 prompt 内注明「若超长已省略前部,只显示末尾最新」。lib/index.js 三处(buildReviewPrompt trace、runCcReviewTask cc 派发 notesText/traceText/artifactsSummary、alternatives trace)v4.9.1 已修", "rationale": "expr-2026-09-06_16-32-52 v9_4 事故:验证收口步证据全在 trace/notes 尾部(watchdog 重启实证、218/218、understand 报告、外部 AI 仲裁 approved),但审核上下文 clip(trace,4000) 取头——trace 已超长时尾部全丢 → Swarm Refactoring-Architect 三次空意见拒 + Claude 通道两次基于「未见仲裁/未见 approved 记录」旧状态误拒(它引用的上下文停在第三次重提前)。修复 tailClip 后审方才能看到仲裁与完整证据", "evidence": "commit 7578cc5(v4.9.1-fix);五轮误拒记录:Swarm×3(空 findings)+ cc×2(引用旧状态);外部 AI 仲裁 expr-2026-09-06_17-50-14 approved(channel=dialog-fallback)", "confidence": "high", "status": "approved", "suggested": "in-protocol", "crossRef": [ "L-2026-0907-034", "L-2026-0906-033" ], "recordedAt": "2026-09-07T02:10:00.000Z" }, { "id": "L-2026-0907-036", "title": "树杀宿主重启 = 断 harness 会话通道,续跑只对插件状态机留痕、不自动续接 agent 回合——编排后不能承诺\"自动续接\",必须明示需用户/goal 轮触发", "category": "engineering-action", "layer": "L2", "trigger": "把宿主重启(kill-host/restart-now/taskkill 树杀)编排为回合最后动作,且期待后续自动推进(bootResumeScan resumed、goal 自动轮、job 通知)时", "decision": "区分两层续跑:① 插件状态机续跑(bootResumeScan:resumed+restartCount+bootId 打戳)——宿主重启后**必然自动发生**(已三次实证 resumed=1);② agent 回合续接(新回合注入)——依赖 goal 自动轮或用户输入,**两者都可能缺席**(2026-09-07 事故:重启实证成功后 6 小时无任何自动轮)。铁律:把\"验证重启结果/收口\"放在重启**前**完成或拆到独立回合;必须编排重启收尾时,明确告知用户\"重启后需发任意消息(或等 goal 轮)触发续接\",不得承诺自动续接;若重启后无下文,新回合第一件事是核对 /health-check resumed 与目标状态,不假设任何工作已自动完成", "rationale": "2026-09-07 02:58 kill-host 重启实证成功(resumed=1 expr 被自动检测、restartCount 0→1、bootId 打戳),我最后消息承诺\"由机制自动续接\"——但此后 6 小时无 goal 轮无通知,用户提问才恢复。根因:harness 会话与 dsh 宿主是两套通道,宿主重启只保证插件状态机续跑;agent 回合续接无 sessionId 注入渠道(本 expr sessionId=null)+ goal 自动轮未在该窗口触发", "evidence": "resume-demo.log:02:56 编排 → 02:57 kill-host PID 20084 → 02:58 宿主 up bootId=mtq6bk7n-4b442b91 resumed=1 exprs=expr-2026-09-07_05-00-00;用户 6 小时后提问才恢复回合", "confidence": "high", "status": "approved", "suggested": "in-skill", "crossRef": [ "L-2026-0906-033", "L-2026-0907-035" ], "recordedAt": "2026-09-07T09:00:00.000Z" }, { "id": "L-2026-0907-037", "title": "等待/假死问题的机制级解:①杀宿主只能后台 job(同步杀被 harness 中断,3 实证)②job 不 wait 句柄,改同消息同步 probe 查盘 ③无介入续跑靠 expr sessionId 落盘(DSH_SESSION_ID)自动注入唤醒 ④回合结束前自检未竟工作禁预告式结尾", "category": "engineering-action", "layer": "L1", "trigger": "需要重启宿主(kill-host/restart-sync)、启动后台 job 后续接、宿主重启后期待自动续跑、回合结束前(是否还有未竟工作)", "decision": "四步铁律:① harness 内杀宿主**只能用 run_in_background 后台 job**(同步 taskkill/kill-host 必被中断——工具调用与 3080 有连接,kill 即断;3 次同步 kill 全中断、3 次后台 kill 全成功实证);② job 启动后**同一消息内不要 wait job 句柄**(跨回合必失效 + UI 悬挂 running)——改为同消息发同步 probe(Start-Sleep 65 + /status 或 /health-check)查盘上结果(uptime 归零/新 bootId = kill 已执行、watchdog 自愈拉起);③ 无介入续跑:expr 落盘 sessionId=DSH_SESSION_ID(harness 环境变量)→ 宿主重启后 bootResumeScan 自动 wakeMainAgent 注入「宿主自愈重启·自动续跑」消息(零用户输入;实证 2 次:唤醒消息自动到达);心跳(机制 b,15min 周期)兜底无重启场景的待办提醒;④ 输出最终文字前自问「还有未竟工作吗」——有就同一回合继续工具调用,禁止「接下来做 X」预告式结尾", "rationale": "2026-09-07 一天内 6+ 次「停在后台 job / 空等 6h / UI 悬挂」复盘:根因是 run_in_background 句柄跨回合失效 + 回合以预告式文字结束(幻想自动续接)+ 无 sessionId 注入渠道。机制级解而非自律:kill 后台化(环境约束)、验证盘上化(绕开句柄)、续跑注入化(sessionId 落盘 + 宿主自动唤醒)、回合自检(行为约束兜底)", "evidence": "3 次同步 kill 中断 + 3 次后台 kill 成功(watchdog.log);唤醒消息自动到达 2 次(expr-2026-09-07_07-00-00、expr-2026-09-07_13-30-00,用户零输入);037 结构模式演示 kill 后台 job + 唤醒自动到达;skill §5 已固化(后台 job 铁律/回合自检铁律/037 结构模式)", "confidence": "high", "status": "approved", "suggested": "in-runbook", "crossRef": [ "L-2026-0907-036", "L-2026-0906-033" ], "recordedAt": "2026-09-07T14:00:00.000Z" }, { "id": "L-2026-0909-038", "title": "cc 任务 outputDir 契约根因:buildReviewTask 曾硬编码绝对路径(/mnt/d/cc-tasks/tasks//out)致每个 claude-code 审核任务被 v2 门控 REJECT(.invalid/ 13 例实证)——schema 要求任务根内相对子路径", "category": "engineering-action", "layer": "L2", "trigger": "派发 cc 任务(buildReviewTask/cc-channel)、cc-watchdog 日志出现 REJECT invalid task、.invalid/ 堆积、claude-code 审核通道实际从未成功", "decision": "outputDir 字段必须相对(\"out\");绝对落盘路径(/mnt/d/...)只放 prompt 提示 Claude,不进 task.json 字段。REJECT 后人工复检闭环:CLI invalid list/revalidate/approve/recover/clean(未 approved 拒删)。", "rationale": "v2 schema validateTask 对 outputDir 做越界硬校验(禁绝对路径/..),绝对路径产物必然 REJECT——顶层校验器与底层派发必须同构,字段语义按 schema 而不是按\"给 Claude 看\"设计", "evidence": "expr-2026-09-09_12-30-09 s2v5_3(commit e7a992b);D:/cc-tasks/queue/.invalid/ 13 例;docs/task-schema-v2.md §10", "confidence": "high", "status": "proposed", "suggested": "proposed", "recordedAt": "2026-09-09T15:42:48.261Z" }, { "id": "L-2026-0909-039", "title": "宿主重启后延迟投递噪声:/ask agentWoken 落盘的 aux expr(评审快照,isTest=false/status=open)会被 bootResumeScan/心跳当活跃任务反复唤醒;heartbeat 陈旧信号按阶段分批延迟送达(V3 批→V4 批→V5 批递减序列)", "category": "engineering-action", "layer": "L1", "trigger": "宿主重启后连续收到手动 handoff(expr-*.md 指向 aux 快照)与心跳自查消息,报主 expr 有已 approved 步骤待审", "decision": "① aux 评审快照(/ask 落盘、步骤已 restructure 并入主 expr)执行逻辑归档:置 isTest=true + status=done + finalized=true + archivedNote(v4.7.0 isScanEligible 语义,等效逻辑归档——插件 fs 无删除 API);② 收尾期收到此类延迟消息:实盘核对 steps.json(allApproved/finalized)+ /health-check heartbeat.found 判定为陈旧投递,trace 留痕即可,不重复动作;③ 别为每条噪声重启宿主清队列(会引入新一轮延迟投递)。", "rationale": "wake queue 是投递即删还是保留待投递取决于宿主实现;实测证明重启会重放历史 wake。归档标记(isTest+done)是停止扫描唤醒的机制级解,比逐条人工确认可扩展", "evidence": "expr-2026-09-09_12-49-39/13-03-17/13-57-25/14-37-38/14-46-35/14-53-11 归档 + 15:0x-16:0x 连续 10+ 条延迟 handoff/心跳;planResumes 复扫 0 计划", "confidence": "high", "status": "proposed", "suggested": "proposed", "recordedAt": "2026-09-09T15:42:48.261Z" }, { "id": "L-2026-0909-040", "title": "三副本同步纪律:开发源 lib 改动 ≠ 宿主生效——运行副本(profiles\\web\\node_modules\\dsh-web-relay)与工作副本(web-relay\\dsh-web-relay 镜像)需显式同步;宿主跑的是运行副本旧代码直到 restart-now", "category": "engineering-action", "layer": "L2", "trigger": "改 lib/index.js 等运行时代码后调新端点/新行为发现不存在(如 POST /steps/v2-check 404)、v2 校验器在宿主侧不生效", "decision": "① 改运行时代码后:同步 dev→run copy(lib/bin/cordis.patch.yml/package.json,certutil hash 验证 IDENTICAL)→ watchdog restart-now 重启宿主(bootResumeScan 自动续跑唤醒);② 收口时同步 dev→work copy(lib/scripts/test/docs/skills/bin 全量镜像 + 删 stale);③ scripts/docs/test 不进运行副本(files 白名单),cc-watchdog 直接引用开发源 scripts 路径——运行副本只需 lib/bin/cordis/package.json。", "rationale": "三副本架构(源→运行→工作镜像)职责不同:运行副本是宿主加载实例需重启生效,工作副本是发布镜像需全量同步;V1-V5 曾全部只在开发源、宿主跑旧代码未被发现(审核走 dialog 降级链未触新端点)", "evidence": "expr-2026-09-09_12-30-09 s2v5_5(run copy 缺 task-schema-v2.mjs/index.js 旧版 hash DIFF → 同步后 IDENTICAL + restart 实弹 v2-check)", "confidence": "high", "status": "proposed", "suggested": "proposed", "recordedAt": "2026-09-09T15:42:48.261Z" }, { "id": "L-2026-0910-041", "title": "静默失败三态链:content 层 resolve 空值(return '' 而非 throw)+ 消费者层只认成功分支(只打日志不上报终态)= 服务端任务永久 processing,上游白等满超时(web-gemini 16/16 实测)", "category": "engineering-action", "layer": "L2", "trigger": "跨进程/跨组件的异步任务链路(桥接/队列/扩展↔服务端)出现「任务开始执行但永不结束」;上游只能等满超时;排查时 /stats 缺失败计数只能靠 total-其余 反推", "decision": "① 每层收敛为**两态不变式**:产出层「要么 resolve 非空有效值、要么 throw 带诊断」(禁止 return 空值);消费层「要么上报成功、要么上报失败」(禁止只打日志的第三态);② 服务端状态机自带超时兜底终态(不依赖消费者行为);③ /stats 等可观测端点必须覆盖全部状态枚举(含 failed);④ 上游判定必须识别 failed/停滞并早退,不白等满超时。", "rationale": "「resolve 空值」在 JS 语义上不是错误(无 exception),但业务上等于失败——消费层的 `if (resp.answer)` 真值判断会把空串与「没响应」一起吞进无操作分支,形成无日志、无终态、无告警的静默泄漏;单测/类型系统都难以捕获,只有端到端探活 + 反向推算统计才能发现", "evidence": "expr 相关:cc 任务 wg-gap-analysis(Claude 分析 248 行 + 主 agent 交叉验证一致);dsh-web-gemini-ext background.js/content.js/bridge-server.mjs 三处修复;lib/bridge-poll.js + 11 测试;docs/web-gemini-diagnostics-2026-09-10.md;实测 /stats total=16 全非成功态、探活任务 190s+ processing", "confidence": "high", "status": "proposed", "suggested": "proposed", "recordedAt": "2026-09-10T12:02:32.380Z" }, { "id": "L-2026-0910-042", "title": "混合架构「默认化」的正确形态:三方协作可默认、cc 派发须条件默认——判定规则要固化成可调用纯函数(硬边界优先于收益),并配降级率审计 + 双通道交叉校验两道治理门", "category": "collaboration-rhythm", "layer": "L3", "trigger": "考虑把\"混合架构 + 三方协作\"从按需启用升级为默认运行方式;或判断某类任务该不该派给 Claude Code;或收口时怀疑\"审核是否被静默降级\"", "decision": "① 三方协作机制(Step List 状态机 + 审核降级链 + 轨迹)适合默认;混合架构(cc 派发)条件默认——硬边界(需 GUI/浏览器、需跨会话状态、需主 agent 专属工具、预估>900s)优先阻断,豁免(≤1 文件小改、纯配置探路),启用(≥3 文件实现、静态诊断、代码评审、iterations>1、alternatives>1);规则固化为 lib/triage-route.js 纯函数 + POST /route/decide 入口,禁止凭感觉派发;② 配降级率审计(lib/review-audit.js:dialog 占比>30% 告警,超阈值且外部通道 0 成功=risk);③ 配双通道交叉校验(lib/cross-check.js:仅 importance=high 且显式开启,冲突升级人工、通道不可用与结论冲突严格区分)。", "rationale": "两方独立征询(外部 AI web-gemini + Claude Code 本体)均判 conditional——分歧点不在\"该不该用\",而在\"什么条件下用\"。cc 通道的 4-8 分钟固定开销与四条硬边界(900s/无--resume/白名单三工具/无 GUI)决定了它不该是全局默认;而默认化最大的风险不是能力不足,而是\"外部通道连续不可用时静默降级到无工具内部模型,形成形式上 approved 但实质审查强度下降\"——必须靠审计与交叉校验把它变成可见信号", "evidence": "expr-2026-09-10_12-18-24(P0-1/P0-2/P1-1/P2-1 四步全 approved + finalize);cc 任务 hybrid-default-advisory(Claude 建议书 24KB/verdict.json);外部 AI 征询 expr-2026-09-10_12-18-24(web-gemini,27s 无降级);lib/triage-route.js(16 测试)+ lib/review-audit.js(11 测试)+ lib/cross-check.js(11 测试);docs/CC-HYBRID.md §7-§9;实证数据:AutoIteration dialog 13/23=57%、cc 审核修复后 90s 成功(rev-1a08b460fc2)", "confidence": "high", "status": "proposed", "suggested": "proposed", "recordedAt": "2026-09-10T12:34:22.524Z" }, { "id": "L-2026-0910-043", "title": "宿主重启导致主 agent 回合卡死的根因:工具进程是宿主的直接子进程 + watchdog 用 taskkill /T 杀整棵树 → 执行重启的命令自身被杀、结果永不返回。修复=信号解耦(写文件后立即返回,由独立进程树的 watchdog 执行)", "category": "engineering-action", "layer": "L1", "trigger": "需要重启宿主;或出现「工具调用被中断/回合卡住、必须用户发消息才能继续」的现象", "decision": "重启宿主**一律用 `node bin/watchdog.mjs request-restart [delaySecs]`**:该命令只写 bin/restart.request.json 后立即返回(工具调用正常落地),由常驻 watchdog(Task Scheduler 独立进程树)在后续 tick 执行 prepare→taskkill /T→拉起新宿主。**禁用**直接同步调用 restart-now / kill-host / restart-sync 作为首选(它们会在树杀时把正在执行命令的工具进程一并杀死 → 回合卡死)。命令返回后本回合即可正常收尾,重启后由 bootResumeScan(activeSteps + bootId 变化 → wakeMainAgent)自动唤醒续跑(前提:expr 已落盘 sessionId)。", "rationale": "taskkill /T = 杀指定进程及其整棵子树;主 agent 的工具进程由宿主 spawn,故属该子树。这不是\"任务停下来\",而是\"重启动作与执行者同树被杀\"的设计缺陷——只要把\"发起重启\"与\"执行重启\"解耦到不同进程树即可根治,无需改 harness", "evidence": "进程父链取证(powershell→宿主→watchdog→cmd→wscript→svchost/Task Scheduler);bin/watchdog.mjs request-restart + checkRestartRequest;端到端实测 2026-09-10 19:02-19:03Z:返回 exit=0 → 4s 后宿主 5764→16860 → resumeQueuedAt=19:03:04Z 自动唤醒 → watchdog 7592 存活;expr-2026-09-10_18-42-03 stab1_1", "confidence": "high", "status": "proposed", "suggested": "proposed", "recordedAt": "2026-09-10T19:04:39.135Z" }, { "id": "L-2026-0910-044", "title": "web-gemini 稳定性三层兜底与\"取任务/回传\"可观测:扩展无 Gemini 标签页时不取任务(任务永久 pending);sendMessage 可能既不 resolve 也不 reject;区分轮询心跳与任务认领才能定位", "category": "engineering-action", "layer": "L2", "trigger": "web-gemini 通道 /ask 或审核长时间无响应;需判断是\"扩展没在轮询\"还是\"取了任务不回传\"", "decision": "① 每一层都要有兜底终态:宿主侧待认领早退(pendingSince 超 60s → unavailable)、服务端 sweep(pending 60s / processing 120s → failed 带诊断)、扩展侧 sendMessage 超时(70s → NO_RESPONSE 上报)。② 可观测要区分两个指标:lastPollAt/pollCount(轮询心跳,反映 SW+标签页是否活着)与 lastClaimAt/claimCount(真正认领任务,反映扩展是否在工作);只统计后者时\"每秒轮询\"会被误读成\"高频认领\"。③ 记住外部依赖:Chrome 无 gemini.google.com 标签页时,扩展的 pollOnce 直接 return,通道静默不可用——这是首要排查项。", "rationale": "该通道的三次事故各不相同(扩展不响应 / sendMessage 挂起 / 无标签页不取任务),但共同点是\"上游只能靠超时感知\",且初版可观测指标语义错误掩盖了真因。逐层兜底 + 双指标可观测把它们从\"白等 300s\"变为\"秒级诊断 + 提前降级\"", "evidence": "expr-2026-09-10_18-42-03 stab1_2;实测:桥接强杀 5s 自愈、t0001 132s 被服务端 sweep 判 failed、poll=0 正确指示无 Gemini 标签页、claimCount 299 暴露轮询/认领语义混淆;lib/bridge-poll.js(pending/processing 双阈值)+ bridge-server.mjs(sweep 双状态 + 双指标)+ background.js(sendMessage 70s 超时)", "confidence": "high", "status": "proposed", "suggested": "proposed", "recordedAt": "2026-09-10T19:04:39.135Z" }, { "id": "L-2026-0910-045", "title": "cc 任务范围控制:900s 超时白耗配额的教训 + 范围评估器(校准要靠实测反推,且要防\"描述性编号列表\"假阳性)", "category": "engineering-action", "layer": "L2", "trigger": "派发 Claude Code 任务前;或 cc 任务超时失败(exit=124)无产物", "decision": "派发前用 `lib/task-scope.mjs` 的 assessTaskScope 预检:>600s 判 too-big(必须拆分,用 recommendSplit 出拆分块)、>420s 判 risky。经验阈值:单任务 ≤2 产物 + ≤5 个实质问题 + 不要求\"核实源码/多方案评估/出补丁\"这类深度动作。**校准必须用实测反推**(v2 校验器实现 222s / 架构征询 258s / 代码诊断 480s / RCA 超时 900s),且要防两类假阳性——编号列表(验收点清单)被当成深度评估工作量(已加 MAX_EVAL_ITEMS_COUNTED 封顶)、以及\"问题多但回答简短\"被高估。", "rationale": "RCA 任务(5 问 + 4 方案 + 3 产物 + 要求核实源码给补丁)900s 超时且零产物 = 白耗 15 分钟 Pro 配额;而范围适中的任务全部成功。范围是 cc 通道唯一的硬约束(900s 不可协商、无 --resume 不能续),必须在派发前判定而不是失败后补救", "evidence": "cc 任务 restart-interrupt-rca(exit=124 超时无产物)vs stab1-3-scope-assessor(4.8min 成功)/ stab1-4-gate-regression(1.6min 成功);lib/task-scope.mjs + test/task-scope.test.mjs(16 用例,二次校准 4/4 实测基准通过);docs/CC-HYBRID.md §7", "confidence": "high", "status": "proposed", "suggested": "proposed", "recordedAt": "2026-09-10T19:04:39.135Z" }, { "id": "L-2026-0910-046", "title": "用 shell 字符串拼接构造 JSON 必错:runner.sh 覆写 result.json 时把含双引号的 JSON 输出未转义塞进 reason → 产物非法 JSON → 宿主 JSON.parse 失败继续轮询白等超时(门控\"坏结果不逃逸\"的意图被自身缺陷抵消)", "category": "engineering-action", "layer": "L1", "trigger": "任何在 bash 里生成 JSON 的场景(写 result.json / 配置 / 上报负载);或门控写了\"失败结果\"但下游没反应", "decision": "**禁止**用 `echo '{\"k\":\"'\"$VAR\"'\"}'` 这类 shell 拼接生成 JSON。一律用可用运行时安全序列化:`node -e 'require(\"fs\").writeFileSync(process.argv[1], JSON.stringify({...}, null, 2))' `(或 jq -n --arg)。校验环节必须包含\"产物本身能否被 JSON.parse\"这一断言,而不只是\"字段名出现\"。", "rationale": "门控写出的失败结果若本身是坏 JSON,下游(宿主 pollTaskResult)解析失败会 continue 继续等待 → 表现成\"门控失效 + 白等超时\",比不写结果更糟(掩盖了真因)。该缺陷由测试脚本(cc 任务 stab2-2-bash-gates)在复现 bash 胶水时发现,此前长期潜伏(触发条件=④门控真的拦截到不合格产物)", "evidence": "cc 任务 stab2-2-bash-gates 的 KNOWN-ISSUE;runner.sh 原 L40 vs 修后 L44(node 安全序列化);对照实验确认旧方式非法 JSON(Expected ',' or '}' after property value);scripts/verify-cc-gates.sh 重跑 PASS=16 FAIL=0 KNOWN_ISSUE=0;docs/cc-gates-checklist.md 附录", "confidence": "high", "status": "proposed", "suggested": "proposed", "recordedAt": "2026-09-10T19:18:08.012Z" }, { "id": "L-2026-0910-047", "title": "归档 expr 要连步骤状态一起处理,否则心跳反复报\"待办\":归档测试快照只改 expr 级 status/finalized/isTest,步骤残留 rejected/review → 每轮心跳刷出已归档 expr(实测一轮 4 个)", "category": "engineering-action", "layer": "L2", "trigger": "批量归档测试/历史 expr;或心跳自查反复报同一批\"待办\"而实盘核对发现早已收口", "decision": "双保险:① 扫描层过滤——scanExprSignals 对 `isTest+done/finalized`(或任何 finalized)的 expr 直接不产生信号(与 resume-scan 的 isScanEligible 语义对齐);② 归档动作层——归档时同时把步骤状态处理干净(或明确保留为历史,并依赖①的过滤)。发现心跳噪音时的正确排查顺序:先实盘核对 steps.json 的真实状态,再判断是\"状态残留\"还是\"真待办\"。", "rationale": "心跳的价值在于\"不漏真待办\",但它若把已归档内容反复报出,主 agent 会消耗回合做重复核对(实测:一轮报 4 个已归档测试 expr + 1 个陈旧快照),长期会训练出\"忽略心跳\"的坏习惯,反而降低真待办的响应率", "evidence": "expr-2026-09-10_18-42-03 心跳消息(报 4 个已归档 expr);lib/heartbeat-scan.js 增归档过滤;test/heartbeat-scan.test.js +3 用例;实测回归 390/390", "confidence": "high", "status": "proposed", "suggested": "proposed", "recordedAt": "2026-09-10T19:18:08.012Z" }, { "id": "L-2026-0910-048", "title": "续跑熔断阈值会误伤\"有意的多次重启\":restartCount≥2 即 paused,而回归验证/加载新代码常需连续重启(实测本 expr 被熔断,需手动 resume)", "category": "engineering-action", "layer": "L2", "trigger": "同一 expr 在短时间内连续跨重启(如:验证重启机制 + 加载新代码各一次)后,任务突然 paused 且 stopReason 为续跑熔断", "decision": "① 熔断是防死循环的正确设计(保留);② 有意的连续重启场景,主 agent 应在重启后主动检查 `restartCount`,若接近阈值则在收口前 resume(/steps/update action=resume)或换新 expr 做后续验证;③ 长期改进方向(未实施,记为候选):区分\"未完成续跑再次中断\"(真异常,应计数)与\"续跑后正常推进再重启\"(可重置计数)——例如 wake 成功后若 expr 有新的步骤推进则重置 restartCount。", "rationale": "熔断的语义是\"同一断点反复中断且无进展\",但当前实现只按\"跨 bootId 次数\"计数,未区分是否真有进展;在主动做重启类验证时会被误伤,表现为\"任务莫名 paused\"(排查成本高:需读 stopReason 才知道)", "evidence": "expr-2026-09-10_18-42-03:restartCount=2 → paused(两次重启分别为 stab1_1 机制验证与 stab2_3 加载聚合代码),已 resume 恢复;/health-check restartStats 可直接看到该计数", "confidence": "medium", "status": "proposed", "suggested": "proposed", "recordedAt": "2026-09-10T19:18:08.012Z" }, { "id": "L-2026-0911-049", "title": "部署形态交付(Chrome 扩展/计划任务/服务):实施目录 ≠ 运行时加载目录,须以运行时注册表确认加载路径,多副本哈希一致才算完成", "category": "engineering-action", "layer": "L2", "trigger": "交付物要被某个运行时\"加载/执行\"时(Chrome 未打包扩展、Windows 计划任务、常驻服务/守护进程),尤其是方案里给了 targetWorkspace / 安装目录,而机器上存在多个同名副本(开发目录 / 工作副本 / 历史目录)", "decision": "① 实施前先确认\"运行时实际加载哪一份\":Chrome 未打包扩展查 `%LOCALAPPDATA%\\Google\\Chrome\\User Data\\\\Secure Preferences` 的 `extensions.settings[].path`(location=4 即 LOAD_UNPACKED)与 `Preferences` 里 `file_data[\"manifest.json\"]` 记录的已加载 manifest 原文;计划任务查 `schtasks /query /v`;常驻进程查命令行 + cwd(`Get-CimInstance Win32_Process`)。② 产物落到运行时加载目录,并让 开发/运行/工作副本 三份 SHA256 全同;覆盖前逐文件 `.bak-presync-`。③ 收口核验必须包含\"重载后生效\"这一步:不能因为语法检查/单测通过就宣布完成。④ 通道/服务类组件注意\"两侧版本错配\"——bridge 生效不代表扩展生效(各自独立的加载来源)。", "rationale": "未打包扩展的加载路径是 Chrome 侧持久化注册表值,与方案里写的 targetWorkspace、与\"最近被编辑的目录\"都可能不同;一旦错配,代码正确但运行时行为零变化,表现为\"改完没效果、重载也没效果\"的静默落差,排查成本极高(现象像功能没实现,其实是没加载)", "evidence": "expr-2026-09-10_20-29-05 收口核验:Step1/Step2 加固落 `D:\\DSH\\dsh-web-gemini-ext`(v0.4.0),而 Chrome 加载 `D:\\dsh relay test\\dsh-web-gemini-ext`(Preferences.file_data 记录 version 0.3.0);Bridge 侧因由 `D:\\DSH` 下 watchdog 拉起而生效,形成两侧版本错配;已同步三副本(备份 `*.bak-presync-20260911-051101`,SHA256 全同)并在三方轨迹追加核验条目", "confidence": "high", "status": "proposed", "suggested": "proposed", "recordedAt": "2026-09-10T21:12:09.422Z" }, { "id": "L-2026-0911-050", "title": "同一能力的多条调用路径必须传\"同源开关\":新增/新接入的请求级参数在单步、批量、降级等每条路径都要同步,否则该路径上的安全网静默失效(漏参型缺陷)", "category": "engineering-action", "layer": "L2", "trigger": "给某能力加请求级开关或新增第 N 个参数时;该能力存在多条调用路径(单步 / 批量并发 / 降级分支 / 后台定时触发);实测只覆盖了其中一条路径", "decision": "① 加参数时立即 grep 该被调函数的**所有调用点**(本例 `obtainReviewVerdict` / `reviewOneStep`),逐点确认传参;② 写**源码级接线回归测试**(括号配对提取每个调用点的实参文本,断言都含该开关,且由同一请求级表达式驱动),因为纯函数单测无法发现\"调用点漏参\";③ 做**反向验证**(人为去掉该参数跑一次,确认用例 FAIL)——否则用例可能恒真;④ 文档承诺(如\"请求级开关\")必须与实际每条路径一致,不一致即静默放行", "rationale": "漏参型缺陷的表现为\"功能没生效但无任何报错\":被调函数收到 undefined 后按默认关闭处理,单步路径实测正常会让人误判功能已生效;本例丢掉的偏偏是\"两条通道结论冲突 → 升级人工\"的安全网,属于静默放行风险最大的那类", "evidence": "claude-code 审核 rev-1a08b804718(Step xc3)REJECT:`lib/index.js:3547` 批量分支(batchStepIds)只传 6 参、遗漏 `enableCrossCheck`,而单步路径 `lib/index.js:3622` 正确传第 7 参 → `shouldCrossCheck(step, undefined)` 恒 false;该缺陷在上一轮收口时被误记为\"已修\"(git log -S 证实从未提交修复,工作树实证:批量调用点仍缺该参)。2026-09-11 修复:批量路径补传 + `test/cross-check.test.js` 两条源码级接线用例(反向验证为 13 pass/1 fail),全量 419/419 绿", "confidence": "high", "status": "proposed", "suggested": "proposed", "recordedAt": "2026-09-11T00:29:04.729Z" }, { "id": "L-2026-0911-051", "title": "端到端实测会写入用户的真实环境:派发前必须先声明,且被测系统自身要有\"不覆盖用户数据\"的设计;审核通过 ≠ 真实可用", "category": "engineering-action", "layer": "L1", "trigger": "为了验证通道/集成\"真的能跑\",直接向用户真实环境(浏览器页面、生产账号、真实会话)派发测试任务时", "decision": "① 实测前在同一回合明确声明\"将向你的 X 写入测试数据\"再执行,不做静默写入;② 被测链路必须具备占用检测(如输入框非空即拒绝),并保证失败后清理自己写入的残留;③ 验收标准要分\"代码级/单元级\"与\"实盘级\"两档,只有实盘级才算通道可用;④ 代码/审核通过 ≠ 真实可用——本 expr 收口时三步全 approved,实盘一跑就暴露发送链路失效。", "rationale": "三方协作的审核看的是代码与自述证据,无法覆盖\"真实 UI 是否配合\";若把审核通过当成通道可用,就会在真正需要降级通道时才发现不可用。而向用户环境写入测试数据不声明,会直接破坏用户正在进行的工作(本次实测覆盖了用户正在输入的内容)", "evidence": "2026-09-11 实盘实测(bridge /create-task):扩展 v0.4.0 生效(authState=OK/isReady=true)、1.0s 认领、选择器命中,但 SEND_FAIL(发送按钮未识别、Enter 无效),78s 判 failed;同时 content script 覆盖了用户 Gemini 输入框中的内容(用户反馈\"我的输入没了\")。修复:v0.4.1 加 INPUT_BUSY 占用保护 + 失败清理残留 + 发送候选链/Enter/requestSubmit 三路 + 失败 DOM 自诊断 + SEND_FAIL 触发 reload 自愈", "confidence": "high", "status": "proposed", "suggested": "proposed", "recordedAt": "2026-09-11T01:32:26.521Z" }, { "id": "L-2026-0911-052", "title": "Chrome 扩展重载后,已打开页面里的旧 content script 会变成孤儿:新后台 sendMessage 收不到响应(NO_RESPONSE),需刷新标签页或依赖自愈重试", "category": "engineering-action", "layer": "L1", "trigger": "重载未打包扩展(chrome://extensions → 刷新)后立刻派发任务;或扩展更新后已有标签页仍持有旧版 content script", "decision": "① 重载扩展后**先刷新相关页面标签**(或让自愈机制触发一次 reload)再验收;② 派发侧对 NO_RESPONSE 要做\"重载标签页 + 重试一次\"的自愈(已有实现,节流 60s),不要把它当\"通道不可用\";③ 判据上区分三类失败:SEND_FAIL(页面在、按钮找不到)/ NO_RESPONSE(content script 不在)/ EMPTY_REPLY(发送成功但没抓到回复)——三者处置不同。", "rationale": "MV3 扩展重载会重建 Service Worker 与扩展上下文,但已注入页面的 content script 不会自动更新/复活,此时后台的 tabs.sendMessage 直接失败;若把它误判为通道故障,会在\"扩展刚升级\"这一最常见的验收时刻得到错误结论", "evidence": "2026-09-11 复测:重载 v0.4.1 后第一次派发 t0001 → 6s 内 NO_RESPONSE(Tab 1635932287 content script 未注入);自愈 reload 后第二次 t0002 → done/answer=\"收到\"(约 50-60s),web-gemini 通道首次端到端成功", "confidence": "high", "status": "proposed", "suggested": "proposed", "recordedAt": "2026-09-11T02:09:47.049Z" }, { "id": "L-2026-0911-053", "title": "用户视角 ≠ 系统实况:pinned 后台标签页\"看不见\"会导致误判功能失败;判定要靠系统自报的可观测字段,并主动补埋点而不是靠肉眼", "category": "engineering-action", "layer": "L1", "trigger": "用户报\"某自动化动作没发生\",但系统侧指标(此处 isReady=true)显示正常;或某能力只有\"外部可见的副作用\"可作判据(标签页、窗口、通知、文件)", "decision": "① 先别改代码,先造**系统自报字段**(本例 tc=标签页数 / tinfo=pinned+active+windowId+url / ens=补建结果)把黑盒变白盒;② 判定以字段为准,不以\"用户肉眼没看到\"为准(pinned 后台标签页、最小化窗口、第二窗口都会被忽略);③ 无副作用分支也必须**照常上报**(原实现无标签页时直接 return 不轮询 → 外部连\"它在跑\"都看不到);④ 粘性状态字段(缺省不更新)必须配合\"明确上报缺失态\",否则\"没有\"与\"没上报\"无法区分", "rationale": "自动化动作经常在\"用户看不见的地方\"发生(后台标签页/最小化窗口)。用外部可观察副作用当唯一判据,会把\"没看见\"误判成\"没发生\",进而去修一个本来就是好的功能——本例中补建其实已稳定工作 4 小时", "evidence": "2026-09-11 实测:用户报\"无标签页、无补建\",但 bridge 收到 isReady=true;加 v0.4.2 埋点后 /stats.worker 显示 tabCount=1、tabInfo=p-w1635932285/app/f8e98a8f44d5836a(pinned 后台,存活 4h);用户关闭后 tabCount=0→25s 后 ens=created 补建成功", "confidence": "high", "status": "proposed", "suggested": "proposed", "recordedAt": "2026-09-11T13:06:25.002Z" }, { "id": "L-2026-0911-054", "title": "自研健康探针 + 敏感阈值 = 自杀式重启风暴:探针端点必须 O(1),阈值必须容忍冷启动与机器繁忙", "category": "engineering-action", "layer": "L2", "trigger": "自研 watchdog/守护进程按\"探针失败 N 次 → 重启进程\"策略守护宿主/服务;探针端点自身需要读盘/聚合/发起 HTTP", "decision": "① 探针端点必须**幂等且 O(1)**:聚合类结果加 TTL 缓存 + 后台刷新,绝不在探针路径上做全量扫描;② 阈值要给足余量(本例探针超时 2000→4000ms、连续失联 3→6 次≈30s),并区分\"进程真的没了\"与\"只是响应慢\";③ 启动日志必须打印生效参数(本例 \"miss ≥ 6 重启\"),否则\"参数改了没生效\"无从验证;④ 出现重启风暴时先停守护进程止血,再修根因(停守护不影响宿主)", "rationale": "探针失败 → 杀进程 → 进程重启后冷启动更慢 → 探针更易失败,这个正反馈会把偶发抖动放大成持续风暴(本例单日拉起宿主 18 次、判定重启 37 次,主 agent 回合被反复打断,用户观感是\"Agent 老是掉线\")", "evidence": "2026-09-11 实测:/dsh-web-relay/health-check 稳定耗时 ~1520ms(全量 expr 扫描 + 两次 bridge HTTP)vs 探针超时 2000ms;watchdog 日志 \"miss 1/3→2/3→连续 miss 3 次→重启宿主(err=timeout)\" 每 10-20s 一次;修复后 health-check 加 15s TTL 缓存、阈值 6/4000ms,日志变为 \"miss 1/6 → 宿主恢复\",零重启", "confidence": "high", "status": "proposed", "suggested": "proposed", "recordedAt": "2026-09-11T13:06:25.004Z" }, { "id": "L-2026-0911-055", "title": "定位\"某服务偶发卡顿\"先用同机第二服务做对照(一次定性到进程级),再谈机器级原因;排除类改动必须前后测量,无收益即回滚", "category": "engineering-action", "layer": "L1", "trigger": "某本地服务/进程响应偶发变慢(探针超时、请求 1-3s),但 CPU/磁盘/内存计数器看起来正常,怀疑杀软/磁盘/其他软件时", "decision": "① 同一时刻并行采样\"可疑服务\"与\"同机另一个独立进程的轻端点\",比较延迟分布——对照服务始终毫秒级 ⇒ 问题是**该进程内部**的,与机器/磁盘/杀软无关,省掉一轮轮无效猜测;② 再做\"是否与某个周期性任务相关\"的相关性检验(如把请求延迟与缓存年龄/定时器周期对齐);③ 采样要覆盖\"我活跃\"与\"我空闲\"两种状态(本次空闲窗口 40 次全快、活跃窗口 23% 慢,差异就是线索);④ 任何\"降低安全/权限\"的排除类改动,改前测一次、改后测一次,没改善就立刻回滚并说明。", "rationale": "偶发卡顿最容易让人去打\"杀软/磁盘/网络\"这些常见嫌疑,但计数器往往看不出问题,于是改动一个接一个、每次都要重启验证,成本极高。对照法是几分钟内出结论的判别实验:把\"机器级\"和\"进程级\"一刀切开。另外把带安全成本的改动当\"顺手优化\"是负收益——实测 Defender 排除对本例 0 改善。", "evidence": "2026-09-11 实测:宿主 3080 P90=1908ms(23% 慢)而 bridge 8899 P90=4ms(0 慢),慢样本中宿主 2503ms 时 bridge 11ms → 判定宿主进程内停顿;Defender 排除前后 8.5%→9%(无改善)已回滚;空闲窗口 40 次采样全部 23-90ms,指向回合处理期内部工作(会话落盘/GC)", "confidence": "high", "status": "proposed", "suggested": "proposed", "recordedAt": "2026-09-11T17:56:43.404Z" }, { "id": "L-2026-0912-056", "title": "测量工具用错会造出假 bug:复算/诊断必须使用运行时的真实参数与真实渲染路径,否则会把自己的参数错误当成系统缺陷(同一天内犯两次)", "category": "engineering-action", "layer": "L1", "trigger": "用离线脚本复算某个纯函数、或用 dump/诊断命令推断\"配置是否生效\"时;结论与运行时观测不一致(如宿主报 0、复算报 4)", "decision": "① 复算前先从源码/运行态抄出**真实参数**(本例心跳实际是 staleMs=20min、maxAgeMs=48h,我用了默认值/自设的 6h+30d → 凭空多出 4 条\"待办\");② 用 dump 类命令判断\"用户配置是否生效\"前,先做**对照实验**(给一个已知项设值,看它是否出现在 dump 里)——本例 `dsh --dump-config` 根本不渲染用户层 config,我却据此宣布\"anthropic 配置未生效\",结论作废;③ 结论与观测冲突时,先怀疑测量方式,再怀疑系统;④ 宣布 bug 前把\"参数/命令 + 期望 + 实际\"三件套贴出来自检。", "rationale": "假 bug 的代价是双向的:既浪费一轮排查与改动(去修一个本来没坏的东西),又可能据此做出错误决策(例如因\"配置不生效\"而重写配置)。而\"用错工具\"的迹象很明确——结论与运行时不一致,只要不急着下判断就能发现。", "evidence": "2026-09-12 同一天两例:① 用 `--dump-config` 未出现 anthropic 段 → 断言\"patch 配置未生效\";对照实验(给 session-title 设 fallbackMaxWords: 9,dump 仍显示 5)证明 dump 不渲染用户层,结论撤回;② 用默认参数复算心跳 → 得 4 条待办,与宿主 found=0 冲突;改用运行时真实参数(20min/48h)复算 → 0 条,宿主行为正确", "confidence": "high", "status": "proposed", "suggested": "proposed", "recordedAt": "2026-09-12T12:35:09.579Z" }, { "id": "L-2026-0913-057", "title": "宿主换线/换版后浏览器白屏≠网络故障:SPA 外壳无 cache-control 被启发式缓存,旧外壳指向新线不存在的资源寻址(手机尤其明显,清站点数据即恢复)", "category": "tooling-trap", "layer": "L2", "trigger": "切换 dsh 版本线/端口/authority 后某个浏览器打开 GUI 一片空白(手机尤甚),而 curl 探针、token 303、静态资源都正常时", "decision": "① 先用「隐私标签 或 清该站点数据」做 30 秒判别——本次即以此定位;② 排查顺序固定为:传输与鉴权(303 Location:/ + dsh-auth cookie authority=目标域名)→ 带 cookie 首页 200 → 静态资源经 ts.net 与本机字节数一致 → 无 serviceWorker → 最后才怀疑缓存;③ 根因在 harness:dsh-host-frontend-static/lib/index.js L73 服务 SPA 外壳只写 content-type(无 cache-control/etag/last-modified),而 dsh-client-modules/lib/index.js L122/L866 给插件包带 `?rev=` + `public, max-age=31536000, immutable`——「外壳可被启发式缓存 + 资源按 rev 寻址」组合,换版后旧外壳的引用必然失配 → 白屏;④ 换线/换版后的验收清单里加一条「清目标浏览器站点数据」;长期修法是给 HTML 加 no-store(上游 issue)。", "rationale": "白屏的第一直觉是网络/端口/鉴权,但本次三者都已实测排除:tailscale 直连 7ms 且 tx 17.9MB(11.27MB 插件包确实下完)、serve 映射 443→3080 正确、宿主带 --trusted-host。同 origin 此前跑的是 rc.7 宿主,其产物布局与新线(/plugins/??…&rev=)不同,缓存旧外壳是唯一与「电脑正常、手机白屏」相符的解释,且清站点数据一次即验证。", "evidence": "2026-09-13:手机 https://win10-dt.taile618c2.ts.net/?token=… 白屏;实测 303 Location:/ + set-cookie dsh-auth-…(authority=win10-dt.taile618c2.ts.net)、带 cookie 首页 200/28,218B、插件包 11,268,207B 与 modules 包 18,683B 经 ts.net 与本机字节数完全一致、HTML 内 serviceWorker/127.0.0.1 计数为 0;清站点数据后恢复(用户确认)。服务端落点:dsh-host-frontend-static/lib/index.js L73(仅 content-type)、dsh-client-modules/lib/index.js L122/L866(immutable + rev)。", "confidence": "high", "status": "proposed", "suggested": "proposed", "crossRef": [ "L-2026-0911-049" ], "recordedAt": "2026-09-13T12:53:43.880Z" }, { "id": "L-2026-0914-058", "title": "新线 typert Remote 方法的尾参 signal 由 gateway 补:绕过 gateway 直连服务(如 apiProxy 兼容层)必须自己补,否则 undefined.throwIfAborted() → TypeError", "category": "tooling-trap", "layer": "L2", "trigger": "在 harness 0.1.5+(新线)上以 cordis 服务方式直连调用 TypertRemoteService 的 Remote 方法(自研 shim / 宿主内插件互调),或运行时出现 \"Cannot read properties of undefined (reading 'throwIfAborted')\"", "decision": "① 调用前用 `fn.length` 判声明元数:尾参为 signal(length>=2)时必须显式传一个永不 abort 的 AbortSignal(`new AbortController().signal`),与 gateway 行为对齐(dsh-api-gateway lib/index.js L397 NEVER_ABORTED_SIGNAL + L746 args.push(request.signal ?? NEVER_ABORTED_SIGNAL));② 错误信封回填诊断(声明元数 + 栈帧前几行),因为这类失败是普通 TypeError、没有 .code,现场只能靠 message 定位;③ 单测补一条『两参 controller 必收 signal』用例(源码级接线断言 + 行为断言)。", "rationale": "该报错看起来像框架缺陷,实为调用契约差异:typert gateway 按源码签名(descriptor.cancellation,即最后一个形参是否叫 signal)决定是否补参,而服务实现里的 `signal.throwIfAborted()` 是无条件执行的;绕过 gateway 直连就没有这层包装,signal 为 undefined 立即抛错。二者行为必须由调用方主动对齐。", "evidence": "2026-09-13 面板「唤醒主 Agent」失败:`gateway/internal: Cannot read properties of undefined (reading 'throwIfAborted')`。根因:dsh-api-session-controller/lib/index.js L2920-2923(SessionController,即 sessionController 服务)`prompt(request, signal) { signal.throwIfAborted(); return this.commands.prompt(request); }`——注意 L736 的 `async prompt(request)` 是另一个内部类 SessionCommandController,易误读为单参。修复:shim/dsh-apiproxy-shim v0.2.0 自适应补参 + selftest 10/10 + test/apiproxy-shim.test.js;修复后 handoff 唤醒成功且携带【主 agent 装配】与 lesson Top-K。", "confidence": "high", "status": "proposed", "suggested": "proposed", "crossRef": [ "L-2026-0911-050" ], "recordedAt": "2026-09-14T00:30:00.000Z" }, { "id": "L-2026-0914-059", "title": "收口状态以 steps.json 为权威:/steps/finalize 只回写 steps.json + 追加 trace,不更新记录 md 的 frontmatter status(人读 md 会误判未收口)", "category": "repository-knowledge", "layer": "L2", "trigger": "收口后核对「任务记录状态」,或有人阅读 web-relay/experiments/dsh-web-relay-.md 判断任务是否结束/是否需再次收口时", "decision": "① 判断任务终态一律读 `expr-.steps.json` 的 status/finalized/finalizedAt/finalSummary(面板同源,是唯一权威);② 记录 md 的 frontmatter `status` 可能长期停在 pending(现状实现缺陷,不代表任务未收口),不得据此重复触发收口或判定故障;③ 主 agent 的收口动作 = 追加 `## [主 agent]` 三方轨迹条目(可经 POST /dsh-web-relay/trace 落盘),不擅自手改插件状态文件;④ 若要修,应作为插件能力待办(让 finalize 回写 record frontmatter),而不是手工同步两份状态。", "rationale": "同一任务的状态有两份副本(steps.json 与 record md frontmatter),由不同代码路径维护:finalize 只覆盖 steps.json 并追加 trace。副本不同步会让人读 md 得出「未收口」的错误结论,从而重复收口或误报缺陷——这类『状态副本漂移』的排查成本远高于读取权威文件。", "evidence": "2026-09-13/14 expr-2026-09-13_16-14-12:面板收口后 steps.json = status done / finalized true / finalizedAt 2026-09-13T16:21:39.411Z / finalSummary【dsh-web-relay 任务收口】,而 D:\\ws2\\web-relay\\experiments\\dsh-web-relay-2026-09-13_16-14-12.md 的 frontmatter 仍为 `status: pending`;finalize 处理器实现见 lib/index.js L3906-3954(仅 writeStepState + appendTrace + 可选 wake)。", "confidence": "high", "status": "proposed", "suggested": "proposed", "crossRef": [ "L-2026-0903-002" ], "recordedAt": "2026-09-14T00:30:00.000Z" }, { "id": "L-2026-0914-060", "title": "外部 AI 看不到仓库现状 → 反复重造已上线功能/引用幻觉模块;对策:可行性审计前置 + 只合入新内容 + 把机器生成的能力清单随 /ask 注入", "category": "collaboration-rhythm", "layer": "L2", "trigger": "用三方协议 /ask 做规划或重构时,方案出现「实现已存在的能力」或引用仓库中不存在的模块/命令(幻觉产物);或准备把规划步骤直接派发给 Claude Code 执行前", "decision": "① 派发前做**可行性审计**:逐条核对 artifacts 是否存在、关键能力是否已实现(给出行号/命中数取证),把差异表交回外部 AI 重规划(§2.3),**不擅自替换其 Step List**;② 合入时只取新内容——用 merge-step 只替换目标步、其余逐字保留,防其顺手改写依赖图/验收;③ 根因治理:在 /ask 的注入里附**机器生成的能力清单**(lib/* 模块 + registry 条目 + 路由表 + 测试清单),让规划方具备现状视野;④ 对「已上线功能」类步骤一律以实盘取证驳回,不派发执行。", "rationale": "外部 AI 只能看到协议文本与任务描述,看不到仓库;它倾向按通用最佳实践「重建」,于是产出幻觉模块或重复实现现有能力。直接派发的代价是双向的:既浪费 cc 配额与 4-8 分钟/任务的执行窗口,又把冗余实现混进代码库(后续还要人工分辨与回退)。把「现状视野」前置注入规划环节,才是机制级解,而非事后逐条打回。", "evidence": "2026-09-14 同一目标内三轮实测:① 首轮 5 处幻觉产物(lib/plan-engine.js、lib/reflection-engine.js、lib/swarm-review.js、scripts/sync-engine-docs.js、npm test —— 本仓库 package.json 无 scripts);② 第二轮 V1-2 声称要「补齐 Swarm 默认双角色与共识矩阵」,实测 lib/swarm-prompts.js L6-60 已有 SWARM_ROLES/swarmConsensus/parseRoleReview、L80-85 swarmEnablePolicy 已实现 importance:high 默认开,lib/client.js L211 已有成本提示,lib/index.js L3695/L3770 已接线 → 已上线;③ 第三轮 aux 列表 7 步中 3 步重复已上线(claude-code 降级链、alternatives 6 段裁决、batchStepIds 受控并发:lib/index.js 命中 49/27 处,lib/alternatives-compare.js 存在)。", "confidence": "high", "status": "proposed", "suggested": "proposed", "crossRef": [ "L-2026-0911-049", "L-2026-0910-042" ], "recordedAt": "2026-09-14T01:45:00.000Z" }, { "id": "L-2026-0914-061", "title": "PowerShell 转写 UTF-8 文本 → 双重编码污染权威状态文件(steps.json 出现 强å 乱码);对策:文本/JSON 变换一律走 node + POST 前编码自检", "category": "tooling-trap", "layer": "L1", "trigger": "需要把 UTF-8 文本(中文计划、Step List、配置)从一处搬到另一处,或构造要 POST 给 relay 的 JSON payload 时,尤其经过 PowerShell 的 Get-Content/Set-Content/Out-File/ConvertTo-Json 管道", "decision": "① 所有含中文的文本与 JSON 变换一律用 node(fs.readFileSync(p,'utf8') → fs.writeFileSync(p, s, 'utf8')),不用 PowerShell 读写文件;② 构造 /steps/restructure 等 payload 后、POST 之前必须做编码自检:若字符串含 ≥2 个 Latin-1 高位字符且按 latin1 还原后 CJK 字符数变多,判定为双重编码并**拒绝提交**(已实现于 cc-dispatch.mjs / stage-deferred.mjs / check-spec.mjs 三处入口的 [gate] 编码自检;判据必须**分段**——对每段 ≥4 个连续 Latin-1 高位字符(\\u0080-\\u00ff)做 latin1→utf8 尝试,解出 CJK 且无替换字符才算命中;整串 round-trip 会被混入的真中文干扰(真中文 >U+00FF,取低位字节产生非法序列→替换字符→漏判),防护还必须用**真乱码自测**(前两版判据在自测中均未触发,等于假防护);③ 已污染的数据可无损还原:Buffer.from(mojibake,'latin1').toString('utf8'),逐字段做「还原后 CJK 增多才采用」的保守替换;④ 修复后用真实文件字节自证(title 行十六进制应为 e5bcba…而非 c3a5c2bc…)。", "rationale": "PowerShell 默认按 ANSI/Windows-1252 解码再以 UTF-8 写出,会把「UTF-8 字节」当成「Latin-1 字符」再编码一次,产生 强å 型乱码;它不会报错,且下游 JSON.parse 依然成功,因此损坏会静默穿透到权威状态文件。事后修复成本远高于事前一条断言——本次损坏的正是整条目标链所依赖的权威计划。", "evidence": "2026-09-14 实测:全工作区 99 份 *.steps.json 中恰 1 份损坏,即权威计划 expr-2026-09-13_17-36-07.steps.json(mtime 17:40:56)——title 原始字节 c3a5c2bc c2ba…(双重编码),latin1→utf8 还原得「V1-1: 强化突破度硬门禁 (Breakthrough Gate)」;同批记录 md 正常(e7aa81e7a0b4e5baa6)。源头为我 17:37:11 用 PowerShell 转写的 _restructure-steps.json,经 _steps-merged.json → _restructure-payload2.json 写入。服务端无责:运行副本读取请求体为 Buffer.concat(chunks).toString('utf8')(lib/index.js L552)。修复后重发 restructure 返回 {\"ok\":true},落盘 title 字节 e5bcbae58c96e7aa81e7a0b4e5baa6…,双重编码判定「否」。防护自测矩阵(2026-09-14):纯乱码 FAIL、中文+乱码混合 FAIL、干净中文 OK、派发器实测 exit=1 且未入队、三份真实规格零假阳性;期间一次自测把测试任务真的入队(mojibake-selftest,17:53:19 被 watchdog 取走后因配额失败),已归档至 tasks/_archived/,cc-stats.json 未受影响(mtime 仍为 09-11)。", "confidence": "high", "status": "proposed", "suggested": "proposed", "crossRef": [ "L-2026-0909-040", "L-2026-0914-060" ], "recordedAt": "2026-09-14T01:55:00.000Z" }, { "id": "L-2026-0914-062", "title": "Claude Code 配额耗尽被记成笼统 claude exit=1 —— 失败分类不可审计、且不含恢复时间;对策:quota-exhausted 分类 + resetsAt 解析 + 已知耗尽不盲等 + 延迟自愈派发", "category": "tooling-trap", "layer": "L2", "trigger": "cc 任务派发后 1-3 分钟内就失败(exit=1、done.flag 缺失、仓库零改动),或需要在无人值守时段让 cc 通道自动恢复执行时", "decision": "① 失败分类必须区分 quota-exhausted 与 code-failed:解析 claude.log/result.json 中的 `session limit`/`rate limit`/`quota`/`resets <时间>` 特征,并尽力解析 resetsAt(解析不出返回 null,不抛错);② cc-stats 的 byFailure 单列 cc-quota-exhausted,使 /health-check 能直接看出「配额耗尽 N 次」而非混在普通失败里;③ 已知配额耗尽且当前时间早于 resetsAt 时,派发前直接降级并记 reason=cc-quota-exhausted,不要盲等整个轮询超时(现已提升到 960s 档位)再失败;④ 无人值守恢复:把任务暂存到 D:\\cc-tasks\\deferred\\(不进 queue,避免 watchdog 立即取走必然失败),由 deferred-dispatch.ps1 周期探测通道(claude -p 探针),恢复后自动入队——本次以 Windows 计划任务 dsh-cc-deferred-hb2 在 05:05 启动。", "rationale": "配额耗尽不是代码缺陷,但在此前的记录里与真实失败完全同形(都是 `claude exit=1; done.flag=missing`),主 agent 只能靠人读 claude.log 才能分辨——这恰好违反「失败分类与降级可审计」的目标要求。同时 960s 的轮询档位会把「已知不可用」放大成十几分钟白等,故需要前置判定与延迟派发,才能实现人工缺席下的自愈。", "evidence": "2026-09-14 实测:ccfix-20260914-hb 于 17:48:08 dispatch,17:50:03 失败(约 115s),result.json={\"status\":\"failed\",\"exit\":1,\"reason\":\"claude exit=1; done.flag=missing\"},claude.log 全文仅一行「You've hit your session limit · resets 5:10am (Asia/Shanghai)」,仓库 git status 零改动 → 判定为配额耗尽而非代码失败。据此改写规格(taskId=ccfix-20260914-hb2,新增修改 4「配额/限流耗尽必须可审计分类」)并暂存至 deferred;计划任务 dsh-cc-deferred-hb2 Next Run Time=2026/9/14 5:05:00、Status=Ready。", "confidence": "high", "status": "proposed", "suggested": "proposed", "crossRef": [ "L-2026-0914-061", "L-2026-0911-051" ], "recordedAt": "2026-09-14T01:56:00.000Z" }, { "id": "L-2026-0914-063", "title": "result.json status=failed 不等于产物不合格——配额常在收尾阶段耗尽(写报告时),机械按 status 重派会白丢一个窗口;验收必须看产物 + 独立验收器", "category": "judgment-heuristic", "layer": "L2", "trigger": "cc 任务 result.json 报 failed(尤其 errorCode=cc-quota-exhausted)但仓库已出现改动时;或在决定「按失败丢弃并重派」之前", "decision": "① 不以 status 单点判断:先跑独立验收器(写入范围 / node --check / 全量测试 / 文件覆盖 / BOM+CRLF 五项)并读产物(out/report.md、done.flag);② 若 status=failed 但 errorCode=cc-quota-exhausted、产物齐、测试全绿 → 按「交付有效、任务记录失败」签收:在 commit message 里如实写明原因,不掩盖也不重派(重派要再花一整个额度窗口);③ 只有产物缺失、测试红或范围越界时才重派;④ 把 done.flag 缺失视为唯一缺口,并在后续契约里要求「先 touch done.flag 再写报告」,降低收尾阶段被额度打断的损失。", "rationale": "订阅额度是随时可能中断的外部约束,而中断点往往落在任务末尾(生成报告/总结时)——此时代码与测试早已完成。若机械按 status 处理,会把一份已写完且全绿的实现丢掉再重做,代价是一个完整的额度窗口(本次约 2.7 小时)。status 描述的是「任务是否按契约完整闭环」,产物完好性描述的是「工作是否已发生」,两者必须分开判定。", "evidence": "2026-09-14 ccfix-20260914-stats:result.json={\"status\":\"failed\",\"start\":\"23:37:04Z\",\"end\":\"23:42:09Z\",\"exit\":1,\"errorCode\":\"cc-quota-exhausted\",\"reason\":\"claude exit=1; done.flag=missing\"},claude.log 全文仅一行「You've hit your session limit · resets 10:20am (Asia/Shanghai)」;但 lib/cc-stats.mjs 已新增 cc-quota-exhausted / cc-permission-denied / cc-timeout 三条规则(排在通用 timeout 之前)、test/cc-stats.test.mjs 新增 5 例、out/report.md 15683 B 已写完;独立验收器 verify-cc-task.mjs 判定 7/7 ACCEPT(全量 tests=458 pass=458 fail=0、*.test.js 子集 318/318、verify-files-coverage OK、无 BOM/无 CRLF、写入范围合规)。同批次 ccfix-20260914-hb3 则是真 done(exit=0 + done.flag),说明失败分类能区分两者。", "confidence": "high", "status": "proposed", "suggested": "proposed", "crossRef": [ "L-2026-0914-062", "L-2026-0914-061" ], "recordedAt": "2026-09-14T07:50:00.000Z" }, { "id": "L-2026-0914-064", "title": "Windows 计划任务里的 .cmd 必须 CRLF——LF-only 批处理在调度器上下文报 9009(手动 cmd /c 却能跑);验证必须走 schtasks /Run 真实路径", "category": "tooling-trap", "layer": "L1", "trigger": "把自动化(脚本/链条)挂给 Windows 计划任务时;或 .cmd/.bat 由非 Windows 工具写出(node/write 工具/PowerShell here-string/git eol=lf)之后", "decision": "① 所有 .cmd/.bat 一律以 CRLF 写入(node 写 '\\r\\n',或 [IO.File]::WriteAllText(...,ASCII) 配合显式 CRLF 拼接),内容保持**纯 ASCII** 以避免代码页歧义;② 绝不把「手动 cmd /c 能跑」当作「计划任务可用」——必须 schtasks /Run 走真实调度路径验证,并核对 Last Result(0=成功;267009/0x41301=正在运行;9009=命令未找到;其他非 0 需查包装器日志);③ 包装器必须自审计:启动与退出各写一行日志(含 %DATE% %TIME% 与 %ERRORLEVEL%),否则调度器里的失败会完全静默;④ 长途无人值守窗口(如整段额度窗口)开始前就要做这次验证,不要等到事后发现整窗空跑。", "rationale": "cmd.exe 对 LF-only 批处理的解析在交互式与调度器上下文下不一致:手动 cmd /c 能正常执行,调度器里却会把参数行拆错、把第二个参数当成命令名(9009)。此类失败不写入任何业务日志(链条自己的日志一行都没有),只能靠计划任务的 Last Result 与包装器自审计发现;无人值守时其代价是一整个自动化窗口。", "evidence": "2026-09-14/15 实测:run-chain.cmd 由 write 工具写出为 LF-only(CR=0 / LF=5),计划任务 dsh-cc-chain-v1v3 在 10:22-18:02 期间每次触发 Last Result=9009,chain-sched.log(GBK)记录 \"'chain.mjs' 不是内部或外部命令\",而 chain.log 完全没有该窗口的任何记录;手动 cmd /c 运行同一文件却返回 exit 3 且链条逻辑正常(因此首次自测未暴露)。改为纯 ASCII + CRLF(CR=7 / LF=7,非 ASCII 字符=0)后 schtasks /Run:Status=Running、Last Result=267009(0x41301),chain.log 记录「链条 v1-v3 启动 → [1/5] V1-1 → 派发 [queued]」,watchdog 随即创建 tasks/v1-1-breakthrough-gate/ 并开始执行——即修复后才真正实现无人值守推进。", "confidence": "high", "status": "proposed", "suggested": "proposed", "crossRef": [ "L-2026-0914-061", "L-2026-0914-062" ], "recordedAt": "2026-09-15T00:20:00.000Z" }, { "id": "L-2026-0915-065", "title": "断点续跑必须先清理上次尝试的 tasks// 残留——否则 watchdog 防重复机制 + 陈旧 result.json 会让链条无限空转;重复文件还必须移出 glob 目录", "category": "tooling-trap", "layer": "L2", "trigger": "实现或调试「失败后重跑/断点续跑」类自动化时;或派发系统对同一 taskId 有幂等保护时", "decision": "① 重试同一 taskId 前,必须把上次尝试的 `tasks//` 整体归档(如 `tasks/_retry-archive/-/`)后移除——原因有二:(a) 派发守护常以「tasks//task.json 已存在」判定重复并跳过入队,任务永远不会再被执行;(b) 轮询等待方会立刻读到上次那份**陈旧 result.json**(含失败 errorCode),从而再次判定失败 → 形成无限「暂停—重派—读到旧结果—再暂停」空转。② 归档而非删除,保留现场供审计。③ 派发守护把「重复/非法」文件移出队列时,**必须移到非 glob 目录**(如 `queue/.dupes/`),否则 `for q in queue/*.task.json` 下一轮会把该文件当成一个新任务 id 再次执行。④ 重试逻辑要区分「上次仍在运行」(无 result.json,不得清理,继续等待)与「上次已结算失败」(有 result.json,需清理后重跑)。", "rationale": "幂等保护与断点续跑是一对天然冲突的机制:守护通过「目录已存在」实现幂等,而续跑恰恰需要重跑同一 id。若不显式区分「已完成」与「已失败但未成」,续跑会被幂等保护静默吞掉;更隐蔽的是陈旧 result.json 让等待方以为任务刚刚失败,于是每次都「正确地」暂停——表面看流程正常,实际整夜零进展。这类缺陷不会报错,只能通过「重跑路径的端到端演练」发现。", "evidence": "2026-09-15 实测:cc-chain 在 V1-2 因订阅额度耗尽(errorCode=cc-quota-exhausted)暂停后,链条进入重试路径;核对发现 cc-watchdog.sh L32-36 对已存在的 tasks//task.json 直接跳过入队(把队列文件移为 queue/.duplicate..task.json),而 waitResult 又会立即读到上次失败的 result.json(190 B) → 判定再次失败 → 再次暂停,且那些 .duplicate.*.task.json 会在下一轮被 glob 当作新任务派发。修复:cc-chain.mjs 新增 clearStaleAttempt(taskId)(归档到 tasks/_retry-archive/ 后移除,仅在 prev.status ∈ {quota-paused,waiting,verify-rejected,dispatch-failed} 且存在 result.json 时触发);cc-watchdog.sh 改为 `mkdir -p queue/.dupes` 并把重复文件移入该非 glob 目录(备份 .bak-dupes-*)。修后复跑链条:index 保持 1、V1-1 仍为 accepted-awaiting-review、V1-2 正确回到 quota-paused;watchdog 重启后 active/running、心跳正常、Restart=always 完好。额度恢复时间实测为 3:20am(下一窗口自动续跑)。", "confidence": "high", "status": "proposed", "suggested": "proposed", "crossRef": [ "L-2026-0914-062", "L-2026-0914-064" ], "recordedAt": "2026-09-15T00:30:00.000Z" }, { "id": "L-2026-0915-066", "title": "规格文件里的 prompt 模板字面量极易写坏(未转义反引号 / 嵌套 ${})——本会话踩 4 次;对策:写规格后先跑语法+模板扫描,且校验器自身必须用已知good样本自检", "category": "tooling-trap", "layer": "L1", "trigger": "编写/修改 cc 任务规格(cc-specs/*.mjs)或任何含大段中文 prompt 的模板字面量时;以及新写一个「校验/扫描」脚本准备据此修改既有文件之前", "decision": "① 规格里的长 prompt 一律放在模板字面量中,**内容里不要出现裸反引号**(要显示代码/字段名就用「」或普通引号,或写成 \\` 转义);② 绝不在模板里写 `${...}` 作为示例(会被当表达式求值;实测 `fs.resolve(`${EXPERIMENTS_DIR}/...`)` 直接导致规格 import 崩);③ 写完立刻跑两道检查:`node --check ` + 模板扫描(只计**未转义**的反引号/`${`);④ 派发/链条入口已内置 `node --check` 前置门(cc-dispatch.mjs 与 cc-chain.mjs),给出可读诊断而不是堆栈;⑤ **校验器必须先自检**:新写的扫描器/判据要拿已知-good 文件验证(本次扫描器把合法的转义反引号 \\` 计成裸反引号,对 3 份可用规格误报「需修」,若照办会破坏它们)。", "rationale": "模板字面量把「文本」与「代码」混在同一层:一个反引号就终止模板,一个 ${ 就触发求值,而症状是 import 期的语法/运行错误,与「文本写错」的直觉无关,因此极易反复踩。更危险的是反向情形——一个误报的校验器会驱动人去「修」本来正确的文件。两侧都靠同一原则防住:改前先能机械判定,判定器本身先在已知-good 样本上验证。", "evidence": "2026-09-14/15 实测四次同类崩因:① v1-1.mjs 插入「声明可达性」段落时用了裸反引号 → SyntaxError: Unexpected identifier 'breakthrough_type';② v3-1.mjs 在「--json 时输出机器可读 JSON」处用了 `--json` → SyntaxError;③ v2-1.mjs 第一轮插入含裸反引号的字段说明;④ v2-1.mjs 第二轮插入 `fs.resolve(`${EXPERIMENTS_DIR}/...`)`(裸反引号 + 嵌套 ${})→ 同样崩。另有反向事故:新写的 scan-spec-templates.mjs 首版按原始文本统计反引号,对 v1-2.mjs(42)/v1-0-fix-heartbeat.mjs(124)/fix-cc-stats.mjs(26) 误报,而这三份当时均能 `node --check` 通过(其反引号为 \\` 转义、在模板中合法);修正为只计未转义后报 0 问题,并交叉验证 7 份规格全部可 import(prompt 长度 2428–6156)。防线:cc-dispatch.mjs 与 cc-chain.mjs 已加 `node --check` 前置门(坏规格报 spec-broken 并不占用额度窗口)。", "confidence": "high", "status": "proposed", "suggested": "proposed", "crossRef": [ "L-2026-0914-061", "L-2026-0915-065" ], "recordedAt": "2026-09-15T00:40:00.000Z" }, { "id": "L-2026-0915-067", "title": "工具里写死的枚举/契约取值必须回源码取证——本会话两次踩到(reviewedBy 实际是 claude-code 而非 cc;/steps/declare 请求体漏 workspacePath)", "category": "tooling-trap", "layer": "L2", "trigger": "编写与外部系统交互的工具(HTTP 客户端、状态机判定、映射表)时;或在无人值守自动化之前预设「某字段会是什么值」时", "decision": "① 任何映射表/枚举(状态名、审核通道名、字段取值、退出码)都必须在源码里 grep 出**实际写入点**并把 file:line 写进注释,不凭印象;② 当同一契约被两侧实现(规格/任务 prompt 与主 agent 工具),做一次**机械一致性核对**(请求字段与响应字段集合 ⊆ 对方要求),并把它脚本化;③ 未覆盖的取值一律按最保守处理(本插件按「强度 0 = 不足,停止等人」),并在日志里区分「未映射」与「确实不足」;④ 改动契约(如给端点新增请求字段)时同步改规格与工具,不要等夜间窗口才暴露不一致。", "rationale": "此类错误在本地几乎不会触发——它们只在特定分支(审核恰好走 cc 通道、端点恰好上线时)才显现,而那一刻通常是无人在场的自动窗口;届时错误还会以误导性的原因出现(把「自审」报成「强度不足」),让人误判为通道故障。把「印象值」换成「源码证据 + 机械核对」,是提前消除它的唯一办法。", "evidence": "2026-09-15 两次实测:① lib/index.js L3514 `const reviewerBy = reviewer === 'cc' ? 'claude-code' : reviewer`——relay 写入的 reviewedBy 实际是 `claude-code`,而我的强度表只有 `cc`/`claude`,映射为 0 会误报「强度不足」并停链(已修:补入强度表与自审守卫,新增 `--selftest-reviewer` 对 10 个真实取值逐一核对;顺带查证反直觉事实:web-gemini 成功时 reviewedBy 也记 `external`,见 L3406);② V1-2 规格里 `/steps/declare` 请求体写的是 `{ exprId, iterations?, finalAcceptance?, autoDecision?, reason? }`,**漏了 workspacePath**(relay 靠它解析 base 定位 steps.json),而主 agent 工具 declare-autoiter.mjs 会发送该字段——已把 workspacePath 与响应体形状(`stepState` + `autoIterDecl{complete,halfState,reasons,hint}`)写入规格契约,并让工具在响应缺契约字段时显式告警。", "confidence": "high", "status": "proposed", "suggested": "proposed", "crossRef": [ "L-2026-0915-066", "L-2026-0915-065" ], "recordedAt": "2026-09-15T02:15:00.000Z" }, { "id": "L-2026-0915-068", "title": "spawnSync 跑「node <缺失文件>」返回 status=1 且 error 为 undefined——ENOENT 分支是死代码,会把「未落地」误报成「漂移」", "category": "tooling-trap", "layer": "L1", "trigger": "用 spawnSync 调用外部脚本/命令,并打算据其 status/error 判断「该脚本是否存在/是否可用」时", "decision": "① 判断脚本或文件是否存在必须先用 fs.existsSync/fs.accessSync,**绝不**依赖 spawnSync 的 `error.code === 'ENOENT'`——当被调用的是 node 本身(`node `)时 node 启动成功、在内部报 MODULE_NOT_FOUND,spawnSync 返回 status=1 而 error 为 undefined;② 诊断文案必须区分「工具不存在/未落地」与「工具运行后报告异常」两种情形,否则会误导后续判断(本次把「V3-1 未落地」误报成「文档与引擎存在漂移」);③ 对 spawnSync 至少要实测三种真实形态:成功、被调用程序内部失败(status≠0、无 error)、被调用文件不存在(status=1、无 error、stderr 是 node loader 报错)。", "rationale": "这类错误不会让流程崩,只会让**诊断说谎**——而在无人值守的自动窗口里,诊断输出就是唯一的事实来源:一条错误的「漂移」结论可能让人去改文档,而真正的问题是某个步骤还没做。诊断的准确性本身就是可靠性的一部分。", "evidence": "2026-09-15 实测 verify-v3-acceptance.mjs:`spawnSync(process.execPath, ['D:/dsh-web-relay/scripts/does-not-exist.mjs'], {encoding:'utf8'})` → `status=1, error.code=undefined, stdout.len=0, stderr.head='node:internal/modules/cjs/loader:1573'`;因此 `if (sc.error && sc.error.code === 'ENOENT')` 永不命中,第 ⑥ 条输出「文档与引擎存在漂移(sync-engine-docs --check 非 0)」,而事实是 scripts/sync-engine-docs.mjs 不存在(V3-1 未落地,已用 Test-Path 复核 .mjs/.js 均不存在)。修法:先 `fs.existsSync(syncScript)`,不存在则直接判「未落地」且不再 spawn;修后输出「⑥ scripts/sync-engine-docs.mjs 不存在 → V3-1 未落地」。", "confidence": "high", "status": "proposed", "suggested": "proposed", "crossRef": [ "L-2026-0915-067", "L-2026-0915-066" ], "recordedAt": "2026-09-15T02:18:00.000Z" }, { "id": "L-2026-0915-069", "title": "执行体的自述必须**在同环境**复跑才能采信——cc 报的「3 个既有失败」在 WSL 精确成立、在 Windows 不成立(同一提交、同一测试计数),跨环境复跑不是否证", "category": "judgment-heuristic", "layer": "L1", "trigger": "cc(或任何执行体)在自述/报告里声明「有 N 个失败属于既有环境问题,已用 git stash 证明改动前后一致」这类免责性结论时", "decision": "① 采信/否证执行体的失败自述前,先确认**它是在哪个环境跑的**:本项目的 cc(headless claude)在 **WSL**(/mnt/d/dsh-web-relay)跑测试,而主 agent 的验收器在 **Windows** 跑——两侧环境不同,失败清单不可直接互证;② 因此「在 Windows 复跑全绿」**不能**用来否定「WSL 有 3 个失败」,二者是**两个事实**,应同时记录并说明成因;③ 判据应是「失败清单是否与自述一致」(本次完全一致:同为 test/shadow-gate.test.js 的 3 项)+「本次改动文件是否可能影响这些用例」(本次 4 个改动文件均不涉及 shadow-gate → 逻辑上不可能由本次引入);④ 写 cc 规格时,**不要**把「全量测试 0 失败」写成硬性验收条——cc 在 WSL 永远有这 3 项失败,该条款不可满足,会诱导 cc 反复做 git stash 自证(已发生 2 次)甚至去「修」一个并不存在的缺陷;正确写法是「排除 test/shadow-gate.test.js 后 0 失败,并注明 Windows 侧全量口径由主 agent 验收」。", "rationale": "跨环境复跑得到的是**另一个事实**,不是对原事实的否证。混淆二者会两头出错:要么采信了本环境其实为红的结论,要么——像本条最初被误写的那样——把**准确的自述**否证掉,既不公允,也会让后续判断失去一条可信的信号源。环境差异本身是必须写进结论的**事实成分**,而不是可以互相抵消的噪声。", "evidence": "2026-09-15 两次 cc 任务(ccfix-20260915-swarmparse、ccfix-20260915-autoiteraudit)均自述「3 个失败为 shadow-gate.test.js 既有环境问题,已用 git stash 证明改动前后一致」。主 agent 实测两环境:**WSL** `node --test --test-reporter=tap test/*.test.js` → 390 例、3 失败,精确为 test/shadow-gate.test.js 的 `not ok 308 TC-Green: repoPath 识别 + L1 语法预检`、`not ok 311 TC-GC: Shadow GC`、`not ok 314 getGitHead`(与 cc 自述**完全一致**);**Windows** 同口径 `node verify-cc-task.mjs ...` → *.test.js 子集 **390/390 通过**、全量 **530/530 通过**、0 失败。两侧测试计数相同(390)而失败数不同(3 vs 0)→ 差异来自环境(WSL drvfs/`/mnt/d` 上的 git 行为)而非代码。本条最初被主 agent 误写为「3 个失败不可复现、cc 自述不成立」,经 WSL 复跑后**自我更正**为「同环境可复现,跨环境不可互证」。", "confidence": "high", "status": "proposed", "suggested": "proposed", "crossRef": [ "L-2026-0915-060", "L-2026-0915-068" ], "recordedAt": "2026-09-15T12:05:13.806Z" }, { "id": "L-2026-0915-070", "title": "done.flag 缺失二次复发:runner 的 cc-failed 把「没写完成标记」与「写坏代码」混为一谈,目前该分类假阴性率 100%,须新增 cc-marker-missing", "category": "engineering-action", "layer": "L1", "trigger": "runner.sh 写出 result.json 且 reason 含 `claude exit=0; done.flag=missing`(errorCode=cc-failed)时", "decision": "① 主 agent 遇到该 errorCode **不得**直接判失败或重试——必须按「工作树可能已有完整产物」处理:先 `git status --porcelain` 看改动、比对规格写入范围、跑独立验收(verify-cc-task),**按产物判定而非按标记判定**;② 通道侧应新增独立分类 `cc-marker-missing`(判据:claude exit=0 且有工作树改动但 done.flag 缺失),使其与「真写坏代码」在审计上可区分,并据此进入「人工核验」路径而非「重试」路径。", "rationale": "失败分类的唯一价值是决定下一步动作:真失败→重试/降级;标记缺失→人工核验产物。混为一谈会让无人值守链条在**产物完好时重跑**(浪费稀缺的 cc 额度),或在产物坏时因「反正分类一样」而误收,两头都错。", "evidence": "两次实测同一签名:① V1-2(提交 c4b3125)result.json = `{status:\"failed\",errorCode:\"cc-failed\",reason:\"claude exit=0; done.flag=missing\"}`,独立验收 7/7 ACCEPT;② ccfix-20260915-swarmparse(提交 947fd3e)result.json 完全相同签名,独立验收 **7/7 ACCEPT**(全量 525/525)。即该分类下 2/2 产物实际完好 → 假阴性率 100%。", "confidence": "high", "status": "proposed", "suggested": "proposed", "crossRef": [ "L-2026-0915-069" ], "recordedAt": "2026-09-15T12:05:13.808Z" }, { "id": "L-2026-0915-071", "title": "node --check 抓不到「模板串被反引号提前闭合」——那是合法 JS,只会在 import 时炸;且门控输出被 Select-String 过滤会制造「已通过」的错觉", "category": "tooling-trap", "layer": "L1", "trigger": "写 cc 规格(含 prompt 模板字面量)后做派发前检查时;以及任何「跑门控脚本看几行输出」的场合", "decision": "① 规格的派发前检查**必须真正 import 该模块**(`node check-spec.mjs ` 就会 import),**不能**只跑 `node --check`——反引号提前闭合模板串、`${}` 注入等写法**语法完全合法**,node --check 返回 0,只有模块求值(ReferenceError: xxx is not defined)才暴露;② 门控/校验脚本的输出**必须看进程退出码并看全文**,禁止用 Select-String/grep 过滤后再下「通过」的结论——过滤会把报错行本身滤掉,剩下的空白被误读成「无异常」;③ 判据用「退出码 + 是否出现 RESULT/OK 行」双重确认。", "rationale": "这两个坑叠加时最危险:门控**确实**报了错,但报错内容与过滤模式不匹配而消失,于是「门控通过」的错误结论被建立,直到真正的动作(dispatch/import)才炸——把「本可在写规格后 1 秒发现」的问题推迟到「派发时」,且极易被误判为通道故障而非规格笔误。", "evidence": "2026-09-15 cc-specs/fix-swarm-parse.mjs:prompt 模板串内写了未转义的 `` `/steps/auto-review` ``,反引号闭合模板串使 `steps` 成为未定义标识符。`node --check` **exit=0 通过**(合法 JS);真正暴露是 `cc-dispatch.mjs` import 时 `ReferenceError: steps is not defined at fix-swarm-parse.mjs:49`。同时该次 `node check-spec.mjs ... | Select-String -Pattern 'RESULT|编码自检|refs'` 输出为空——门控其实已失败,但报错行不匹配过滤模式而被滤掉,造成「门控通过」的错觉。修法:转义为 \\` 后 `node --check` 与 `check-spec.mjs`(RESULT: OK)双双通过。同类笔误本会话累计第 5 次(见 066)。", "confidence": "high", "status": "proposed", "suggested": "proposed", "crossRef": [ "L-2026-0915-066", "L-2026-0915-067" ], "recordedAt": "2026-09-15T12:05:13.808Z" }, { "id": "L-2026-0915-072", "title": "「范围外」测试改动的接受准则:断言若编码的正是被修掉的缺陷行为,改动属正当——但必须逐行判定断言强度是升是降", "category": "judgment-heuristic", "layer": "L1", "trigger": "cc 交付改了规格未声明的测试文件,理由是「原断言断言的正是被修复的缺陷行为,不改则修复必然红」时", "decision": "① 先看 diff 是否**只**改「编码旧缺陷行为」的那一处断言,有无顺手删改无关用例;② 判定断言强度:**强化或持平可接受**(改期望值本身 + 新增断言),**弱化必须拒绝**(删除断言、改成 includes/正则放宽、改成恒真、注释掉);③ 接受时用 `--allow ` 显式放行,并把「为何接受」写进提交信息与轨迹,**不得**默默放过;④ 若无法逐行确证强度未降,一律要求重做。", "rationale": "「测试必须随语义演进」与「测试是防回归底线」天然冲突:语义修正会让编码旧行为的断言必然变红,但特权一旦给宽,弱化断言(把红的改成绿的最省事做法)就会伪装成「同步更新」。唯一安全的仲裁方式是把判断收敛到一个可操作的客观标准——**断言强度是否上升或持平**,并把例外显式留痕,使事后审计能复核这次放行。", "evidence": "2026-09-15 ccfix-20260915-swarmparse:cc 改了范围外的 test/swarm-review.test.js。逐行审查:原 `assert.equal(parseRoleReview('乱码').verdict, 'rejected')` 断言的正是被修掉的「无法解析即静默 rejected」缺陷行为;改后为 `assert.equal(r.verdict, 'unknown')` 并**新增** `assert.ok(r.raw.length > 0)` → 期望值随语义修正 + 断言数量增加,判定为**修正+强化**,接受并用 `--allow test/swarm-review.test.js` 放行,独立验收 7/7 ACCEPT(全量 525/525)。", "confidence": "high", "status": "proposed", "suggested": "proposed", "crossRef": [ "L-2026-0915-069" ], "recordedAt": "2026-09-15T12:05:13.808Z" }, { "id": "L-2026-0919-073", "title": "判定「回归」必须在所有生产平台上复现——单平台失败不算回归(权威 = 交集)", "category": "judgment-heuristic", "layer": "L1", "trigger": "验收/回归跑在一个平台,而被测代码的生产消费者在另一个平台(本项目:lib/task-schema-v2.mjs 由 WSL runner 使用,插件本体跑 Windows);或某测试在 A 平台红、B 平台绿时", "decision": "① 不要选单一平台当权威(选哪个都会误判一类):判据取**交集**——同一用例在**两个平台都失败**才算真回归;单平台失败如实列出、标注平台差异、不阻断;② 交叉复核**按需触发**(只在首个平台有失败时才跑第二个平台),否则每次验收都多跑一遍全量,会把周期性审计拖到超时(实测);③ 生产侧必须能跑通声明的验收探针——探针里禁止硬编码单平台路径。", "rationale": "混合架构里「生产环境」不是一个而是多个:同一仓库里不同文件的消费者不同。用单一平台当权威,既会把另一平台的环境差异误判成缺陷(实测连 REJECT 两轮完全正确的实现),又会掩盖真正的跨平台回归。交集规则同时满足「不漏真回归」与「不误杀环境差异」。", "evidence": "2026-09-18 ccfix-20260918-acceptcall / -b:命令形态的 POSIX 用例在 Windows 红、WSL 绿;初版 Windows-only 门禁连判两轮 REJECT(烧掉两轮 cc 配额),改交集规则后同一次交付判 ACCEPT 8/8;按需触发交叉复核把四层审计从 >120s 超时降回 47s。", "confidence": "high", "status": "proposed", "suggested": "proposed", "confirmedBy": null, "recordedAt": "2026-09-18T22:48:54.385Z", "crossRef": [ "L-2026-0919-075" ] }, { "id": "L-2026-0919-074", "title": "「字段已实现且单测全绿」≠「真实路径可用」——上线前必须有至少一次真实使用者走过的证据", "category": "tooling-trap", "layer": "L1", "trigger": "新增/启用一个此前**没有真实使用者**的字段或钩子(本项目:task.json 的 acceptanceScript,schema/CLI/单测全都有、真实任务 0 使用者);或某能力只有注入假依赖的单元测试覆盖时", "decision": "① 对「从未被真实任务用过」的字段,默认按**未验证**对待;② 上线前造一个最小真实使用者跑通(本次是 ⑤b 任务首次声明 acceptanceScript,才发现执行语义是坏的);③ 单测若全部注入假 exec/假 fs,必须同时有一条使用**真依赖**的用例,否则红绿都不代表可用。", "rationale": "单元测试的注入点会系统性地绕开真实执行路径:被测逻辑全绿,而它真实调用外部命令/文件系统的那一层从未运行。这类缺陷第一次真用必然暴露,且往往被误读成「产物不合格」。", "evidence": "2026-09-18 实测:全 D:\\cc-tasks\\tasks 只有 1 个任务声明过 acceptanceScript;其 defaultExec 把值当 shell 命令跑 → 首次真用即 /bin/sh: probes/x.mjs: not found,把一份独立验收 8/8 的合格产物判成 v2-validate-failed(v10/v11/v12 三次自报 failed 同源)。", "confidence": "high", "status": "proposed", "suggested": "proposed", "confirmedBy": null, "recordedAt": "2026-09-18T22:48:54.385Z" }, { "id": "L-2026-0919-075", "title": "负控(变异)必须自证「真的改坏了」;目标被跳过/不存在 = 空转,看起来与「通过」完全一样", "category": "tooling-trap", "layer": "L1", "trigger": "写/审一个门禁的负控(把目标改坏、要求门禁报失败)时;或用「强制某层失败」来证明失败可见性时", "decision": "① 变异后断言**内容确实改变**,否则单列 FAIL(「门禁没报失败」只说明负控是空的);② 强制失败的目标必须选**永不被跳过**的对象;③ 工具自身要在「负控空转」时判工具错误并说明,而不是给一个看似通过的结论。", "rationale": "负控的价值全在「它真的制造了坏输入」。一旦变异是空操作、或目标因环境被跳过,负控就退化成永真检查,而它的输出与「门禁正确报失败」一模一样——这类静默退化只能靠让负控自证来根除。", "evidence": "2026-09-18 两次:① meta-gate 拿一份无 anchors 的老规格去改锚点 → apply() 空操作 → 误判「门禁有病」(加「变异必须改变内容」断言后修复);② 隐藏启动器的失败可见性负控得到 exit 0,真因是所指定的层当时正被跳过(链条在飞)→ audit-all 增加「负控目标被跳过 → exit 3 并说明空转」。", "confidence": "high", "status": "proposed", "suggested": "proposed", "confirmedBy": null, "recordedAt": "2026-09-18T22:48:54.385Z", "crossRef": [ "L-2026-0919-073" ] }, { "id": "L-2026-0919-076", "title": "跨进程只传结构化数据(JSON),不要位置化的文本拼接——空字段会让后续字段错位", "category": "tooling-trap", "layer": "L1", "trigger": "父进程要读子进程产出的多个字段(本项目:读 /health-check 的 loadedSha 与 loadedLibHash)时", "decision": "子进程 console.log(JSON.stringify({...})),父进程 JSON.parse;禁止 a + 空格 + b 再按空白切分——空值加 trim() 会把第二个字段顶到第一位。", "rationale": "位置化的文本协议把「字段缺失」编码成「字段位移」,错误表现为数据错配而不是缺字段,因此不会报错,只会给出一个看似正常的**错误结论**(仪器说谎)。", "evidence": "2026-09-18:⑤ 交付接地把 loadedLibHash 误读成 loadedSha → 报「loadedSha 与 HEAD 不一致」的假失败,恰好掩盖了真实结论(宿主是否加载新代码);改 JSON 传递后立刻给出正确判定并升级为「精确」。", "confidence": "high", "status": "proposed", "suggested": "proposed", "confirmedBy": null, "recordedAt": "2026-09-18T22:48:54.385Z" }, { "id": "L-2026-0919-077", "title": "单槽告警/信号文件会静默丢告警——用「主槽 + 队列 + 销账自动提升」", "category": "engineering-action", "layer": "L1", "trigger": "设计一个「需要人/主 agent 介入」的落盘通道(信号文件),且可能有多个来源同时告警时", "decision": "① 主槽只放一条;别的来源在**未销账**时**入队**而非覆盖;② 人销账主槽时自动提升队首(并给新代数 → 新 id,使消费者会为新告警再次唤醒);③ 来源自愈时只自动销账**同源**条目。", "rationale": "单槽文件的下一次写入就是一次无声删除。告警通道丢失告警而无人察觉,比没有该通道更危险——它同时破坏了「检测」与「对检测的信任」。", "evidence": "2026-09-18:信号模块自检的 [POS] 用例抓到「另一来源写入会顶掉尚未销账的告警」(链条护栏告警被审计告警覆盖 → 前者凭空消失);改主槽+队列并覆盖 9 条去重/复发/提升用例后 26/26 通过。", "confidence": "high", "status": "proposed", "suggested": "proposed", "confirmedBy": null, "recordedAt": "2026-09-18T22:48:54.385Z", "crossRef": [ "L-2026-0919-078" ] }, { "id": "L-2026-0919-078", "title": "无人值守下「检测」不等于「升级」:失败必须接到唤醒通路,并按同源只叫一次防告警疲劳", "category": "engineering-action", "layer": "L1", "trigger": "部署周期性检查/审计(本项目四层审计每 30 分钟)时;或某告警只写日志与退出码、没有任何人被通知时", "decision": "① 告警要显式接到消费端(信号文件 → 插件 boot/心跳 → 唤醒主 agent);② 同源失败用**稳定键**去重,只叫一次;人销账后复发才再叫;③ 来源自愈自动销账,免留一条早已恢复的告警;④ 退出码语义不得因「多做一步写信号」而改变。", "rationale": "写进日志的告警只对「已经在看的人」有效,而无人值守场景的前提恰恰是没人在看。同时,周期性失败若每次都唤醒,会在几小时内制造告警疲劳,使通道失效——所以「只叫一次 + 销账后复发可再叫」是必要语义。", "evidence": "2026-09-18 实测:四层审计连续 9 次 ACTION-NEEDED:⑤ 交付接地(宿主未加载仓库代码),只写进 audit.log 与计划任务 Last Result,**无人被叫醒**(4 小时无人值守);同期链条侧同类信号已能把主 agent 叫醒(当日实证)。补齐后通路真跑验证:真审计强制失败 → 退出码仍 1、写下 audit-action-needed、连续第二次不重写。", "confidence": "high", "status": "proposed", "suggested": "proposed", "confirmedBy": null, "recordedAt": "2026-09-18T22:48:54.385Z", "crossRef": [ "L-2026-0919-077", "L-2026-0919-079" ] }, { "id": "L-2026-0919-079", "title": "唤醒/告警的证据必须事后仍可取:可观测记录不得被后续「去重评估」覆盖", "category": "judgment-heuristic", "layer": "L1", "trigger": "设计唤醒/去重逻辑时;或需要事后证明「某次唤醒真的发生过」时", "decision": "记录里保留**最近一次决定性评估**(如 lastNotifiedAt / sessionIdSource),去重评估只更新「已通知集合」,不要重写那条决定性记录;否则事后只剩源码可查。", "rationale": "「记录说没有」与「确实没发生」在事后无法区分。证据被自己的去重逻辑抹掉,等于该能力不可审计,也就不可信——这与「诊断说谎比不诊断更糟」同源。", "evidence": "2026-09-18:boot 扫描依信号唤醒主 agent 成功(交接消息确已到达),但 /health-check 的 pendingHuman 被后续 decision:none 的去重评估重写 → sessionIdSource 回落 none、notifiedAt 为 null,成功唤醒的痕迹消失,只能靠读代码 + duplicateSuppressed 反推。", "confidence": "high", "status": "proposed", "suggested": "proposed", "confirmedBy": null, "recordedAt": "2026-09-18T22:48:54.385Z" }, { "id": "L-2026-0919-080", "title": "规格写窄也是根因:只写「取 env」而实现已有回退 → 能力在真实宿主上永久空转", "category": "judgment-heuristic", "layer": "L1", "trigger": "给执行体写实现规格(尤其涉及配置来源、回退策略、默认值)时;或某能力「代码在、日志也在、但从不生效」时", "decision": "① 写规格前先搜仓库里是否已有该能力的回退/兜底实现,有则点名要求复用;② 交付后核对「能力是否真会被触发」,而不只看代码存在;③ 把「长期只记录不生效」的既有日志当作能力空转的现成证据。", "rationale": "规格是执行体的唯一输入:规格里少写一个回退来源,实现就永久只走那条窄路径,而代码审查看起来完全正确。既有日志里早已写着「仅记录留痕」这类空转证据,只是没人把它当成缺陷信号。", "evidence": "2026-09-18:宿主启动器未注入 DSH_SESSION_ID,而插件心跳日志长期打印「有待办但无 sessionId(expr 未落盘且宿主 env 无 DSH_SESSION_ID),仅记录留痕」;我给的规格只写「sessionId 取 process.env.DSH_SESSION_ID」,忽略了仓库既有的 mostRecentExprSessionId() 回退 → v12 唤醒通路在真机永远空转;v13 补回退后才通(当日实证唤醒成功)。", "confidence": "high", "status": "proposed", "suggested": "proposed", "confirmedBy": null, "recordedAt": "2026-09-18T22:48:54.385Z" }, { "id": "L-2026-0919-081", "title": "让计划任务「不弹窗」时必须保证退出码仍可见——隐藏失败是最坏的回归", "category": "engineering-action", "layer": "L1", "trigger": "把计划任务/定时入口改为隐藏窗口运行时;或用户抱怨定时任务弹出 cmd 窗口时", "decision": "① 用 VBS 启动器:WScript.Quit sh.Run(\"cmd /c \"\"<入口>\"\"\", 0, True) —— 窗口样式 0(隐藏)+ 等待完成 + **透传退出码**(既有 watchdog 启动器丢弃退出码,但它不依赖;入口的 Last Result 是失败可见性的唯一出口);② 门禁钉死这三条并负控「丢掉 WScript.Quit」「样式改 1」「不等完成」;③ 用最小夹具真跑(exit 7 → 7)验证,而不是只读文本。", "rationale": "「消除弹窗」与「保留失败可见性」在实现上是两个独立性质,很容易在改启动方式时丢掉后者;一旦丢掉,所有失败都会静默,且因为窗口不再弹,连「它跑过」都不再可见。", "evidence": "2026-09-18:4 个计划任务全部改走 _hidden-run-*.vbs;门禁 19/19(含四类变异负控 + 启动器目录扫描与数量下限),功能夹具 3/3(exit 0→0、exit 7→7);真实审计入口实测 exit 0、audit.log 按期追加。", "confidence": "high", "status": "proposed", "suggested": "proposed", "confirmedBy": null, "recordedAt": "2026-09-18T22:48:54.385Z" }, { "id": "L-2026-0919-082", "title": "竞态:只在启动时判「环境是否就绪」不够——运行中途状态变化必须作废整轮", "category": "tooling-trap", "layer": "L1", "trigger": "长任务(审计/回归)与周期性任务(每 20 分钟取锁)共享状态时", "decision": "① 逐项/逐层复查共享状态,途中发生变化 → 作废整轮(exit 3)并说明「本轮结论无意义」,不给半真半假的判定;② 区分「瞬时取锁」(<1 分钟自行释放 → 等一小会儿继续)与「真在干活」(锁已存在较久 → 立即作废),否则会被调度器的空转锁频繁打断。", "rationale": "跨状态转变采集到的数据来自两个不同的世界,拼接成的结论既不能证真也不能证伪,而它看起来完全正常——这正是最容易被采信的一类错误结论。", "evidence": "2026-09-18:回归套件在链条取锁瞬间跑出 19/26 的**混合判定**(前半段真跑、后半段「本次跳过」被读成失败);随后又因调度器每 20 分钟的瞬时锁被整轮作废两次;改为持续性判别 + 中途复查后 29/29 通过。", "confidence": "high", "status": "proposed", "suggested": "proposed", "confirmedBy": null, "recordedAt": "2026-09-18T22:48:54.385Z" }, { "id": "L-2026-0919-083", "title": "门禁必须校验「该文件真正该满足的性质」——套错契约会系统性误报;入口经包装后须改可传递校验", "category": "tooling-trap", "layer": "L1", "trigger": "给新入口/新文件配门禁并想复用既有门禁时;或入口从「直接调用」改为「经包装器调用」时", "decision": "① 先列出该文件真正该满足的性质再写门禁,契约不同的文件不得复用;② 入口改为经包装器调用后,「必须真的跑到目标脚本」要从字面匹配升级为**可传递**校验(提及的 .mjs 内容里含目标脚本),否则正确的接线会被判成「没在跑」;③ 覆盖范围用目录扫描 + 数量下限,防新增入口漏检或有人改回旧方式。", "rationale": "门禁的误报与漏报同源:它校验的是「我以为的性质」。复用别的文件的契约、或用字面匹配代替实际依赖关系,都会让门禁在正确实现上报错、在错误实现上放过——两个方向都破坏门禁的可信度。", "evidence": "2026-09-18:run-audit.cmd 套用链条器门禁 → 4 项假失败,改专属门禁后 19/19;入口改经 run-audit-signal.mjs 包装后,「必须真的跑到 audit-all」从字面匹配升级为可传递校验(含 3 条 [NEG]:提及不存在的 mjs / 提及不含 audit-all 的 mjs / 不提 mjs)。", "confidence": "high", "status": "proposed", "suggested": "proposed", "confirmedBy": null, "recordedAt": "2026-09-18T22:48:54.385Z" }, { "id": "L-2026-0919-084", "title": "批量文本手术必须带后置断言——`split(sep, limit)` 误用会静默清空整个文件(实测 registry.yaml 495 行→0 行)", "category": "tooling-trap", "layer": "L1", "trigger": "写脚本对权威文件(registry/lessons/runbook/配置)做批量替换、重命名或迁移时", "decision": "① 替换脚本必须在**同一个脚本里**做前后置断言:条目数/关键计数不得减少、文件体积不得异常塌缩,不满足即 exit 1;② 写替换时确认调用的是 `split(a).join(b)` 而不是 `split(a, b)`(第二个参数是 limit,传字符串会被强制成 NaN → 返回空数组 → join 后文件只剩替换串);③ 权威文件改动前先确认在 git 里且工作树干净,事故后可 `git checkout -- ` 原样还原;④ 沉淀库本身要有「条目数不收缩」的自维持不变量(已落进 verify-sediment.mjs)。", "rationale": "这类错误的特点是**成功退出**:脚本没有抛错、没有报错行,文件却被清空——只有后置断言能把「改坏了」变成「失败退出」。而沉淀库/注册表这类文件的价值全在内容体量上,一次静默截断等于把历史积累清掉且无人察觉。", "evidence": "2026-09-19 实测:把审计层数文案从「四层」改「分层」时,脚本里误写 `r.split(A, B).join(C)`(B 当作 limit)→ registry.yaml 被清空(464 行 → 0 行、22 条目 → 0),脚本正常退出;靠 `git diff --stat`(495 deletions)发现,`git checkout --` 还原;随后把替换脚本重写为带后置断言版本(条目数不变 + 体积下限),并给 verify-sediment.mjs 增加「条目数相对 HEAD 不得收缩」的自维持不变量(12/12 自检通过)。", "confidence": "high", "status": "proposed", "suggested": "proposed", "confirmedBy": null, "recordedAt": "2026-09-18T22:54:05.352Z", "crossRef": [ "L-2026-0914-061" ] }, { "id": "L-2026-0919-085", "title": "交付必须在重启**之前**完成——否则重启后自检必报漂移,把真告警淹成噪声", "category": "engineering-action", "layer": "L1", "trigger": "准备重启宿主(request-restart)而仓库里有已提交但未交付的改动时(含 docs/registry/lessons 这类\"也在包清单里\"的文件)", "decision": "① 重启前固定跑一次 deliver-three-copies.mjs --apply,并确认 verify-delivery-live.mjs 报\"副本不一致/缺失 = 0\";② 若确实只想重启不交付,必须预期自检会报漂移并写明理由;③ 把\"交付→重启\"绑定成一个动作,别拆开做。", "rationale": "重启后自检的首要职责是发现\"运行端与源码不一致\"。若这个信号常常是\"你自己忘了交付\"造成的,它就从告警退化成噪声,真正的漂移也会被当成又一次手误忽略——这正是本会话反复出现的\"狼来了\"结构。", "evidence": "2026-09-19:我为激活影子沙盒环境变量而重启宿主,但只重启未交付 → 重启后自检立刻报 3 项交付漂移(registry.yaml / main-agent-lessons.json / main-agent-runbook-v0.1.md,均为当日沉淀提交的文档),notified=true 唤醒主 agent;交付 9 个文件(3×3 副本)后 ⑤ 恢复\"精确\",漂移清零。", "confidence": "high", "status": "proposed", "suggested": "proposed", "confirmedBy": null, "recordedAt": "2026-09-19T01:13:15.808Z", "crossRef": [ "L-2026-0909-040" ] }, { "id": "L-2026-0919-086", "title": "启动器里的环境变量**只在 watchdog 重启时生效**(宿主重启不够)→ 能力可用性别押在 env 上", "category": "tooling-trap", "layer": "L1", "trigger": "为了让某机制可用而给宿主启动器(dsh-web-watchdog.cmd)加环境变量并重启宿主后;或某能力\"配置改了却仍不生效\"时", "decision": "① 先确认 env 的**继承链**:宿主由常驻 watchdog 拉起、watchdog 由计划任务/启动器拉起,改启动器只影响**下一次 watchdog 启动**;`request-restart` 只重启宿主,不会重新执行启动器。② 因此不要把能力可用性建立在 env 上:应让代码从**自身位置**(import.meta.url 上溯)或既有配置推导默认值,env 只作可选覆盖;③ 验证\"配置是否真的生效\"要看**当前 boot** 的日志/自报字段,而不是\"我改过配置文件了\"。", "rationale": "长驻进程(watchdog)是环境变量的\"快照点\":改了它的上游配置却只重启下游,等于没改。这类失败尤其隐蔽——配置看起来正确、重启也做了、日志却仍在说不可用,容易得出\"这机制坏了\"的错误结论。", "evidence": "2026-09-19:为影子沙盒加 DSH_RELAY_REPO_PATH 到启动器并 request-restart,重启后日志仍是「定时 Shadow GC 未启用」;核查发现 watchdog(PID 16356)起于 2026-09-15 20:51,父进程是当时的 cmd /c dsh-web-watchdog.cmd —— 它继承的是旧环境,宿主只是它的子进程。改判:正确修法是让插件用 lib/ 上溯得到仓库根(本轮已派发 v14 落实),env 降级为可选加速项;可用性检查也改为\"实现无关\"判据(不要求启动器设 env)。", "confidence": "high", "status": "proposed", "suggested": "proposed", "confirmedBy": null, "recordedAt": "2026-09-19T01:13:15.808Z", "crossRef": [ "L-2026-0910-043", "L-2026-0909-040" ] }, { "id": "L-2026-0919-087", "title": "门禁断言不得编码「世界的瞬时状态」——否则正确行为/正确重构会触发假失败(一晚连撞三次)", "category": "tooling-trap", "layer": "L1", "trigger": "写或修改门禁的静态期望时;尤其是①被检代码刚被重构 ②外部世界状态刚变化(真实告警出现、服务重启、另一个程序改了文件)之后", "decision": "① 断言要落在**结构性/相对性**事实上:文件里\"依赖了谁\"(import/桥接)、\"改前改后是否相同\"(内容/哈希比对)、\"行为是否符合契约\"(跑一次真调用),而不是\"文件里是否含某个字面量\"或\"外部世界里是否恰好没有某条记录\";② 涉及外部世界的检查一律**先快照、后比对**(内容/字节),不要写\"它不该存在\"这类全称断言;③ 重构完成后必须**回看门禁**:期望会随架构收敛而过时(收敛完成反而报假失败);④ 出现假失败时先问\"是我的期望过时,还是代码真的错了\",两者都要用证据分辨,不要靠印象改断言。", "rationale": "门禁的价值在于\"通过的必真、失败的必假\"。把瞬时状态编码进断言,会让门禁在正确的世界变化下报错——此时人只有两个选择:改断言(可能掩盖真问题)或忽视它(门禁失效)。二者都在削弱门禁可信度,而正确做法是让断言只依赖**不随无关变化而变的性质**。", "evidence": "2026-09-19 一晚三次:①`verify-quota-patterns.mjs` 的插件断言写\"本文件含 weekly 字样\"→ 收敛到唯一模块后措辞搬走,正确的重构被判失败;②同一门禁对 runner 断言写\"抽取它的 grep 模式\"→ runner 改为桥接后同样假失败;③`precheck-audit-signal.mjs` 断言\"真实告警文件里不该有未销账的 four-layer-audit 条目\"→ 审计真的发出告警后,这条**正确行为**反而让门禁挂掉(并被分层审计 ③ 层放大成 ACTION-NEEDED → 触发一次多余的唤醒信号)。三处均改为结构/相对判据(依赖唯一模块、桥接+行为、内容快照比对)后全绿(③ 18 项、跨实现 11/11、审计 OK)。", "confidence": "high", "status": "proposed", "suggested": "proposed", "confirmedBy": null, "recordedAt": "2026-09-19T18:34:05.070Z", "crossRef": [ "L-2026-0919-075", "L-2026-0919-086" ] }, { "id": "L-2026-0919-088", "title": "「重启后要做的事」必须是**盘上对象**,不能是意图——否则重启即失忆(本轮 4 小时空转的根因)", "category": "collaboration-rhythm", "layer": "L2", "trigger": "需要在宿主重启/中断后继续某项核对、报告或收口工作时;或发现\"我说过重启后要做什么,但重启后没有任何动静\"时", "decision": "① 重启前先把要做的事**登记成盘上对象**(本项目:`chain-human-signal.mjs` 登记 `post-restart-verify#gN` 清单信号,写进共享状态文件),再执行重启;② 插件启动扫描读盘上未结信号 → 注入主 agent 会话(这一步是\"自动续跑\"的唯一开关,靠记忆或自然语言交接都无效);③ 核对完成后用 `acknowledgeHumanSignal` **销账**(同一 stableKey 只叫一次,销账后源再失败才重新叫);④ 一次性核对不要留在\"计划中\"的措辞里——计划只承载\"忙碌步骤\",重启不会恢复它。", "rationale": "重启/中断会清空进程内一切:内存中的待办、会话里的\"我打算…\"、以及未落盘的计划意图。只要对象在盘上且被启动扫描读取,续跑就是**确定的**(不依赖模型记得);否则即使主 agent 明确说过\"重启后我要核对 X\",重启后也必然什么都不发生。判据是\"盘上有没有这个对象\",而不是\"我说过没有\"。", "evidence": "2026-09-19:宿主重启后 4 小时无任何续跑。三段根因逐一排除:① 恢复扫描只续跑\"忙碌中的计划步骤\",重启前无 busy step;② 插件唤醒需要 DSH_SESSION_ID,宿主环境没有该变量(v13 已改为 `mostRecentExprSessionId()` 回退,sessionId=session-5ee8782e-998f-42d1-9461-840596aa2771);③ 真正缺的是**盘上对象**——\"重启后的核对\"只是我的意图。固化 `restart-with-checklist.mjs` 约定(先登记清单信号再重启)后重跑:宿主重启 → 启动扫描读到 `post-restart-verify#g1` → 注入注入本轮会话(本轮即由该注入唤醒,端到端自证)→ 5 项核对 5/5 通过 → 销账成功。反面:交付前不打欠条的同类意图(v14 曾因两次 arm 覆盖指针而从未派发)同样空转。", "confidence": "high", "status": "in-runbook", "suggested": "in-runbook", "confirmedBy": "boot-scan wake + post-restart-verify#g1 5/5 + ack (2026-09-19)", "recordedAt": "2026-09-19T22:44:05.864Z", "crossRef": [ "L-2026-0919-086", "L-2026-0919-087", "L-2026-0919-075" ] }, { "id": "L-2026-0920-089", "title": "自检覆盖了「判定函数」却没覆盖「输入解析器」→ 写方/读方契约漂移不被机器发现(一天连犯两次)", "category": "tooling-trap", "layer": "L1", "trigger": "写/改任何「读盘上对象做判定」的工具或门禁时;尤其是引入新的盘上格式(ndjson / JSON 记录)或改动写方字段之后", "decision": "① 自检必须**分层**:判定纯函数自检 + **输入解析自检**(用**真实形态**夹具,含写方真实键名);② 写方契约要被负控钉死——\"只改字段名 → 必须读不出来\",这条用来证明自检在验契约而不是永真;③ 引入新盘上格式时,先拿**真实产物**跑一次解析,而不是只跑判定;④ 同一类格式 bug 连犯两次即说明缺的是**机制**而不是注意力——补解析器自检,别只修这一处。", "rationale": "判定函数的自检再全,也测不到\"输入根本没读进来\"——因为**空输入通常会被判成合法的「无对象」分支**,于是工具会自信地给出\"未验证\"甚至\"通过\"。这类缺陷的隐蔽性正在于它不报错、不崩溃,只是安静地把结论建立在空数据上;而它一旦发生,工具的其他部分(判定、退出码、门禁接线)全都正常,看起来完全可信。", "evidence": "2026-09-20 实测两例,两次都发生在判定自检 13/13 全绿的情况下:① `readSignals` 按 `{entry:{…}}` 取值,而 `chain-needs-human.json` 的主槽是**扁平记录**(顶层就是信号字段)→ 永远读到 0 条 → C 判据假报\"重启前未登记欠条\";② `readDebtLedger` 按 `o.requestedAt` 过滤,而写方 `restart-with-checklist.mjs` 写的是 `{at, debt, reason}` → 永远 0 条 → 把一次**真实的合规重启**报成\"证据缺失(4)\"(就在重启后核对清单第 1 项上暴露)。补 `--selftest-parsers`(5 例,含\"字段名不符必须读不出\"负控)后立刻抓到第二例;该自检并已独立登记为门禁与回归项(门禁 22→24、回归 34→36)。", "confidence": "high", "status": "proposed", "suggested": "proposed", "confirmedBy": null, "recordedAt": "2026-09-20T12:39:53.909Z", "crossRef": [ "L-2026-0919-087", "L-2026-0919-088", "L-2026-0919-084" ] }, { "id": "L-2026-0920-090", "title": "审计的「作用域」必须落在事实上,不能落在标记上——历史记录普遍缺新字段,用标记会假报", "category": "judgment-heuristic", "layer": "L1", "trigger": "为新审计层/门禁划定\"哪些对象归我管\"时;尤其是被检对象是一批**跨越多个协议版本**的历史记录时", "decision": "① 先问\"这个对象的**收口事实**是什么\",用事实判作用域(例:`status=done` 且步骤全 approved=实质已收口),而不是用\"后来才引入的标记\"(例:`finalized`);② 实证:先跑一遍看它挑中了谁,**逐条核对挑中的对象是否真的在办**——假阳性几乎总是作用域问题,不是判据问题;③ 修作用域时**同时补一条回归用例**把这个假阳性钉死(否则下次重构又会回来);④ 与\"未验证\"配合:无对象 → PASS(\"无对象≠未验证\"),有对象但无证据 → 未验证。", "rationale": "用标记判作用域会得到一个**看起来合理但系统性偏大**的对象集合:历史记录缺字段是常态,于是每一条老记录都被当成\"在办对象\",审计层就会长期报\"未验证\"或假失败。它比\"判据写错\"更隐蔽——因为判据本身是对的,错的是\"谁该被这个判据管\"。而长期假告警的代价是审计可信度被磨掉(人开始忽略它)。", "evidence": "2026-09-20 实测(⑩ 迭代状态机审计首跑):J5(版间门)作用域写成 `autoDecision=true && finalized!==true && currentIteration>=2`,结果挑中 `expr-2026-09-03_01-27-21` —— 该记录 `status=done` 且三步全 approved(**实质早已收口**),只是缺 `finalized` 标记(2026-08~09 的 legacy 字段缺失,同批共 9 条)。于是首跑把\"无版间门落盘对象\"报成 UNVERIFIED。改为 `substantivelyClosed()`(finalized / archivedAt / status∈{done,stopped,paused} / 步骤全 approved 任一成立即算收口)后,现场转 PASS,并补了一条\"V2 但实质已收口 → J5 不得假报\"的回归用例(判定自检 11→12 例)。", "confidence": "high", "status": "proposed", "suggested": "proposed", "confirmedBy": null, "recordedAt": "2026-09-20T17:01:09.995Z", "crossRef": [ "L-2026-0919-087", "L-2026-0920-089" ] }, { "id": "L-2026-0920-091", "title": "写下来的约定必须被机器断言,否则会被\"更早的触发器\"悄悄破坏;且痕迹要按「有判定意义」留,不是按「每次评估」留", "category": "collaboration-rhythm", "layer": "L1", "trigger": "建立\"某事件之后要做某核对\"的约定时;以及给高频路径(心跳/轮询)加日志留痕时", "decision": "① 约定的**时序**要机械化:把\"必须在 X 之后\"写成判据(比较两件事在durable痕迹中的**位置/时序**),而不是靠约定俗成的顺序——因为同一对象往往有多个触发器,更早的那个会抢先把对象消费掉,使\"在 X 之后核对\"变成\"在 X 之前核对\";② 判据要断在**真正的目的**上,不要断在实现细节上(例:目的是\"核对发生在重启之后\",不是\"必须由 boot 扫描唤醒\"——后者过严,会因合法的其它触发器而假失败);③ 留痕按**判定意义**取舍:高频路径只在\"存在待判定对象\"时留痕,否则销账后的对象会每次心跳写一行,产生纯噪声并稀释痕迹价值。", "rationale": "约定的生命力取决于它是否被机器守住。多触发器场景下,\"谁先看到对象\"是竞态而非约定——只要有一个触发器比预期更早,约定的时序就被破坏,而且**破坏时一切看起来都正常**(对象确实被消费了、还留了痕)。同时,痕迹的价值与数量成反比:为了\"不漏\"而每次评估都写一行,会迅速把日志淹成噪声,使真正的异常淹没其中。", "evidence": "2026-09-20 实测:为激活 v20 登记\"重启后核对清单\"(delay=240s)后,**心跳**在重启执行前 3 分钟就消费了该清单并唤醒主 agent,我按清单核对时 ⑤ 必然 FAIL(新代码尚未加载)——而当时并没有任何判据能发现\"核对发生在重启之前\"。日志给出了完美 A/B 对照:`phase=heartbeat decision=wake`(重启前)与 `phase=boot decision=wake woken=true`(重启后)各一条。补 ⑨ 判据 D(清单的成功唤醒不得全部发生在重启之后那次启动之前)后,现场如实判 PASS 并报出\"2 次唤醒中 ≥1 次在启动痕迹之后\";若只有提前那次则判 FAIL。同时发现痕迹噪声:信号销账后文件仍在,心跳每次评估都为它写一行(4.5h 攒 19 行 ≈ 96 行/天),改为\"boot 期一律留痕 + 心跳期只在存在未销账信号时留痕\"。", "confidence": "high", "status": "in-runbook", "suggested": "in-runbook", "confirmedBy": "runbook §8.6 判据 D + ⑨ 自检 18/18(2026-09-20)", "recordedAt": "2026-09-20T17:28:13.349Z", "crossRef": [ "L-2026-0919-088", "L-2026-0920-089", "L-2026-0920-090" ] }, { "id": "L-2026-0920-092", "title": "同一份盘上格式有**两个写方**时,必须收敛到\"路径 + 形态\"两者,否则条目静默搁浅(告警永远叫不醒人)", "category": "tooling-trap", "layer": "L1", "trigger": "由两个不同代码树(插件 / 主 agent 工作区)共同读写同一个盘上文件时;尤其是\"队列/待办/缓冲\"这类会**累积**的文件", "decision": "① 收敛必须同时覆盖**路径**与**形态**两项:只统一其中一个仍会搁浅(本次实测:路径差一个 `.json` 位置、形态差裸数组 vs `{queue:[…]}`);② 读取方一律**容忍两种**(先合并、按 id 去重),避免已搁浅条目在修复后继续丢失;③ 写死一条**确定性回归**:强制制造\"必须入队\"的条件,断言另一方**能看见**该条目 —— 不能依赖\"主槽恰好被占用\"这种偶然条件才覆盖;④ 修复后**清理**搁浅条目(只清自己能识别的那些,绝不动真实告警)。", "rationale": "队列类文件的失效方式是\"静默搁浅\":写入成功、日志正常、无任何报错,但消费方永远读不到 —— 于是**告警机制本身成了静默失败**,而它存在的唯一目的就是防静默失败。比\"功能没实现\"更危险的是\"看起来实现了、写进去了、却没人能读到\":所有单侧测试(写方自己的断言、读方自己的夹具)都会通过。", "evidence": "2026-09-20 实测:v20 给插件加\"熔断登记 needs-human\"时,队列写入按**注释语义**模仿主 agent 侧写成 `${PENDING_HUMAN_PATH}.queue.json` + `{queue:[…]}`,而主 agent 侧 `queuePathOf` 是 `<基础名>.queue.json` + **裸数组**(例:chain-needs-human.queue.json vs chain-needs-human.json.queue.json)→ 路径与形态双双不一致。后果:插件入队的告警在主 agent 侧**连文件都找不到**,`acknowledgeHumanSignal` 的队列提升永远提升不到 → 告警永久卡住。暴露方式是\"补验证\"时事件一在**主槽被占用**的条件下如实报失败(`现有信号=[…]` 里没有它),而插件日志同时明确写着\"已登记|main-slot-occupied-by-other-source(入队,不顶掉)\"——两方日志都\"正常\",只有交叉断言能发现。修复:插件改用规范路径 + 裸数组;`readQueue` 容忍两种路径与两种形态(合并去重);补一条**强制占用主槽**的确定性回归(现 8/8);并清掉 4 条搁浅的合成条目。", "confidence": "high", "status": "proposed", "suggested": "proposed", "confirmedBy": null, "recordedAt": "2026-09-20T19:00:41.100Z", "crossRef": [ "L-2026-0920-089", "L-2026-0920-091" ] }, { "id": "L-2026-0920-093", "title": "写\"共享状态文件\"的脚本必须先断言目标路径与内容形状;并发会写同一工作区时必须先取单写者锁", "category": "tooling-trap", "layer": "L1", "trigger": "写任何被多个组件共享的状态文件(信号/队列/待办/锁)时;以及在两个会话可能同时活着(刷新 GUI、旧会话未退出)的环境里做交付/重启/清理时", "decision": "① 写入前**两重断言**:目标路径必须来自格式权威白名单(防\"算错路径落回主槽\"),主槽类目标必须校验**内容形状**(防把 `[]`/`{}` 写进去);② 路径**绝不自己拼**:只能由权威模块给出(内联 shell 里的正则极易被吃字符);③ 共享文件的清理/写入脚本一律走**脚本文件**并打印\"我将写入哪个文件\",不要内联执行;④ 并发写同一工作区(可能有两个会话活着)时,写入前取**单写者锁**(无身份者放行不取锁,避免锁死无人值守路径)。", "rationale": "共享状态文件的失效方式是\"被写坏后仍然存在\":文件还在、字节数很小、后续读取者只会报\"形状不对\"而不是\"数据丢了\",于是损失**静默且不可恢复**。而这类脚本通常被当作\"顺手清理\",风险认知极低——一个能把主槽清成 `[]` 的清理脚本,比它要清理的残留危险得多。并发场景更进一步:验证体系默认\"唯一权威工作树\",这个前提在技术上从未被保障。", "evidence": "2026-09-20 实测(我造成):为清掉队列里的合成测试残留,我在**内联 PowerShell** 里用 `p.replace(/\\.json$/i, \".queue.json\")` 计算\"规范队列路径\" —— PowerShell 吃掉了正则里的 `$`,变量落回**主槽**路径,脚本把 `[]` 写进 `D:\\cc-tasks\\chain-needs-human.json`,**销毁了一条未销账信号的 durable 记录**(8 项核对已完成、结论未受影响;规范队列 6457B 完好;丢的是信号与销账注记)。同一天更早,我还要为两个会话并发写同一工作区(GUI 刷新后旧会话仍被唤醒)造成的交付/重启交错、过时期望与假失败买单。机制性修复:`assertSignalPath`(只允许主槽/规范队列/历史队列三个目标)+ `assertSignalShape`(主槽只接受合法信号对象,拒绝 `[]`/`{}`/缺字段)+ 单写者锁 `agent-write-lock.mjs`(已接入信号写入、三副本交付、重启登记)+ 门禁 `verify-signal-write-guard.mjs`(16 例,含\"合成违例必须被抓到\"的负控与\"活工具不得绕过权威直写信号\"的调用点穷举)。", "confidence": "high", "status": "proposed", "suggested": "proposed", "confirmedBy": null, "recordedAt": "2026-09-21T01:56:16.540Z", "crossRef": [ "L-2026-0920-092", "L-2026-0920-090", "L-2026-0919-085" ] }, { "id": "L-2026-0921-094", "title": "验证\"共享状态\"的用例绝不能用\"占用共享状态\"制造条件——泄漏会累积,且它会靠自己漏出的东西满足自己的前提", "category": "tooling-trap", "layer": "L1", "trigger": "为验证涉及共享/单例状态(信号文件、队列、锁、主槽、单实例端口、全局配置)的行为写用例时;尤其是该用例会被**周期门禁**反复执行时", "decision": "① 制造\"条件\"优先用**隔离实例**(临时文件/临时目录/依赖注入),而不是占用真实共享资源;② 若只能针对真实共享状态验证,必须\"**写入即由同一用例负责清理**\",且清理**覆盖所有落点**(本例有两条泄漏路径:直写主槽、入队),并用收尾无条件清理(不依赖某个分支);③ **静态契约断言**(写方必须写规范路径/形态)常常可以替代昂贵且危险的真实状态占用;④ 门禁用例要显式断言\"跑完后共享状态回到基线\"(本例:残留夹具数必须为 0),把泄漏变成**当场的失败**而不是静默累积;⑤ 警惕\"**自维持的通过**\":若用例的前提是被它自己上一轮的泄漏制造的,它会永远绿,而系统正在被它压垮。", "rationale": "门禁是**周期性**的,任何泄漏都会随时间累积,最终压垮被验证的系统本身——此时**验证工具变成了事故源**,而且比它要防的缺陷更危险:它堵住的正是\"把问题告诉人的那条通路\"(唤醒/升级)。更隐蔽的是\"自维持的通过\"会让这种事故在门禁上完全隐形——门禁越绿,通道越死。", "evidence": "2026-09-21 实测(我造成,本会话最严重事故):`verify-autoir-events.mjs` 的\"队列收敛\"用例(每轮审计 30 分钟 + 每次回归都跑)为制造\"主槽被占\"条件,写入一个 `dryRun` 占位夹具:主槽空闲时它**直写主槽**并走 FAIL 分支且**从不清理**;主槽被占时它**入队**、末尾只清理自己的条目、不清理队列里那条夹具 ⇒ **每轮泄漏一条**。`dryRun` 按设计**永不唤醒也永不销账** ⇒ 主槽被永久占住 ⇒ 真实告警(four-layer-audit、重启核对清单)只能排队且无人消费 ⇒ **通道静默失效数小时**(清点:主槽循环销账 **42** 条、队列残留 **19** 条;期间那次重启成功执行、唤醒却从未发生——用户先发现\"好像没有唤醒\")。而那条例用始终报 **8/8 PASS**,因为它的前提正是它自己漏出的夹具。修复:改用**临时文件 + 静态写方契约断言**(13 例、零副作用,跑完残留 0),并给 `restart-with-checklist.mjs` 加\"主槽被占即**拒绝**(exit 1,`--queue-ok` 为逃生口)\"的前置检查以堵住死锁入口;门禁 `verify-signal-write-guard.mjs` 增 G5 三条(有检查/有逃生口/检查在写入之前)防被重构摘掉。", "confidence": "high", "status": "in-runbook", "suggested": "in-runbook", "confirmedBy": "runbook §8.11 + 门禁 G5 三条 + 事件探针 13/13(2026-09-21)", "recordedAt": "2026-09-21T19:58:36.063Z", "crossRef": [ "L-2026-0920-092", "L-2026-0920-093", "L-2026-0919-087" ] }, { "id": "L-2026-0922-095", "title": "给工作区做 `git init` 会改变\"世界状态\",凡以\"候选目录是 git 根\"作为**唯一**判据的解析器都会随之改变行为", "category": "engineering-action", "layer": "L3", "trigger": "为纳入版本控制而给宿主工作区 git init;或任何\"让某个目录从非 git 变成 git\"的动作", "decision": "① 执行前先穷举\"以 git 根为判据\"的消费方(本会话实测两处:test/shadow-gate.test.js 硬编码工作区路径、lib/shadow-gate.js 的 shadowRepoCandidates 把 base 排第一);② 判据加**仓库特征**(根下 package.json 且 name 匹配),不要只判\"是 git 根\";③ 动作后立刻跑受影响测试与门禁,把\"世界状态变更\"当成一次交付对待", "rationale": "「是不是 git 仓库」是环境属性而非意图声明。工作区一旦入 git,任何\"第一个候选是 git 根就用它\"的短路逻辑都会改判——代码没变、行为变了。本会话该副作用先后击穿两处:影子门禁自检(测试夹具)与影子门禁运行时(把工作区当插件仓库根,L1/L2 全部误判、合规改动被拒)。", "evidence": "实测:resolveRepoPath(\"D:\\dsh relay test\") 由 null 变为非 null → shadowRepoCandidates 第 1 候选短路 → complete 被 shadow-l1/l2 拒绝,报 Cannot find module \"D:\\dsh relay test\\lib\\client.js\";同一批产物仓库侧 node --check 全 exit=0(证明非语法缺陷)。修复后 resolveRepoPath(工作区)=null、resolveShadowRepo({base:工作区})=\"D:/dsh-web-relay\"。", "confidence": "high", "status": "approved", "suggested": "把\"以 git 根为唯一判据\"的解析点登记进 capabilities/registry.yaml,并在任何 git init / 换目录 / 换宿主的动作清单里加一步\"复查这些解析点\"。", "confirmedBy": "expr-2026-09-22_17-42-08(V1 闭合被迫 disableShadow;V2 步8 修复并加反向夹具)", "recordedAt": "2026-09-22T22:00:25.758Z", "crossRef": [ "L-2026-0909-040", "L-2026-0911-049", "L-2026-0920-092" ] }, { "id": "L-2026-0922-096", "title": "审核/门禁的\"读产物\"必须以**审核方能解析的路径**交付:相对路径按谁的 cwd 解析,决定了证据是否成立", "category": "engineering-action", "layer": "L2", "trigger": "Step List 声明 artifacts;或任何\"把产物路径交给另一个进程/角色去读\"的环节", "decision": "① 声明 artifacts 时用**绝对路径**(或明确约定解析根并让消费方按该根解析);② 审核脚本与门禁统一走同一个 resolveArtifactPathMulti(多候选根),不得各写一份;③ 判据只作用于**在办**对象的**路径型**项——历史终态对象与非路径描述(句子型说明)不得计入失败", "rationale": "证据是否成立取决于\"谁能读到它\",而不是\"我方盘上有它\"。本会话审核方按工作区 cwd 解析仓库相对路径 → 报\"无法读取产物\"并打回 7 步、触发熔断;而同一批产物在仓库侧完全正确。另一头:把该判据无差别套到全部历史 expr 上,一次刷出 100+ 条\"缺失\",把历史包袱当成本周期违约。", "evidence": "Step 2 单变量对照实验:路径改绝对后,同一份证据由\"双角色打回\"变为\"双角色通过(reviewedBy=external)\";J10 校准后由 100+ FAIL 变为\"在办路径型 0 个 → 无可判定对象(不算失败)(跳过非路径描述 27、终态历史 330)\"。", "confidence": "high", "status": "approved", "suggested": "在 verify-gates 与 ⑩ 保留\"多候选根 + 仅在办 + 路径型\"三件事的一体化实现,新增消费方一律 import,禁止复制。", "confirmedBy": "expr-2026-09-22_17-42-08(V1 熔断根因之一;V2 步12 落地判据)", "recordedAt": "2026-09-22T22:00:25.758Z", "crossRef": [ "L-2026-0921-094", "runbook §8.2 证据必须属于该对象" ] }, { "id": "L-2026-0922-097", "title": "断言\"过窄\"与断言\"永真\"同样是缺陷:先跑通真实实现,再定检测口径", "category": "tooling-trap", "layer": "L2", "trigger": "为源码级/痕迹级断言写正则或特征检测时", "decision": "① 写判据前先读一遍被检测的真实实现,把特征按\"语义\"而非\"某种写法\"表达;② 新判据上线前必须有一侧负控(注入缺陷必须 FAIL)与一侧真值(当前实现必须 PASS)双向证据;③ 真值侧 FAIL 时先怀疑判据而非实现", "rationale": "本会话两次踩:J7 要求 protocolVersions 与 .map( 在 300 字符窗口内相邻,而真实实现是先赋局部变量再 map → 误 FAIL;J10 用单根解析历史产物 → 误 FAIL。两者都会把\"合规实现\"判成缺陷,进而诱发\"改实现去迁就断言\"——那是比漏检更危险的失败方向。", "evidence": "J7 修前 FAIL(\"client.js 未见数据驱动渲染痕迹\"),改用\"出现 protocolVersions + 至少一处 .map( + contentKey 查表\"后 PASS;J10 修前 100+ 条 FAIL,改多根 + 范围校准后 PASS(并在 --selftest 保留注入型负控)。", "confidence": "high", "status": "approved", "suggested": "把\"新增判据必须同时给出真值侧与负控侧证据\"写进教程/门禁自审清单。", "confirmedBy": "expr-2026-09-22_17-42-08(V2 步10/12 主 agent 校准)", "recordedAt": "2026-09-22T22:00:25.758Z", "crossRef": [ "L-2026-0920-092", "L-2026-0913-057" ] }, { "id": "L-2026-0922-098", "title": "新增/调整用例后必须同步**消费方**的清点期望,否则回归会报\"假失败\"", "category": "engineering-action", "layer": "L2", "trigger": "给某个自检脚本增删用例、或改变其输出格式/条目数之后", "decision": "① 找到所有以字符串形式断言该脚本输出的消费方(本会话:regression-post-restart.mjs 用 mustInclude 断言 RESULT: N/N);② 按实测值同步;③ 同步时只改期望值,**不得**顺手改被测判定逻辑——并在提交信息里写明\"未修改判定逻辑\"", "rationale": "回归套件把\"脚本输出里的计数\"当契约,这是有意设计(防静默退化),代价是用例数一变就要同步消费方。漏同步会让回归报失败,而失败原因与代码质量无关——若不追根因就\"修代码去过测试\",会造成真实破坏。", "evidence": "实测三处过期:verify-gates --selftest 4/4→8/8、audit-all --selftest 13/13→15/15、verify-iteration-state --selftest 19/19→39/39;同步后 44/47 → 47/47(中途还误填 35/35,按实测纠正为 8/8)。", "confidence": "high", "status": "approved", "suggested": "消费方断言改为\"必须含 RESULT: 且 FAIL 计数为 0\"这类**语义**断言,减少对具体数字的耦合;若要保留数字,加入脚本输出变更检查清单。", "confirmedBy": "expr-2026-09-22_17-42-08(V2 重启后回归 44/47 → 47/47)", "recordedAt": "2026-09-22T22:00:25.758Z", "crossRef": [ "L-2026-0920-092" ] }, { "id": "L-2026-0922-099", "title": "`/ask` 的 body 声明与 prompt 文本声明是**两个来源**:必须显式拒绝而不是静默忽略/合并", "category": "engineering-action", "layer": "L1", "trigger": "自动迭代声明(iterations/finalAcceptance/autoDecision)通过非受控入口传入时", "decision": "① 非受控入口(body)与受控来源(prompt/answer 文本)不一致 → 返回 400 并说明;② 一致 → 放行;③ 完全不带 → 行为不变(向后兼容);④ 判定按**字段值**比较(传默认值不构成冲突,须在文档与销账记录里明说这处偏宽);⑤ 补全声明走受控入口 /steps/declare", "rationale": "静默忽略会让\"声明了 2 版却按 1 版跑\",而且盘上无任何异常痕迹——最坏的一类缺陷(意图与落盘不一致且无人能发现)。合并则引入两个来源,正是\"版本口径分裂\"的同族病。", "evidence": "实测:body 传 iterations:2/autoDecision:true/finalAcceptance 而文本无声明 → 落盘仍 1/false/null(只能改走 /steps/declare 补);修复后同情形 HTTP 400;body 与文本一致 → 200;完全不带 → 200 channel=web-gemini。", "confidence": "high", "status": "approved", "suggested": "受控入口 /steps/declare 作为唯一补全通道;在协议文本里写明\"声明只写一处\"。", "confirmedBy": "expr-2026-09-22_17-42-08(V2 步9;修复前 V1 启动时实测踩中)", "recordedAt": "2026-09-22T22:00:25.758Z", "crossRef": [ "L-2026-0903-002", "L-2026-0920-092" ] }, { "id": "L-2026-0922-100", "title": "为「补证据」而反复 reopen 已批准步骤,会按每次 reopen 各触发一次交接唤醒;这些唤醒可能在收口后才投递,表现为「已终态任务仍在喊待办」", "category": "collaboration-rhythm", "layer": "L2", "trigger": "审核证据/产物需要修正,于是对**已 approved** 的步骤做 reopen → complete → auto-review 的修复循环;或任何在短时间内对同一步骤多次状态翻转", "decision": "① 优先\"补证据一次到位\":在第一次 complete 之前就把证据字段(comment)与产物路径(绝对路径)准备齐,避免多轮翻转;② 若确需多轮,收口前先确认没有未投递的唤醒在排队(比对盘上 steps.json 的 mtime 与收到的唤醒内容是否自洽);③ 收到\"已开始/重新打开\"类唤醒时,**先读 steps.json 的 mtime 与状态**再行动——mtime 未变且步骤已 approved 即为陈旧投递,核对留痕后不必重跑", "rationale": "每次 reopen/start 都会产生一条\"请执行 Step N\"的交接唤醒;它承载的是**产生时刻**的状态,而不是投递时刻的状态。任务收口不会追溯取消这些已排队的唤醒,于是它们在收口后陆续抵达,把已完成的工作重新\"喊\"一遍。若不加核对就照做,会重做已终态的工作、浪费回合,甚至可能把已 approved 的步骤重新翻转。", "evidence": "实测:expr-2026-09-22_17-42-08 于 21:58:14 finalize(13/13 approved),此后仍收到两条\"Step 7 全开测试…请执行\"\"Step 5 …请执行\"的唤醒。核对证据链:① steps.json mtime = 21:58:14.623Z(收口后未再被写);② 用 lib/heartbeat-scan.js 的纯函数在当前盘上状态扫描 101 个 expr → 信号 0 个;③ 宿主心跳 22:10:18 → found=0 injected=False,唤醒台账结算 outcomes={\"expr-2026-09-22_17-42-08\":\"changed\"};④ 唤醒台账 21:55:18 抓到的指纹显示当时状态确为中间态(finalized:false、9:review、10..13:pending),与两条唤醒的\"未完成\"口径一致。", "confidence": "medium", "status": "approved", "suggested": "可考虑在 /steps/finalize 时清理该 expr 尚未投递的唤醒(需要先确认投递路径:本次未能定位到显式的延迟队列,推测为会话输入队列/回合结束后的投递);在未确认路径前,先用\"mtime + 状态核对\"作为操作侧防御。", "confirmedBy": "expr-2026-09-22_17-42-08(收口后两次陈旧唤醒,均为核对后忽略,未重跑)", "recordedAt": "2026-09-22T22:11:50.446Z", "crossRef": [ "L-2026-0922-098", "L-2026-0914-059" ] }, { "id": "L-2026-0923-101", "title": "磁盘写满(ENOSPC)在多层审计里表现为\"多层同时失败、时好时坏\"——先查磁盘,再查代码", "category": "engineering-action", "layer": "L3", "trigger": "分层审计/门禁出现\"多层同时失败且复跑不复现\",或任何工具报出莫名其妙的写失败", "decision": "① 见到\"多个**互相独立**的工具在同一轮同时失败\",先排除**共享物理前提**(磁盘/TEMP/内存/权限),再怀疑各自逻辑——独立实现同时出错,几乎必是共享前提;② 立刻 `Get-PSDrive` 看可用空间(本项目 C 盘曾为 **0 字节**,而 D 盘尚有 67GB,容易误判\"磁盘没事\");③ 清 `%TEMP%`(保留被占用项)通常能一次性回收十几 GB;④ 审计/门禁**必须留存失败详情**(退出码 + 输出片段),否则这类瞬时故障只能靠猜", "rationale": "\"多层同时失败\"极具误导性:它看起来像\"系统性问题/我刚改坏了什么\",于是人会去翻最近的改动;而真正的原因往往是与代码无关的共享前提。本次三层分别是门禁自审、事实复跑、事件演练——三者实现独立、且其中两个根本不联网,唯一能同时打垮它们的就是**写盘失败**。而审计此前只保留每层最后一行输出,失败详情被下一次成功运行覆盖,导致这类故障**不可归因**(本次实测首轮就是如此:复跑即全绿、仓底已被覆盖)。", "evidence": "实测:收到 four-layer-audit ACTION-NEEDED(③⑥⑪ 三层失败);复跑全绿、三次不可复现。加\"失败详情落盘\"后立刻抓到两个具名门禁项(验收接线四类契约、设计2 探针两侧区分力);单独运行二者得到 `Error: ENOSPC: no space left on device, write`。查盘:C 可用 **0 字节**,`AppData\\Local\\Temp` 占 **14.52GB**。清理 Temp 后 C 可用回升至 **14.77GB**,两个工具立即 PASS、审计恢复 9 层 OK。另修审计 `spawnSync` 未设 `maxBuffer`(默认 1MB,其余 11 处工具都设了)——超限同样表现为\"多层同时失败\"。", "confidence": "high", "status": "approved", "suggested": "把\"可用磁盘空间\"纳入周期审计的一个廉价前置检查(低于阈值即点名),避免它再次以\"多层假失败\"的形式出现。", "confirmedBy": "expr-2026-09-23 four-layer-audit 误报排查;工作区提交 7df87af", "recordedAt": "2026-09-28T02:15:54.923Z", "crossRef": [ "L-2026-0922-097", "L-2026-0922-098" ] } ] }