首页
/ get-shit-done 阶段移除实战:用 /gsd:phase remove 安全删除未启动阶段并保持路线图线性

get-shit-done 阶段移除实战:用 /gsd:phase remove 安全删除未启动阶段并保持路线图线性

2026-09-09 11:54:54作者:晏闻田Solitary

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.mdROADMAP.md 的内容,用于解析"当前阶段位置"——这是下一步校验的前提。

第三步:校验目标必须是未来阶段(validate_future_phase)

核心安全约束:只能删除尚未启动的未来阶段。校验逻辑:

  1. 将目标阶段号与 STATE.md 中的当前阶段号比较;
  2. 目标必须 大于 当前阶段号。

如果目标小于等于当前阶段号,输出错误并退出:

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.mdSUMMARY.md 文件),CLI 会报错;只有在用户明确确认后才使用 --force

RESULT=$(gsd-sdk query phase.remove "${target}" --force)

执行完成后,从结果中提取:removeddirectory_deletedrenamed_directoriesrenamed_filesroadmap_updatedstate_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 归一化阶段号,并用 phaseTokenMatchesphases 目录里精确匹配目标目录;匹配不到报 "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.md06-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-b06.3-hotfix-c 变成 06.2-hotfix-c

ROADMAP.md 更新(5 个精准正则) updateRoadmapAfterPhaseRemovalsdk/src/query/phase-lifecycle.ts)通过 readModifyWriteRoadmapMd 原子读写,执行 5 类替换:

  1. 删除阶段小节:用深度感知的 lookahead 正则匹配 ### Phase N: 小节及其正文,直到下一个同深度(\k<h>(?!#))的阶段标题。这一设计同时解决两个历史 bug:删除 ### Phase 2: 时不能误删同级小数的 ### Phase 2.1:(#3601),但删除 ### Phase 27: 时要连它的子级 #### Phase 27.1: 一起删(#3355)。
  2. 删除引用该阶段的 checkbox 行- [ ] Phase N: ...)。
  3. 删除引用该阶段的表格行| N. ... |)。
  4. 重编号后续阶段标题### Phase N: / ### Phase N.M: 中的数字减一,带小数后缀则保留后缀。这里特意用 5 个精准正则而非循环,因为循环实现有三个隐患:会匹配到 YYYY-MM-DD 日期子串并破坏日期(bug-2435)、会误改 999.x 后备箱阶段(bug-2434)、正则重叠匹配会导致同一阶段被多次减号(bug-3355)。
  5. 重编号引用:包括 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: NN-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.jsoncommit_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.addphase.insertphase.removephase.complete 在同一个处理器族中共存,保证四类阶段操作对目录、路线图和状态文件的处理口径一致。

结语

remove-phase 工作流是 get-shit-done 规划治理中"做减法"的标准姿势:先校验(只删未来阶段)、再确认(展示影响面)、后委托(交给 phase.remove 原子执行)、终提交(git 记录历史)。从源码层面看,它的稳健性来自三个设计要点——重编号的降序排序避免命名冲突、ROADMAP 更新用 5 个带守卫的精准正则替代循环以避免日期破坏和重复减号、STATE.md 在文件锁保护下只做减一。理解这套机制后,无论是清理规划错误还是响应需求变更,你都能在保持路线图线性的前提下安全地移除阶段,并让每一次移除都有据可查。

相关延伸阅读:insert-phase.md(小数阶段插入,与 remove 形成闭环)、pause-work.md(放弃当前工作的正确途径)、state.md(STATE.md 结构与字段约定)。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
858
1.35 K
docsdocs
暂无描述
Markdown
899
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
923
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.83 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
532
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.03 K
524
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
393