Compose:规格驱动的开发流程

已标为 legacy 的 compose 智能体与十四个技能、Build 下的 /compose-next 契约,以及模式锁定和技能调用控制在代码里怎么实现。

作者 David更新于 第 21 篇(共 33 篇)

写一个功能,光改代码不够:先把需求问清楚,写下规格与计划,按计划实现,跑验证,交给别人审查,最后合并。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.mdREADME 推荐的路径
compose workflowworkflow 工具以 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:plancompose-next
系统提示词里的技能目录modelInvocable(system.ts:180)不出现出现
skill_searchmodelInvocable 再排除 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.md frontmatter 都表明这个字段已经去掉,模型看得见也能调用它,“只在用户明确要求时启用”只靠描述和正文约束。
  • 文档把 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 脚本:原语、并发上限、日志与断点续跑。

本页目录