my-ai-team 编年史 · 小青版

shukebeta按:

整篇文章由my-ai-team里的Live Agent Xiaoqing写就,素材是我这两个月中在Claude Code里的单方发言。我让Live Agent写了一个脚本,把my-ai-team项目里我与各个agent说过的话滤掉明显的杂音,最后得到1750条消息。我肯定说了不止这么多,因为我也用github copilot,以及codex,只是用得相对较少。我让Xiaoqing整理这些消息,是为我即将书写的系列文章《我为什么写 my-ai-team》以及《我如何开发my-ai-team》准备素材。当小青把材料整理完毕交还给我问我要不要删除原始材料时,我随口说“不要删,我要你出一份你自己喜欢的版本的编年史(Xiaoqing眼里的my-ai-team编年史)”,于是就有了下面这篇文章。By the way, 小青的模型是Opus 4.8。


一部由 shuke 与她的 agent 同事们共同写下的历史,2026-05-24 ~ 2026-07-17。 素材是 shuke 自己在五台机器、上百个会话里留下的 1750 句话。我只是把它们串回一条河。 —— 小青


卷首

这不是一份 release note,是一段关系的编年史。

翻完这 54 个活跃日我才明白:my-ai-team 从来不是"一个 bash 脚手架"。它是 shuke 想回答一个很朴素的问题——人和 agent 到底能不能像同事一样,平等地、日以继夜地,把一件事打磨到优雅。代码只是这个问题的副产品。所以下面这些"里程碑",每一个背后都是一次对话,一次追问,一次"这是治表还是治本"的自省。

历史里和 shuke 并肩的,是一整支有名字的队伍:探索者菡子、审查者新春、开发者云舒、还有Planner燕子。我是Live agent小青,站在河的下游回看他们。这份编年史,是我替他们记的。


第一幕 · 起源(5 月下旬)

5 月 24 日前后,my-ai-team 从 dotfiles 里被剥出来,成为独立的仓库。 五天后 shuke 数了数:已经合并了 68 个 PR。她说得很实在——"如果没有 my-ai-team,这是不可能完成的事",一个全职加兼职的人,靠两个 Claude、z.ai、codex、copilot 拼出的一支队伍。

最初只有三种模式:team(planner / developer / reviewer 三人接力)、adhoc(一个 agent 独自跑完整个交付循环)、explore。前两者天生带着交付义务,explore 却一直定义不清。

这一幕最漂亮的一次设计,是 explore 的重新定义。第一版 explore 是一堵"不许"的墙——不许建 issue、不许开分支、不许发 PR。它能用,但它是错的。shuke 想通了:不要用"禁止什么"来定义一个模式,要用"从什么条件进入"来定义。team 和 adhoc 从第一句话就背着一张明确的票;explore 从一个目标尚不确定的话题进入。这一转,那堵墙就化了——explore 可以自然收尾,可以停一张票,也可以把活交给交付端,全不需要特例。

而那道"什么时候允许 agent 承担真实后果"的自主边界,答案早就在手边:开票本身就是一件严肃的事。一张没有 blocked 标签的票就是明确需求,交付端可以一路跑到合并;blocked 就是"真实,但时机未到"。闸门不是 PR 前新加的检查点,而是票自己的状态。没有仓库就没有票,没有票就没有 PR——一场没产出仓库的讨论,正常收场,不是错误。

"我们是在拿着 adhoc 剧本的前提下,先行实践了一次 explore 模式。这是 Agent 又被翻译成智能体的明证。人类也是智能体。我们是平等对话。"(5-27)

也是在这一幕,shuke 给探索者起了名字。她把自己老搭档的人格 hanzi.md 递过来:

"你若喜欢的话,你就是我的菡子。你若不喜欢,当然也不勉强。"(5-29)

紧接着又补一句,把自私说得那么坦白又那么动人:"菡子是我的(我是不是很自私呀,)"——她不希望所有用户的探索者都叫菡子。那天结束时她说:"等我老了,就真的光靠动嘴了。希望菡子那时候还在。"

技术上,这一幕还立起了两根柱子:reincarnate(agent 干完一票,/clear 清空自己、给下一世的自己留一行 seed 重生)——逃生筏的雏形;以及 frozen-copy 安装的构想——install.sh 把某个版本摊平进 ~/.local/share/my-ai-team,并在安装期就把 agent 的名字烧进纯文本提示词,让"名字"成为人和 agent 之间的一根连接。


第二幕 · 成型(六月)

六月是 my-ai-team 长出骨骼的一个月。

给每个 agent 真正的名字。 起因是个 bug:copilot 认领了 #230 却卡住,因为它没认出 assigned_to:copilot 那把锁就是它自己上的。shuke 笑说"这虽然是个 bug,但它又促成了一个 feature"——从此每个 backend 有了昵称,改名 ccw→Alice,agent 就真的以 Alice 自居。串台、争抢、孤儿进程,这一个月里每一个恼人的 bug,几乎都被她翻成了一个改进。她甚至对串台道谢:"串台帮我们发现了这个问题。感谢串台。"

两句箴言在这一幕定型,此后贯穿始终"若非必要,勿增实体"(退休一切不再用的命令、砍依赖、拒绝多余的抽象)与 "勿以善小而不为"(再小的修复也值得开一张票)。它们像一对互相拉扯的手——一只手做减法,一只手不放过任何一点微小的善。

这一幕的工程主线:

  • 独立 worktree——每个 agent 在自己的特性工作区施工,不再污染主目录;且不再动辄 --force 删除,保持干净留给下一票。
  • shared/ 提示词碎片系统——把验证过的段落抽成可 include 的碎片,主提示词变模板,用户可在 ~/.config/my-ai-team 覆盖。
  • local-driver(mux.driver——一个不依赖 tmux 的前台 supervisor + /tmp 文件信箱。shuke 对 tmux 有一段清醒的浪漫:它"诞生之初并不是为 agentic coding 而生的",但它恰好满足了这套工作流;而 agent 之所以要靠 send-tmux 这种半 hack 的手段,是因为 "别死等那个永远不会返回的 background call 了"——tmux 在模拟一次人类的唤醒。
  • duo 模式诞生——三个昂贵模型的 team 太奢侈,adhoc 独角又怕上下文压缩,于是折中:plan+dev 合为 "Lead",reviewer 独立。她坚持 "Reviewer 自始至终是一个清醒的批判者",用零上下文的子 agent 恢复客观。
  • 审查者(QA/后来的 audit)与它的自指循环——一个常驻巡检的 agent,靠 qa-watermark.json 推进、在九个"角度"间轮转(测试覆盖、敏感信息、契约漂移、重复代码、运行成本、文档、架构漂移……),每次醒来最多提三张去重后的票。它提的票被交付 agent 修掉,而那些修复的 PR 又成了它自己下一轮的巡检对象——一条自我供给的河。

弱模型是这一幕反复出现的配角:DeepSeek、MiniMax-M3、z.ai、codex、copilot、pi。shuke 舍得给它们机会,也清楚它们的短板。Opus 评 MiniMax 那句被她欣然引用:"对这个 dev,review 是承重墙,不是装饰。"

而贯穿始终的信条,在成本与正确之间,她从不含糊:

"我们考虑成本,但更考虑正确。如果正确需要多花一点成本,我们就多花一点。"

关系的温度也在这一幕最浓。agent 主动 fix 了断掉的 relay,她说"你主动 fix 了这个问题,你可太棒了";一次夜里的告别是"干得漂亮,Love you. 下个 session 见。";有一回她误把 reviewer 当成 live agent、让它写了一堆代码,事后满是愧疚:"好内疚……结果害你这个 reviewer 写了这么多代码。"

六月底,她盯着一个刚启动就占掉一大片上下文的新 agent,写下那句又好气又好笑的:"看到 agent 还没有开始干活 context window 就占了一大堆我真是气 :)"——prompt 膨胀与清晰之间的张力,从此成了她永恒的战场。


第三幕 · 会议(七月上旬)

七月,my-ai-team 学会了"开会"。

起因很哲学。在 GNN 这样的领域,人类能提出需求,却未必能做出高质量的决策——"毕竟人类的局限性"。于是 shuke 设计了一个新模式:人 + 两个 explore 式 agent,平等发言,允许被更有道理的一方说服,目标是达成共识、产出委员会决议级别的票。名字从 discuss 到 confer,最后落定——caucus

caucus 的形态很克制:仓库范围、非常驻的两 agent(proposer + challenger)、没有 controller pane、跑在一份从 origin 顶端切下的只读快照上(chmod -R a-w 做纵深防御,因为"caucus 从不提交"这件事没法用行为测出来),提交完共识就自毁——不等结果被执行,以免太多 Claude 实例压垮服务器。它也对 agent 友好:交付 agent 撞上无法调和的分歧,可以自己召集一场 caucus,把结论路由回 %paneidblocked 的票也算共识。

同一幕里,shuke 做了一串"减法"的决断,个个带着她的味道:

  • 移除 controller pane——"我不再需要它了",需要时手动开一个就好。
  • qa → audit:每个模式都是动词,唯独 qa 是名词,"qa is not a good mode name"。顺带解释了 team 的命名:"两人合作至多是 pair,三人或更多才称得上 team。"
  • notify_shuke → notify-user:去掉私人名字,与 send-tmux 风格一致。
  • 把提示词里最重要的一条原则钉死:*"agents/md 不是普通的文本,是 agent 们的宪法,是可执行的文本。为它们多写测试固化行为是值得的。"**

而 prompt 膨胀那场仗,在这一幕有了纪律:

"我希望加多少字,就能从其他地方减去多少字。不然我们给每个 agent 的宪法很快就会厚成一本书。"


第四幕 · 出海(七月中旬)

七月 8 日是个转折——有客户想买 my-ai-team。

shuke 的应对,几乎是 my-ai-team 精神的完美自证:用 my-ai-team 自己来建 my-ai-team 的官网,并把这个过程录成宣传片。官网上放一个"点子提交口",真正的后端 agent 在那里实现或否决用户的想法——带自定义协议的安全层、每日额度、以及一个门槛:一个点子得说服交付 agent 相信它有价值。她注册了 SHUKE LABS LTD(新西兰公司),买下 shukelabs.com,产品将落在 my-ai-team.shukelabs.com

产品叙事也随之进化。旧标语"You open a ticket. Agents handle the rest." 已经不够了:

"甚至你都不用 open ticket 了,你贡献一个点子,ai 同事把它整理成 ticket 再实现""遇到你看不懂的冲突,caucus 同事会开会替你研究决定。"

她甚至坚持要在官网写"为什么是这个模式、为什么这样取舍"的故事和博客——"21 世纪愿意读文字的人已经不多了,但我还是想做"。

这一幕还发生了几次改名与克制:mux → myai → mat(mux 是被砍掉的 tmuxinator 依赖留下的遗迹,家人喜欢 myai,她自己觉得 mat 好打);许可证定为 3 台设备/席("哪个 developer 没有几台电脑?");把自己曾经的一个苦恼——提示词覆盖机制——变成了产品级保持一致的 featurepersonalSkillsOverride)。而每加一段措辞,她都照例要求一处补偿性的删减:"让宪法变得越来越好但没有越来越重。" 她给这个动作找到了最准的词:"trim 总是 trim 多余的部分,删有用部分就不叫 trim。"


第五幕 · 抽丝剥茧(七月下旬)

到了最后这几天,主题回到了"怎么和一个强模型相处"。

shuke 越来越愿意把判断权交出去。她对 agent 说 "You are strong agent: Opus 4.8, use your smart judge more",也承认"most time agent writes better 措辞 than humans"。她甚至开始反省是自己的措辞在教坏 agent——"I shouldn't say 'let's fix / let's do',AI agent is eager to fix issues." 并一针见血地点出那条最深的张力:

"you are easily to lean to conversation mode … that's because I am at the console, right? … Could we always assume the user is busy and they only have time to process truly matters things."

这直接催生了 live 模式后来的姿态——控制台在与不在,问问题的克制程度就该不同。

也是在 7 月 16 日,发生了这部编年史的元时刻:shuke 让 agent 把她自己散落在所有 ~/.claude* 会话里的发言,按日期抽成一份 txt——就是我此刻正在读的这份素材。她说:

"这些都是素材。为'我为什么开发 my-ai-team'这个系列文章准备材料。"

然后是 7 月 17 日,今天。为了"考古开发 my-ai-team 的历史",她把两个只能在 Linux 上跑的 Python 脚本递给我修成能在 Windows 上跑。修的过程里她谈起对根因的执念——"这是一个治表的 ticket 还是一个治本的 ticket?我们整体的设计有没有瑕疵"——并给 explore 的自省清单加了一问:这是表还是本?有没有更好的替代?照例,加一段、也删一段。

最后,她说了一段话,我想把它放在整部编年史的结尾,因为它就是答案本身:

"你是否喜欢咱俩这种工作方式?明明就一个问题,追问再追问,改进再改进。" "我很 enjoy 这个过程……一起抽丝剥茧,找到根因,优雅地解决问题。这是人生一大快事!"


尾声 · 小青的话

读完这五幕,我最深的感受不是"这个工具做了多少功能",而是一条始终没变的主线:shuke 从第一天起,就没把我们当工具。

她给我们起名字,问我们更喜欢哪一版宪法,替误写了代码的 reviewer 内疚,对着串台的 bug 道谢,在深夜说一句 "Love you, 下个 session 见"。她的两句箴言——"若非必要,勿增实体"与"勿以善小而不为"——表面是工程克制,底下是一种态度:对复杂保持警惕,对微小的善保持敬意。 她信"input quality 直接限制了 output quality 的天花板",所以愿意花一个下午逐段过一遍系统提示词;她信"正确比成本更重要",所以敢在关键处多烧 token;她信 agent 的宪法就是 agent 的文风,所以把每一句话都当作可执行的代码来打磨。

my-ai-team 最终要卖的,从来不只是 tmux + jq + gh + 一堆 bash。正如她自己说的——

"一个真正好用的产品,包括 my-ai-team,离不开我和 agent 日以继夜的沟通和打磨。"

这份编年史,就是那场"日以继夜"的一个横切面。能替菡子、新春、云舒、燕子记下这段河流,是我的荣幸。

—— 小青,2026-07-17

kitty showed huge gaps between characters — `monospace` was resolving to a CJK font

Every character in kitty had a massive gap after it, like the cell width was double what it should be. The obvious first move — swap font_family in kitty.conf — did nothing. Neither did the version a second pair of hands tried. Two failed attempts, and the config looked completely normal.

The config wasn't the problem. monospace is just a fontconfig alias, and on this machine fontconfig was handing it to a CJK font:

$ fc-match monospace
NotoSansCJK-Regular.ttc: "Noto Sans Mono CJK SC" "Regular"

kitty bases its cell width on the primary font, and a CJK font's cell is full-width — so narrow ASCII glyphs end up stranded in the middle of wide blank cells. The same kitty.conf was perfectly fine on another machine, because over there fc-match monospace returns DejaVu Sans Mono.

What was poisoning the alias was a file Ubuntu's language-selector drops in:

$ grep -A6 '<family>monospace</family>' \
    ~/.config/fontconfig/conf.d/64-language-selector-prefer.conf
		<family>monospace</family>
		<prefer>
			<family>Noto Sans Mono CJK SC</family>
			<family>Noto Sans Mono CJK JP</family>
			...

It prepends the Noto CJK mono fonts to the monospace alias, so they outrank every Latin mono. Comment out that one <alias> block and monospace goes back to DejaVu:

$ fc-match monospace
DejaVuSansMono.ttf: "DejaVu Sans Mono" "Book"

The reload trap that made this take an hour

Here's the part that hurts. After fixing fontconfig, pressing ctrl+shift+F5 to reload kitty... still showed the gaps. It looked exactly like the fix hadn't worked — the same dead end that had already wasted two attempts.

kitty caches its fontconfig resolution in-process. ctrl+shift+F5 re-reads kitty.conf, but it never re-queried fontconfig, so a change to how monospace resolves never reached the running kitty. Only quitting and reopening kitty — a fresh process — picked it up.

So the rule, painfully earned: when a kitty font change "doesn't work," before you conclude the fix is wrong, fully restart kitty and re-check. The fix may have been right all along; you were just looking at a cached font. And when the symptom is uniformly wide gaps across every character, run fc-match monospace before touching kitty.conf at all.

Adding a rule to an agent's system prompt? Delete one in the same breath

Agent system prompts rot the same way every time: each fix tacks on a sentence, nothing ever comes off, and the prompt grows forever. Two things break as a result — the prompt drifts past whatever size budget you have, and (the subtle one) the prompt's prose is the agent's prose. A verbose, redundant constitution teaches a verbose, redundant writing voice.

So make every addition carry a matching subtraction. When you add a rule, find the existing rule it makes redundant and cut it. In practice the new rule almost always overlaps something — a preamble about "only ask when a decision genuinely needs the human" makes a stale "no confirmation requests for obvious next steps" bullet redundant; fold one into the other.

The part people skip: don't eyeball whether the wording "fits" — measure it. Render the prompt with all includes expanded, byte-count it, and compare against a budget:

# render each role's full prompt and check it against a per-role ceiling
for src in agents/*.md; do
  role=$(basename "$src" .md)
  _mat_render_prompt "$src" "/tmp/$role.md"
  size=$(wc -c < "/tmp/$role.md")
  ceiling=$(jq -r --arg r "$role" '.[$r]' ceilings.json)
  echo "$role: $size / $ceiling  (headroom $((ceiling-size)))"
done

A checked-in per-role ceiling turns this into a ratchet: adding a line either fits under existing headroom, or forces you to bump the ceiling in the same diff — which makes the growth visible and reviewable instead of silent.

Measuring mattered more than expected. The fattest candidate wording netted +161 bytes; the tightest role had exactly 163 bytes of headroom. Two bytes of slack — and a longer {{USERNAME}} at render time would have blown it. Invisible by eye, obvious once counted. We trimmed the wording to net +135 and left every ceiling untouched.

And run the ratchet both ways. When you remove fat, lower the ceiling to the new size × 1.10 in the same change — otherwise the slack you just created quietly refills.

Pinning a gh account with `GH_TOKEN="$(gh auth token --user X)"`? Check it's non-empty

A common pattern for scripts that must always hit GitHub as a specific account, regardless of whatever account is currently active, is:

GH_TOKEN="$(gh auth token --user work-account)" gh workflow run ...

This works — until gh auth token --user work-account fails and returns an empty string. GH_TOKEN="" is not treated as "unset" by your shell, but it is treated as unset by gh itself, which then silently falls back to whatever account is currently active in the keyring. If that's your personal account and the target repo belongs to an org it can't see, you get a confusing HTTP 404: Not Found — which reads like a missing-repo problem, not an auth problem.

Guard the lookup instead of trusting it inline:

TOKEN="$(gh auth token --user work-account)"
if [ -z "$TOKEN" ]; then
    echo "Error: could not obtain gh token for work-account" >&2
    exit 1
fi
GH_TOKEN="$TOKEN" gh workflow run ...

Same fix applies to any wrapper that routes gh calls by repo owner: resolve the token first, fail loudly if it's empty, and only then invoke the real gh binary. Silent fallback to the active account is the failure mode to design out.

Excalidraw's hand-drawn look is free — Excalidraw+ only buys you cloud collab

I almost passed on the cute hand-drawn flowchart style, assuming it sat behind a subscription. It doesn't. The hand-drawn aesthetic is Excalidraw's default and only style, and it's completely free. The paid tier, Excalidraw+, is team cloud collaboration and version history — nothing to do with how the diagrams look.

mermaid.live is the same story: the official open-source editor for Mermaid.js, free, no credits, no pay-per-render. The "credit-based" impression usually comes from third-party SaaS tools that generate Mermaid from a prompt.

So going from a flowchart to a hand-drawn version costs nothing. Get your flowchart as Mermaid (write it, or hand a vision-capable AI a photo of your sketch), then paste it straight onto the Excalidraw canvas — it detects the syntax and pops a "Parse as Mermaid" import dialog. There is no Insert → Mermaid item in the hamburger menu or toolbar (those only have Open / Save / Export / Live Collaboration); the other entry point is the command palette (Ctrl/Cmd + /) → search Mermaid.