用 Forge 的 `:fixme` 自定义命令自动扫描并修复代码中的 FIXME 注释
用 Forge 的 :fixme 自定义命令自动扫描并修复代码中的 FIXME 注释
fixme 是 Forge 内置的快捷命令(slash command),它让 AI 自动遍历当前项目源码文件中的 FIXME 注释,并逐一尝试修复对应代码。本文以 .forge/commands/fixme.md 为骨架,结合仓库中命令加载、解析与优先级的源码实现,讲解该命令的文件格式、工作原理、注册机制,以及如何举一反三编写属于自己的自定义命令。
1. fixme 命令是什么
fixme 是 Forge(AI enabled pair programmer)预置的一条内置命令。它的完整定义存放在仓库根目录的 .forge/commands/fixme.md,全文如下:
---
name: fixme
description: Looks for all the fixme comments in the code and attempts to fix them
---
Find all the FIXME comments in source-code files and attempt to fix them.
它的语义非常直接:扫描源码文件里所有 FIXME 注释,然后尝试修复它们。也就是说,当你在 Forge 对话中敲入 :fixme,Agent 会先通过文件搜索定位所有包含 FIXME 标记的源码文件,再逐条分析注释上下文、判断遗留问题,并直接动手修改代码把问题修掉。
在 crates/forge_app/src/fixtures/git_ls_files_many_extensions.txt 这个仓库自带的目录清单 fixture 中,可以看到 .forge/commands/fixme.md 与 check.md、pr_description.md 一同出现在 .forge/commands/ 目录下,印证了这类命令文件正是以独立 Markdown 文件的形式存在于该目录中。
2. 命令文件的结构:YAML frontmatter + Markdown 正文
从 fixme.md 可以看出,Forge 的每条自定义命令都由两部分组成:
- YAML frontmatter(元数据区):位于文档开头的
---与---之间,用于声明命令的name与description。 - Markdown 正文(提示词区):frontmatter 之后的所有内容,就是真正发给模型的提示词(prompt),指导 Agent 如何执行该命令。
这套格式在源码中被严格建模。Command 结构体定义于 crates/forge_domain/src/command.rs,注释明确写着:命令从 Forge 的命令目录中发现的 .md 文件中加载,name 和 description 来自 YAML frontmatter,prompt 则是文件正文:
pub struct Command {
/// 调用该命令时使用的名字(如 `github-pr-description`)
pub name: String,
/// 在命令列表中展示的简短描述
pub description: String,
/// 提示词模板正文(frontmatter 之后的 Markdown 内容)
pub prompt: Option<String>,
}
解析逻辑在 crates/forge_services/src/command.rs 的 parse_command_file 函数中:先用 gray_matter 库以 YAML 引擎解析 frontmatter,再做类型安全的反序列化,最后把正文部分挂到 prompt 字段上:
fn parse_command_file(content: &str) -> Result<Command> {
let gray_matter = Matter::<YAML>::new();
let result = gray_matter.parse::<Command>(content)?;
let command = result
.data
.context("Empty command frontmatter")?
.prompt(result.content);
Ok(command)
}
Command 结构体上标注了 #[derive(Deserialize)],因此 frontmatter 中的 name、description 会被自动映射为结构体字段;frontmatter 缺失时解析会报错(Empty command frontmatter)。这一点与 fixme.md 严格成对出现的 name/description 键完全一致。
仓库测试 crates/forge_services/src/command.rs 中的用例也验证了这一格式:test_parse_basic_command 用 fixture basic.md 断言 name、description 与 prompt 都被正确解析;test_parse_command_with_multiline_prompt 则用 multiline.md 验证正文支持多行 Markdown(步骤列表等),也就是说像 fixme 这样的正文完全可以写得非常详细,Agent 会完整读取。
3. fixme 命令的注册与加载机制
fixme 不是写在二进制里的硬编码逻辑,而是以标准命令文件的形式被加载器统一发现并注册。整条链路在 crates/forge_services/src/command.rs 的 CommandLoaderService 中实现:
- 加载内置命令:
init_default()先把二进制内嵌的命令(如github-pr-description,源码见 commands/github-pr-description.md)加载进来,作为最低优先级。 - 加载全局自定义命令:从
command_path()(默认base_path/commands,即~/forge/commands)读取所有*.md文件。 - 加载项目本地命令:从
command_path_local()读取项目根目录下的.forge/commands/目录,fixme.md正是在这一层被发现的。 - 解决命名冲突:
resolve_command_conflicts用 HashMap 按名字去重、保留最后一条,从而形成 项目本地 > 全局自定义 > 内置 的优先级顺序。
命令目录的路径定义在 crates/forge_domain/src/env.rs:
/// 全局命令目录(base_path/commands)
pub fn command_path(&self) -> PathBuf {
self.base_path.join("commands")
}
/// 项目本地命令目录(.forge/commands)
pub fn command_path_local(&self) -> PathBuf {
self.cwd.join(".forge/commands")
}
目录扫描由 init_command_dir 完成:目录不存在时直接返回空;存在时通过 DirectoryReaderInfra 并行读取目录下所有 *.md 文件,并以文件名(去掉扩展名)作为命令名——这正是 fixme.md 对应命令名为 fixme 的原因。也就是说,如果你想新增一条命令,只需在 .forge/commands/ 下新建一个 Markdown 文件即可,文件名即命令名。
项目根目录的 README.md 也给出了同样的使用说明:“Custom commands: Place YAML files in .forge/commands/ (project) or ~/forge/commands/ (global) to define shortcut commands available via :commandname.” 即命令可通过 :命令名 的形式在对话中直接触发。
test_parse_builtin_commands 测试用例把 fixme 当作内置命令 fixture 来解析并断言其 name、description、prompt 均非空,进一步确认 fixme 是经过测试保障的标准内置命令。
4. 实际使用方式
在 Forge 会话中直接输入:
:fixme
Forge 会解析出 fixme 命令的提示词,即“在源码文件中找出所有 FIXME 注释并尝试修复”,随后 Agent 按提示词执行:
- 定位:搜索当前项目源码中的
FIXME注释(大小写通常不敏感,README 中:fixme的说明原文即为fixme comments)。 - 分析:逐个阅读注释上下文,判断该处遗留的问题性质与修复意图。
- 修复:在保证不破坏其他逻辑的前提下直接修改源码,把注释对应的缺陷或待办事项落地解决。
- 验证:修复后可结合
:check(同样注册在内置命令列表中的检查命令)或项目测试来确认改动没有引入回归。
使用前提是当前会话所在的目录是一个 Forge 项目(即存在 .forge/ 配置),并且项目源码可被 Agent 通过文件工具正常读写。
5. 举一反三:编写你自己的自定义命令
理解了 fixme.md 的格式与加载机制后,完全可以自己编写类似的命令。例如在项目根目录 .forge/commands/ 下新建 refactor.md:
---
name: refactor
description: Find duplicated code blocks and suggest refactoring
---
Scan the source-code files for duplicated code blocks that appear in three or
more places. For each duplication found:
1. Summarize the repeated logic and its locations.
2. Propose a refactoring that extracts the shared logic.
3. Apply the refactoring and verify the code still compiles.
保存后,在新的 Forge 会话中直接输入 :refactor 即可触发。需要注意:
- 命令名即文件名(去掉
.md后缀); - 前端
name字段应与文件名保持一致,避免歧义; description会展示在命令列表中,建议写清楚命令的作用;- 正文 prompt 可以像 multiline.md 那样使用多行列表,让 Agent 按步骤执行;
- 若与内置命令重名,项目本地命令会覆盖全局与内置版本(优先级:项目本地 > 全局自定义 > 内置)。
6. 小结
fixme 命令是 Forge 自定义命令机制的一个典型样本:它由 .forge/commands/fixme.md 这样一个“YAML frontmatter + Markdown 正文”的文件定义,经 crates/forge_services/src/command.rs 的加载器扫描、解析与去重后注册为 :fixme 快捷命令,最终把“扫描并修复 FIXME 注释”的意图作为提示词交给 Agent 执行。掌握这套文件格式与加载优先级,你就能像 fixme 一样,把团队里高频的编码巡检、重构、代码生成等重复工作沉淀为一条条可复用的 Forge 命令。