名称: 技能学习 描述: 扫描代码库中的 FIX:/NOTE:/TODO: 标签并创建结构化任务,通过交互式选择。为 /learn 命令调用。 允许的工具: Bash, Grep, Read, Write, Edit, AskUserQuestion
学习技能(直接执行)
直接执行技能,用于扫描文件、交互式呈现发现并创建用户选择的任务。取代之前的基于委托的方法,采用同步执行和 AskUserQuestion 提示。
关键行为: 用户总是在任何任务创建之前看到标签扫描结果。用户通过交互式提示选择要创建的任务类型。
上下文参考
参考(不要急切加载):
- 路径:
@specs/TODO.md- 当前任务列表 - 路径:
@specs/state.json- 机器状态
执行
步骤 1: 解析参数
从命令输入提取路径:
# 从命令输入解析
paths="$ARGUMENTS"
# 如果未指定路径,默认项目根目录
if [ -z "$paths" ]; then
paths="."
fi
注意: --dry-run 标志不再支持。交互式流程本质上是“先预览”——用户总是在任务创建前看到发现。
步骤 2: 生成会话 ID
生成用于跟踪的会话 ID:
session_id="sess_$(date +%s)_$(od -An -N3 -tx1 /dev/urandom | tr -d ' ')"
步骤 3: 执行标签提取
使用文件类型特定模式扫描标签。使用 Bash 和 grep 实现一致输出解析。
3.1: 提取 FIX: 标签
Lua 文件(Neovim 配置):
grep -rn --include="*.lua" "-- FIX:" $paths 2>/dev/null || true
LaTeX 文件:
grep -rn --include="*.tex" "% FIX:" $paths 2>/dev/null || true
Markdown 文件:
grep -rn --include="*.md" "<!-- FIX:" $paths 2>/dev/null || true
Python/Shell/YAML 文件:
grep -rn --include="*.py" --include="*.sh" --include="*.yaml" --include="*.yml" "# FIX:" $paths 2>/dev/null || true
3.2: 提取 NOTE: 标签
与上面相同模式,将 FIX: 替换为 NOTE:。
3.3: 提取 TODO: 标签
与上面相同模式,将 FIX: 替换为 TODO:。
3.4: 解析结果
对于每个 grep 匹配,提取:
- 文件路径
- 行号
- 标签类型 (FIX, NOTE, TODO)
- 标签内容(标签后的文本)
示例原始输出:
nvim/lua/plugins/telescope.lua:67:-- TODO: 为 git 工作树添加自定义选择器
docs/KEYMAPS.md:89:<!-- FIX: 使用新绑定更新键映射表 -->
分类为三个数组:
fix_tags[]- 所有 FIX: 标签note_tags[]- 所有 NOTE: 标签todo_tags[]- 所有 TODO: 标签
步骤 4: 显示标签摘要
在用户选择之前呈现发现:
## 标签扫描结果
**扫描的文件**: {paths}
**发现的标签**: {total_count}
### FIX: 标签 ({count})
- `{file}:{line}` - {content}
- ...
### NOTE: 标签 ({count})
- `{file}:{line}` - {content}
- ...
### TODO: 标签 ({count})
- `{file}:{line}` - {content}
- ...
步骤 5: 处理边缘情况
未发现标签
如果未发现标签:
## 未发现标签
扫描的文件在: {paths}
未检测到 FIX:、NOTE: 或 TODO: 标签。
无需创建。
优雅退出,无提示。
仅某些标签类型
仅显示存在的标签类型的任务类型选项:
- 存在 FIX: 标签 -> 提供“修复任务”
- 存在 NOTE: 标签 -> 提供“修复任务”和“学习任务”
- 存在 TODO: 标签 -> 提供“TODO 任务”
步骤 6: 任务类型选择
如果发现标签,提示用户选择任务类型:
{
"question": "应创建哪些任务类型?",
"header": "任务类型",
"multiSelect": true,
"options": [
{
"label": "修复任务",
"description": "将 {N} 个 FIX:/NOTE: 标签合并为单个任务"
},
{
"label": "学习任务",
"description": "从 {N} 个 NOTE: 标签更新上下文"
},
{
"label": "TODO 任务",
"description": "为 {N} 个 TODO: 项创建任务"
}
]
}
重要: 仅包含标签类型存在的选项:
- 仅当 FIX: 或 NOTE: 标签存在时包含“修复任务”
- 仅当 NOTE: 标签存在时包含“学习任务”
- 仅当 TODO: 标签存在时包含“TODO 任务”
如果用户未选择任何选项,优雅退出:
未选择任务类型。未创建任务。
步骤 7: 单个 TODO 选择
如果选择了“TODO 任务”且存在 TODO: 标签:
标准情况(<=20 个 TODO)
{
"question": "选择要创建为任务的 TODO 项:",
"header": "TODO 选择",
"multiSelect": true,
"options": [
{
"label": "{内容截断至 50 字符}",
"description": "{file}:{line}"
},
...
]
}
大量 TODO(>20)
在顶部添加“全选”选项:
{
"question": "选择要创建为任务的 TODO 项:",
"header": "TODO 选择(多项)",
"multiSelect": true,
"options": [
{
"label": "全选({N} 项)",
"description": "为每个 TODO 标签创建任务"
},
{
"label": "{内容截断至 50 字符}",
"description": "{file}:{line}"
},
...
]
}
如果选择“全选”,包含所有 TODO。否则,仅包含选定项。
步骤 7.5: TODO 项的主题分组
条件: 用户选择了“TODO 任务”且选择了多于 1 个 TODO 项
如果仅选择 1 个 TODO 项,跳至步骤 8(无分组益处)。
7.5.1: 提取主题指示器
对于每个选定 TODO 项,提取主题指示器:
关键术语: 从 TODO 内容提取重要单词(名词、动词)。忽略停用词(the, a, is, to, for 等)。
文件部分: 按文件路径前缀分组(例如,Logos/Layer1/ 与 Logos/Shared/)。
操作类型: 识别常见操作模式:
- “添加/实现/创建” → 实现任务
- “修复/处理/纠正” → 修复任务
- “文档/更新文档” → 文档任务
- “测试/验证” → 测试任务
- “重构/优化” → 改进任务
示例提取:
TODO: "为工作树添加自定义选择器" 在 nvim/lua/plugins/telescope.lua:67
→ 关键术语: ["选择器", "工作树", "望远镜"]
→ 文件部分: "nvim/lua/plugins/"
→ 操作类型: "实现"
TODO: "为工作树添加预览窗口" 在 nvim/lua/plugins/telescope.lua:89
→ 关键术语: ["预览", "工作树", "望远镜"]
→ 文件部分: "nvim/lua/plugins/"
→ 操作类型: "实现"
TODO: "优化懒加载" 在 nvim/lua/config/lazy.lua:23
→ 关键术语: ["优化", "懒", "加载"]
→ 文件部分: "nvim/lua/config/"
→ 操作类型: "改进"
7.5.2: 按共享术语聚类 TODO
分组共享 2 个或更多重要术语 或共享 文件部分 + 操作类型 的 TODO。
聚类算法:
- 从第一个 TODO 开始作为初始组
- 对于每个剩余 TODO:
- 如果与现有组共享 2+ 关键术语 → 添加到组
- 如果与现有组共享文件部分 AND 操作类型 → 添加到组
- 否则 → 开始新组
- 从组中最常见的共享术语生成主题标签
示例聚类:
组 1: "望远镜工作树"(共享: 工作树, 望远镜, nvim/lua/plugins/, 实现)
- 为工作树添加自定义选择器
- 为工作树添加预览窗口
组 2: "配置优化"(共享: nvim/lua/config/, 改进)
- 优化懒加载
单项目组: 如果一个 TODO 未与其他项聚类,它成为自己的单项目组。
7.5.3: 存储分组主题
存储主题组以供步骤 7.5.4 使用:
topic_groups = [
{
label: "望远镜工作树",
items: [
{file: "nvim/lua/plugins/telescope.lua", line: 67, content: "为工作树添加自定义选择器"},
{file: "nvim/lua/plugins/telescope.lua", line: 89, content: "为工作树添加预览窗口"}
],
shared_terms: ["工作树", "望远镜"],
action_type: "实现"
},
{
label: "配置优化",
items: [
{file: "nvim/lua/config/lazy.lua", line: 23, content: "优化懒加载"}
],
shared_terms: [],
action_type: "改进"
}
]
步骤 7.5.4: 主题组确认
条件: topic_groups 包含至少一个具有 2+ 项的组
如果所有组仅 1 项,跳至步骤 8(无分组益处)。
通过 AskUserQuestion 呈现主题组:
{
"question": "如何将 TODO 项分组为任务?",
"header": "TODO 主题分组",
"multiSelect": false,
"options": [
{
"label": "接受建议的主题组",
"description": "创建 {N} 个分组任务: {group_summaries}"
},
{
"label": "保持为单独任务",
"description": "创建 {M} 个单独任务(每 TODO 项一个)"
},
{
"label": "创建单个组合任务",
"description": "创建 1 个任务包含所有 {M} 个 TODO 项"
}
]
}
其中:
{N}= 主题组数量{M}= 选定 TODO 项总数{group_summaries}= 逗号分隔列表,如“S5 定理(2 项),效用优化(1 项)”
存储用户选择: grouping_mode = "分组" | "单独" | "组合"
步骤 8: 创建选定任务
对于每个选定任务类型,创建任务。重要: 当 NOTE: 标签存在且同时选定修复和学习任务时,首先创建学习任务,以便修复任务可以依赖于它。
8.1: 获取下一个任务编号
next_num=$(jq -r '.next_project_number' specs/state.json)
8.2: 依赖感知任务创建顺序
检查 NOTE: 依赖条件:
has_note_dependency = (NOTE: 标签存在) AND (用户同时选定“修复任务”和“学习任务”)
如果 has_note_dependency 为 TRUE:
- 首先创建学习任务(步骤 8.2a)
- 存储学习任务编号为
learn_it_task_num - 第二创建修复任务并带有依赖(步骤 8.2b)
如果 has_note_dependency 为 FALSE:
- 首先创建修复任务(如果选定)
- 第二创建学习任务(如果选定)
- 无依赖关系
8.2a: 学习任务(当为依赖首先创建时)
条件: has_note_dependency 为 TRUE
{
"title": "从 NOTE: 标签更新上下文文件",
"description": "基于学习更新 {N} 个上下文文件:
{按目标上下文分组}",
"language": "元",
"effort": "1-2 小时"
}
存储任务编号: learn_it_task_num = next_num
递增: next_num = next_num + 1
8.2b: 修复任务(当 has_note_dependency 时带有依赖)
条件: 用户选定“修复任务”且 (FIX: 或 NOTE: 标签存在)
当 has_note_dependency 为 TRUE 时:
{
"title": "修复来自 FIX:/NOTE: 标签的问题",
"description": "处理来自嵌入标签的 {N} 个项:
{具有文件:行引用的项列表}
**重要**: 进行更改时,从源文件中移除 FIX: 和 NOTE: 标签。保留 TODO: 标签不变(它们创建单独任务)。",
"language": "{源文件中主要语言}",
"effort": "2-4 小时",
"dependencies": [learn_it_task_num]
}
当 has_note_dependency 为 FALSE 时:
{
"title": "修复来自 FIX:/NOTE: 标签的问题",
"description": "处理来自嵌入标签的 {N} 个项:
{具有文件:行引用的项列表}
**重要**: 进行更改时,从源文件中移除 FIX: 和 NOTE: 标签。保留 TODO: 标签不变(它们创建单独任务)。",
"language": "{源文件中主要语言}",
"effort": "2-4 小时"
}
语言检测:
如果大多数标签来自 .lean 文件 -> "lean"
否则如果大多数来自 .tex 文件 -> "latex"
否则如果大多数来自 .opencode/ 文件 -> "元"
否则 -> "通用"
8.3: 学习任务(当无依赖创建时)
条件: 用户选定“学习任务”且 NOTE: 标签存在且 has_note_dependency 为 FALSE
{
"title": "从 NOTE: 标签更新上下文文件",
"description": "基于学习更新 {N} 个上下文文件:
{按目标上下文分组}",
"language": "元",
"effort": "1-2 小时"
}
8.4: Todo 任务(如果选定)
条件: 用户选定“TODO 任务”且用户选定特定 TODO 项
检查 grouping_mode(来自步骤 7.5.4,如果步骤 7.5.4 被跳过,默认“单独”):
8.4.1: 分组模式 (grouping_mode == “分组”)
对于 topic_groups 中的每个主题组:
{
"title": "{topic_label}: {item_count} 个 TODO 项",
"description": "处理与 {topic_label} 相关的 TODO 项:
{item_list}
---
共享上下文: {shared_terms_description}",
"language": "{从组中主要文件类型检测}",
"effort": "{缩放努力}"
}
其中:
{topic_label}= 生成标签(例如,“望远镜工作树”){item_count}= 组中项数{item_list}= 格式化项列表:- [ ] {content} (`{file}:{line}`) - [ ] {content} (`{file}:{line}`){shared_terms_description}= 简要描述为何项分组(例如,“与望远镜工作树功能相关”)
努力缩放公式:
基础努力 = 1 小时
缩放努力 = 基础努力 + (30 分钟 * (item_count - 1))
示例:
1 项 → 1 小时
2 项 → 1.5 小时 (1h + 30min)
3 项 → 2 小时 (1h + 60min)
4 项 → 2.5 小时 (1h + 90min)
8.4.2: 组合模式 (grouping_mode == “组合”)
创建单个任务包含所有选定 TODO 项:
{
"title": "处理 {item_count} 个 TODO 项",
"description": "从扫描中组合的 TODO 项:
{all_items_list}
---
文件: {unique_files_list}",
"language": "{从主要文件类型检测}",
"effort": "{缩放努力}"
}
其中:
{item_count}= 选定 TODO 项总数{all_items_list}= 格式化所有项列表带复选框{unique_files_list}= 涉及唯一文件的逗号分隔列表
努力缩放: 与分组模式相同公式。
8.4.3: 单独模式 (grouping_mode == “单独” 或默认)
对于每个选定 TODO 项单独:
{
"title": "{标签内容,截断至 60 字符}",
"description": "{完整标签内容}
来源: {file}:{line}",
"language": "{从文件类型检测}",
"effort": "1 小时"
}
Todo 任务语言检测(所有模式):
.lua (nvim/) -> "neovim"
.tex -> "latex"
.md -> "markdown"
.py/.sh -> "通用"
.opencode/* -> "元"
步骤 9: 更新状态文件
对于每个创建的任务:
9.1: 更新 state.json
读取当前状态,添加新任务条目,递增 next_project_number:
# 从标题创建 slug
slug=$(echo "$title" | tr '[:upper:]' '[:lower:]' | tr ' ' '_' | tr -cd 'a-z0-9_' | cut -c1-50)
# 读取当前状态
current=$(cat specs/state.json)
# 使用 jq 添加任务(使用两步模式避免转义问题)
# 步骤 1: 将任务数据写入临时文件
# 步骤 2: 使用 jq 和 slurpfile
对于当 has_note_dependency 为 TRUE 时的修复任务,包括依赖数组:
{
"project_number": {N},
"project_name": "{slug}",
"status": "未开始",
"language": "{language}",
"dependencies": [learn_it_task_num]
}
对于所有其他任务,无需依赖字段。
9.2: 更新 TODO.md
在 ## 任务 部分前置新任务条目(新任务在顶部):
标准格式(无依赖):
### {N}. {标题}
- **努力**: {估计}
- **状态**: [未开始]
- **语言**: {语言}
- **开始**: {时间戳}
**描述**: {描述}
---
当 has_note_dependency 为 TRUE 时的修复任务格式:
### {N}. {标题}
- **努力**: {估计}
- **状态**: [未开始]
- **语言**: {语言}
- **依赖**: {learn_it_task_num}
- **开始**: {时间戳}
**描述**: {描述}
---
步骤 10: 显示结果
显示创建的任务摘要:
## 从标签创建的任务
**处理的标签**: {N} 跨扫描文件
### 创建的任务
| # | 类型 | 标题 | 语言 |
|---|------|-------|----------|
| {N} | 修复 | 修复来自 FIX:/NOTE: 标签的问题 | {lang} |
| {N+1} | 学习 | 从 NOTE: 标签更新上下文文件 | 元 |
| {N+2} | todo | {title} | {lang} |
---
**后续步骤**:
1. 在 TODO.md 中审阅任务
2. 运行 `/research {first_task}` 开始
3. 通过 /research -> /plan -> /implement 循环推进
步骤 11: Git 提交(后处理)
如果创建了任务,提交更改:
task_count={创建的任务数}
git add specs/TODO.md specs/state.json
git commit -m "learn: 从标签创建 $task_count 个任务
会话: $session_id
共同作者: Claude Opus 4.5 <noreply@anthropic.com>"
错误处理
路径访问错误
当路径不存在或无法访问时:
- 为每个无效路径记录警告
- 继续处理有效路径
- 如果无有效路径剩余,报告并退出
未发现标签
这不是错误条件:
- 信息性报告
- 退出无提示
state.json 更新失败
如果 jq 失败:
- 记录错误带命令和输出
- 尝试两步 jq 模式
- 如果仍然失败,报告部分成功(发现标签但未创建任务)
TODO.md 解析错误
如果 TODO.md 格式损坏:
- 记录错误
- 跳过 TODO.md 更新
- state.json 更新可能仍成功
- 报告部分成功
Git 提交失败
非阻塞:
- 记录失败
- 任务仍成功创建
- 报告提交失败但任务存在