一篇 issue 就是一篇笔记
我们之前写过 Issue 类型 和 Turn。一个讲流程怎么写成一篇笔记,一个讲看板为什么按「轮到谁」分列。这两篇都绕着同一样东西转,却一直没有正面讲它:issue 本身。
这篇补上。
issue 是一篇笔记
在 Bonsai 里,一个项目的 issue 放在项目文件夹下面:
1_Projects/<项目>/Issues/12.md
文件名就是编号。编号在这个项目里唯一,一旦有了就不改,标题改了文件名也不用跟着变。看板上的每一张卡片,都是这样一篇笔记。
没有第二份数据库。看板、issue 页、AI 通过 MCP 调 list_issues,读的都是这些文件。你可以在笔记编辑器里直接打开它,可以搜索它,可以在别的笔记里用 [[…]] 链接它。项目做完归档,它跟着一起归档。
我们这么做,是因为 issue 里写的东西本来就是知识:为什么要做这件事、验收标准是什么、做的过程中拍过哪些板、学到了什么。把它放进一个和项目笔记分开的系统里,下一次 AI 来干活时就得去两个地方找。
上半部分:事实
一篇 issue 的 front matter 只记这件事的事实。一个做到一半的 feature 大概长这样:
---
type: Feature
state: Reviewing
priority: high
owner: Guoliang
goal: 9
blocked_by: 10, 11
repo: livewithbonsai/Bonsai
branch: issue/12-cache-hint
pr: livewithbonsai/Bonsai#89
---
逐个说:
| 字段 | 是什么 |
|---|---|
type | 这是哪一类事。指向你的一篇类型笔记,比如 Feature、Bug、Task、Goal |
state | 现在走到流程的哪一步。可选的值由类型笔记决定 |
priority | urgent、high、medium、low 四档,或者不写 |
owner | 负责人。一定是一个人,从开始到结束不变 |
goal | 这件事属于哪个目标:同一项目里一篇 Goal 类型 issue 的编号 |
blocked_by | 要等哪几篇做完才能开始,几个编号 |
repo | 代码在哪个仓库。不写就用项目笔记里登记的那个 |
branch、pr | 做这件事的分支和 PR,由 AI 开出来之后写回 |
界面上这一栏叫「状态」(英文界面是 Status),但 front matter 里的键一直是 state。这是因为 status 在 Bonsai 的笔记格式里是保留给服务端的键。
更值得说的是没有写在这里的东西。
没有模型,没有执行端。 这篇 issue 由哪个模型写、在哪台电脑上跑,不是这件事的事实,是「这一类事怎么做」的规则。它们写在类型笔记和角色笔记里。想把审代码的模型从一个换成另一个,改一篇角色笔记,所有 issue 一起跟着变。
没有 assignee。 看板卡片上显示的「现在轮到谁」是算出来的:issue 在 Agent’s turn 时,是类型笔记里这一步派的那个角色;在 Your turn 时,就是 owner。人组成的团队里,assignee 是一个要不停手动改的字段。和 agent 一起干活,它每小时都在变,所以干脆不存它。
没有 run 的记录,没有 CI 状态。 这一次 run 在跑还是跑完了,是「现在」,不是「事实」,Bonsai 在别处记。PR 合没合、CI 绿没绿,GitHub 才是那份事实,issue 里只放一个指针。
所以 issue 很轻。我们常拿 Claude 里开一个对话来比:你不会为一个对话填一张表,你只说你要什么。
下半部分:给人读,也给 AI 读
front matter 下面是正文。新建一篇 issue 时,正文从它类型笔记里的模版开始,内置的模版有四节:
Description。 要做什么、为什么现在做、可测的验收标准,以及 AI 动手之前必须先读的几篇笔记的链接。验收标准是你写给 AI 的 spec。审代码的 agent 逐条对照,但没有哪个 agent 能自己改它。
Decisions。 需要你拍板的事。一个决定一个小标题,下面几个选项,AI 推荐的那个标「推荐」,最后永远有一个「其他」。你在 issue 页上点一个选项,就是拍板,issue 接着往下走。
You should know。 AI 每做完一次,把这一节整个重写,告诉你这一轮最该知道的几件事。值得留到这件事之后的,标上「值得记住」。
Log。 只追加,不修改。每一次 run 一小节:哪天、哪个角色、第几轮、做了什么。review 的每一轮也记在这里。
这四节的顺序是按你打开 issue 时的需要排的:先是这件事是什么,然后是你现在要做的决定,然后是你该知道的,最后才是完整的经过。
类型决定它怎么走
一篇 issue 自己不知道下一步该干什么,它的 type 知道。
类型笔记里写着这类 issue 有哪些状态,每个状态属于四个 Turn 中的哪一个,到了 Agent’s turn 的某个状态该派哪个角色,做完能把 issue 改到哪里,最多做几轮。内置的 Feature 是这样走的:
Ready → Developing → Reviewing → Review PR → Deploying to Staging → Verify Staging → Deploying to Production → Done
粗体的步骤轮到 agent,斜体的轮到你。issue 在两边之间来回,看板按 Turn 分列,Your turn 在最左边。
这个分工让 issue 保持干净:issue 记「这件事」,类型记「这类事」。同一件事你不需要每次重新安排一遍流程。
引擎:到了就派,该停就停
现在,issue 走到 Agent’s turn 的一个状态,Bonsai 就按类型笔记里那一行,把对应的角色派到你的电脑上去做,不用你点。这就是 Automation 引擎。
它的规则很少,而且故意很笨:
- 它只看 front matter 里的
type、state和这篇 issue 的 run 记录。它不读正文,不判断代码写得好不好,也不挑模型。 - 同一篇 issue 同一时间只有一个 run 在跑。
- agent 做完了却没有把 issue 交出去,或者交到了那一行没允许的状态,或者这一步已经做到了
limit的轮数,issue 就被挪到 Needs Decision,Log 里记一行原因,等你。 - 到了 Your turn 的状态,它什么都不做。那一步没有行,就是停下来等人的地方。
- 它从不把 issue 改成 Done、Canceled 这类收尾的状态。收尾是人的事。
我们希望你能预测它会干什么。一个能猜出你心思的调度器听上去很好,但当它猜错的时候,你根本不知道该去哪里找原因。
Goal:一篇 issue 带着一串 issue
Goal 不是另一种东西,它也是一篇 issue,只是类型叫 Goal。它的正文里多一张 Plan 表,子 issue 用 goal: <编号> 指回它。
一个目标从提出到做完是这样的:
- 你建一篇 Goal,写一句完成条件和为什么。它停在 Draft。
- 在 Goal 旁边打开对话,这个对话就是 Planner。它读项目笔记和代码(只读),把目标拆成几篇子 issue,写好依赖
blocked_by,填好 Plan 表,然后停下。它不改任何状态,也不派任何活。 - 你看过这份拆分,在 Goal 页上点「批准」。这是整个目标里你唯一需要批准的一次。
- 引擎接手:依赖都已经收尾的子 issue 进入第一个 Agent’s turn 的状态开始做,同时最多两篇(可以在项目里调)。一篇做完,被它挡住的下一篇就能开始。
- 子 issue 全部收尾,Goal 自己也收尾。
一路上,每一篇子 issue 照样会停在 Review PR、Verify Staging、Needs Decision 这些地方等你。批准一次 Goal,不等于把后面的每一次合并都交出去。
人留着的几件事
把这些放在一起,你在一篇 issue 上始终握着的是:
- 写验收标准,以及批准 Goal 的拆分。
- 拍板:Decisions 里的每一个选择。
- 审 PR、验收 staging:Your turn 里的那些状态。
- 收尾:Done、Canceled、Duplicate 都由你来改。
- 改规则:类型笔记和角色笔记放在
_开头的文件夹里,AI 的任何授权都写不了。
issue 做完的时候,distill 角色读一遍它的 You should know,把标了「值得记住」的那几条写回项目笔记。下一次 AI 来这个项目,读到的就是这一次留下的东西。
现在做到哪了
上面说的都已经在线上:issue 的字段、四节正文、类型笔记、按 Turn 分列的看板、Automation 引擎、Goal 和 Planner。issue 页顶部列着全部字段,状态、类型、优先级、负责人、Goal、依赖、仓库、PR 都可以直接改。
每个字段的完整说明在文档里:Issue。流程怎么写见 Issue 类型,引擎的规则见 自动推进,拆目标见 Planner。