get-shit-done 阶段移除实战:用 /gsd:phase remove 安全删除未启动阶段并保持路线图线性
get-shit-done 的 remove-phase 工作流(get-shit-done/workflows/remove-phase.md)定义了一套完整的"阶段移除"操作规范:删除路线图上尚未启动的未来阶段目录、对后续所有阶段统一重编号以维持线性序列,并通过一次 git commit 记录移除历史。本文以该工作流为主体,结合 SDK 中 gsd-sdk query phase.remove 的源码实现(sdk/src/query/phase-lifecycle.ts)与测试用例(sdk/src/query/phase-lifecycle.test.ts),带你掌握从参数解析、未来阶段校验、二次确认、原子化执行到提交收尾的完整链路,以及整数阶段、小数阶段重编号背后的排序与防冲突原理。
工作流定位:为什么需要"移除阶段"
在 get-shit-done 的规划体系中,.planning/ROADMAP.md 维护着整个项目的阶段路线图,.planning/phases/ 下按 NN-slug/(整数阶段)或 NN.M-slug/(小数阶段)存放每个阶段的目录。随着里程碑推进,可能会遇到两种情况需要移除阶段:
- 规划变更:某个未来阶段因需求调整被取消,直接从路线图中删除;
- 误规划清理:规划阶段时产生的多余或重复阶段需要回收。
无论哪种情况,直接手动删除目录都会留下"断号"——后续阶段的编号、目录名、ROADMAP 中的引用、依赖关系都会错位。remove-phase 工作流的核心价值就在于把"删除 + 重编号 + 文档同步 + 提交记录"封装成一次可确认、可审计的原子操作,只允许删除尚未启动的未来阶段,已完成(存在 SUMMARY.md)或正在执行的阶段一律拒绝,除非显式 --force。
该工作流与 insert-phase.md(插入小数阶段处理紧急工作)互为镜像:insert 用小数编号避免整体重排,remove 则彻底删除并重编号恢复线性序列。
第一步:参数解析(parse_arguments)
工作流的入口是解析命令行参数,接受整数或小数阶段号:
| 示例命令 | 解析结果 |
|---|---|
/gsd-remove-phase 17 |
phase = 17 |
/gsd-remove-phase 16.1 |
phase = 16.1 |
如果没有提供参数,直接输出用法并退出:
ERROR: Phase number required
Usage: /gsd-remove-phase <phase-number>
Example: /gsd-remove-phase 17
值得说明的是,工作流文档中的 /gsd-remove-phase 是命令形态的旧式写法,命令清单中与之对应的现代形态是 /gsd:phase --remove(可与 insert-phase.md 中 /gsd:phase --insert 的写法对照)。两种形态最终都会路由到 SDK 的 phase.remove 查询处理器。
第二步:初始化阶段操作上下文(init_context)
在动手之前,工作流要求先加载阶段操作上下文:
INIT=$(gsd-sdk query init.phase-op "${target}")
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
@file: 前缀说明查询结果过大时 SDK 会把 JSON 写入临时文件,调用方需要读回内容。从返回的 JSON 中提取以下字段:
phase_found:目标阶段是否存在于路线图/磁盘phase_dir:阶段目录的规范路径phase_number:归一化后的阶段号commit_docs:项目是否配置了提交文档(对应.planning/config.json中的commit_docs)roadmap_exists:.planning/ROADMAP.md是否存在
这一步骤的底层实现是 SDK 中的 initPhaseOp 处理器(sdk/src/query/init.ts)。它的逻辑值得注意:先通过 findPhase 在磁盘上查找阶段目录,再通过 roadmapGetPhase 查询 ROADMAP;如果唯一匹配来自已归档的里程碑,则优先使用当前 ROADMAP 中的阶段信息(shouldDropArchivedPhaseMatch);如果目录不存在但 ROADMAP 有记录,则退回用路线图元数据补齐 phase_info。同时它还根据 project_code 计算预期的规范目录名(computeExpectedPhaseDirName),保证与 phase.add 的命名约定一致。
同时,工作流要求读取 STATE.md 和 ROADMAP.md 的内容,用于解析"当前阶段位置"——这是下一步校验的前提。
第三步:校验目标必须是未来阶段(validate_future_phase)
核心安全约束:只能删除尚未启动的未来阶段。校验逻辑:
- 将目标阶段号与
STATE.md中的当前阶段号比较; - 目标必须 大于 当前阶段号。
如果目标小于等于当前阶段号,输出错误并退出:
ERROR: Cannot remove Phase {target}
Only future phases can be removed:
- Current phase: {current}
- Phase {target} is current or completed
To abandon current work, use /gsd:pause-work instead.
这里明确区分了两种语义:想放弃当前正在做的工作应该走 pause-work.md(暂停工作流),而不是 remove-phase。当前/历史阶段承载着已执行的计划和产物,删除它们会破坏项目的历史轨迹。
第四步:二次确认(confirm_removal)
通过校验后,向用户展示移除摘要并等待 y/n 确认:
Removing Phase {target}: {Name}
This will:
- Delete: .planning/phases/{target}-{slug}/
- Renumber all subsequent phases
- Update: ROADMAP.md, STATE.md
Proceed? (y/n)
确认步骤给了用户一次反悔机会——删除目录是不可逆操作,虽然 git 提交保留了记录,但物理删除前明确告知影响范围是工作流的惯例。
第五步:委托 SDK 执行移除(execute_removal)
工作流明确要求:整个移除操作委托给 gsd-sdk query phase.remove,不要手动重编号:
RESULT=$(gsd-sdk query phase.remove "${target}")
如果目标阶段目录下存在已执行的计划(*-SUMMARY.md 或 SUMMARY.md 文件),CLI 会报错;只有在用户明确确认后才使用 --force:
RESULT=$(gsd-sdk query phase.remove "${target}" --force)
执行完成后,从结果中提取:removed、directory_deleted、renamed_directories、renamed_files、roadmap_updated、state_updated。
源码视角:phaseRemove 的完整执行链
phaseRemove 处理器(sdk/src/query/phase-lifecycle.ts)严格实现了工作流描述的每一步,并且在参数层做了更细的防御:
参数与前置校验
- 只接受
--force一个可选旗标,其他--xxx一律抛出GSDError("phase remove does not support {token}"); - 位置参数只能有一个阶段号,多了报错;
- 阶段号不能含 null 字节(
assertNoNullBytes); .planning/ROADMAP.md不存在时报 "ROADMAP.md not found";- 通过
normalizePhaseName归一化阶段号,并用phaseTokenMatches在phases目录里精确匹配目标目录;匹配不到报 "Phase {target} not found"。
已执行工作防护
未加 --force 时,会扫描目标目录下所有 *-SUMMARY.md / SUMMARY.md 文件,数量大于 0 就抛出:
Phase {targetPhase} has {N} executed plan(s). Use --force to remove anyway.
这正是 phase-lifecycle.test.ts 中 "requires --force to remove phase with SUMMARY files" 用例所验证的行为。
删除目录
用 rm(..., { recursive: true, force: true }) 递归删除目标阶段目录。
重编号(核心难点) 删除后按阶段类型分别处理(sdk/src/query/phase-lifecycle.ts):
- 整数阶段(如删除 5 → 6 变 5、7 变 6):
renameIntegerPhases用正则^(\d+)([A-Z])?(?:\.(\d+))?-(.+)$解析目录名,支持字母后缀(如 12A)和小数后缀(如 6.1)。关键设计:按降序排序后再重命名(b.oldInt - a.oldInt),避免先重命名低编号目录导致后续目录目标名被占用。同时跳过>= 999的后备箱(backlog)阶段——999.x 是"暂存想法"的保留编号区间,重编号会破坏该约定并污染下游查询(源码注释明确标注这是 bug-2434 的教训)。 - 小数阶段(如删除 6.2 → 6.3 变 6.2、6.4 变 6.3):
renameDecimalPhases同样按小数部分降序处理,同时重命名目录内文件名中包含旧阶段 ID 的文件。
目录重命名后,文件也会同步重命名:整数阶段按 oldPrefix 前缀(含补齐的零位、字母、小数后缀)替换为新前缀,例如 07-01-PLAN.md → 06-01-PLAN.md;小数阶段则是把文件名中的 06.2 替换为 06.1。测试用例(phase-lifecycle.test.ts)完整验证了这两种场景:删除整数 6 后 07-api 变成 06-api 且内部 07-01-PLAN.md 变成 06-01-PLAN.md;删除小数 6.1 后 06.2-hotfix-b 变成 06.1-hotfix-b、06.3-hotfix-c 变成 06.2-hotfix-c。
ROADMAP.md 更新(5 个精准正则)
updateRoadmapAfterPhaseRemoval(sdk/src/query/phase-lifecycle.ts)通过 readModifyWriteRoadmapMd 原子读写,执行 5 类替换:
- 删除阶段小节:用深度感知的 lookahead 正则匹配
### Phase N:小节及其正文,直到下一个同深度(\k<h>(?!#))的阶段标题。这一设计同时解决两个历史 bug:删除### Phase 2:时不能误删同级小数的### Phase 2.1:(#3601),但删除### Phase 27:时要连它的子级#### Phase 27.1:一起删(#3355)。 - 删除引用该阶段的 checkbox 行(
- [ ] Phase N: ...)。 - 删除引用该阶段的表格行(
| N. ... |)。 - 重编号后续阶段标题:
### Phase N:/### Phase N.M:中的数字减一,带小数后缀则保留后缀。这里特意用 5 个精准正则而非循环,因为循环实现有三个隐患:会匹配到 YYYY-MM-DD 日期子串并破坏日期(bug-2435)、会误改 999.x 后备箱阶段(bug-2434)、正则重叠匹配会导致同一阶段被多次减号(bug-3355)。 - 重编号引用:包括 checkbox 摘要引用、表格单元格裸整数、
NN-NN形式的计划引用(如07-01-cherry-pick-foundation-PLAN.md,带 slug 变体)、以及**Depends on**: Phase N依赖声明。
所有重编号 helper(decrementRoadmapPhaseNumber / decrementRoadmapPhaseToken / decrementRoadmapPaddedPhaseNumber)都带同一组守卫:非整数不动、num <= removedInt 不动(已重编号区间的阶段保持原位)、num >= 999 不动。
STATE.md 更新(减一)
在 acquireStateLock / releaseStateLock 的锁保护下(sdk/src/query/phase-lifecycle.ts),对 STATE.md 做三类减一操作:
- frontmatter 中的
total_phases: N→N-1; - 正文中的
Plan: 2 of 3这类 "of N" 模式; Total Phases字段(经stateReplaceField)。
返回结构:最终返回 { removed, directory_deleted, renamed_directories, renamed_files, roadmap_updated, state_updated },与工作流文档要求提取的字段一一对应。
第六步:提交记录(commit)
移除是破坏性变更,git 提交就是它的历史档案:
gsd-sdk query commit "chore: remove phase {target} ({original-phase-name})" --files .planning/
这条命令委托给 SDK 的 commit 处理器(sdk/src/query/commit.ts),其关键行为包括:
- 路径范围:显式传入
--files .planning/时只提交.planning/下的变更,绝不把外部预暂存内容带进来(#3061); - commit_docs 门控:若
.planning/config.json中commit_docs === false且未加--force,提交会被拒绝; - 策略分支保障:提交前
ensureStrategyBranch确保处于正确的策略分支上,分支切换失败会中止提交,避免在错误分支上留下提交记录(#3749); - 消息净化:
sanitizeCommitMessage清理提交信息,且只剥离已知旗标(--force/--amend/--no-verify/--respect-staged),不会误删任意--foo内容。
第七步:完成摘要(completion)
操作收尾时向用户呈现完整摘要,并给出后续导航建议:
Phase {target} ({original-name}) removed.
Changes:
- Deleted: .planning/phases/{target}-{slug}/
- Renumbered: {N} directories and {M} files
- Updated: ROADMAP.md, STATE.md
- Committed: chore: remove phase {target} ({original-name})
---
## What's Next
Would you like to:
- `/gsd:progress` — see updated roadmap status
- Continue with current phase
- Review roadmap
---
反模式清单:必须避免的坑
工作流明确列出以下反模式,每一条都有对应的源码级理由:
| 反模式 | 原因 |
|---|---|
不加 --force 删除含 SUMMARY.md 的已完成阶段 |
已完成阶段承载执行历史,phaseRemove 会硬性拦截 |
| 删除当前或历史阶段 | 违反"仅未来阶段"约束,应改用 pause-work.md |
| 手动重编号 | 重编号涉及降序排序防冲突、5 个防 bug 正则、STATE.md 减一,手动必然出错 |
| 在 STATE.md 中添加"removed phase"备注 | git 提交本身就是移除的历史记录,STATE.md 只做计数减一 |
| 修改已完成阶段的目录 | 只删除目标目录,renameIntegerPhases/renameDecimalPhases 严格只动目标之后编号大于它的目录 |
成功标准与测试佐证
工作流定义的完成标准为:
- [ ] 目标阶段被校验为未来/未启动阶段
- [ ]
gsd-sdk query phase.remove执行成功 - [ ] 变更以描述性消息提交
- [ ] 用户已被告知变更内容
这些标准在 phase-lifecycle.test.ts 的测试套件中均有对应用例:整数阶段删除与重编号、小数阶段删除与兄弟小数重编号、SUMMARY 存在时的 --force 拦截、--force 放行,以及 --force 位置在阶段号之前也能被接受(bug-3409 回归防护)。此外,phase.add、phase.insert、phase.remove、phase.complete 在同一个处理器族中共存,保证四类阶段操作对目录、路线图和状态文件的处理口径一致。
结语
remove-phase 工作流是 get-shit-done 规划治理中"做减法"的标准姿势:先校验(只删未来阶段)、再确认(展示影响面)、后委托(交给 phase.remove 原子执行)、终提交(git 记录历史)。从源码层面看,它的稳健性来自三个设计要点——重编号的降序排序避免命名冲突、ROADMAP 更新用 5 个带守卫的精准正则替代循环以避免日期破坏和重复减号、STATE.md 在文件锁保护下只做减一。理解这套机制后,无论是清理规划错误还是响应需求变更,你都能在保持路线图线性的前提下安全地移除阶段,并让每一次移除都有据可查。
相关延伸阅读:insert-phase.md(小数阶段插入,与 remove 形成闭环)、pause-work.md(放弃当前工作的正确途径)、state.md(STATE.md 结构与字段约定)。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00