get-shit-done 相位目录命名漂移修复详解:统一 project_code 前缀在全部 GSD 工作流中的解析路径
在 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-op 与 config-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-op 与 init.plan-phase 的首次触碰(first-touch)场景,#3306(即本文主题)补齐了 plan-milestone-gaps、import、add-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.md 中 plan_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;
从源码结构看,这里有几个关键设计点:
- 先校验、后计算:
assertSafeProjectCode对前缀做安全性断言(防止配置值被用于构造危险路径),再交给computeExpectedPhaseDirName生成规范名。 - 幂等的存在性判断:如果相位目录在磁盘上已存在,
expected_phase_dir直接为null,消费方以phase_dir为准,避免“按配置重建本不该重建的目录”;仅在“首次触碰”(目录尚不存在)时提供应创建的名字。 - 路径归一化:结果经
toPosixPath与相对项目根的relative处理,保证跨平台下工作流拿到的始终是可以直接mkdir -p的 POSIX 相对路径。 - 返回契约:该值随
init.phase-op的 JSON 结果以expected_phase_dir字段输出,同对象还携带phase_found、phase_dir、phase_slug、padded_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-gaps、import、add-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),这个案例提供了一个通用启示:凡是多个流程共同创建的派生命名资源,都应收敛到同一个“规范名计算函数”,而不是让每个流程各自推导——否则前缀、填充、大小写这类命名细节迟早会在某个低频工作流中漂移。
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 StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00