get-shit-donegsd-toolsJSON 结构化错误模式(--json-errors)完整指南:从线格式、错误码分类到测试断言实践
【免费下载链接】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 为gsd-toolsCLI 内置了一套JSON 错误模式(JSON Error Mode):开启后,任何错误都会以单行结构化 JSON 写入 stderr,而不是自由文本。本文围绕 docs/json-errors.md 展开,讲解其激活方式、wire 格式、错误码分类(error code taxonomy)、如何在测试中按类型断言,以及如何为系统新增一个错误码——并深入对应源码(core.cjs、gsd-tools.cjs)与测试实现(feat-3255-json-errors-mode.test.cjs),让读者既可直接上手调用,也能理解其底层设计动机。
JSON 错误模式是什么,为什么要用它
gsd-tools是 get-shit-done 的 CLI 工具层(入口文件 头注释将其定位为“CLI utility for GSD workflow operations”),集中承载了配置解析、模型解析、phase 查找、git 提交、summary 校验等约几十个原子命令与命令族。在默认模式下,error()会把Error: <message>这样的自由文本写到 stderr——对人友好,但对测试与自动化工具不友好:断言一个错误只能对原始文本做子串匹配或正则匹配,而文本内容随时可能被改写。
JSON 错误模式正是为程序化消费方设计的结构化表面:
- 错误对象携带类型化错误码(typed reason code),测试与工具可以稳定地断言错误类型;
- 无需对 stderr 原始文本做
grep/.includes()/正则,规避了脆弱匹配; - 这是仓库测试规范的推荐表面,对应 CONTRIBUTING.md 中"Prohibited: Raw Text Matching on Test Outputs"一节的强制要求。
从源码看,这一能力最初随 issue #2974 落地(冻结枚举注释、测试文件头部均引用该编号),随后在 #3255 中补全了模式开关与测试覆盖,并在 #3310 中把检测时机提前到所有 flag 解析之前,确保连--cwd与 workstream 解析失败也能输出结构化 stderr。
激活方式:两种开关,各有用武之地
JSON 错误模式既可以通过命令行 flag 开启,也可以通过环境变量开启,两种方式的底层效果完全一致(都会调用core.setJsonErrorMode(true)):
# 方式一:命令行 flag(测试代码中推荐) node gsd-tools.cjs --json-errors <command> [args] # 方式二:环境变量(shell 包装与 CI 推荐) GSD_JSON_ERRORS=1 node gsd-tools.cjs <command> [args]flag 的解析时机:在一切错误发生之前
--json-errors的检测在 gsd-tools.cjs 中处于最优先位置——在进入任何命令分发、--pick/--cwd等 flag 解析之前完成:
const jsonErrorsIdx = args.indexOf('--json-errors'); if (jsonErrorsIdx !== -1) { core.setJsonErrorMode(true); args.splice(jsonErrorsIdx, 1); } else if (process.env.GSD_JSON_ERRORS === '1') { core.setJsonErrorMode(true); }代码中有三点值得注意的设计:
- 先从 argv 中剔除该 flag。如果不
splice,调度器后续会把--json-errors当成一个未知命令;文档给出的命令形态node gsd-tools.cjs --json-errors <command>(flag 在前)正是依赖这一步。 - 提前检测是为了全链路结构化。即使
--cwd <path>传入的目录不存在、甚至 workstream 解析失败,也已经在 JSON 模式下,产生的仍是结构化 stderr(源码注释中明确指向 #3310)。 - 默认关闭。
core.cjs中_jsonErrorMode默认false,普通人类用户的操作始终拿到的是可读的纯文本诊断信息(getJsonErrorMode/setJsonErrorMode定义见 core.cjs);结构化形式是按需启用的,不影响既有调用方。
Wire 格式与字段契约
发生任何错误时,进程只向stderr写入恰好一行JSON,然后以退出码1结束:
{ "ok": false, "reason": "<error_code>", "message": "<human text>" }字段契约如下表:
| 字段 | 类型 | 说明 |
|---|---|---|
ok | false | 错误对象中恒为false。 |
reason | string | 来自下方错误码分类的类型化错误码(稳定,可断言)。 |
message | string | 人类可读的错误描述(可能变动,不要对其断言)。 |
底层实现:error() 如何切换两种输出
reason的默认兜底与输出逻辑都收敛在 core.cjs 的error()函数中:
function error(message, reason = ERROR_REASON.UNKNOWN) { if (_jsonErrorMode) { const payload = JSON.stringify({ ok: false, reason, message }) + '\n'; fs.writeSync(2, payload); } else { fs.writeSync(2, 'Error: ' + message + '\n'); } process.exit(1); }要点:
reason是error()的第二个参数,缺省为ERROR_REASON.UNKNOWN;- JSON 模式下通过
fs.writeSync(2, ...)同步阻塞写入单行 payload 后再process.exit(1),避免管道场景下异步 stdout/stderr 缓冲未被消费就退出进程; - 对象顶层结构恰好是
{ok, reason, message}三个键,无多余字段——这在测试中被显式验证(见下文“单次调用只输出一行”)。 - 当命令经由 SDK bridge 转发时,SDK 侧带
.reason的GSDError也会把类型化错误码透传回error()(见 gsd-tools.cjs 的_dispatchNonFamily,注释点名了config_key_not_found这类 reason 的透传链路,涉及 Bugs #2943、#3086),确保跨 CJS/SDK 分发的错误码不退化回unknown。
错误码分类(Error Code Taxonomy)
错误码是定义在 core.cjs 中ERROR_REASON冻结常量对象的全小写 snake_case 字符串:
const ERROR_REASON = Object.freeze({ // config-get / config-set CONFIG_KEY_NOT_FOUND: 'config_key_not_found', CONFIG_NO_FILE: 'config_no_file', CONFIG_PARSE_FAILED: 'config_parse_failed', CONFIG_INVALID_KEY: 'config_invalid_key', // SDK / gsd-tools dispatch SDK_FAIL_FAST: 'sdk_fail_fast', SDK_UNKNOWN_COMMAND: 'sdk_unknown_command', SDK_MISSING_ARG: 'sdk_missing_arg', // workflow / phase PHASE_NOT_FOUND: 'phase_not_found', SUMMARY_NO_PLANNING: 'summary_no_planning', // graphify GRAPHIFY_NO_GRAPH: 'graphify_no_graph', GRAPHIFY_INVALID_QUERY: 'graphify_invalid_query', // hooks HOOKS_OPT_OUT: 'hooks_opt_out', // security-scan SECURITY_SCAN_FAILED: 'security_scan_failed', // generic USAGE: 'usage', UNKNOWN: 'unknown', });该对象通过Object.freeze冻结,防止运行时被篡改;命名上按子系统前缀分组(CONFIG_*、SDK_*等),并在 gsd-tools.cjs 顶部随core一起导出(core.cjs)。下面是文档给出的完整发射场景对照。
Dispatch 错误(gsd-tools 路由层)
| Code | 发射时机 |
|---|---|
sdk_unknown_command | 未知顶层命令(gsd-tools bogus-cmd) |
sdk_unknown_command | 未知点分命令(gsd-tools foo.bar,其中foo不是已知命令) |
sdk_unknown_command | 域内未知子命令(如gsd-tools intel bogus-sub) |
sdk_missing_arg | SDK 层守卫发现缺少必填参数 |
sdk_fail_fast | 触发 SDK fail-fast 策略 |
补充:在 gsd-tools.cjs 的命令路由里,多个命令族(如template、frontmatter、requirements、milestone)对未知子命令统一用error('Unknown ... subcommand. Available: ...', ERROR_REASON.SDK_UNKNOWN_COMMAND)抛错,这与上表“域内未知子命令”行相互印证。
Usage / flag 错误
| Code | 发射时机 |
|---|---|
usage | --pickflag 后未跟值 |
usage | 版本 flag(--version、-v)——gsd-tools永不接受 |
usage | 顶层无参调用(打印 usage 文本) |
源码佐证:在 gsd-tools.cjs 中,--pick后缺值会调用error('Missing value for --pick', ERROR_REASON.USAGE);NEVER_VALID_FLAGS集合(--version/-v)命中后调用error(..., ERROR_REASON.USAGE)。之所以显式拒绝版本 flag,是因为 AI Agent 偶尔会幻觉出--version,静默忽略可能让破坏性操作在未经确认的情况下继续执行(源码注释 #3019 附近的说明);而--help类 flag 则被单独提前处理,渲染 usage 后以 0 退出。
Config 错误(config-get、config-set、config-ensure-section)
| Code | 发射时机 |
|---|---|
config_key_not_found | config-get查询配置文件中不存在的键 |
config_no_file | 配置文件.planning/config.json不存在时执行配置操作 |
config_parse_failed | 配置文件存在但不是合法 JSON |
config_invalid_key | config-set写入白名单之外的键 |
实践提示:若要稳定触发config_key_not_found分支,需先执行config-ensure-section初始化出配置文件,否则会落入config_no_file分支(这正是 feat-3255-json-errors-mode.test.cjs 的做法)。
Phase / workflow 错误
| Code | 发射时机 |
|---|---|
phase_not_found | phase 目录查找无匹配 |
summary_no_planning | 不存在.planning/目录时执行 summary 操作 |
Graphify 错误
| Code | 发射时机 |
|---|---|
graphify_no_graph | 尚未构建 graph 时执行 graphify query 或 diff |
graphify_invalid_query | graphify query 携带格式错误的查询串 |
Hook / 安全错误
| Code | 发射时机 |
|---|---|
hooks_opt_out | 通过 opt-out 配置禁用了 hooks |
security_scan_failed | 安全扫描产生阻断性 finding |
兜底
| Code | 发射时机 |
|---|---|
unknown | 所有未显式分配具体 reason code 的其他错误 |
测试断言规范:解析后按类型断言,绝不匹配原始文本
JSON 错误模式存在的根本目的是服务测试。文档给出的正确/错误写法对照如下:
// CORRECT: 先 JSON.parse 再断言类型化字段 const result = runGsdTools(['--json-errors', 'bogus-command'], tmpDir); assert.strictEqual(result.success, false); const err = JSON.parse(result.error); assert.strictEqual(err.ok, false); assert.strictEqual(err.reason, 'sdk_unknown_command'); // WRONG: 文本匹配(被 lint-no-source-grep 策略禁止) // assert.ok(result.error.includes('Unknown command'));其制度性根源在 CONTRIBUTING.md 的"Prohibited: Raw Text Matching on Test Outputs"一节:无论文本来自源码文件、渲染产物、子进程 stdout 还是自由格式的reason字符串,对被测系统产出的文本做子串/正则匹配一律禁止。该节给出了一组典型违规样例(对.cmd内容做.includes、对 stdout 做assert.match、用“结构化解析器”包装字符串操作、对 JSON 报告中自由格式的reason做正则等),并总结了规则表:
| 输出类型 | 要求的结构化表面 | 测试断言的依据 |
|---|---|---|
| CLI 人类可读格式化输出 | 提供--json模式,结构化地输出同一份数据 | report.results[0].reason === REASON.FAIL_X |
| 错误 / 状态 / reason | 冻结枚举(Object.freeze({ FAIL_X: 'fail_x', ... })) | assert.equal(result.reason, REASON.FAIL_X) |
其核心规则可概括为:如果被测代码产出文本,被测代码必须同时暴露一个类型化的结构化中间表示,测试只断言该 IR,绝不针对渲染后的文本。--json-errors正是error/reason这一行“结构化的 IR”在 CLI 边界的承载者;测试则一律JSON.parse(stderr)后断言ok/reason字段。
官方测试如何验证这套契约
feat-3255-json-errors-mode.test.cjs 是该模式的专项测试,其辅助函数runJsonErrors先把--json-errors拼到参数最前,然后断言进程失败、并强制要求 stderr 可被JSON.parse解析,否则直接抛出“必须输出合法 JSON”的错误。它覆盖了十个典型分支:
- 未知顶层命令 →
sdk_unknown_command; - 未知点分命令(
foo.bar)→sdk_unknown_command; --pick缺值 →usage;config-get缺失键(先跑config-ensure-section初始化)→config_key_not_found;- 域内未知子命令(
intel bogus-subcommand-xyzzy)→sdk_unknown_command; GSD_JSON_ERRORS=1环境变量产出与 flag 相同的结构化错误;- 成功命令不受
--json-errors影响(generate-slug hello-world依旧成功、stdout 非空); - 错误对象恰好只有
{ok, reason, message}三个顶层键,无多余字段; - 单次调用只输出一行 JSON(进程在首个错误处即退出);
- 未知版本 flag(
--version)→usage。
第 6 条验证了环境变量路径与 flag 路径的等价性;第 8、9 条把“wire 格式”从文档承诺固化成了可回归的机器约束。仓库中的runGsdTools测试辅助函数(tests/helpers.cjs)负责在临时项目目录中启动真实 CLI 进程并收集 stdout/stderr,测试可直接复用。
如何新增一个错误码
如果需要为某个新错误路径提供结构化码,按文档给出的四步走:
- 在 core.cjs 的
ERROR_REASON中新增常量——取值使用snake_case 小写,并按子系统前缀分组命名(如CONFIG_*、SDK_*、PHASE_*)。常量名与 wire 值是两个东西:常量名是源码内引用句柄,wire 值是reason字段实际出现的字符串。 - 在调用点把它作为
error()的第二个参数传入,例如error('some message', ERROR_REASON.YOUR_NEW_CODE);若省略该参数,会自动落到unknown兜底码。 - 在本错误码分类文档中补一行(更新 docs/json-errors.md 对应分组表格)。
- 新增一个测试,通过
--json-errors运行并JSON.parse(stderr),断言新的reason值。
遵循这些约定后,新错误码会自动具备稳定可断言的语义:上层工具无需解析自然语言即可判断“哪类操作失败了”,而message字段仍可自由演进以优化人类可读性,两者互不耦合。
结语:结构化错误的收益边界
把gsd-tools的错误输出从“给人看的一段话”升级为“给机器读的一行 JSON”,换来的是测试与自动化链路的确定性:
- 稳定性契约清晰:
reason冻结在 core.cjs 的ERROR_REASON枚举中,断言reason而非message; - 文本可自由演进:人类文案的润色、格式重排不再破坏任何消费方;
- 纯文本诊断保留:模式默认关闭,人机两套输出互不干扰,
error()在 core.cjs 中一条函数同时服务两种形态。
对于希望稳健消费gsd-tools的测试套件、CI 脚本或封装工具,请把--json-errors(或GSD_JSON_ERRORS=1)当作标准姿势:先JSON.parse(stderr),再对reason做相等断言,并克制住对错误文本做.includes()的冲动——这正是本仓库 test-output 规范的核心理念。
【免费下载链接】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),仅供参考