get-shit-done #3156:为什么 /gsd-plan-phase 会被 OpenCode 的 agent 自动派发送进“子代理死胡同”——编排命令的上下文契约与结构性回归测试
【免费下载链接】get-shit-doneA light-weight and powerful meta-prompting, context engineering and spec-driven development system for Claude Code by TÂCHES.项目地址: https://gitcode.com/GitHub_Trending/getshi/get-shit-done
本篇基于 changeset 3156-plan-phase-opencode-dispatch.md 与对应源码、测试,讲解 get-shit-done 项目中一个跨运行时兼容缺陷的完整机理:agent:frontmatter 指令如何导致/gsd-plan-phase在 OpenCode 中丢失子代理派生能力,修复如何同时应用于plan-phase.md与mvp-phase.md,以及一个扫描全部命令文件 frontmatter 的结构性回归测试如何防止该类问题复发。读完你将掌握“命令文件 frontmatter 即运行时契约”的排查方法,以及如何在自己的多运行时项目中为这类上下文错配建立防线。
一、背景:/gsd-plan-phase是一个依赖 Agent 工具的编排器
/gsd-plan-phase的定位不是“自己干活”,而是“调度别人干活”。从 commands/gsd/plan-phase.md 的<objective>段落可以看到其编排角色定义:
Orchestrator role:Parse arguments, validate phase, research domain (unless skipped), spawn gsd-planner, verify with gsd-plan-checker, iterate until pass or max iterations, present results.
默认流程为Research(按需)→ Plan → Verify → Done,它需要派生三个子代理:
| 子代理 | 职责 |
|---|---|
gsd-phase-researcher | 为阶段做技术研究,产出RESEARCH.md |
gsd-planner | 生成可执行的PLAN.md |
gsd-plan-checker | 计划质量审查,未通过则进入修订循环(最多 3 次迭代) |
这三个角色在 agents/gsd-phase-researcher.md、agents/gsd-planner.md、agents/gsd-plan-checker.md 中分别定义;编排细节位于 get-shit-done/workflows/plan-phase.md,其中通过subagent_type="gsd-phase-researcher"、subagent_type="gsd-plan-checker"等调用点显式派生子代理。
关键证据在 commands/gsd/plan-phase.md 的 frontmatterallowed-tools列表中——它显式声明了Agent工具(子代理派生器):
--- name: gsd:plan-phase description: Create detailed phase plan (PLAN.md) with verification loop argument-hint: "[phase] [--auto] [--research] [--skip-research] ..." allowed-tools: - Read - Write - Bash - Glob - Grep - Agent # 编排器依赖此工具派生子代理 - AskUserQuestion - WebFetch - mcp__context7__* requires: [discuss-phase, phase, review, update] ---也就是说:Agent出现在allowed-tools里,说明该命令的正常工作方式必须以主代理上下文运行,否则派生动作无从谈起。
二、根因:OpenCode 对agent:frontmatter 指令的语义是“自动派发”
缺陷的触发点是一行看似无害的 frontmatter。修复前的plan-phase.md携带:
agent: gsd-plannerchangeset 原文给出的根因解释是:
Per the OpenCode commands spec,
agent: <name>causes the runtime to auto-dispatch the command to a named subagent context where theAgent(subagent-spawner) tool is unavailable.
即 OpenCode 的命令规范将agent: <name>解释为“把该命令自动派发(auto-dispatch)到名为<name>的子代理上下文中执行”。这与 Claude Code 的 slash command 语义并不对齐——同一条命令文件,在两种运行时中的执行上下文完全不同:
- 在“普通”运行时中,该命令在主会话上下文中展开执行,
Agent工具可用,编排流程正常; - 在 OpenCode 中,同一份 frontmatter 使运行时直接把它送进
gsd-planner子代理上下文,而这个子代理上下文不具备Agent工具。
于是出现了一个自相矛盾的处境:一个allowed-tools里声明需要Agent的命令,却被 frontmatter 指令送进了一个拿不到Agent的上下文。
三、失效模式:编排器“退化为单线程”,全量工作回落到内联执行
changeset 描述了实际观察到的失效行为:
The
/gsd-plan-phaseorchestrator relies onAgentto spawngsd-phase-researcher,gsd-planner, andgsd-plan-checkersubagents; in the auto-dispatched context it fell back to doing all work inline.
从源码结构看,这一退化是必然的:get-shit-done/workflows/plan-phase.md 中的工作流按“派生 researcher → 派生 planner → 派生 checker → 修订循环”组织,每一环都是对子代理的 spawn 调用。当Agent工具不可用时,LLM 无法按工作流原样执行,只能回退为在主上下文中内联完成研究、规划、校验的全部工作。后果是:
- 上下文窗口被三个阶段的全部中间产物占满,原本由子代理隔离上下文的设计目的完全失效;
- “Research → Plan → Verify”的门控循环(含最多 3 次迭代的修订机制)被实质性绕过或简化;
- 缺陷只在 OpenCode 运行时上显现,在 Claude Code 上测试完全正常——典型的跨运行时行为分歧。
四、修复:移除agent:指令,让命令回到主代理上下文
修复方式极其克制——不是增加兼容性代码,而是删除一行 frontmatter:
The
agent: gsd-plannerdirective has been removed fromplan-phase.mdso the command runs in the main agent context whereAgentis available.
对照当前仓库中 commands/gsd/plan-phase.md 的 frontmatter,agent:指令已不存在,allowed-tools中保留Agent,命令在 OpenCode 中也将运行于主代理上下文,派生能力恢复。
同样的修复被同步应用到 commands/gsd/mvp-phase.md。changeset 说明:
The same fix was applied to
commands/gsd/mvp-phase.md, which carried the same directive and had the identical failure mode.
mvp-phase是垂直 MVP 切片的用户入口:它先引导用户写下 “As a / I want to / So that” 用户故事、执行 SPIDR 拆分检查,然后委托给/gsd plan-phase <N>。其 frontmatter 同样在allowed-tools中声明了Agent,因此一旦携带agent:指令就会落入与plan-phase完全相同的失效模式。两个文件一并修复,消除了整条 MVP 规划链路上的同类隐患。
该修复随版本 v1.41.0 发布,见 docs/RELEASE-v1.41.0.md 中的变更记录:
/gsd-plan-phaseno longer auto-dispatches to a subagent on OpenCode — theagent: gsd-plannerfrontmatter directive caused OpenCode to run the orchestrator in a context where theAgenttool is unavailable. Directive removed.
五、防线:扫描全部命令 frontmatter 的结构性回归测试
单点修复解决的是已知的两个文件,但真正有价值的产出是那条结构性回归测试。changeset 最后一句:
A structural regression test parses the YAML frontmatter of every
commands/gsd/*.mdfile and asserts that no command carries anagent:directive.
对应实现是 tests/bug-3156-plan-phase-opencode-dispatch.test.cjs,其设计要点如下:
1. 为什么可以直接断言“frontmatter 即行为”
测试文件开头有一段注释,明确了这条测试的正当性:
// allow-test-rule: source-text-is-the-product // commands/gsd/*.md files are the deployed skill surface. Their frontmatter // IS the runtime contract. Checking frontmatter fields checks deployed behaviour.在这个项目里,commands/gsd/*.md就是实际部署给各 AI 运行时使用的技能面(skill surface),frontmatter 字段会被 OpenCode 等运行时直接消费。因此“解析 frontmatter 并断言字段”不是文本风格检查,而是对部署行为的检查——这正是测试能以纯静态解析方式存在的原因。
2. 测试体结构:两层断言,belt-and-suspenders
测试对commands/gsd目录下每一个.md文件解析 YAML frontmatter(parseFrontmatter处理首个---分隔块,支持多行列表字段),然后执行两个describe块:
第一层(全量硬约束):任何命令文件都不得携带agent:frontmatter 指令。
describe('#3156 — no command file may have an `agent:` frontmatter directive', () => { for (const { name, content } of commandFiles) { test(`${name}: no agent: directive in frontmatter`, () => { const fm = parseFrontmatter(content); assert.ok( !Object.prototype.hasOwnProperty.call(fm, 'agent'), `${name}: has \`agent: ${fm['agent']}\` in frontmatter — this causes OpenCode to auto-dispatch to a subagent context where the Agent tool is unavailable...`, ); }); } });断言用的是hasOwnProperty检查键的存在性,而非值匹配,避免对空值等边缘形态漏判;失败信息直接给出根因与修复指引(移除指令、回到主代理上下文)。
第二层(精确复现失效模式):allowed-tools包含Agent的命令(即编排器)绝不允许携带agent:指令。测试注释坦承这一层与第一层冗余(“belt-and-suspenders”),但它精确刻画了 #3156 的失败模式:
describe('#3156 — orchestrator commands (allowed-tools: Agent) must not have agent:', () => { const orchestrators = commandFiles.filter(({ content }) => { const fm = parseFrontmatter(content); const tools = allowedTools(fm); return tools.includes('Agent'); }); ... });其中allowedTools辅助函数同时兼容多行 YAML 列表与逗号分隔两种写法。这一层断言的价值在于:即便未来放宽第一层的“全局禁令”,第二层仍能守住“编排器自毁”这一具体失效面。
3. 这类测试的通用启示
从测试结构可以提炼出一条可复用的经验:当一个项目的“配置即行为”(markdown frontmatter、TOML 段落、JSONC 配置直接驱动运行时),那么对这些声明面做全量结构化扫描,比针对单条命令写功能测试更廉价也更全面——每个文件自动变成一个测试用例,新增命令文件时无需修改测试代码即被纳入约束。
六、经验总结:命令 frontmatter 是跨运行时的上下文契约
综合 changeset 与源码证据,#3156 给出的规则可以归纳为:
agent: <name>不是“提示 LLM 用这个 agent 风格”,而是运行时级的上下文切换指令。在 OpenCode 中它会触发自动派发,把命令送进子代理上下文,且该上下文没有子代理派生工具。- 凡是
allowed-tools声明了Agent的编排型命令,frontmatter 中必须省略agent:指令,否则命令会被送进一个自身无法工作的上下文。测试注释将其总结为:“Commands that need to run in the main agent context (i.e., all GSD commands) must omit this directive.” - 同一 frontmatter 在不同运行时中语义可能不同,跨运行时支持的项目必须在目标运行时上实际走查一遍编排链路,不能以某一运行时的测试通过作为全量验证。
- 用结构性回归测试固化契约:全量扫描 frontmatter、按“部署面即契约”的原则断言,使未来任何新增命令文件自动继承该保护。
延伸阅读(仓库内相对路径)
- changeset 原文:.changeset/3156-plan-phase-opencode-dispatch.md
- 被修复的命令:commands/gsd/plan-phase.md、commands/gsd/mvp-phase.md
- 编排工作流(researcher/planner/checker 调用点):get-shit-done/workflows/plan-phase.md
- 回归测试:tests/bug-3156-plan-phase-opencode-dispatch.test.cjs
- 发布记录:docs/RELEASE-v1.41.0.md
【免费下载链接】get-shit-doneA light-weight and powerful meta-prompting, context engineering and spec-driven development system for Claude Code by TÂCHES.项目地址: https://gitcode.com/GitHub_Trending/getshi/get-shit-done
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考