# v0.1 完成报告 > 目标:把「验收通过」从模型的声称,变成一个**能被机器校验、且能被强制**的状态。 > 状态:**核心机制已在真实 harness 中验证可用。** --- ## 一、交付内容 | 组件 | 类型 | 作用 | |---|---|---| | **`tools/pre-execute` 门禁** | 钩子 | **核心**。拦截 `team_task_update` 的 `complete` 动作,没有验收记录就拒绝 | | `pdev_preflight` | 工具 | 开工前能力与风险检查(团队工具、项目占用、git、已有记录) | | `pdev_verify_record` | 工具 | 登记独立验收记录 —— 完成任务**唯一**的凭据 | | `pdev_gate_status` | 工具 | 只读自查:门禁状态与记录覆盖情况 | 全部实测通过:开发 profile `pdevdev`(headless + agent-team bundle),**用户正在跑的 `web` profile 未被触碰**。 --- ## 二、一处重要的设计修正:不重造任务板 原设计要自己实现 `pdev_task_create` / `pdev_task_update`。**调研平台源码后放弃了这个计划** —— 平台已经有了,而且做得比我计划的更好: `@deepseek-ai/dsh-experimental-tool-agent-team` 提供 `team_task_create/list/get/update`,并且**已经在强制**: - 状态机:`pending → in_progress → completed`,非法转移报 `TEAM_TASK_INVALID_TRANSITION` - 依赖就绪检查:`claim` 要求 `taskReady()`,被阻塞的任务领不了 - **revision CAS**:过期编辑被拒绝,不会覆盖更新的成果 - `blocked_by`(DAG)+ `write_scopes`(写范围)都支持 **重复实现只会更差。** 平台没做的只有一件事: > **`complete` 不需要任何验收。** 任何 teammate 都能把自己的任务标成完成,零证据。 所以 v0.1 的最终形态是:**用平台的任务板,只补这一个洞。** 这比原设计少了两千行代码,价值反而更集中。 --- ## 三、测试逼出来的两个真实 bug ### Bug 1:preflight 误报「没有团队工具」 **现象**:第一次端到端测试里,`pdev_preflight` 报 `TEAM_TOOLS_MISSING`,但团队工具明明能用(模型照常建了任务、认领、完成)。 **根因**(读源码找到的): 1. 团队工具是用 `agent.ctx.tools.register(...)` 注册到**每个 agent 的作用域**里的,不是全局表; 2. `ToolRuntime.get(name, scope?)` 的 **`scope` 参数就是 agent**,不传等于看全局视图 —— 全局视图里看不到任何 agent 作用域的工具。 **修法**:把调用方 agent 作为 scope 传进去。 **为什么这个 bug 值得单独记**:一个**假阻断项比不检查更糟** —— 它会让人放弃本来正确的做法。而且这次是模型自己指出"这个 blocker 是误报",我才去查的。 ### Bug 2:验收记录跨会话串号(比较严重) **现象**:第二轮端到端测试里,新建的任务 `task-1` 没登记任何记录,`complete` 却**被放行了**。 **根因**:任务 id 是**每个会话重新计数**的,都从 `task-1` 开始;而验收记录落在项目目录的磁盘上、**跨会话存活**。只按 `task_id` 命名,上一轮会话给 `task-1` 写的记录,会把这一轮**另一个** task-1 放行。 **这是最危险的一类 bug**:门禁看起来在工作(有拦截、有日志、有记录文件),但它放过的是一份毫不相干的旧记录。如果没有端到端测试,这个洞会一直藏着。 **修法**:记录按**团队身份**隔离。团队身份取「成员列表里的 Lead id」—— 实施方与验收方看到的是同一份成员列表,两边算出来的 key 一致;单 Agent(无团队)时退回会话 id。 文件名形如 `team-__task-1.json`。 ### 顺带补上的第三个检查:自证 **现象**:模型在自己登记的验收记录里,主动标注了"我是本任务的实施方,而门禁要求验收方独立"。 这提醒我:`verifier_id` 只是自由文本,实施方可以随便填一个名字绕过。**补上了交叉检查** —— 通过 `agentTeams.getTask()` 拿任务的 `ownerName`,与调用方所属成员的 target 比对,相同就拒绝登记。 判定链任何一环拿不到时,如实记为"未能判定"并留痕,不假装检查过。 --- ## 四、验证证据 ### 真实 harness 中的端到端链路 | 步骤 | 结果 | |---|---| | 建任务 → claim → **直接 complete** | ❌ **被拒绝**:「任务 task-1 没有验收记录,不能标记为完成」,并给出该怎么做的指引 | | 登记 passed 验收记录(绑定 sha) | ✅ 写入 `team-__task-1.json` | | 再次 complete | ✅ **放行**,任务置为 `completed` | | preflight(修复后) | ✅ `ok=true, team.ready=true, missing=[]` | ### 单元级验证(mock ctx,覆盖边界) | 场景 | 结果 | |---|---| | 没有记录就 complete | ✅ deny,理由可执行 | | 有记录后 complete | ✅ allow | | **验收后代码又改了**(HEAD ≠ 记录 sha) | ✅ deny,并打印两个 sha 对比 | | 证据为空时登记记录 | ✅ 拒绝登记 | | 只拦 `complete`,不误伤 `claim` 与其他工具 | ✅ allow | | **A 团队的记录不放过 B 团队的同名任务** | ✅ deny | | **实施方自己登记验收** | ✅ 拒绝登记 | | 独立验收方登记 | ✅ 放行 | | 任务无 owner 时 | ✅ 放行并留痕(不强判) | --- ## 五、当前边界(诚实记录) 1. **还没装进 `web` profile。** 只在开发 profile `pdevdev` 验证。要让团队用,需要正式安装(`dsh plugin add` 或打成 tarball),并做一次 web profile 的冒烟测试。 2. **UI 一点没做。** 看板是 v0.2 的事。 3. **`verifier_id` 的独立性靠交叉检查,不是密码学保证。** 实施方如果冒用另一个成员的身份登记,当前检测不出来(记录里留了 `verifier_agent_id` 可供事后审计)。 4. **门禁用的是 `tools/pre-execute`(可重排的策略层)。** 官方还提供 `ctx.tools.guard()`——单调的最终拒绝,后续监听器无法撤销。如果担心被其他插件的顺序影响,应该升级到 `guard()`;v0.1 先用文档推荐的策略点。 5. **记录落在项目目录 `.pdev/`**,不是插件自己的存储。好处是可读、可 diff、可进版本库;代价是文件被删就等于验收凭据丢失(这本身也是一种可审计性)。 --- ## 六、下一步(v0.2) 按设计文档,v0.2 是**看板 UI**:侧栏迷你依赖图 + 全宽依赖图 + 并发时间轴。 前置条件已经具备: - `webServer` 可用(web profile 挂了 `host-webserver`),插件能 serve 独立看板页 - 任务板数据可从 `agentTeams.listTasks()` 读 - 依赖关系(`blockedBy`)与写范围(`writeScopes`)都在任务视图里,DAG 画得出来 要补的一件事:**时间轴需要时间戳**。当前平台的 `TeamTaskView` 只有 `revision` 和 `status`,没有状态变更时刻。依赖图可以先做;时间轴要在插件侧记录状态变化历史(订阅团队任务变化事件,落盘成时间线)。 --- ## 七、追加:豁免机制与 web profile 安装(用户反馈后) ### 7.1 豁免机制(`pdev_waive`) 用户指出"需要有豁免机制,尤其是用户亲自指示时"。做的时候有一个关键约束:**不能让模型随便声称"用户同意了"** —— 那样豁免通道就等于把门禁关了。 实现成**双重核对**: 1. 插件订阅 `ctx.on('session/event', ...)`,缓存本会话观察到的用户消息; 2. 调用 `pdev_waive(task_id, user_quote)` 时,`user_quote` 必须在缓存里**逐字命中**某条真实用户消息; 3. 而且那条消息里必须含**明确的豁免意思**(豁免 / 免验收 / 不用验收 / 跳过验收 / waive / skip verification …)。 两条缺一不可。实测: | 场景 | 结果 | |---|---| | 引用一句用户没说过的话 | ❌ 拒绝,并列出最近几条真实用户消息供纠正 | | 引用真话,但内容是"帮我用并行开发做这个功能" | ❌ 拒绝:「用户提过这个任务」不等于「用户同意不验收」 | | 引用真话且含"豁免验收" | ✅ 登记豁免,之后 complete 放行 | | 未豁免的另一个任务 | ❌ 仍被挡住 | 豁免记录落盘为 `team-__.waiver.json`,含用户原话、命中的关键字、时间、登记 Agent,**可审计**。`pdev_gate_status` 会列出所有豁免项,提示应写进最终验收报告。 **已知边界**:关键字匹配是启发式,不是密码学保证。用户如果用了很偏的说法(比如"这个先放着吧"),可能需要换一句更明确的话再说一次。这比"模型可以随便声称"好得多,但不是绝对可靠。 ### 7.2 装进 web profile 用 `install-web.sh` / `uninstall-web.sh` 两个脚本,只做两件事:同步包进 `node_modules`、把包名加进 `dsh.profile.bundles`。 - 安装前备份了 `package.json` 与 `cordis.patch.yml` 到 `.backup-parallel-dev/` - 实测 `dsh --profile web --help` 即触发加载,banner 打出: `[dsh-parallel-dev] v0.1.0 loaded` + 四个工具注册成功 - **用户正在跑的 GUI(19387)全程未受影响** ### 7.3 完整使用效果演示(实测) 在真实会话里跑通了两条路径: | 步骤 | 结果 | |---|---| | `pdev_preflight` | `ok: true`,`team.ready: true`,blockers 为空 | | 建两个任务并 claim | task-1 / task-2 均 `in_progress` | | **直接 complete task-1** | ❌ **被拒绝**:「任务 task-1 没有验收记录,不能标记为完成」 | | 用户说「task A 这个只是个小改动,豁免验收,直接合进来」→ `pdev_waive` | ✅ 命中关键字「豁免」,complete 放行,task-1 → completed | | task-2 走正常流程(真写文件、真提交、登记 6 条证据的 passed 记录) | ✅ complete 放行,task-2 → completed | 模型自己在总结里加了一句:**「最终验收报告须列出豁免项:task-1 经用户原话豁免,未经独立验收;task-2 有独立验收记录,两者不可混同。」** —— 这正是豁免机制该有的样子:放行,但留痕,不混同。