get-shit-done 中 /gsd:resume-work 的 zsh NOMATCH 检查点丢失修复:从裸 glob 链到 find 的健壮化实践
这篇文章基于 get-shit-done 仓库的 changeset 记录 .changeset/3689-resume-glob-nomatch-fix.md(对应 issue #3689 / PR #3693)展开:它会解释为什么 /gsd:resume-work 在 macOS 默认的 zsh 环境下会静默丢失 .planning/.continue-here*.md 续接检查点,根因是 zsh 默认 NOMATCH 选项对"裸 glob 链式 ls"的词展开期中断行为,以及项目如何改用 find 命令并配套真实 shell 行为回归测试来彻底消除这一隐患。读完后,你将掌握 shell 元字符展开时机差异(bash vs zsh)这一类隐蔽 bug 的诊断方法,以及为"由 Agent 执行的嵌入式 bash 工作流"编写可移植、可验证命令片段的最佳实践。
背景:.continue-here 检查点是什么,谁在写、谁在读
get-shit-done(GSD)是一套面向 Claude Code 的元提示与规格驱动开发系统,其会话续接能力围绕 .planning/ 目录下的工件构建。其中 .continue-here*.md 文件是"计划执行到一半"时写下的续接检查点,用于让下一次会话知道"我们停在哪里"。
写入方是 /gsd:pause-work 工作流。从 pause-work 工作流 的路径探测规则看,检查点的落盘位置取决于暂停时的上下文类型:
- 阶段(phase)工作 →
.planning/phases/XX-name/.continue-here.md - Spike 工作 →
.planning/spikes/SPIKE-NNN/.continue-here.md(目录不存在时创建) - Sketch 工作 →
.planning/sketches/.continue-here.md - 讨论(deliberation)工作 →
.planning/deliberations/.continue-here.md - 研究工作或无上下文 →
.planning/.continue-here.md
检查点的格式由 continue-here 模板 定义,包含 YAML frontmatter(phase、task、total_tasks、status、last_updated)和 <current_state>、<completed_work>、<remaining_work>、<decisions_made>、<blockers>、<context>、<next_action> 等语义区块,模板明确要求"具体到足以让一个全新的 Claude 实例立即理解",并在恢复后被删除(一次性工件)。
读取方则是 /gsd:resume-work 命令——它的前置声明见 命令定义,该命令路由到 resume-project 工作流,其中 check_incomplete_work 步骤负责探测四类未完成工作:结构化交接文件 HANDOFF.json、.continue-here 检查点、没有 SUMMARY 的 PLAN(执行未完成的计划)、以及被中断的 agent。
Bug 剖析:zsh 的 NOMATCH 如何在词展开期中断整条命令
问题场景很具体:一个新项目布局下,.planning/ 目录已经存在,里面有一个带后缀的检查点文件(例如 .planning/.continue-here-AT-1234.md),但 spikes、sketches、deliberations 等子目录尚未创建。这正是"刚开工、还没有 spike/sketch"的常见状态。
修复前的 check_incomplete_work 步骤使用了一条链式 ls,串联约六个裸 glob 参数来一次性探测所有可能的检查点位置。从回归测试的文本不变量断言可以确认,旧模式中包含 .planning/spikes/*/.continue-here*.md 这样的裸 glob(测试用正则 ls\s+\.planning\/spikes\/\*\/\.continue-here 来检测残留)。可以将其还原为如下形态(按测试断言与 changeset 描述重建):
# 旧实现(重建示意):一条 ls 串联多个裸 glob
ls .planning/spikes/*/.continue-here*.md \
.planning/sketches/.continue-here*.md \
.planning/deliberations/.continue-here*.md \
.planning/.continue-here*.md 2>/dev/null || true
在 bash 默认行为下,不匹配的 glob 保持字面量,ls 报 "No such file or directory"(被 2>/dev/null 吞掉),匹配到的路径仍会正常输出,整条命令"可用"。但在 zsh 的默认 NOMATCH 选项下(macOS 的默认 shell 正是 zsh),行为完全不同:
- zsh 在执行命令之前进行 glob 词展开;
- 第一个无法匹配的 glob 直接触发
NOMATCH错误,整条命令在词展开阶段就被中止,连ls都来不及执行; - 关键点在于"中止发生在第一个未匹配模式处"——排在它后面的所有模式(包括真正持有有效检查点的
.planning/.continue-here*.md)都从未被求值; - 命令末尾的
2>/dev/null || true只抑制ls自身的 stderr 和退出码,对 shell 在执行前的中止完全无效。
结果就是:在新项目布局下,/gsd:resume-work 探测不到任何检查点,且没有任何报错——续接上下文被静默丢弃,用户"回来了"却被告知项目没有未完成工作。这是一个典型的"只在默认 shell 是 zsh 的机器上复现"的环境相关 bug。
修复:用两条 find 取代裸 glob 链
changeset 给出的修复方案是:放弃依赖 shell 做 glob 展开,改为两条 find 调用。当前 resume-project 工作流 的 check_incomplete_work 步骤中的实际实现如下(完整继承原文,含注释):
# Check for structured handoff (preferred — machine-readable)
cat .planning/HANDOFF.json 2>/dev/null || true
# Check for continue-here files (phase + non-phase + legacy fallback).
# Use `find` rather than a chained `ls` of bare globs: under zsh's default
# NOMATCH option (macOS default shell), a single non-matching glob aborts
# the entire command during word-expansion — silently dropping every
# pattern after the first miss, including `.planning/.continue-here*.md`.
# `find` does not use shell glob expansion and tolerates absent
# directories on both bash and zsh.
find .planning -maxdepth 3 -name '.continue-here*.md' -print 2>/dev/null || true
find . . -maxdepth 1 -name '.continue-here*.md' -print 2>/dev/null || true
(注:第二行按仓库原文为 find . -maxdepth 1 -name '.continue-here*.md' -print 2>/dev/null || true,此处照录原文。)
这个方案的安全性来自 find 的两个特性,恰好针对 zsh NOMATCH 的两个失败点:
- 不做 shell glob 展开:
-name '.continue-here*.md'中的通配符由find自行匹配,shell 从不参与通配展开,因此不存在"第一个未匹配模式中止整条命令"的问题; - 容忍不存在的目录:
find <missing-dir> -maxdepth N -name PATTERN -print 2>/dev/null在目录缺失时(如新项目的.planning/spikes不存在)只是不产出结果,两条find均以|| true收尾,在 bash 和 zsh 下都能干净地以退出码 0 结束。
两条命令的分工也值得注意:
| 命令 | 作用范围 | 覆盖的检查点位置 |
|---|---|---|
find .planning -maxdepth 3 -name '.continue-here*.md' |
.planning/ 内深度 3 |
phase 目录、spikes/SPIKE-NNN/、sketches/、deliberations/ 下的检查点 |
find . -maxdepth 1 -name '.continue-here*.md' |
仓库顶层深度 1 | 遗留布局中放在项目根目录的顶层 handoff 文件(changeset 特别指出旧链式 ls 因第一个未匹配模式中止而"吞掉"的正是这个模式) |
工作流对探测结果的消费逻辑不变:.continue-here 文件命中即判定"Found mid-plan checkpoint",读取文件恢复具体上下文,且其优先级低于 HANDOFF.json(结构化交接)和被中断 agent、高于"PLAN 无 SUMMARY"的兜底。完整的路由规则见 resume-project.md 的 determine_next_action 步骤。
回归测试:用真实 shell 做行为验证,而不是源码字符串断言
这次修复配套了一个设计很有代表性的回归测试 tests/bug-3689-resume-glob-nomatch.test.cjs。它的文件头注释开宗明义:
Workflow
.md文件是被 Claude Code 作为嵌入式 bash 执行的运行时契约。断言 resume-project.md 的文本、以及嵌入式片段在真实 shell 下的行为,是对工作流本身的行为测试,不是"源码 grep 表演"。
测试覆盖四组场景:
- zsh
-o nomatch下列出.planning/.continue-here-AT-1234.md:构造"新项目布局"(.planning/存在、有带后缀检查点、无spikes/sketches/deliberations子目录),在工作流嵌入的find片段原文下用spawnSync('zsh', ['-o', 'nomatch', '-c', FIND_SNIPPET])实际执行,断言退出码 0 且 stdout 匹配.planning/.continue-here-AT-1234.md; - bash 默认行为:同一布局、同一命令,断言检查点同样被列出;
- 空工作区:
.planning/完全不存在、无任何检查点时,zsh-o nomatch下命令退出码 0、stdout 为空、无报错(纯 greenfield 场景不会误报); - 工作流文本不变量:直接读取 resume-project.md 的全文,断言其不再包含
ls .planning/spikes/*/.continue-here的脆弱模式,且包含find .planning -maxdepth 3 -name '.continue-here*.md'的新模式——防止未来有人在编辑工作流文档时把裸 glob 链改回去。
测试中 FIND_SNIPPET 常量被显式标注"与 resume-project.md 的 check_incomplete_work 步骤保持同步",即测试与工作流文档形成双向约束:行为测试守护运行时语义,文本不变量守护文档不漂移。这种"对 Agent 执行的 markdown 工作流做回归"的思路,是 GSD 仓库测试体系的特色之一(该测试文件位于 tests/ 目录,与 300 余个 bug-*.test.cjs 回归测试并列)。
可移植性经验:给"嵌入式 shell 片段"的三条实践准则
这个 fix 体量很小,但它暴露的问题在"Agent 驱动的工作流里嵌入 shell 命令"这一类系统中具有普遍性。结合本仓库的实现,可以提炼出三条可复用的准则:
- 避免依赖 shell 的裸 glob 展开来枚举可能缺失的路径。
ls a/*.md b/*.md c/*.md这种写法在 bash 下"碰巧可用"(未匹配项变字面量、靠 stderr 抑制和退出码兜底),在 zsh 默认NOMATCH下则第一个未匹配项即中止整条命令。需要跨 shell 枚举"可能存在也可能不存在"的路径时,优先find <root> -maxdepth N -name PATTERN -print 2>/dev/null || true——模式匹配交给find,存在性交给退出码兜底; - 区分"抑制命令输出"与"抑制 shell 展开期错误"。
2>/dev/null || true只能兜住命令执行阶段的 stderr 和非零退出;对发生在命令执行之前(词展开阶段)的 shell 级中止无效。写跨 shell 片段时,应假定读者机器上的默认 shell 可能是 zsh 的默认配置,而不是 bash 的宽松默认; - 用真实 shell 的行为测试守护文档型工作流。GSD 的工作流是
.md文件,其内嵌 bash 由 Agent 实际执行,传统的单元分层在这里不适用。bug-3689 测试 的做法——spawnSync真实zsh/bash执行片段原文 + 对工作流文档做文本不变量断言——提供了一个可直接借鉴的模板:前者锁住语义,后者锁住"没人把坏模式改回来"。
小结
/gsd:resume-work 在 zsh 默认 NOMATCH 下丢失 .continue-here 检查点的根因,是链式裸 glob ls 在词展开期被首个未匹配模式中止,且该中止发生在 ls 执行前、不受 2>/dev/null || true 约束;修复方案是用两条 find 命令(深度分别为 3 与 1,-name '.continue-here*.md')取代裸 glob 链,使检查点探测在 bash 与 zsh 下、目录存在与缺失的场景中行为一致;回归测试 tests/bug-3689-resume-glob-nomatch.test.cjs 通过真实 shell 执行片段原文加文档文本不变量双重守护了这一修复。相关实现分布在 resume-project 工作流、continue-here 模板、pause-work 工作流 与 gsd:resume-work 命令 中,changeset 原文见 .changeset/3689-resume-glob-nomatch-fix.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 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