news 2026/9/8 23:57:17

get-shit-done 状态机修复实战:让 `state complete-phase` 幂等化,彻底杜绝 STATE.md 被重复执行回滚

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
get-shit-done 状态机修复实战:让 `state complete-phase` 幂等化,彻底杜绝 STATE.md 被重复执行回滚

get-shit-done 状态机修复实战:让state complete-phase幂等化,彻底杜绝 STATE.md 被重复执行回滚

【免费下载链接】get-shit-doneA light-weight and powerful meta-prompting, context engineering and spec-driven development system for Claude Code by TÂCHES.项目地址: https://gitcode.com/GitHub_Trending/getshi/get-shit-done

本篇技术指南围绕 get-shit-done(一个基于 Claude Code 的轻量 meta-prompting、上下文工程与 spec-driven 开发系统)中的一次真实缺陷修复展开:当state complete-phase --phase <N>对已经标记完成的阶段被二次调用时,曾会把规划状态文档 STATE.md 静默回滚到该阶段完成时刻的旧内容。文章从缺陷症状、根因、修复后的"先读后写"防护逻辑到回归测试逐一展开,读者读完可以掌握该系统中 STATE.md 状态推进的底层机制,以及如何为一个"状态写入型命令"设计与验证幂等语义。

背景:STATE.md 在 get-shit-done 中的核心地位

在 get-shit-done 中,.planning目录下的STATE.md是贯穿规划与执行的单一事实来源。它会记录:

  • Status(当前整体状态)
  • Current Phase(当前阶段,规范字段)
  • Last ActivityLast Activity Description(最近活动及说明)
  • ## Current Position(当前进展正文,内含Phase:Status:Last activity:等行)

这段正文被众多下游消费者信任与读取——包括/gsd-progress、planner(规划器)、以及下一阶段开始时的 discuss-phase 上下文加载器。一旦 STATE.md 被写入错误的历史内容,整套进度流都会被引导回错误阶段。

本次修复(PR #3489)即属于该状态机的一个关键子命令:state complete-phase。其职责是把"当前阶段"在 STATE.md 中正式标记为 COMPLETE,让项目可以推进到下一阶段。

缺陷复现:重复执行一次,进度回滚一次

原始实现中,state complete-phase --phase <N>(等价于通过 SDK 通道执行gsd-sdk query state.complete-phase --phase <N>不具备幂等性

  1. 当阶段<N>已被合法标记完成、且项目随后已推进(例如插入了后续阶段02.2.1,或下一阶段已经开始);
  2. 此时若某个下游工具因重跑而再次对<N>执行complete-phase
  3. 旧实现会无条件把 STATE.md 重写一遍,将其内容回滚到"阶段<N>完成那一刻"的取值,静默破坏:
    • Status
    • Last Activity
    • Last Activity Description
    • ## Current Position正文

结果就是:依赖 STATE.md 的下游消费者(/gsd-progress、planner、下一阶段 discuss-phase 的上下文加载器)全部被路由回已回滚的旧阶段,造成进度倒退、上下文错位。

该行为在源码中留有注释佐证,见 get-shit-done/bin/lib/state.cjs:注释明确指出重新调用在旧版本中会把 STATE.md 回滚到<N>完成瞬间,破坏上述四个字段。

根因分析:cmdStateCompletePhase的写路径

修复前,命令处理器cmdStateCompletePhase的处理流程位于 get-shit-done/bin/lib/state.cjs。它的主流程大致是:

function cmdStateCompletePhase(cwd, raw, overridePhase) { const statePath = planningPaths(cwd).state; if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; } const content = fs.readFileSync(statePath, 'utf-8'); const resolvedPhase = resolvePhaseIdForCompletePhase(content, overridePhase); if (!resolvedPhase || /^phase$/i.test(resolvedPhase)) { output({ error: 'Unable to resolve current phase. Pass an explicit phase: state complete-phase --phase <N>' }, raw); return; } // ... 此处过去直接进入 readModifyWriteStateMd 写路径 readModifyWriteStateMd(statePath, (content) => { // 更新 Status / Last Activity / Last Activity Description / ## Current Position ... return content; }, cwd); output({ updated, phase: resolvedPhase }, raw, updated.length > 0 ? 'true' : 'false'); }

其中阶段解析函数resolvePhaseIdForCompletePhase(源码)会依次回退读取--phase参数、Current Phase字段、Phase字段,并只接受规范的阶段 token(如3033A3.310.2):

function resolvePhaseIdForCompletePhase(content, overridePhase) { const candidate = overridePhase || stateExtractField(content, 'Current Phase') || stateExtractField(content, 'Phase') || ''; // Accept canonical phase token only (e.g. 3, 03, 3A, 3.3, 10.2) const phaseMatch = String(candidate).match(/(\d+[A-Z]?(?:\.\d+)*)/i); return phaseMatch ? phaseMatch[1] : null; }

问题在于:解析出目标阶段后,旧代码没有把"目标阶段"与"STATE.md 当前正处于的阶段"做比较,直接进入readModifyWriteStateMd(见 get-shit-done/bin/lib/state.cjs 的读写改写封装)把文件整体重写一遍——即使该阶段早已完成且项目早已前进。

修复方案:写入前先读,发现已推进就直接 no-op

修复的核心思路非常直接:写之前先读 STATE.md,用其规范字段Current Phase判断项目是否已经越过目标阶段。

修复后,处理器在进入写路径之前插入了一道防护(get-shit-done/bin/lib/state.cjs):

const existingCurrentPhaseRaw = stateExtractField(content, 'Current Phase') || ''; const existingCurrentPhaseMatch = String(existingCurrentPhaseRaw).match(/(\d+[A-Z]?(?:\.\d+)*)/i); const existingCurrentPhase = existingCurrentPhaseMatch ? existingCurrentPhaseMatch[1] : null; if (existingCurrentPhase && existingCurrentPhase !== resolvedPhase) { output( { updated: [], phase: resolvedPhase, idempotent: true, note: 'phase already superseded; no-op' }, raw, 'false', ); return; }

防护逻辑的判定语义可以归纳为下表:

Current Phase现状传入的--phase行为
不存在(历史/旧格式文件)任意跳过防护,走原写路径
与目标阶段相同目标阶段正常执行完成写入(首次合法完成,或对同一阶段重写同值)
与目标阶段不同(已推进到其他阶段)较早阶段<N>no-op,直接返回,完全不触碰 STATE.md

其中字段提取函数stateExtractField/stateReplaceField位于 get-shit-done/bin/lib/state-document.generated.cjs,用于在 STATE.md 中定位和替换**Field:** value形式的规范字段。

no-op 的返回值协议

当防护触发时,命令不做任何文件写入,并返回结构化 JSON 载荷以让下游消费者可检测:

{ "updated": [], "phase": "<N>", "idempotent": true, "note": "phase already superseded; no-op" }

语义说明:

  • updated: []—— 明确声明本次没有任何字段被更新;
  • phase: "<N>"—— 回显本次被请求完成的阶段;
  • idempotent: true—— 幂等标志,下游工具据此识别"这是一次无害的重复调用";
  • note: "phase already superseded; no-op"—— 人类可读的原因。

合法完成时仍会正常更新哪些字段

防护只拦截"项目已推进过去"的重复调用,不影响正常完成流程。当目标阶段确实是当前进行中的阶段时,readModifyWriteStateMd依旧会执行完整的状态更新(源码):

  1. StatusPhase <N> complete
  2. Last Activity→ 当天日期(new Date().toISOString().split('T')[0]
  3. Last Activity DescriptionPhase <N> marked complete
  4. ## Current Position正文(通过正则定位到下一个##或文件尾):
    • Phase:Phase: <N> — COMPLETE
    • Status:Status: Phase <N> complete
    • Last activity:Last activity: <today> -- Phase <N> marked complete

更新结束后返回{ updated: [...], phase: <N> },其中updated会列出实际被修改的字段(如StatusLast ActivityLast Activity DescriptionCurrent Position)。

命令的派发与可达路径

state complete-phase经由 get-shit-done/bin/lib/state-command-router.cjs 路由,直接解析--phase命名参数后调用 CJS 处理器:

'complete-phase': () => { const { phase: p } = parseNamedArgs(args, ['phase']); state.cmdStateCompletePhase(cwd, raw, p || args[2]); },

从该路由注释可见,complete-phase目前标注为"CJS-only — no SDK counterpart",即由运行时库直接承载(该行注释仅说明其在 SDK 层没有独立 TypeScript 对等实现,CLI 与gsd-sdk query通道仍可触发同一处理器,与本次变更说明中的两条调用形式一致)。需要执行时的两种等价形式为:

# 直接 CLI 形式 gsd state complete-phase --phase <N> # 通过 SDK query 通道 gsd-sdk query state.complete-phase --phase <N>

不传--phase时,处理器会回退到从Current Phase/Phase字段解析;解析失败或命中字面量phase时,会返回{ error: 'Unable to resolve current phase. Pass an explicit phase: state complete-phase --phase <N>' }并提示显式传参。

回归测试如何锁定幂等语义

本次修复配套的回归测试位于 tests/bug-3489-complete-phase-idempotent.test.cjs,通过runGsdTools(['state', 'complete-phase', '--phase', ...], tmpDir)在临时工程内真实执行命令,断言 STATE.md 内容与 JSON 输出:

测试一:重复对已完成阶段执行不得回滚 STATE.md

  • 前置 STATE.md:Current Phase: 02.2.1(阶段02.2已合法完成,后续又插入了02.2.1作为进行中阶段);
  • 执行state complete-phase --phase 02.2
  • 硬断言:文件与调用前快照逐字节一致after === before),即不允许任何重写发生;
  • 输出断言:payload.updated为空数组、payload.phase === '02.2'payload.idempotent === true

测试二:完成真正进行中的阶段仍正常工作(防误伤)

  • 前置 STATE.md:Current Phase: 03
  • 执行state complete-phase --phase 03
  • 断言 STATE.md 中**Status:** Phase 03 complete被写入;
  • 断言输出payload.idempotent !== true(首次完成绝不能被标记为幂等),且payload.updated为非空数组。

两组用例合起来验证了防护的双向正确性:既阻止了历史阶段的重复回滚,又不会对合法完成产生"假阳性幂等"。测试文件顶部还注明了一条项目约束:STATE.md 是部署产物,直接断言其字面文本即是对部署契约的测试。

工程经验总结

  1. 状态写入型命令必须自证幂等。像complete-phase这类"推进状态机"的命令,天然会被自动化工作流多次触发(例如下游工具的兜底重跑),没有幂等防护时,重复执行造成的不是"重复写入"而是"状态回滚"这类更难察觉的数据倒退。
  2. 判据要选规范字段。修复以 STATE.md 的Current Phase作为唯一事实判据,而不是依赖调用者传参;只有在规范字段明确指向"已越过目标阶段"时才判定 no-op,既不会误伤同阶段重写,也能向后兼容缺少该字段的旧文件。
  3. no-op 也要有机器可读的协议idempotent: true+updated: []+ 人类可读的note,让任何下游 Agent / 工具都能在不解析正文的情况下识别幂等调用,这是回归测试可以直接断言的关键设计。
  4. 回归测试要同时覆盖正向与负向。只测试"不再回滚"是不够的,还必须证明"首次正常完成"不被误拦——这正是本仓库测试套件中 tests/state.test.cjs、tests/bug-3489-complete-phase-idempotent.test.cjs 等文件长期维护的验证传统。

该修复已随 v1.42.1 相关变更记录在 docs/RELEASE-v1.42.1.md(条目:"Phase completion is idempotent and refreshes state")。对于任何基于 get-shit-done 构建多阶段流水线的团队,理解complete-phase的幂等语义与 STATE.md 的字段契约,是避免"进度被静默回拨"类事故的关键前提。

【免费下载链接】get-shit-doneA light-weight and powerful meta-prompting, context engineering and spec-driven development system for Claude Code by TÂCHES.项目地址: https://gitcode.com/GitHub_Trending/getshi/get-shit-done

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 23:57:11

res-downloader 入门教程:网络资源嗅探,三步完成无水印视频下载

res-downloader 入门教程&#xff1a;网络资源嗅探&#xff0c;三步完成无水印视频下载 【免费下载链接】res-downloader 视频号、小程序、抖音、快手、小红书、直播流、m3u8、酷狗、QQ音乐等常见网络资源下载! 项目地址: https://gitcode.com/GitHub_Trending/re/res-downlo…

作者头像 李华
网站建设 2026/9/8 23:56:02

C++高频交易系统:无锁队列与低延迟架构实战拆解

简介&#xff1a;C高频交易源码包面向中高级C开发者和量化交易爱好者&#xff0c;旨在展示一套接近实战的高频交易系统骨架&#xff0c;覆盖算法交易、低延迟通信、实时行情处理、订单生成与风控等核心主题。压缩包内共16个文件&#xff0c;包含5个cpp源文件用于实现主流程与业…

作者头像 李华
网站建设 2026/9/8 23:55:23

opencode终端AI编码代理:从安装配置到Skills与LSP进阶实战

最近折腾终端AI编程工具&#xff0c;绕了一圈还是停在了opencode上。之前用过的几款终端Agent&#xff0c;不是安装过程太绕&#xff0c;就是配置文件看着头疼&#xff0c;或者模型选择上被绑得太死。opencode算是我目前遇到的&#xff0c;在“轻量”“配置灵活”和“真正能拿来…

作者头像 李华
网站建设 2026/9/8 23:55:21

Agent Harness与Agent Runtime区别详解:从概念到生产实践

干Agent开发这两年&#xff0c;我最常被问到的问题不是“LangGraph怎么用”&#xff0c;而是“Agent Harness 和 Agent Runtime到底有什么区别”。不光刚入门的人懵&#xff0c;很多已经上线过Agent项目的团队&#xff0c;嘴上说着“运行时”“执行框架”&#xff0c;实际排查问…

作者头像 李华
网站建设 2026/9/8 23:54:44

Claude Code实战:从榜单第一到安装配置与排错

1. 榜单更新&#xff1a;Intelligence Index v4.2 把谁推上了第一先说结论&#xff1a;Artificial Analysis 这期 Intelligence Index v4.2 发布之后&#xff0c;Claude Fable 5.1 直接冲到了综合智力指数榜首。这个结果在我的预期之内&#xff0c;但看到正式榜单出来的时候&am…

作者头像 李华