## 编辑 Plan 与诊断

<!-- slot: 目标 -->
第 5 章你拿到了一份生成的 Plan 草稿，也读懂了它的结构。但生成的草稿很少一次就完全正确——字段没串通、审批人没配、异常没着落，这些都要在发布前改掉。本章带你动手编辑 Plan：在 YAML 和可视化画布两种视图里改，并用诊断把错误一个个揪出来、修到干净。读完你能把一份“差不多”的草稿，编辑成一份通过校验、可以发布的 Plan。

<!-- slot: 边界 -->
本章覆盖编辑 Plan 草稿与跑诊断；不展开发布与版本（第 7 章）、运行与观察（第 8、9 章）。本章的产物是一份 `diagnostics=[]` 的干净 Plan，但还没有发布。Human / Desktop 对普通未关联 Team 的 Plan 执行 creator guard：只有创建时冻结的 creator 可以保存草稿、修改元数据或发起其他创作变更；Team-linked Plan 则继续按 Team ownership 校验。当前公共 Agent CLI 的 ordinary `update` / `publish` / `archive` 路径尚未实现“认证 Human member 与 creator 相等”的比较，不能把 Agent 命令成功当成 creator 授权证明，也不能用它绕过 Human / Desktop 边界。

<!-- slot: 提示词 -->
本章不涉及从提示词创建或重新生成。如果你发现草稿偏差太大、要改的 Node 太多，回第 5 章调提示词重新生成一份，往往比逐个 Node 手改更快。

<!-- slot: 流程 -->
把草稿编辑到可发布，主路径是“改一轮、校一轮”，循环到干净：

1. 在 YAML 或可视化画布里改：调节点的配置、补缺失的输入声明、加缺失的边、改 `bindings`。
2. 先看编辑器即时给出的客户端结构诊断，再显式点 **Validate**，或用公共入口 `loop exec workflow plan` 的 validate 动作调用服务端编译器。
3. 有诊断就逐条读：先看 `code`，再结合返回中存在的 `path`、`line` / `column`、`nodeId`、`edgeId` 定位。
4. 按诊断改，再校验，直到 `diagnostics=[]`。
5. 干净后进入第 7 章的发布流程；在 Human / Desktop 中，若普通 Plan 不是由当前成员创建，应请其 immutable creator 操作，不要把 Workspace 可见或可读误当成可编辑。当前 Agent CLI 缺少对应 member→creator 比较，这是一项实现边界，不是可依赖的授权通道。

编辑时最常改的是三类：**输入契约**（某个 Node 用了 `input.X` 但没在 `input:` 里声明 `X`）、**边的 bindings**（上游的输出没绑到下游的输入）、以及**回调与审批的配置**（`responder` 指向哪个 Agent、`assignee` 引用哪个审批人、`summary` 渲染什么）。把这三类改对，大多数诊断就会清掉。一个省力的节奏是“改一处、校一次”——不要攒一堆改动再校验，那样出了新问题很难定位是哪处引起的。

用一个具体的例子感受这个循环。假设你打开第 5 章生成的 Bug 分诊草稿，显式点了一次 **Validate**，服务端编译器返回 `expression.unknown_reference`；其中 `nodeId` 是 `triage`，消息提示引用了 `input.triage_agent` 但没声明。你切到 `triage` 的 YAML，在它的 `input:` 里补上 `triage_agent: {type: agent, required: true}`，再在 `graph.entry.input` 里加 `triage_agent: {ref: plan.input.triage_agent}` 把它从 Plan 输入注入进来；同时在 `sendMessage` 里补 `responder: {ref: input.triage_agent}` 指明交给哪个 Agent。再点一次 **Validate**，这条诊断就没了——但编译器可能返回一条新的，比如 `approve` 的 `assignee` 引用了 `input.approver` 却没声明。你再照样补 `approver` 的声明和绑定。就这样一条条消化，直到 `diagnostics` 完全为空。这个“改一处、显式校一次、看诊断”的循环，就是编辑 Plan 最踏实的节奏。

要分清编辑器中的两层反馈。你每次在 YAML 或画布里编辑，客户端 TypeScript 检查器都会立即检查可解析性、节点与边等结构契约；它不等于 Go 编译器。`expression.unknown_reference`、`binding.required_target`、`binding.passthrough_incompatible`、`expression.invalid_condition` 这类编译诊断，只有显式点 **Validate** 或调用公共 validate 后才产生。继续编辑会使上一轮服务端诊断失效并被清空，因此修改后要再次 Validate。拿到诊断后，关联 Node 会按严重程度标色，鼠标停在 Node 上可通过原生提示查看消息；有关联 `edgeId` 的边也会着色，但边的问题应以底部诊断面板为准，不要依赖边悬停或不存在的边属性面板。新手可先用画布建立结构直觉，再切到 YAML 做精确、可 diff 的改动；两种视图编辑的是同一份 Plan。

<!-- slot: 配置 -->
校验一份 Plan，把它保存为 `plan.yaml` 后用公共入口：

```bash
$ python3 -c 'import json;print(json.dumps({"action":"validate","source":open("plan.yaml").read()}))' | loop exec workflow plan -
```

```text
{"data":{"diagnostics":[],"digest":"sha256:…"}}
```

`diagnostics` 为空说明源码通过编译。如果有错，每条诊断会包含 `code` 与 `message`，还可能带定位字段。先按 `code` 理解问题，再结合 `path`、`line` / `column`、`nodeId`、`edgeId`（存在时）定位；这些字段都不是每条必有，文档级问题可能只有路径或行列信息。回去修改后要重新显式校验。画布会给有关联位置的 Node 或边着色，完整消息仍以底部诊断面板或命令返回为准。

这里有三件相互独立的事，不要混为一谈：

- **保存草稿**只是把 source/layout 通过 PUT 写入草稿资源并推进 revision，不会顺带运行编译器。
- **Validate** 是独立动作。编辑器按钮会把当前 source 发给服务端；上面的 `loop exec workflow plan` 也会经 Desktop / Agent API 调用服务端编译器，所以它不是离线、本地或“不依赖网络”的检查。
- **发布**有自己的编译门禁：当前发布流程会保存待发布草稿、校验确切的 draft revision，并在服务端从已存草稿编译不可变版本。先前手动 Validate 通过不能替代发布时对目标 revision 的检查。

当前服务端 validate 使用的仍是纯 Plan 源码编译器，不会查询 Workspace 目录来证明成员或 Agent 是否真实存在，也不会证明调用者在运行时拥有相应可见性或权限。审批人和 callback responder 等身份引用要到运行投递时才解析。因此，`diagnostics=[]` 的准确含义是“这份源码通过编译”，不是“所有运行时身份、权限和外部依赖都已就绪”。发布前除了清空诊断，还应单独核对目标 Workspace 中的成员、Agent、App / Extension 与权限配置。

<!-- slot: 异常 -->
常见的几类服务端编译诊断与修法如下。定位时仍应先认 `code`，再使用该条实际返回的可选定位字段；不要假设一定有 `nodeId`：

- `expression.unknown_reference`：某 Node 引用了 `input.X`，但 `X` 没在它的 `input:` 契约里声明。修法：在 Node 的 `input:` 里声明 `X` 的类型，并确保 `entry.input` 或上游边的 `bindings` 把值传进来。
- `binding.required_target` / `binding.passthrough_incompatible`：边的 `bindings` 缺了下游需要的字段，或写了不兼容的透传。修法：在边的 `bindings` 里补上 `{字段名: {ref: …}}`。
- `expression.invalid_condition`（例如 `unexpected trailing token "or"`）：`switch` 的 `when` 用了单词 `or`，但表达式只支持符号 `||`。修法：把 `or` 改成 `||`（同理 `and` → `&&`、`not` → `!`）。

按诊断逐条改、再校验，是编辑 Plan 最稳的节奏。

<!-- slot: 审批 -->
编辑审批相关的要点：`request_approval` 的 `assignee` 在渲染时走 `{{ }}` 插值（如 `{{ input.approver }}`），运行投递时再解析成具体成员——不要写没有语义的占位（如 `owner`），改用真实的成员引用；`summary` 也会被插值，把审批人需要看到的信息（如 `{{ input.assignee_team }}`）写进 `summary`。`request_approval` 没有创作期超时——如果你想要“超时升级”，那是要在需求里设计的人工兜底或运营策略，不是节点自带的。`assignee` 的运行时解析接受精确 member ID，或唯一匹配的姓名、用户名、邮箱；引用的成员必须存在于目标工作空间。把 Plan 搬到另一个工作空间时要重新确认审批人，但不要期待 Validate 替你完成这项目录检查。

<!-- slot: 外部系统 -->
编辑涉及外部能力的部分时记住：Workflow 图里没有 HTTP 节点、没有外部系统节点。要让 Agent 调外部系统，用 `send_message` 加 `completion: callback`，并用 `sendMessage.responder` 指明交给哪个 Agent；具体的 API 调用由 Agent 经 App / Extension 完成。如果你发现自己在图里加了一个 `type: http` 的节点，那方向错了——删掉它，改成 `send_message`。

<!-- slot: 验收 -->
- [ ] 能在 YAML 和可视化画布两种视图里编辑 Plan 草稿。
- [ ] 能区分客户端即时结构检查与显式服务端 compiler validate，并说明保存、Validate、发布前检查彼此独立。
- [ ] 能用公共 `loop exec workflow plan` 校验得到 `diagnostics`，并按 `code` 及实际存在的 `path`、行列、`nodeId`、`edgeId` 定位问题。
- [ ] 能修掉至少三类常见诊断（`unknown_reference`、`binding`、`invalid_condition`），把 Plan 改到 `diagnostics=[]`。
- [ ] 能区分 Human / Desktop 对普通 Plan 的 immutable creator guard、当前 Agent CLI 尚未实施的 member→creator 比较，以及 Team-linked Plan 的 Team ownership 边界。

<!-- slot: 媒体槽位 -->
<!-- media: fig-06-01-edit-diagnose.png -->
图 6-1 编辑与诊断循环：改 → 校验 → 读诊断 → 再改（截图槽位：YAML 编辑器 + 诊断面板 + 可视化画布上的错误标记）。
