个人工作站 › 调查报告 ← 返回报告清单

Workflow · 实况记录

把这套 skill 走一遍:
从拷问到复盘的一份实况

Matt Pocock 的 skill 不是十几个独立工具,是一条有先后、有出口的流水线。这篇不解释名词,而是把整条线真跑一遍:在 JLPT 学习站里新开一个「假名翻牌」练习模块,每一步都留下可打开的东西——样张、规格、票、分支、PR、复盘。

案例 kana-memory · 假名翻牌 仓库 Yang-Code01/jlpt-library 分支 feat/kana-memory → main PR #40(draft)

八格是同一条线,不是八个工具:每一格的输入都是上一格的产物,除了第 06 格可以随时回去把第 03 格挖深一层。下面按这个顺序走,每格都带一个真产物。

阶段 00

先别动手:问地图,再摸地

ask-matt 路由 · research 后台调研 · 前置 setup-matt-pocock-skills 只装一次

这套 skill 的入口不是「找一个技能」,而是「先认路」。ask-matt 里只有一张表:你现在处在哪一段,该叫谁。它把整条线分成三段——想清楚(grill-with-docs → handoff / prototype)、写成规格再切票(to-spec → to-tickets)、实施与收尾(implement | implement-spec → pr → retro),另有三条岔路(triage / diagnosing-bugs / wayfinder)和一套给「重构老代码」用的健康检查。

会话记录 · 起点用户
做一个 mattpocock skill 的 workflow 的 html。页面分为两部分,左右,左小右大,
左边竖列排列 skill 的介绍和使用场景。一个一个 skill 一个模块。右边是正文。
从头到尾的先以流程为大体框架。主要内容是做一个案例。一步步介绍,
案例里面要有每一步的截图。主要产物。等。

按地图,这句话落在「想清楚」这一段——但有个陷阱:想清楚的前提是知道这块地上已经有什么。这个仓库是静态站,门户、测试、行尾、数据管线都有既成规矩,凭印象改必然踩。所以先派了一个只读的 research 子代理去把「新增一个顶级模块要动哪些地方」查清楚,主线程同时继续访谈。

research 的产出长这样

不是一篇散文,是一串带行号的坐标:门户卡片在哪一行、哪三处断言写死了卡片数、进度模块的白名单为什么不许扩、行尾规则钉在哪个文件。这些坐标后来直接变成了实施阶段的硬约束——子代理拿到的任务书里,每条约束都能追到行号。

research 结论 · 摘门户改动面
index.html:1610-1667   日语组 8 张卡,全部手写;data-tags 必须在词表内
index.html:1593-1599   data-tags 受控词表(全部/资料/词汇/听力/真题/练习/工具)
tools/tests/test_area.js:48        写死「12 张卡」
tools/tests/test_reading.js:283    写死「12 张卡」
tools/tests/test_typing_en.js:380  写死「12 张卡」
index.html:1553-1586   左栏 11 个 .rail-item(与 shell.mjs 的 12 个 ITEMS 早已不同步)
assets/progress.js:60  RE_UNIT 白名单不含新目录 → 新模块默认不计进度
typing/typing.js:48    独立 localStorage 键先例:jlpt-typing-best

这一步没有产物入库(调研笔记放在仓外的 .scratch/),但它改变了后面所有决定:模块不引共享样式、不接进度模块、不往左栏加条目、三处断言同批改。调研的价值不在结论正确,在于它把「猜」换成了「查过」。

阶段 01

拷问:把「做一个游戏」问成九条不能反悔的决定

grill-with-docs = grilling(拷问)+ domain-modeling(顺手写 ADR 与术语)

grilling 的规矩很硬:一次只问一个问题,等你答完再问下一个;每问必带「推荐答案 + 理由」;不许把选项写成一大段让人自己挑。它的目标不是收集需求,是把含糊处逼到不能含糊——因为含糊的每一处,后面都会变成返工。

这一轮被问掉的东西,事后看分成三类:

类别被问的是什么答完改变了什么
不可逆案例是真的跑一遍,还是编一个?真跑:于是有了真实分支、真实提交、真实 PR,报告里的每个截图都能点开核对
不可逆新开一个模块,还是塞进现有模块?新开:于是「新增顶级模块」这条最贵的路径被完整走了一遍,包括门户与文档的七处同步
便宜但影响全篇左栏放几个 skill?十四个模块 + 一张路由表:够覆盖主线,又不至于变成名词表
便宜但影响全篇截图是真截,还是画示意图?真截:界面用无头浏览器出图,会话过程用终端卡讲真实文本
口味案例做什么题材?假名翻牌:练的是「配对回忆」,正好补上站里单向练习的空缺

九轮之后,谈定的话被写成一张表:每一个决定都带一句「为什么」。这是整条流水线里最便宜、也最省事的产物——后面每个子代理都拿它当输入,没有人再问「为什么不用另一种做法」。

.scratch/kana-memory/PLAN.md决策表 · 摘
| 决策       | 结论                                                    |
| 案例真实性 | 真跑一遍流程,产物真落盘                                  |
| 主构建技能 | implement-spec(implement 作补充说明)                    |
| 牌面与档位 | 三档 = 三种配对:假名↔罗马音 / 汉字词↔读音 / 日语词↔释义  |
| 分支与 PR  | 从 main 切 feat/kana-memory,draft PR base = main         |
| 视觉方向   | B · 纸感卡片墙(三张样张对比后用户选定)                    |
这一阶段真实的一次越界

拷问的规矩是「只问决策,事实自己去查」。但第一轮我把「这个仓库的测试是怎么跑起来的」也列成了问题——那是事实,查一下就有,问用户等于把成本转嫁给他。用户当场纠正:事实性问题去查文件、查工具,只把决策留给人。这条纠正后来写进了本地约定,也直接决定了后面每一轮都先派调研再提问。

grill-with-docs 比 grilling 多一件事:问的同时把文档写掉。本案例里落的是三条术语(牌墙 / 一局 / 配对)与一份 ADR——「为什么再开一个顶级模块,而不是塞进现有模块」。写文档不是为了留痕,是为了让下一个会话不必重新论证。

阶段 02

样张:三个能打开的答案,比一段文字描述有用

prototype — 一次性代码,只为回答一个问题

prototype 的第一句话是:问题决定形状。要验数据模型,就写成几行脚本把状态打出来;要验界面,就写一个能点的页面。它的产物是「答案」,代码本身该扔——所以本案例的三个样张都放在仓外的 .scratch/ 里,一个字节都不入库。

要问的问题是「这个游戏该长什么样」。这种问题用文字回答必然错位:说「纸感」三个人会想到三张纸。于是同一副牌、同一条洗牌种子、同一组预置状态(已配对一对、已翻开一张),写成三个方向完全不同的页面,再加一页目录说明各自在回答什么:

  • A · 绒布牌桌:深绿felt、奶油牌、金线,翻牌是三维翻转。押的是「像在桌边玩」的沉浸感。
  • B · 纸感卡片墙:纸底、细线网格、墨色正文、朱色强调,配对成功给淡朱底+勾。押的是「和这个站的气质是一家人」。
  • C · 和纸双色:和纸底、藍与朱、明朝体、发丝线,配对后盖一枚朱色小印。押的是「日式印刷品」的调子。
样张 A:深绿绒布牌桌,卡片翻面三分之二,已配对的牌带金色圆环
A · 绒布牌桌。同一副牌,同一组预置状态:左上与中下两对已配对(金环+右上勾),另有一张牌刚被翻开。
样张 B:纸感卡片墙,五列四行,配对的牌淡朱底配勾,一张牌翻开
B · 纸感卡片墙。细线网格把 20 张牌排成一面墙;配对成功不是把牌调灰,而是给淡朱底和勾——「出局」比「变暗」更接近纸上的记号。
样张 C:和纸底配藍与朱,明朝体,配对的牌右下盖朱色小印
C · 和纸双色。翻牌带一点下滑位移,配对后盖一枚朱色「済」小印——最像印刷物的一版,也最容易被当成「贴了张日式壁纸」。

用户看着三个能点的页面选了 B。这个选择不只定了配色,还定了一串下游决定:既然走纸感,就不该引站里那套无衬线设计令牌(那会把共享样式表拖成调色板),于是模块自带一份 CSS,走 typing/ 那条「不引共享资源」的老路。一个视觉选择,最后变成了一条架构边界。

样张里真踩到的两个坑

一、洗牌写错看不出来。三张样张的洗牌数组只用了 0–9 的重复值去索引 20 张牌,牌墙上于是出现两张「き / ki」——看图时才发现的。修法不是删重复,而是换成带种子的 Fisher–Yates:样张要能复现,靠随机数就复现不了。

二、居中会把证据裁掉。样张 A 用 flex 垂直居中,内容一旦高过视口,顶部那排计量(步数 / 用时 / 最佳)在整页截图里就整体消失。截图工具没错,是布局把信息推出去了——判断一个样张能不能用,先看它在截图里还剩下什么。

阶段 03

规格:把谈定的话压成一份能照着写的文件

to-spec — 不访谈,只综合

to-spec 的第一句就是禁令:不要再问用户,把你已经知道的写成规格。它要的产物不是会议纪要,而是这样一份文件:读完之后,一个没参加过对话的子代理能照着写代码,而且知道哪些事不该做。

写规格前还剩最后一个真决策没落:测试从哪儿进。这不是口味题,它决定了后面所有测试的形态,也决定了这个模块能不能被并行开发。问题是这么问的:

会话记录 · 接缝提问
这个模块的算法只有十行(洗牌 + 比 key),风险全在接线:同 id 判定、翻开与配对的互斥、
配错后的定时回翻、计步时机、换档重建牌墙。测试接缝放哪儿?
A. 只测纯逻辑层 —— 快,但上面那些风险一条都测不到
B. DOM 桩驱动真页面脚本 —— 用仓库已有的 VM + 桩路线,慢一点,接线全在射程内
C. 装 jsdom —— 最省事,但要引入外部依赖,破仓库「零依赖」硬约束

选了 B。理由写在规格里,一句话就能服人:「只测纯函数会把五个真风险全漏掉」。这条决定还带出一个副产物:仓库原有的 DOM 桩 classList 是空实现,断不到「已配对」这种状态,于是新套件自带一个把 classList 做真的最小桩——不引依赖,只补缺的那一块。

.scratch/kana-memory/SPEC.md118 行 · 结构
## Problem Statement      练习都是单向的:敲字只练输出,翻转卡没有对局
## Solution               10 对 / 20 张的牌墙;三档 = 三种配对关系
## User Stories           16 条,逐条写「作为谁 / 我想要 / 以便」
## Implementation Decisions
   · 模块边界            新目录,走 typing/ 的「不引共享资源」路线
   · 数据来源            复用 typing/romaji.js 与 typing/data/*.js,不复制表
   · 一局的定义          10 对 20 张、5 列 4 行、步数 = 翻满两张算一步
   · 持久化              只两个 localStorage 键;不动 progress.js 的白名单
   · 交互与反馈          翻牌/配错/配对三态;结算条;键盘;aria-live
   · 门户接入            加一张手写卡,不加 rail-item,三处断言 12 → 13
   · 架构记录            要 ADR,要术语
## Testing Decisions      接缝 = DOM 桩;什么算好测试;要测的九件事
## Out of Scope           七条「不做」,其中两条是别人踩过的坑

十六行 Out of Scope 是这份文件里最容易被跳过、也最值钱的一节。它把「不接进度模块」「不加左栏条目」「不做音频文件」「不改 typing/」逐条写成白纸黑字——后面三个子代理各自独立干活时,谁都可能顺手「改进」其中一条,而这一节给了它们说不的依据。事实上有一次差点破线:进阶档想给牌面小字换个更准的标签,方案里第一反应是「顺手把 CSS 也调一下」,被「票 04 独占 kana.css」和这条边界拦住了。

命名漂移,一份规格才收得住

访谈阶段的施工单里,模块目录写的是 memory/;到规格阶段定稿成 kana-memory/。这种漂移在任何项目里都会发生,危险的是它悄悄分裂:票里写一个名字、测试断言另一个名字、文档再写第三个。定稿的规格把它锁成一个词,后面所有票、分支名、路径、文档行都从这一个词派生。

阶段 04

切票:切成能单独合进去的小刀,而不是按层次切

to-tickets — tracer bullet + 声明的阻塞边

切票最自然的错法是按代码分层切:先做数据层、再做状态层、最后做界面层。这样切的每一刀都看不见东西——数据层写完,页面上什么都没有,没法验证、没法截图、也没法给别人看。to-tickets 要的是另一种切法:每一张票都是一颗能打到目标的子弹,合进去之后立刻能在页面上看见一点新东西。

这个模块被切成五张:

任务图5 张票 · 3 条阻塞边
01 一局能玩(入门档)            ← 无阻塞,第一颗子弹
 │
 ├─→ 02 进阶档 + 档位切换
 │      │
 │      └─→ 03 高手档 + 每档最高分
 │                │
 │                └─→ 05 门户接入与文档
 └─→ 04 手感与可达性(只依赖 01,故可与 02 并行)

看这张图就能看出为什么它值钱:01 一落地,前沿就有两张票(02 和 04),可以同时派两个子代理;03 等 02、05 等 03,串行的那一段只剩两张。并行度不是靠人多,是靠依赖边画得准。

一张票长这样——短、硬、可判:

issues/02-mid-tier-and-switch.md14 行 · 全文
# 02: 进阶档 汉字词 ↔ 假名读音 + 档位切换

**What to build:** 页面顶部出现三个档位(入门 / 进阶 / 高手),点「进阶」重建牌墙:
牌对改为从 typing/data/{n5,n4}.js 抽的汉字词与其假名读音(如 兄 ↔ あに),步数、用时、
配对数全部归零并从这一刻重新计时。纯假名词(汉字写法与读音相同)不进牌池。

**Blocked by:** 01

**Status:** ready-for-agent

- [ ] 三个档位可切换,当前档位有可见的选中态
- [ ] 切到进阶档后牌池全部来自词表条目,且不含 w === k 的词
- [ ] 换档会重建牌墙(牌数与行列不变)、步数 / 用时 / 配对数归零
- [ ] 换档后计时不继续累加,直到下一次翻牌才重新起跑
- [ ] 当前档位在刷新后仍然保留
- [ ] node tools/tests/test_memory.js 增加换档断言并全绿

四条格式很关键,缺一条子代理就会自己发明:
What to build 写成「合进去之后用户能看见什么」,不写成「改哪些文件」(文件归属另有施工约定);
Blocked by 只写票号,机器和人都能读,整张图就是从这些行拼出来的;
Status 让「谁能开工」变成查表而不是讨论;
勾选项 全是可判定的行为,没有一条是「代码质量好」这种没法定案的话。

切票的判据

不是「代码分层」也不是「工作量平均」,而是两条:能单独合入,且合入后立刻可验证。按这个判据,01 必须包含「随机抽 10 对、能翻牌、能配对、计步计时」整条纵切——它一张票就穿过了数据、状态、界面三层,但它是唯一一张「能让用户玩起来」的票,所以它第一。

阶段 05

实施:九个步骤把「任务图」变成一堆子代理

implement-spec — 集成分支 + worktree + 前沿并发(内部调 tdd)

到这一步,活儿其实已经不是「写代码」,而是「调度」。implement-spec 的九步里没有一步在讲怎么实现,全在讲怎么让好几个子代理互不碍事:开集成分支、把每张票丢进自己的 worktree、合完一张就重算前沿、全部合完才让评审进场。

implement-spec/SKILL.md九步 · 摘
3. 建集成分支。若追踪器以 PR 关闭工作,或用户要求,则在第 5 步首次合并后开 draft PR
   (尚未领先 main 的分支开不了 PR),标记为关闭此规格与这些票。
4. 用 implementer 子代理实现每张票,每票在自己的 worktree、自己的分支上。每个 implementer:
   · 开工前确认自己的 worktree 基于集成分支,若不是就 reset 到它上面;
   · 调 Skill 工具跑 tdd 把票建起来;
   · 报完成之前先把集成分支的 tip 合进自己的分支。
5. 每个 implementer 完成后,用 merger 子代理把它合进集成分支。
6. 若前沿变了,就再派一批 implementer —— 这是并发度的来源。
7. 全部完成后,在集成分支上跑 code-review,把问题交给一个 implementer 一次修完。
8. 若有 draft PR,就把它标成 ready;否则按追踪器的规矩逐张关票。
9. 清理所有 implementer 的 worktree。

沟通要稀疏。主要靠上下文指针:指向规格、票、调研笔记、之前的提交。不要转述已经能查到的东西。

最后那段话是整个流程能跑起来的真正原因。派出去的每个子代理看不到主线程的对话,所以任务书里不是「我们刚才讨论了…」,而是一串路径:读 SPEC.md、读 issues/02-*.md、读施工约定 ARCH.md。主线程花在转述上的 token 越少,能同时带多少个子代理就越多。

5.1第一颗子弹:一张票就让它能玩

票 01 的目标不是「搭好架子」,而是「能玩」:随机抽十对、能翻、能配、计步计时。它在自己的一条分支上完成,提交时新增五个文件:

git show --stat 3823508票 01
kana-memory/game.js        110 行   纯逻辑:洗牌 / 开一局 / 翻一张 / 结算
kana-memory/page.js        329 行   接线:建牌 / 绑事件 / 渲染计数 / 档位注册
kana-memory/index.html      58 行   插槽 + 固定脚本顺序
kana-memory/kana.css       131 行   纸感卡片墙(照样张 B)
tools/tests/test_memory.js 430 行   116 项断言,DOM 桩驱动真页面
 tools/test.mjs              1 行   登记进 SUITES
票 01 完成后的页面:纸感底、五列四行的牌墙,全部牌背面朝上
票 01 合进来之后的样子。能玩了,但还没法看:牌背几乎与纸底同色,那一行「かな」淡得快消失,整面牌墙没有重量。这张图后来直接成为票 04 的输入——任务书里附的是这张截图,不是形容词。

注意这一版「不完整」得刚刚好:档位只有一个、最高分列显示破折号、结算条是空的、音效按钮点了没反应。但它已经能玩一局,所以它是可以被验证的——这也是为什么切票时要它先落地,而不是先把三个档位写完。

5.2每个 implementer 开工的第一件事:先写会红的测试

implement-spec 让每个 implementer 「调 Skill 工具跑 tdd」,不是为了仪式感,而是为了不让子代理把测试写成「跑得通就行」。tdd 里真正有用的不是红绿循环本身,是它规定的什么算好测试:只断言外部可观察行为,不断言样式、不测动画时长、不测音频波形。

风险(真实存在)测试怎么捉它
洗牌写错,牌墙出现两张一模一样的牌断言「一局里每对恰好两张、无重复无遗漏」—— 这条是从样张阶段的真实事情留下的
配错后不翻回去推进假定时器到 619ms 仍开着,到 620ms 回背面
同一张牌被点两次计入步数点两次,断言步数不变、状态不变
换档后旧定时器把新牌局改乱换档后推进旧定时器,断言新牌局不受影响(代际守卫)

票 01 交上来的时候测试数字是 ✓ 全部通过 (116),全仓 node tools/test.mjs 是 26 通过 · 0 失败 · 9 跳过 · 2432 项断言。那 9 个跳过不是失败——是需要本地源材料或密钥的套件,在别人克隆的仓库里本来就不该跑。

5.3开二条线:并行写入的边界在哪

票 01 合进集成分支后,前沿变成两张票(02 和 04),于是同时派两个子代理,各自一个 worktree、自己的分支:

git worktree list同一仓、同一条 main 的四个检出口
library           0253041 [feat/kana-memory]   ← 集成分支,只有它往 main 上合
library-wt/01     3823508 [feat/kana-01]       ← 已合并,留作对照
library-wt/02     0253041 [feat/kana-02]       ← 进阶档 + 档位切换(进行中)
library-wt/04     0253041 [feat/kana-04]       ← 手感与可达性(并行)

并行的真正约束不是 CPU,是文件归属。两个子代理同时改一行,合并时就只能手工猜。所以派活时先写一张归属表:谁拥有 kana.css、谁拥有 page.js、谁的测试文件只归谁。这张表还提前把冲突点写成了规则——「02 只能往 page.js 加两处,上限 8 行」,而不是「你们小心点」。

5.4两条线撞在一起:冲突的解法不能靠「看哪边新」

票 04 交上来合并时报了两处冲突,都是并行写入的必然产物,不是谁的错:

git merge feat/kana-04CONFLICT ×2
CONFLICT (content): Merge conflict in kana-memory/index.html
CONFLICT (content): Merge conflict in tools/test.mjs

kana-memory/index.html 的脚本块:
<<< 集成分支(票 02 刚加的)
  <script src="../typing/data/n5.js"></script>
  <script src="../typing/data/n4.js"></script>
  <script src="tiers.js"></script>
=== 票 04 的分支
  <script src="feel.js"></script>
>>> feat/kana-04

低级的解法是「两边都留」——那么 feel.js 会排到 tiers.js 前面还是后面,完全取决于编辑器把哪块放在上面。真正的解法在施工约定里:脚本顺序是**契约**,写死成 romaji → game → n5 → n4 → tiers → score → feel → page。合并时按契约重排,而不是按提交时间或代码块位置。

tools/test.mjs 的冲突同理:两边各往 SUITES 里加了一行测试套件,两行都保留、按契约里的顺序排——一行是「进阶档 / 换档」,一行是「手感与可达性」。

这次合并完整跑了一遍三个套件(118 + 80 + 89 项断言)之后才提交。合并提交的职责不是「让 git 不报错」,是让两条线的语义都还在。

票 04 合并后的页面:牌背变成暖纸色带内衬细线,かな 加深,三档可切换
票 04 合进来之后。牌背从「和纸底一个色」变成暖一档的纸色加内衬细线,かな 从 1.9:1 拉到 7.3:1,牌墙终于像一面墙;同一屏里能看到三档 tab 都出来了。

这张图值得和票 01 那张并排看:同一条命令、同一个视口、同一个页面,中间只隔了一次「按截图改」——任务书里附的是那张截图,不是「请提高对比度」这样的形容词。子代理拿到的是可核对的差,改完也能自证改到了。

5.5守卫到期:子代理第二次停下来问

票 03 干到最后卡在一行测试上。集成分支里有一条断言是票 01 写的:

tools/tests/test_memory.js:412-413票 01 写的守卫
/* 02/04 各自把自己那行 script 加进来时不必改这条:只禁还没实现的 score.js(票 03) */
ok(!/<script[^>]*src="score\.js"/.test(html), 'score.js(票 03)还没被提前引用');

票 03 正是那个要让 score.js 存在的票。它一旦按 ARCH 加那行 <script src="score.js">,这条断言必红;而任务书同时写着「不许改 test_memory.js」「test_memory.js 必须全绿」。三件事不可能同时成立。

它没有选最省事的那条路。它停下来,把冲突摆出来,给了三种解法,并附上建议的那一种——而且指出同一文件里紧邻的两行已经是同构的正向写法(「若引了 tiers.js,它要排在 page.js 之前」)。

「测试变绿」和「守卫到期」不是一回事

这条断言的本意是「别提前引还没落地的东西」——它天生要被落地那一票翻面。所以批准不是「把测试改绿」,而是承认守卫到期了:判据写死成一句话——谁来落地这个文件,谁就负责把它从「还不存在」改成「若存在,必须在某两个脚本之间」。改动范围只有那两行,其余 117 项一个字不动。

这是同一个模式第二次出现(票 02 那次是「不许引 tiers/score/feel」的断言,同样由落地票翻面)。两次都不是流程出错,而是守卫的生命周期:写守卫的人知道它会在哪一票到期,就把到期条件写在注释里;到期那一票的负责人翻面,翻面要同构、要窄。子代理两次都停下来的行为,比它改对更难教——所以 ARCH 里现在有一句明文:规矩互相矛盾时停下来问,不要自己扩范围。

票 03 交上来的东西:score.js 234 行(高手档 + 每档最高分 + 结算条)、test_memory_score.js 551 行 / 139 项断言。两处自定的判断值得记:

  • 牌面小标签取 { a: 'ことば', b: '中文' }。a 面是「读音(附汉字写法)」,叫「読み」盖不住括号里的汉字,叫「ことば」才覆盖整面;b 面是释义,对日语读者不是读法而是意思,就不复用进阶档的「読み」。
  • 这一档的 m 有 11 个重复值、100 条 m 与某个 w 字面相同,所以占位集合必须同时记 a、b 两面——只记一边会发出一局**没有唯一解**的牌墙。
高手档:一张翻开的牌显示 えいがかん(映画館),两行折好不溢出,右下角小标签 ことば
高手档里最长的那类牌面。实测 a 面最长 14 字(せんもんがっこう(専門学校))、13 字以上共 43 条;牌面 white-space:normal 在字间折行,两行放下——这张是 10 字的 えいがかん(映画館)。

结算条是这一票的收尾:本局步数、用时、是否刷新该档纪录,加「再来一局」「换一档」两个出口,出现后牌面不可再翻。设计上最要紧的一条是纪录按档分开记——入门档 10 步满分和高手档 10 步不是一个东西,混在一个键里,进阶难度就没意义了。

通关后的牌墙:20 张全部带勾与朱色边框,顶部 10/10、10 步、00:07、本档最佳 10 步·00:07,底部结算条含两个按钮
一局打完。四行牌全部配对、顶部「本档最佳」写入、底部结算条给出「本局 10 步 · 用时 00:07 / 新纪录」和两个出口。
真实的一次升级:子代理不许自己改契约

票 02 开工五分钟就停下来问了三个阻断点,其中一个很典型:施工约定要求它自己的脚本注册到「档位表」,但那个表要到 page.js 末尾才存在,而 boot() 就在后面一行——于是刷新页面时保存的「进阶档」会被回落成「入门档」。它给了三个方案(改两行 / 加一行守卫 / 一行不改但靠内部实现细节),并说明推荐哪个、为什么不推荐第三个。

这是并行开发里最值得期待的场景:子代理不能自己改契约,但必须把冲突升级上来,而且带着方案。最后选了第一个方案——允许它改 page.js 六行,同时由主线程把施工约定回写一份:契约改了,文档必须同批改,否则下一个子代理会拿着旧约定开干。

5.6最后一张票最轻,却最容易踩人

05 的活儿看着最没技术含量:门户加一张卡、写一份 ADR、补两处文档。它踩到的全是别人早写死的数字——三处测试各写了一遍「语言学习区有 12 张卡」(test_area.js:48、test_reading.js:283、test_typing_en.js:380),加一张卡就得改三遍;谁忘了改,测试红在同一句看不懂的话上。

门户里新加的假名翻牌卡片:kicker 日本語·练习、pill 音效、标题假名翻牌、chips 3 档难度与 10 对牌、badge 3 档、打开 →
新卡落在「日本語」分组的最后一张。data-tags="词汇 练习" 决定它被哪些筛选项命中;pill 写「音效」是因为声音是这台设备上合成出来的,不需要耳机文件;卡片故意不带 data-en-key——全页必须恰好 107 项那条门禁就靠这一点不被误伤。

它顺手处理了一个更麻烦的东西:ARCH 里写的「给新页加 rail」并不该做。这个页面为了自成一格,没有引 assets/style.css,而 rail.mjs 的规则是「引了共享样式才必须有 rail」——照着 ARCH 去加,反而会破另一条先例(typing/ 就是这么自成一格的)。

规格与实现不一致时,改规格,别改实现

这类偏差最容易被当成「漏做」。判据是:哪一边的先例更硬。这里是先例更硬(同仓已有两个模块不引共享样式),所以正确动作是让 ADR 把「为什么不加 rail」写下来,而不是硬加一个 rail 让它跟 typing/ 长得不一样。docs/adr/0013-kana-memory-module.md 就记了这一条,顺带解决了 ADR 编号被 0012 占掉的撞车。

阶段 06

评审:两个轴,故意不合并

code-review — 标准轴 · 规格轴 · 并行两个子代理

code-review 的全部主张就一句话:一次评审只能回答一个问题。所以它把评审劈成两个轴,各派一个子代理,拿回两份报告,并排放着——不合并、不重新排序、不选出「最重要的一条」。

两轴以及它们分别会漏掉什么
标准轴  git diff main...HEAD 里的每个 hunk,违反仓库写明的哪条规矩?
        外加一份「坏味道基线」(Fowler 《重构》第 3 章),仓库有文档就以文档为准。

规格轴  同样一份 diff,对照 SPEC:哪些要求漏了 / 只做了一半?
        哪些行为规格根本没要(scope creep)?哪些看着做了但做法是错的?

代码合乎所有规范,但做错了事 → 标准轴过,规格轴红。
代码完全照规格做,但破坏了项目约定 → 规格轴过,标准轴红。

这个设计要挡住的是**掩盖**:如果一份 diff 的格式问题很明显,一次「什么都看」的评审就会把整篇篇幅花在格式上,规格偏差藏在最后一行;反过来,一个人拿着 SPEC 逐条对账,就会对重复代码和神秘命名视而不见。分开跑,两种漏法都堵住了。

终端钉住不动点
$ git rev-parse main          # 固定点必须先解得出
7ff91f6c3c0b2a8eccbf8a1c67a6d1a0b9f2a7d5

$ git log main..HEAD --oneline
e245b58 feat(kana-memory): 合并票 04(手感与可达性)
28268ac feat(kana-memory): 合并票 02(进阶档 + 档位切换)
0253041 feat(kana-memory): 合并票 01(一局能玩)
0c1dd7d test(kana-memory): 静态断言只禁 score.js…

$ git diff main...HEAD --stat

固定点用**三点** diff(main...HEAD),比较的是 main 与分支的 merge-base,不是 main 的尖端——否则你评审的东西里会混进别人这期间合进 main 的提交。skill 里还有一条前置检查:固定点解不出来、或者 diff 是空的,在这一步就失败,别等到两个并行子代理里才炸。

标准轴要什么,skill 说得很具体:找到仓库里每一份写「代码该怎么写」的文件(CODING_STANDARDS.md、CONTRIBUTING.md 存在就必须在列),再把那份坏味道基线原样贴进子代理的任务书——子代理的上下文是干净的,它看不到你手里的基线。本仓库的清单是四份:CODING_STANDARDS.md、AGENTS.md(架构约束与不变量)、docs/portal-map.md(真源→生成器→校验)、docs/adr/(已决架构,如 ADR-0011 要求新模块同批改四处)。

「判断」和「违规」不是一回事

基线里的每一条都只是带标签的启发式(「可能是 Feature Envy」),永远不是硬违规;而仓库文档里写明的规矩可以是硬的。skill 还给了两条铁律:仓库文档覆盖基线(文档承认的做法,基线就别再挑),工具已经管的事不要报(格式、行尾、Lint 能查出来的,别浪费评审的篇幅)。

6.1 真跑一次:两个子代理,两份报告

任务书里各塞三样东西:固定点(git rev-parse main 解出来的那个 sha)、要读的文件清单、输出路径(.scratch/kana-memory/review/{standards,spec}.md)。两个都跑完、都读完,再动手改任何一行。

两轴各看到什么标准轴 1 硬 + 9 判断 · 规格轴 1 P1 + 3 P2
标准轴  硬违规 1  CONTEXT.md「模块」词条还写「十一张入口卡 / 日语侧七张」,
                  而同批 README.md 已经改成 13 张 —— 两份文档互相矛盾
          判断项 9  四个测试里逐字重复的 fakeTimer / loadPage(该抽进 dom.js)
                   wordPool、fmtMs、restart 三处重复
                   tiers.js 有「KanaApp 已存在」分支 / score.js 无条件 push
                   score.js 反查 DOM 只为取档位顺序(Feature Envy)
                   emit 挂进公开 API 但无调用者(Speculative Generality)
                   「一局 10 对」散在五处(Shotgun Surgery)
                   票号注释失真 8 处
                   反向断言:点 #k-sound 后仍断言 aria-pressed === 'true'
                   门户徽章「3 种模式」与正文「三档」串味

规格轴  P1      ≤640px 仍是 5 列,375px 下每张牌约 63px 宽、
                  正文区 49×60px,而字号是 21px —— 高手档 13 字的牌面
                  要排 7 行(约 147px),牌与牌直接互压
          P2      CONTEXT.md 缺「假名翻牌 / 档位 / 牌对 / 一局」
                  翻牌时 #k-live 播的那句与这张牌的 aria-label 同文(读屏念两遍)
                  结算条的 .k-btn / .k-result-* 造了但 kana.css 里没有规则

注意两轴的口径差别:标准轴把 CONTEXT.md 判成「硬违规」,因为它违反 AGENTS.md 里「术语唯一真源」这条写明的规矩;规格轴把同一件事判成 P2,因为它的依据是 SPEC 里「需要增补词条」的建议性表述。同一处毛病、两个轴、两种严重度——这就是 skill 坚持不合并两份报告的理由:并排放着,你来决定哪个标签算数。

规格轴那条 P1 值得单独说:它做的是拿尺子量——375px 屏、5 列、每张 63px、正文 49×60px、字号 21px、最长牌面 13 字,算出来要 7 行。这种结论代码里看不出来,只能算。我复核时用 playwright 把页面压到 375px 一量,scrollHeight - clientHeight 是 18~28px,跟它算的方向一致。

6.2 修补:一次提交,逐条对上

评审本身不改代码。改代码的是「读完两份报告的人」——也就是我。我挑了自己认的(硬违规 + P1 + 三条便宜的 P2),聚成一个提交 52bf5e4,每条在提交说明里对上评审原文。

52bf5e4fix(kana-memory): 评审后的修补
kana.css      窄屏 5 列 → 4 列 + .k-word{font-size:clamp(12px,3.1vw,19px)}
              .k-face{overflow:hidden} · .k-tile{aspect-ratio:1/1.12}
              补 .k-result-record / .k-result-actions / .k-btn / .k-btn--primary
page.js       删掉翻牌时那次 announce(tileLabel(...))     aria-label 已经念过同一句
score.js      「再来一局」加 k-btn--primary
test_memory.js  #k-sound 那条反向断言改成只验形状
              type=button + aria-pressed ∈ {true,false},真值由 test_memory_feel.js 覆盖
375px 窄屏下的假名翻牌:四列牌墙,高手档长牌面换行不溢出
修完的 375px:grid-template-columns 算出 4 列、每张 81px、最长牌面 13 字不溢出(scrollHeight - clientHeight = 0)。修之前这一格是 18~28px 的溢出。

四条里有一条要承认是没修的:重复的 fakeTimer / loadPage / wordPool / fmtMs。抽出去要同时动四个套件和 tools/tests/dom.js,评审之后、PR 之前的窗口里,这属于「改动面积大于收益」。我把它记进 retro 的「欠账」,让下一轮开票时有据可查——评审报告的价值不在于每条都被修,而在于每条都被记录且被分过类。

评审读的是仓库,不只是 diff

那条硬违规的现场(CONTEXT.md)根本不在 diff 里——它被 .gitignore 排除,只在本机存在。搜索规格轴也一样,它翻的是「术语真源」这份文档本身。这条差点被我判成误报:我改完 CONTEXT.md 想提交,git add 直接回「该路径被忽略」。结论是改名与词条这类「本机真相」类文档,只能靠评审去对,机械校验永远看不见它。

阶段 07

交付:PR 正文只回答三个问题

pr — 图说改动 · 前后证据 · 合并风险

pr 这个 skill 只有一段模板和一句劝告:跳过一切前言,只留下能回答问题的部分。模板三段,各有各的读者:Summary 给想知道「改了什么形状」的人,Evidence 给不信「能跑」的人,Merge Danger 给要按合并键的人。

pr/SKILL.md模板 · 全文
## Summary
<diagram, diff-sketch, or tree>

## Evidence
- **Before:** <screenshot/output/failing test run>
  **After:** <screenshot/output/passing test run>

## Merge Danger
**Door:** <one-way or two-way>
**Blast Radius:** <one-word description>

三段里最容易被写坏的是第一段。Summary 不是「改了哪些文件」的清单,而是一张能让人一眼看懂形状的图:改逻辑就给伪代码,改控制流就给调用树,改文件归属就给浅树,改界面就给组件树或 diff。本案例的改动正好是「多了一整块目录 + 门户上多一张卡」,所以用浅文件树 + 一段 diff:树说清新模块由谁负责,diff 说清既有结构里多了什么——两样都比三段文字快。

第三段是最少人写、最该写的一段。「单向门还是双向门」这个问题逼你想一件事:合下去之后好不好退。新目录 + 一张门户卡 + 四处断言数字,一个 revert 就回去了,是双向门;但「三处断言与新卡是同批改的」这条要写出来——只回退一半,门户测试会红。

draft PR 的时机是流程硬定的

implement-spec 第 3 步说得很具体:首次合并之后再开 draft PR——因为一个还没领先 main 的分支连 PR 都开不出来。这条不是形式主义:有了 PR,后面每合一张票,评审者看到的是一个在长的东西,而不是最后一坨。

终端首次合并之后
$ git push -u origin feat/kana-memory
 * [new branch]      feat/kana-memory -> feat/kana-memory

$ HTTPS_PROXY=$PROXY gh pr create --draft --base main --head feat/kana-memory     --title "假名翻牌:一个新的顶级练习模块" --body-file .scratch/kana-memory/pr-draft.md
https://github.com/Yang-Code01/jlpt-library/pull/40

PR 正文的定稿不在这时候写。draft 阶段它只需要两件事:说清这是什么、勾出票的进度——让人知道现在看它有没有意义。真正的正文留到 code-review 之后,用 pr 的模板重写一遍,然后 gh pr edit 覆盖上去。

GitHub 上的 draft PR 页面:标题、五张票的勾选清单、Draft 标记
draft PR #40。这是第一次合并之后立刻开的那一版:只回答三个问题——这是什么、票到哪、怎么验。模板里最难写的两段(前后证据、合并风险)故意留空,它们要等评审跑完才有内容。

评审跑完、修补提交之后,这一版被整篇覆盖掉。定稿的第一段不再是一段话,而是一棵树加一段 diff:

PR #40 定稿 · Summary树 + diff
kana-memory/
├── index.html      # 页面骨架:牌墙 / 四项计量 / 结算条 三个插槽区
├── kana.css        # 独立样式(不引 assets/style.css,不引 theme.js / progress.js)
├── game.js         # KanaCore:洗牌、发牌、翻牌判定、一局结算
├── tiers.js        # 入门档 假名↔罗马音 · 进阶档 汉字词↔读音
├── score.js        # 高手档 日语词↔中文释义 · 每档最高分 · 结算条
├── feel.js         # 合成音效(无音频文件)· 键盘 · 减少动效
└── page.js         # KanaApp:渲染、计时、播报、事件

index.html · 语言学习区「日本語」分组
   <article class="card" data-tags="词汇 练习">
+    <span class="pill">音效</span>
+    <h3><a href="kana-memory/index.html">假名翻牌</a></h3>
+    <div class="chips">3 档难度 · 10 对牌</div>
+    <span class="badge">3 档</span>
   </article>

第二段用 before/after 成对给:票 01 的牌墙(牌背与纸底同色、かな 1.9:1)对票 04 之后的牌墙(暖纸色 + 内衬线、7.3:1),375px 的溢出数字对 375px 的截图,最后贴四个套件的通过行和全仓 34 通过 · 2 失败——把两个红也贴上去,并写清它们是环境既有(未跟踪的第三方插件、三个英语页漂移),不是这次改出来的。

PR #40 定稿 · Merge Danger两行就够
Door          two-way —— 新目录 + 一张卡 + 三处断言数字,revert 一条命令回去;
              唯一要当心的是三处断言与新卡同批改,只退一半门户测试会红。

Blast Radius  portal-only —— 只碰门户卡片清单与「12 张卡」那三处断言;
              data-en-key 一项没动(全页仍恰好 107 项),既有模块运行时一行没碰。

「单向门还是双向门」这个问题逼你想一件事:合下去之后好不好退。新目录 + 一张门户卡 + 四处断言数字,一个 revert 就回去了,是双向门;但「三处断言与新卡是同批改的」这条要写出来——只回退一半,门户测试会红。

终端覆盖 draft 正文
$ HTTPS_PROXY=$PROXY gh pr edit 40 --body-file .scratch/kana-memory/pr-body.md
https://github.com/Yang-Code01/jlpt-library/pull/40
GitHub 上 PR #40 定稿正文:Summary 是文件树与门户卡片 diff,下面是 Evidence 的 before/after 与测试输出
定稿后的 PR #40。同一个链接、同一份 draft,正文被整篇换掉:Summary 变成树 + diff,Evidence 成对给前后与测试输出(两个红也照贴),Merge Danger 只剩 Door 与 Blast Radius 两行。

PR 本身也留着:github.com/Yang-Code01/jlpt-library/pull/40(draft,base main)——这一篇里的每张截图、每个行号、每条断言计数,都能在上面逐条对上。

阶段 08

复盘:复的不是代码,是环境

retro — 候选按严重度排 · 机械的做成检查 · 判断的写成规范

很多人以为 retro 是「这次哪里做得不好」,retro 的靶子根本不是代码,而是下一次同类任务的环境:导航、自动化检查、编码规范、AGENTS.md、工具经济、信息获取。产物不是感想,是候选改进清单,按严重度排。

它还给了一条判断谁该干什么的原则,用一句话就能说清为什么评审要比实施更严:实施代理的上下文压力最大(要探索、写码、调错),评审代理最小(拿到 diff,不用探索)。所以规范该压在评审这一侧,实施侧只配导航指针。

8.1 这次排出来的七条

复盘候选按严重度降序 · 前三类:信息获取 / 机械违规 / 工具经济
1 信息获取    评审子代理读不到 git
                两份报告开头都写着「无 git/shell 权限,main...HEAD 未亲核」
                只能把「工作区 = 分支 HEAD」当 diff:未跟踪文件、被忽略的本机文档、
                提交粒度全部看不见 → 主代理预先把三点 diff 落成快照塞进任务书

2 自动检查    「12 张卡」写死在三处测试
                test_area.js:48 · test_reading.js:283 · test_typing_en.js:380
                每上一个模块改三遍 → 改成从 portal 的桶推导,张数不是不变量

3 工具经济    为六张证据图写了八个 shot-*.mjs(同一件事的八个小变体)
                → 入库一个 tools/shoot.mjs:文件 / 锚点 / 视口 / 动作脚本

4 自动检查    guardNoFetch 误报第三方:一个未跟踪的 plugins/ 插件点亮了「禁 fetch」
                → vendored 白名单,或只扫本站运行时可达的文件

5 steering    AGENTS.md 太肥 · 6 票号注释过期 · 7 两处 append-only 区域注定冲突
                → 指针化 / 机械的做成检查 / 冲突解法写成一行可执行的话

这七条里没有一条是「下次写 CSS 要更小心」。这正是它跟「自我检讨」的区别:能自动化的就不要写进规范,规范只留给判断类(比如「源码注释只描述当前行为,票号只出现在提交说明里」)。

8.2 走到这里,这条流水线交出了什么

阶段产物规模
01 拷问决策表(题材 / 完成度 / 分支 / 截图形态)4 轮问题
02 样张三份能点开玩的 HTML + 目录页3 份 · 选 1
03 规格SPEC.md(问题 / 方案 / 16 条故事 / 决策 / 测试 / 不做)6 节
04 切票5 张 tracer bullet 票 + 依赖图5 张
05 实施模块 7 个文件 + 4 个套件 + 门户接入 + ADR1202 行 · 427 条断言
06 评审两轴报告 + 一个修补提交1 硬 + 12 判断 · 修 6 条
07 交付PR #40 正文(Summary / Evidence / Merge Danger)3 段
08 复盘七条环境改进候选7 条

整条链子的摩擦只集中在三个地方:契约的边界(三个子代理各自撞上同一处守卫)、共享文件的追加区(每次合并都冲突)、评审看不见 git。剩下的事——切片、并行、测试、截图——都顺得像走过很多遍的路。这也算这个 flow 的实测结论:它省下的是返工,不是思考;省不掉的地方全在接口上。

支线

这套流水线里,有三种活不该走主线

triage · diagnosing-bugs · wayfinder · improve-codebase-architecture

主线的假设是「你知道要做什么,只是还没想清怎么做」。有三类活不满足这个假设,硬套主线会得到一份规格性很强的错误答案。

什么时候叫它skill它做完之后你手上多出什么
别人(或未来的自己)塞给你一堆 issue / 外部 PR,乱七八糟没分诊triage每一条都被分成「需补充 / 可交给 agent / 需人工 / 不做」,可交给 agent 的那些还附了一份能直接开工的简报
它不是「要加什么」,而是「坏了」——报错、慢、偶发diagnosing-bugs一条能复现的证据链和最小修法,而不是一圈猜测式改动
你连「下一步该往哪走」都不知道,方向比方案更缺wayfinder一张把可能性摊开的地图,以及一条便宜的验证路径
没有新需求,但你感觉这块代码越来越难改improve-codebase-architecture按「深模块」重新划一遍边界,给出一批可独立合入的加深手术

本案例一条都没走:需求是新的、没有故障、方向从一开始就明确(新开一个练习模块)、模块本身是新建的(没有历史包袱可体检)。不用某个 skill 不是缺失,是因为它解决的问题这次不存在。左栏把 handoff 也留在这里,理由相同——这一整轮从访谈做到复盘都在同一个会话里,没有需要交接给下一个会话的断层。

反过来看:什么时候必须要 handoff

如果一个会话做不完(上下文要满了),或者要换人接手,就必须把「已经谈定的决定 + 现在做到哪 + 下一步的入口」压成一份文件。它的写法与本篇的流水线一致:指针多于复述——写「规格在哪个文件、第几张票没做完、分支叫什么」,而不是把规格抄一遍。

这份报告属于 个人工作站 › 调查报告 ← 返回报告清单
返回调查报告