Compose:规格驱动的开发流程
已标为 legacy 的 compose 智能体与十四个技能、Build 下的 /compose-next 契约,以及模式锁定和技能调用控制在代码里怎么实现。
写一个功能,光改代码不够:先把需求问清楚,写下规格与计划,按计划实现,跑验证,交给别人审查,最后合并。MiMo Code 把这条链路叫 Compose。它不是上游的东西:OpenCode v1.14.19 里没有 compose 智能体,没有 compose:* 技能,也没有 config/compose.ts。
到 v0.1.14,同一套流程在代码里有三种形态:
| 形态 | 入口 | 载体 | 现状 |
|---|---|---|---|
| compose 智能体 | 发第一条消息前按 Tab 切过去 | 智能体定义、compose.txt 提示词、14 个 compose:* 技能 | 描述里写着 deprecated |
/compose-next | 在 Build 里输入 /compose-next 或明确要求 | 内置技能包里的一份 SKILL.md | README 推荐的路径 |
compose workflow | workflow 工具以 name: "compose" 运行 | workflow/builtin/compose.js | 实验特性,默认不注册 |
compose 智能体的描述以“Compose mode (deprecated)”开头(packages/opencode/src/agent/agent.ts:215),输入栏把它显示成“Compose (legacy)”(packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx:1849);workflow 工具要打开 MIMOCODE_EXPERIMENTAL_WORKFLOW_TOOL 才会注册(packages/opencode/src/tool/registry.ts:312)。本篇讲前两种对话式路径,第三种放在四个内置 Workflow。
怎么用
- legacy 路径:新会话里先按 Tab(或
<leader>a打开智能体列表)切到 compose 再发消息。/compose:plan、/compose:tdd这类斜杠命令只有当前模式是 compose 时才出现在补全里(packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx:388)。 - 产出目录:配置
compose.docs指定 spec、plan、report 的根目录,默认docs/compose;compose.docs_absolute: true时把相对路径展开成 worktree 下的绝对路径(实现见下文)。 - 关掉技能包:环境变量
MIMOCODE_DISABLE_COMPOSE_SKILLS为真时,这 14 个技能既不解压也不扫描(packages/opencode/src/skill/index.ts:206)。 - 推荐路径:留在 Build,输入
/compose-next <需求>,或者用自然语言明确说要走这个流程。说without worktree、without spec可以跳过对应关卡(packages/opencode/src/skill/builtin/.bundle/compose-next/SKILL.md:26)。 - 中途换模式:会话一旦有了消息,只剩 build 与 plan 能互切。非换不可时用
/force-agent或<leader>o打开强制切换对话框(实现见文末)。
compose 智能体
权限:只有它能加载 compose 技能
所有智能体共享的默认规则里,skill 权限先放行 *、再拒绝 compose:*(agent.ts:112)。compose 智能体在默认规则之后追加一条放行(agent.ts:211):
compose: {
name: "compose",
color: "#a7a3d8",
description:
"Compose mode (deprecated). Orchestrates workflows with built-in compose skills. For Fable/Sol-class models, use Build and run /compose-next instead.",
options: {},
permission: Permission.merge(
defaults,
Permission.fromConfig({
question: "allow",
skill: { "compose:*": "allow" },
}),
user,
),
mode: "primary",
native: true,
},规则按“最后匹配优先”求值(见权限系统),所以 14 个技能只对 compose 智能体开放,其他智能体(包括所有子智能体)都命中那条 deny。compose 没有放行 plan_exit,默认规则里它是 deny(agent.ts:116),这与提示词要求“不用原生 plan 工具,改用 compose:plan”一致。定义里也没有 prompt 字段,系统提示词仍按模型选择(packages/opencode/src/session/system.ts:53)。
提示词:挂在第一条 compose 消息上
compose 专属的指令不在系统提示词里,而是在每一步请求前由 insertReminders 插进历史(packages/opencode/src/session/prompt.ts:1160):
const composeModeMsg = input.messages.find(
(msg) => msg.info.role === "user" && msg.info.agent === "compose",
)
const cfg = yield* config.get()
if (composeModeMsg) {
const ctx = yield* InstanceState.context
const composeCfg = cfg.compose
const docsDir = ConfigCompose.resolveDocsDir(ctx.worktree, composeCfg)
const text = PROMPT_COMPOSE
.replace("{{compose_docs_dir}}", `Save compose skill outputs: specs in \`${path.join(docsDir, "specs")}\`, plans in \`${path.join(docsDir, "plans")}\`, reports in \`${path.join(docsDir, "reports")}\`.`)
composeModeMsg.parts.unshift({
id: PartID.ascending(),
messageID: composeModeMsg.info.id,
sessionID: composeModeMsg.info.sessionID,
type: "text",
text,
synthetic: true,
})
}几个细节:
- 找的是整段历史里第一条
agent为 compose 的 user 消息,插在它的最前面,位置固定。 - 同一函数里的 plan 模式提醒走
sessions.updatePart落库(prompt.ts:1352),这里只改内存;主循环每一步都从数据库重读历史(prompt.ts:3633),所以每步重新插一次,不会叠加。 - 查找不看当前智能体:强制切回 Build 后,只要那条 compose 消息还在上下文里,
compose.txt照样注入。
设计文档解释了弃用时为什么没动这份提示词(docs/compose/spec/compose-next.md:105):
any byte change to a model-facing system prompt invalidates prefix cache for every existing Compose session.
packages/opencode/src/session/prompt/compose.txt 共 133 行,要点:
- 技能匹配就必须先加载再行动,能不能跳过由技能正文说了算,不能只看描述决定(
compose.txt:4)。 compose:*技能不可检索,直接按名字加载(compose.txt:7);也不要加载或推荐compose-next,用户点名要就请他切回 Build(compose.txt:14)。- 所有决策、澄清、审批都通过
compose:ask走question工具,不许用一句自然语言提问结束回合;没人应答时自己挑最适合无人值守的选项继续,这条压过技能里的 HARD-GATE(compose.txt:18)。 - 优先级是用户指令高于 compose 技能,compose 技能高于默认系统提示词(
compose.txt:28)。 - GPT/Codex 模式下没有顶层
skill工具,要在exec里调tools.skill(compose.txt:38,见 exec 与 QuickJS 沙箱)。 - 实现必须是最小改动,改动超过任务表面复杂度的 3 倍就停下重想(
compose.txt:48)。 - 末尾的
{{compose_docs_dir}}占位符被替换成上面那句产出目录说明(compose.txt:130)。
产出目录由 resolveDocsDir 计算(packages/opencode/src/config/compose.ts:20):compose.docs 缺省为 docs/compose;相对路径默认原样交给模型,相对的是智能体当时的工作目录,只有 docs_absolute 为真时才按 worktree 根展开成绝对路径。
十四个 compose 技能
打包与解压
技能正文在 packages/opencode/src/skill/compose/.bundle/ 下,每个技能一个目录。bundle.macro.ts 是 Bun 宏,构建时把整个目录读成“技能名 → 相对路径 → 文本”的对象内联进二进制(packages/opencode/src/skill/compose/bundle.macro.ts:16);运行时 extractComposeBundle 把它写回数据目录下的 compose/<version>/skills/<name>/,写完留一个 .extracted 标记,标记在就不再重写,本地开发版(InstallationLocal)则每次启动都重新解压(packages/opencode/src/skill/compose/extract.ts:32)。技能注册表把这个根目录记为 bundled 并扫描其中的 SKILL.md(packages/opencode/src/skill/index.ts:205),同名的用户技能会覆盖 bundled 版本(index.ts:122)。技能名来自 frontmatter,例如 name: compose:plan(packages/opencode/src/skill/compose/.bundle/plan/SKILL.md:2)。内置技能包走的是同一套机制,见内置技能包与技能检索。
来源写在同目录的两份许可说明里:技能派生自 Jesse Vincent 的 superpowers 项目(packages/opencode/src/skill/compose/LICENSE-superpowers:1),compose.txt 里的 Simplicity 准则部分取自 andrej-karpathy-skills(packages/opencode/src/skill/compose/LICENSE-karpathy:1),均为 MIT。
各管哪一步
中文说明取自 TUI 的本地化词条(packages/opencode/src/cli/cmd/tui/i18n/zh.ts:220),衔接关系取自各技能正文:
| 技能 | 说明 | 衔接 |
|---|---|---|
compose:brainstorm | 探索需求与设计,动手前先想清楚 | 终点固定是加载 compose:plan |
compose:plan | 从规格书生成分步实施计划 | 存盘后决定用子智能体还是本会话执行 |
compose:subagent | 分派独立任务给子代理执行 | 每个任务一个新子智能体,先规格审查再质量审查 |
compose:execute | 按实施计划逐步执行,含审查节点 | 全部完成后交给 compose:report |
compose:worktree | 创建隔离工作空间进行特性开发 | 执行前确保有隔离工作区 |
compose:tdd | 测试驱动开发——先写测试再写实现 | 实现者遵循 |
compose:review | 合并前验证工作是否符合需求 | 派审查子智能体,只给它精心准备的上下文 |
compose:verify | 运行验证命令并确认通过 | 任何“完成”声明之前 |
compose:report | 汇总实现成果为终态报告 | 写完交给 compose:merge |
compose:merge | 集成已完成的工作——合并、PR 或清理 | 链路终点 |
compose:ask | 向用户请求决策或澄清 | 其他技能的决策点都引用它 |
compose:debug | 系统化调试,先定位再修复 | bug 场景的入口 |
compose:feedback | 严谨处理代码审查反馈 | 收到审查意见时 |
compose:parallel | 并行执行互不依赖的任务 | 两个以上独立任务时 |
主干是这样接起来的:
compose:plan 选执行方式时先查记忆里 compose-preferences 文件的 execution-style,没有就问用户,选了“always”会写回记忆(plan/SKILL.md:165)。三类产物的命名不同:设计稿是 specs/ 下的 YYYY-MM-DD-<topic>-design.md(packages/opencode/src/skill/compose/.bundle/brainstorm/SKILL.md:35),计划是 plans/ 下带日期的文件(plan/SKILL.md:18),报告是 reports/ 下不带日期、原地覆盖的 <feature-name>.md(packages/opencode/src/skill/compose/.bundle/report/SKILL.md:16)。
compose:ask 统一了“没人回答”的处理:question 工具不在工具表里,或者返回 [Never-Ask] 指令(never-ask 模式,packages/opencode/src/tool/question.ts:30),都由模型自己挑最适合无人值守的选项并说明理由;删分支、强推这类不可逆操作永不自动批准(packages/opencode/src/skill/compose/.bundle/ask/SKILL.md:49)。
/compose-next:一份契约
compose-next/SKILL.md 共 197 行,把整条流程压进一次加载,开头就写明(compose-next/SKILL.md:8):
Compact end-to-end contract for grill → workspace → spec → implement → verify → review → finalize → finish. One skill load, no internal skill hand-offs.
与 legacy 路径相比,关键差别有这些:
- 只在明确请求时进入:描述和正文都写了,不能因为任务看起来大就自行启用(
compose-next/SKILL.md:3、compose-next/SKILL.md:12)。 - 扛得住压缩:要求检查点或压缩摘要里保留一句恢复指令,续跑时若契约不在上下文里就重新加载本技能(
compose-next/SKILL.md:10,检查点机制见检查点写手)。 - 先占工作区:所有路径都必须先过 Workspace,默认在
.worktrees/<slug>建 linked worktree,未经同意不在main、master上动手(compose-next/SKILL.md:30、compose-next/SKILL.md:58)。 - 一个功能一份文档:
docs/compose/spec/<feature-name>.md,不带日期,设计、任务和交付报告都在里面,不再另写 plan 和 report(compose-next/SKILL.md:67);frontmatter 的status依次是 designed、in-progress、delivered,任务带验收标准、covers:与depends:。这个路径写死在技能里,不读<compose_docs_dir>,因为那个块只有 compose 智能体和compose.js才有(compose-next.md:43)。 - 验证与审查严格串行:验证命令全部结束后才派一个新的审查子智能体,审查模型至少不弱于实现者,分别给出规格符合性、正确性、代码库一致性三项结论(
compose-next/SKILL.md:134、compose-next/SKILL.md:152)。 - 不自动收尾:Finalize 把文档改为 delivered 并填好报告,然后停下来报告分支与 SHA,由用户决定合并、开 PR 还是保留(
compose-next/SKILL.md:186)。
仓库自己的 docs/compose/spec/*.md 就是按这个模板写的,frontmatter 里是 feature、status、updated、branch、commits(docs/compose/spec/skill-invocation-control.md:1)。
技能调用控制
docs/compose/spec/skill-invocation-control.md 把两个问题拆开:permission.skill 只管授权,deny 意味着谁都用不了,包括用户;frontmatter 的 disable-model-invocation 只管模型可达性。代码里对应两个取数函数(skill/index.ts:350、index.ts:361):available(agent) 过滤掉 deny 的技能,modelInvocable(agent) 在此基础上再去掉设了 disable_model_invocation 的技能。各入口的取数与效果:
| 入口 | 取数 | Build 下的 compose:plan | compose-next |
|---|---|---|---|
| 系统提示词里的技能目录 | modelInvocable(system.ts:180) | 不出现 | 出现 |
skill_search | modelInvocable 再排除 compose: 前缀(packages/opencode/src/tool/skill-search.ts:36、packages/opencode/src/skill/search.ts:69) | 搜不到 | 可检索 |
skill 工具加载 | skill.get 后 ctx.ask(packages/opencode/src/tool/skill.ts:58) | DeniedError | 可加载 |
用户输入 /名字 注入正文 | available(prompt.ts:1245) | 不注入 | 注入 |
| 斜杠补全、技能对话框 | skill.all() 再由 TUI 过滤(packages/opencode/src/cli/cmd/tui/component/dialog-skill.tsx:27) | 隐藏 | 显示 |
设了 disable-model-invocation 的技能,模型调用 skill 时会拿到一条指路的错误,告诉它只有用户能用 /名字 启动(skill.ts:50)。
对照代码,文档有三处已经过时:
- 正文说
compose-next设置了disable-model-invocation: true;文档顶部 2026-08-12 的补记(skill-invocation-control.md:11)和当前的SKILL.mdfrontmatter 都表明这个字段已经去掉,模型看得见也能调用它,“只在用户明确要求时启用”只靠描述和正文约束。 - 文档把
tool/registry.ts里的describeSkill列为模型可见入口;当前代码里没有这个函数,skill工具的说明是一段不列技能名的静态文本(packages/opencode/src/tool/skill.txt:1)。 - 文档里标注的行号都已漂移,比如它说在 111 行的默认 deny 规则如今在
agent.ts:114,它说在 864 行的提及扫描如今在prompt.ts:1245。
模式锁定
上游 v1.14.19 的 move 只是在主智能体列表里循环。MiMo 在 TUI 的本地状态里加了一道闸(packages/opencode/src/cli/cmd/tui/context/local.tsx:50):
const FREE_SWITCH_GROUP = ["build", "plan"]
const canSwitchTo = (target: string) => {
if (!agentStore.sessionHasMessages) return true
const current = agentStore.current
if (!current) return true
if (current === target) return true
const currentInGroup = FREE_SWITCH_GROUP.includes(current)
const targetInGroup = FREE_SWITCH_GROUP.includes(target)
return currentInGroup && targetInGroup
}sessionHasMessages 由输入框根据会话里有没有 user 消息派生(prompt/index.tsx:585)。Tab 循环时跳过不可达的智能体,一个都去不了就弹出提示;当前模式不在 build、plan 里时,提示是“进入 {{mode}} 模式后无法在运行中切换模式”(zh.ts:343)。重新进入已有会话时,TUI 把模式恢复成最后一条 user 消息的智能体(prompt/index.tsx:604)。forceSwitch 绕过这道闸,对应命令 /force-agent 与快捷键 <leader>o(packages/opencode/src/cli/cmd/tui/app.tsx:688、packages/opencode/src/config/keybinds.ts:63)。
README 说“Compose 一旦进入即被隔离”(README.zh.md:121),代码的实际规则更宽:闸只放行 build 与 plan 之间的切换,compose 以及实验性的 orchestrator、max 进去了就出不来,build 和 plan 也进不去它们。锁只在 TUI 客户端,服务端不校验;Orchestrator 的 session 工具用 setmode 操作仍可把子会话改成 build、plan 或 compose(packages/opencode/src/tool/session.ts:542,见 Orchestrator)。README 提到的“显著提升工具调用成功率”在代码里无从核对。
下一篇:Workflow 运行时——把多智能体编排写成确定性的 JS 脚本:原语、并发上限、日志与断点续跑。