首页
/ get-shit-done 中 /gsd:resume-work 的 zsh NOMATCH 检查点丢失修复:从裸 glob 链到 find 的健壮化实践

get-shit-done 中 /gsd:resume-work 的 zsh NOMATCH 检查点丢失修复:从裸 glob 链到 find 的健壮化实践

2026-09-06 09:31:30作者:何将鹤

这篇文章基于 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(phasetasktotal_tasksstatuslast_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),但 spikessketchesdeliberations 等子目录尚未创建。这正是"刚开工、还没有 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),行为完全不同:

  1. zsh 在执行命令之前进行 glob 词展开;
  2. 第一个无法匹配的 glob 直接触发 NOMATCH 错误,整条命令在词展开阶段就被中止,连 ls 都来不及执行;
  3. 关键点在于"中止发生在第一个未匹配模式处"——排在它后面的所有模式(包括真正持有有效检查点的 .planning/.continue-here*.md)都从未被求值;
  4. 命令末尾的 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.mddetermine_next_action 步骤。

回归测试:用真实 shell 做行为验证,而不是源码字符串断言

这次修复配套了一个设计很有代表性的回归测试 tests/bug-3689-resume-glob-nomatch.test.cjs。它的文件头注释开宗明义:

Workflow .md 文件是被 Claude Code 作为嵌入式 bash 执行的运行时契约。断言 resume-project.md 的文本、以及嵌入式片段在真实 shell 下的行为,是对工作流本身的行为测试,不是"源码 grep 表演"。

测试覆盖四组场景:

  1. 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;
  2. bash 默认行为:同一布局、同一命令,断言检查点同样被列出;
  3. 空工作区:.planning/ 完全不存在、无任何检查点时,zsh -o nomatch 下命令退出码 0、stdout 为空、无报错(纯 greenfield 场景不会误报);
  4. 工作流文本不变量:直接读取 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 命令"这一类系统中具有普遍性。结合本仓库的实现,可以提炼出三条可复用的准则:

  1. 避免依赖 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,存在性交给退出码兜底;
  2. 区分"抑制命令输出"与"抑制 shell 展开期错误"2>/dev/null || true 只能兜住命令执行阶段的 stderr 和非零退出;对发生在命令执行之前(词展开阶段)的 shell 级中止无效。写跨 shell 片段时,应假定读者机器上的默认 shell 可能是 zsh 的默认配置,而不是 bash 的宽松默认;
  3. 用真实 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

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