wayfinder 精读:一个只肯产出『决定』的技能,怎么处理一次会话装不下的规划
wayfinder 把大到一个会话装不下的工作画成一张地图。本文走一遍它的两种触发模式,拆开 map / 子 issue / decision ticket / frontier 的机制,解释它为什么把『plan, don't do』写成第一原则、为什么禁止用 #42 称呼票,以及 fog 与 ticket 的那条分界线画在哪里。
(本文读的是 mattpocock/skills 快照 5beb35b,2026-10-19。引用原文都能按文中路径回仓库核对。这是「逐个技能精读」系列的第七篇,聚焦 wayfinder 一个技能。)
先走一遍触发路径
skills/engineering/wayfinder/SKILL.md 开场就把场景钉死:
A loose idea has arrived, too big for one agent session, and wrapped in fog: the way from here to the destination isn’t visible yet. Wayfinding is about finding that way, not charging at the destination.
触发方式是 /wayfinder,两种模式。Chart the map:你给一个模糊想法,它先调用 grilling 和 domain-modeling 把「destination」定下来,再广度优先地 grill 一轮,把能看见的开放决定摊开;然后创建地图,创建现在就能说清楚的票,用第二遍把阻塞边接上;最后为每张 research 票并行放一个 research 子代理。Work through the map:你给一个 map 的 URL 或编号,它选一张票、先认领、再解开、记录结论。
产物不是代码,也不是工单清单,而是一组挂在 tracker 上的 issue:地图本身是一条带 wayfinder:map 标签的 issue,票是它的子 issue。地图正文固定五节:
## Destination
## Notes
## Decisions so far
## Not yet specified
## Out of scope
它被明确定义为 index, not a store:一条决定只活在一个地方——它的票里;地图只留一行 gist 加一个链接,绝不复述。这样一张地图可以一直长,而每次会话只加载一次「低分辨率」的地图,需要时再去拉某张票的全文。
它和「一般技能」的差别:它产出决定,不产出交付
整个技能里最重要的一句是第一原则,标题就叫 Plan, don’t do:
Wayfinder is planning by default: each ticket resolves a decision, and the map is done when the way is clear, with nothing left to decide before someone goes and does the thing. The pull to just do the work is usually the signal you’ve reached the edge of the map and it’s time to hand off. An effort can override this in its Notes, carrying execution into the map itself, but absent that, produce decisions, not deliverables.
读起来像一句提醒,其实是这个技能和普通「大任务拆解」最本质的分野。普通的拆解产出的是实现票:一张能执行、能交付的工作切片。wayfinder 产出的是决定票(decision ticket):一个问题的答案是一个决定,不是一段代码。文档记载了这个词是专门加的——因为「人们一直把 wayfinder 的票读成普通的实现票」,所以提交 7d694b7 引入了 “decision ticket” 作为前置词,改进了技能描述、开场句和 CONTEXT.md 的领域术语。
票有四种类型,用标签而不是散文编码:research(AFK,去工作目录之外查一个事实)、prototype(HITL,做一个粗糙的具体物件来提升讨论的分辨率)、grilling(HITL,默认类型,刊登在对话里)、task(唯一一个「做」而不是「决定」的类型,但它靠解锁一个决定来挣得位置,而不是靠交付目的地的一部分)。文档还点出了这个类型最常被误读:「Agents read it as an implementation step and start to write product code inside the map.」
还有一条机制把「它不自己干」落到了操作上:一张票解完之后不是写代码,而是发一条 resolution comment、关掉 issue、往地图的 Decisions so far 追加一个上下文指针。 会话到此为止。地图清空之后它也不往下走——而是把你指向 /to-spec,因为「a cleared wayfinder map」是给下游的输入,不是可以直接开工的东西。
它有意识摒弃了什么
一、摒弃了用编号称呼票。 有一节专门叫 Refer by name:
Every map and ticket is an issue, so it has a name: its title. In everything the human reads (narration, the map’s Decisions-so-far), refer to it by that name, never by a bare id, number, or slug. A wall of
#42, #43, #44is illegible; names read at a glance. The id and URL don’t vanish; a name wraps its link, but they ride inside the name, never stand in for it.
提交 ee014fa 加了这条约定。它管的是人读的那一面:id 和链接不消失,它们待在名字的链接里面,只是不许站出来代替名字。
二、摒弃了地图和票上的 triage 标签。 提交 8295b8e 一次性修了三个真实事故,这是第一个:地图和票会被打上 ready-for-agent,而这个标签读起来就像「可以开工实现了」。修法是直接把规则写进 SKILL.md:「wayfinder: labels are the only labels a map and its tickets carry, never a triage label like ready-for-agent: they are decisions, not implementation work.」
三、摒弃了预留给未来的占位链接。 同一次提交的第二个事故:起草时编号 T1–T8 的票,发布后被写成 #1–#8,自动链接到了仓库里无关的老 issue。修法不是加一句警告,是把交叉引用推迟到「第二遍」——先创建票拿到真 id,再在有真 id 的 pass 里写引用。
四、摒弃了 research 分支上开 PR。 第三个事故:research 子代理从一次性的 research/<name> 分支开了 draft PR。新规则一句话:「Push the branch but open no PR: it is never merged.」
五、摒弃了「一次会话解多张票」。 默认是「never resolve more than one ticket per session」,唯一例外是 research 票。文档解释了这个例外为什么成立:research 是把阅读隔离在会话之外的子代理,而 grilling 类的票并行跑会出现一个会话问你另一个会话刚问过的问题——因为它们不共享上下文。
六、摒弃了把整个地图一次铺满。 这是它最反直觉的取舍。地图是故意不完整的:看得见但还说不清楚的东西,不预先切成票,而是写进 Not yet specified,叫 fog of war。分界线只有一条:
Fog or ticket? The test is whether you can state the question precisely now, not whether you can answer it now.
- Ticket when the question is already sharp, even if it’s blocked and you can’t act on it yet.
- Not yet specified when you can’t yet phrase it that sharply. Don’t pre-slice the fog into ticket-sized pieces: it’s coarser than a ticket, and one patch may graduate into several tickets, or none, once the frontier reaches it.
「现在能不能把这个问题说清楚」是唯一判据;「现在能不能答出来」不算。所以一张被阻塞、暂时动不了的票,只要问题是锐的,照样可以现在就开。
原文证据
地图挑票的机制,落在两句话上——claim 和 frontier:
A session claims a ticket by assigning it to the dev driving the map, first, before any work, so concurrent sessions skip it. That assignee is the claim: an open, unassigned ticket is unclaimed.
A ticket is unblocked when every ticket blocking it is closed; the frontier is the open, unblocked, unclaimed children, the edge of the known.
「认领就是 assignee,未认领就是开着且没人 assign」——把并发控制压成了一个字段,不需要额外的锁。
而被问到最多、也最值得抄的一段,是它对「地图清空之后还要不要 spec」的回答。它没把责任揽回来:
Wayfinder’s tickets are decision tickets, and by the time the map closes they are all closed too. What is left is a map full of linked decisions, which is not a build plan.
to-speccollapses those linked decisions into one spec (/to-spec #<map_issue>), andto-ticketsslices that spec into tracer-bullet implementation tickets.
它顺手也承认了自己最大的失败模式——agent 会在会话中途开始写产品代码,而「plan, don’t do」的豁免权写在 Notes 里,Notes 又是由受约束的那个 agent 自己写的。文档把「维持默认」这件事写成了没有硬性拦截的一句话,然后给了人应该做的三件事:读别人画的 Notes、把实现放进单独的会话、把任何看起来像交付切片的 wayfinder:task 当成类型写错了。
可迁移结论
- 给「一次装不下的规划」一个专属容器。 地图是一张 issue,票是子 issue,决定分散在各自的票里、由地图索引——不用一个巨型文档,也就不需要每次会话读完整个历史。
- 用标签编码类型,别用散文。 票体从不声明自己是哪种票,类型在
wayfinder:<type>标签上。这样一条查询就能筛出整类,agent 不必解析自然语言。 - 把「现在能不能说清」当作切分判据。 而不是「现在能不能做」。这条线一旦划对,地图就能在信息不全时照常向前推进,而不是退化成一场全面规划。
- 并发控制压到一个字段上。 assignee 即认领,未 assign 即可抢;不需要状态机。
- 承诺要有明确的边界。 这个技能承诺的是决定,不是交付;地图清空后它把你交给
/to-spec,而不是顺手开工。
系列走到这里,三个技能各自守着链条上的一段:wayfinder 在雾里找路、只产出决定,to-spec 把决定收拢成一份不发明任何东西的记录,to-tickets 把记录切成竖的、带依赖边的票。它们共同的做法是同一个:把技能的能力边界写成明文,而不是靠模型的自觉。
讨论
用 GitHub 账号留言;评论保存在公开仓库chengshu-blog-discussions的 Discussions 里。也可通过 RSS 订阅后续文章。