首页
/ get-shit-done 相位目录命名漂移修复详解:统一 project_code 前缀在全部 GSD 工作流中的解析路径

get-shit-done 相位目录命名漂移修复详解:统一 project_code 前缀在全部 GSD 工作流中的解析路径

2026-09-05 23:09:01作者:温艾琴Wonderful

在 get-shit-done(GSD)的多项目规划体系中,.planning/config.json 中的 project_code 字段决定了相位(phase)目录的最终命名形态。本文围绕变更单 #3298 展开,讲清一次“相位目录前缀漂移”缺陷的来龙去脉:/gsd-plan-milestone-gaps/gsd-import/gsd-capture --backlog 三个工作流曾因使用裸 {NN}-{slug} 模式拼接目录路径,绕过了 project_code 前缀,导致 XR-06-fix-auth/ 被误建成 06-fix-auth/。读完本文,你可以掌握 GSD 中相位目录的规范解析方式(gsd-sdk query init.phase-opconfig-get project_code)、SDK 侧 expected_phase_dir 的底层计算实现,以及 PRED.k015 这一“所有消费方必须应用前缀”的不变式约束。

一、缺陷背景:project_code 前缀与相位目录命名约定

GSD 为每个相位在 .planning/phases/ 下创建形如 {NN}-{slug}/ 的目录,其中 NN 是零填充的相位编号,slug 是相位描述的短横线化名称。当项目配置了 project_code 时,目录名应变为 {CODE}-{NN}-{slug}/。例如一个设置了 project_code: "XR" 的项目,第六个相位目录应为 XR-06-fix-auth/,而非 06-fix-auth/

这个前缀机制的意义在于多项目/多工作流共存时,让磁盘上的相位目录可以自我标识归属项目,同时保证 progress/gsd-plan-phase/gsd-discuss-phase 等所有读写同一目录的消费方都能对位。

本次变更单(3298-phase-dir-prefix-drift-workflows.md,frontmatter 标记为 type: Fixed,对应 PR #3306)记录的缺陷是:三个工作流文件在构造相位目录路径时,直接使用裸 {NN}-{slug} 模式,完全绕过了 .planning/config.json 中的 project_code。在同一仓库中,/gsd-plan-phase/gsd-discuss-phase(由 #3292 修复)能正确产出带前缀的目录名,而 plan-milestone-gaps 创建的是 06-fix-auth/ 这类无前缀目录——同一个逻辑相位在磁盘上出现了两套命名,即“漂移(drift)”。

CONTEXT.md 中对这一缺陷族的登记可以佐证其属于一个已知缺陷模式,而非孤立问题:

DEFECT.PHASE-DIR-PREFIX-DRIFT.examples=#3287 (init.phase-op + init.plan-phase first-touch),
#3306/PRED.k015 (plan-milestone-gaps + import + add-backlog), #3297/#3298 (sibling reports)

可见该缺陷先后波及三批消费方:#3287 处理了 init.phase-opinit.plan-phase 的首次触碰(first-touch)场景,#3306(即本文主题)补齐了 plan-milestone-gapsimportadd-backlog 三个工作流,并沉淀出 PRED.k015 这条前置要求(precondition):project_code 前缀必须在所有消费方处被应用

二、修复策略:把“目录名计算”收敛到唯一权威来源

修复的核心不是逐个打补丁,而是让三个工作流不再自行拼接目录名,而是统一向已有的权威来源查询规范目录名。GSD 的相位生命周期操作在 SDK 侧有统一入口 gsd-sdk query init.phase-op <NN>,它返回一个 JSON 对象,其中 expected_phase_dir 字段就是“按当前配置应创建的规范相位目录(相对项目根)”——这正是各工作流需要的值。

2.1 plan-milestone-gaps:经 init.phase-op 解析目录名

里程碑间隙规划工作流会为每个新的间隙关闭相位(N, N+1, …)创建目录。修复后的步骤见 plan-milestone-gaps.md 第 8 节:

INIT=$(gsd-sdk query init.phase-op "{NN}")
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
expected_phase_dir=$(echo "$INIT" | node -e "process.stdout.write(JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')).expected_phase_dir)")
mkdir -p "${expected_phase_dir}"

文档明确承诺了两种取值结果:当 .planning/config.json 配置了 project_code 时产出 {CODE}-{NN}-{slug}/,否则产出 {NN}-{slug}/——与所有其他相位创建路径保持一致。对每个间隙关闭相位号重复执行一次即可。

这里有一个值得注意的实现细节:INIT 可能以 @file: 形式返回(大 JSON 落盘后给路径以避免 shell 变量膨胀),工作流会先解引用再解析 JSON,最后用 node -e 从 stdin 安全提取 expected_phase_dir 字段,规避了 jq 依赖与引号转义问题。

2.2 import:导入既有计划时同样走权威解析

外部计划导入流程(/gsd-import)在把 PBR 等来源的计划转换为 GSD 的 {NN}-{MM}-PLAN.md 命名后,需要确定写入的相位目录。修复后的做法见 import.mdplan_read_input 提取相位号后的步骤:

INIT=$(gsd-sdk query init.phase-op "{NN}")
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
expected_phase_dir=$(echo "$INIT" | node -e "process.stdout.write(JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')).expected_phase_dir)")

目录不存在则 mkdir -p "${expected_phase_dir}",随后令 phase_dir="${expected_phase_dir}" 供后续写入 PLAN.md 与校验步骤使用。文档注释直白地点名了动机:“This ensures the project_code prefix from .planning/config.json is applied”。

2.3 add-backlog:经 config-get 读取前缀并手工拼装

/gsd-capture --backlog 的 backlog 创建工作流在 add-backlog.md 第 4 步中采用了略微不同的路径——直接通过 config-get 读取 project_code 再拼前缀:

SLUG=$(gsd-sdk query generate-slug "$ARGUMENTS" --raw)
PROJECT_CODE=$(gsd-sdk query config-get project_code --raw 2>/dev/null || echo "")
PREFIX=$([ -n "$PROJECT_CODE" ] && echo "${PROJECT_CODE}-" || echo "")
PHASE_DIR=".planning/phases/${PREFIX}${NEXT}-${SLUG}"
mkdir -p "${PHASE_DIR}"
touch "${PHASE_DIR}/.gitkeep"

随后第 5 步用 gsd-sdk query commit "docs: add backlog item ${NEXT} — ${ARGUMENTS}" --files .planning/ROADMAP.md "${PHASE_DIR}/.gitkeep" 将 ROADMAP 条目与 backlog 占位目录一并提交,第 6 步报告目录并提示用户可后续用 /gsd:discuss-phase 探索、/gsd:review-backlog 提升。

两种写法殊途同归:都从配置单一数据源取得前缀,杜绝了工作流自行猜测目录名。

三、源码级实现:SDK 如何计算 expected_phase_dir

工作流只是消费方,真正的计算逻辑在 SDK 的 init.phase-op 查询处理器中。init.ts 中相关段落展示了完整的规范目录名计算链:

// #3287: compute the canonical directory name with project_code prefix so
// the first-touch mkdir in /gsd-plan-phase stays consistent with phase.add.
const rawProjectCode = (config as Record<string, unknown>).project_code as string || '';
assertSafeProjectCode(rawProjectCode);
const expectedPhaseDirName = phaseDir
  ? null // directory already exists — no need to create
  : computeExpectedPhaseDirName(phaseNumber, phaseName, rawProjectCode);
const expectedPhaseDir = expectedPhaseDirName
  ? toPosixPath(relative(projectDir, join(paths.phases, expectedPhaseDirName)))
  : null;

从源码结构看,这里有几个关键设计点:

  1. 先校验、后计算assertSafeProjectCode 对前缀做安全性断言(防止配置值被用于构造危险路径),再交给 computeExpectedPhaseDirName 生成规范名。
  2. 幂等的存在性判断:如果相位目录在磁盘上已存在,expected_phase_dir 直接为 null,消费方以 phase_dir 为准,避免“按配置重建本不该重建的目录”;仅在“首次触碰”(目录尚不存在)时提供应创建的名字。
  3. 路径归一化:结果经 toPosixPath 与相对项目根的 relative 处理,保证跨平台下工作流拿到的始终是可以直接 mkdir -p 的 POSIX 相对路径。
  4. 返回契约:该值随 init.phase-op 的 JSON 结果以 expected_phase_dir 字段输出,同对象还携带 phase_foundphase_dirphase_slugpadded_phase 等字段,供工作流做分支判断。

此外,SDK 侧的相位生命周期模块 phase.ts 与运行时库 phase.cjs 中同样存在对 project_code 的前缀感知处理,例如 phase.cjs 中“Strip the optional project_code prefix before extracting the leading integer”这类解析逻辑——即读取侧同样能识别带前缀的目录名并把 XR-06 还原为相位号 06。这说明前缀机制是双向的:写入路径加前缀,读取路径去前缀,两侧都由同一配置驱动。

四、PRED.k015 不变式与回归测试保障

变更单将本修复归口到 PRED.k015 要求:“project_code prefix is applied at all consumers”(前缀在所有消费方处生效)。从 CONTEXT.md 的缺陷登记看,这一不变式已被拆分为多个编号的修复批次(#3287、#3292、#3306 等)逐个消费方落实,本次 PR #3306 补上的正是 plan-milestone-gapsimportadd-backlog 三处残留的消费点。

测试侧存在针对 project_code 前缀的专项回归,例如 bug-3599-roadmap-get-phase-project-code-prefix.test.cjs(对应 3599-roadmap-get-phase-project-code-prefix.md 变更单,验证 roadmap get-phase 的解析同样尊重前缀),以及 phase.test.cjs 等覆盖相位目录逻辑的测试。这些测试与本次工作流改动共同构成“配置项 → SDK 计算 → 工作流消费”链路上的回归防护。

五、结论与适用说明

本次修复(#3298 / PR #3306)的价值不在于改了三个字符串拼接,而在于确立了相位目录名的单一解析权威:

  • 新建相位目录的唯一正确姿势是查询 gsd-sdk query init.phase-op {NN} 并采用其 expected_phase_dir 字段;确需在轻量脚本中拼装时,也应通过 gsd-sdk query config-get project_code --raw 取前缀,如 add-backlog.md 所示。
  • 未设置 project_code 的项目行为不受影响,目录仍为 {NN}-{slug}/;设置了前缀的项目则全部消费方统一产出 {CODE}-{NN}-{slug}/,消除了同一逻辑相位双命名的漂移风险。
  • 适用前提:该机制依赖当前仓库版本的 GSD SDK 查询层(gsd-sdk query)与 .planning/config.json 配置体系;阅读 CONFIGURATION.md 可进一步确认 project_code 的配置语义与默认值。

对于需要在多工作流共享磁盘状态的项目(无论是否基于 GSD),这个案例提供了一个通用启示:凡是多个流程共同创建的派生命名资源,都应收敛到同一个“规范名计算函数”,而不是让每个流程各自推导——否则前缀、填充、大小写这类命名细节迟早会在某个低频工作流中漂移。

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