get-shit-done 安装器迁移修复 3541 深度解析:非交互升级中 prompt-user 阻塞的分类解决机制
本文基于 changeset 3541-installer-migration-prompt-user-resolution.md 展开,讲解 get-shit-done(GSD)安装器迁移层如何处理非交互(non-TTY)升级场景下 prompt-user 动作的阻塞问题:通过“按路径分类给出安全默认值(stale SDK 构建产物默认 remove、用户可见 skill 默认 keep)+ 结构化日志 + 按原因分组的错误信息 + GSD_INSTALLER_MIGRATION_RESOLVE 环境变量兜底”的组合方案,使 /gsd:update 在 CI、脚本或 Claude Code 代执行等无终端场景下不再卡死。读完本文,你能理解 GSD 首次基线扫描为何会产生 prompt-user 阻塞、自动解决器的分类规则如何落地到具体计划动作,以及运维人员面对残余阻塞时应如何处置。
问题背景:首次基线扫描会主动“拦路”
GSD 的安装/升级行为由一个显式的安装器迁移层(Installer Migration Module)承载,其设计目标在 docs/adr/0008-installer-migration-module.md 中定调:模糊或未知文件一律默认保留,破坏性变更必须持有 manifest 所有权证据或明确的用户选择。完整的模块契约(动作类型、执行流、安全策略、运行时配置契约注册表)定义在 docs/installer-migrations.md。
其中 prompt-user 动作的文档定义是:“在非交互模式下停止破坏性迁移,在交互模式下询问用户;提示必须给出 preserve、back up、remove、move 等具体选项,默认是 preserve”,用于“分类存在歧义、猜测可能丢数据”的场景。这正是 #3541 事故的起点。
首次安装基线迁移记录(2026-05-11-first-time-baseline-scan,实现于 000-first-time-baseline.cjs)负责在破坏性迁移运行前对既有安装面做分类。它的判定链是(见 L149-L219):
- 已知用户拥有的路径(如
get-shit-done/USER-PROFILE.md、skills/gsd-dev-preferences/SKILL.md)→baseline-preserve-user; - manifest 可证明的受管文件或已知生成 agent →
record-baseline; - 看起来像 GSD 产物、但没有 manifest 证明的“stale-gsd-looking”文件 →
prompt-user(choices: ['keep', 'remove'],reason 为 “GSD-looking file is not proven manifest-managed and needs explicit user choice”); - 其余未知文件 →
baseline-preserve-user。
判定“看起来像 GSD 产物”的规则在 isStaleGsdLookingPath:文件名以 gsd- 或 gsd_ 开头,或位于 skills/gsd-* / agents/gsd-* 二级目录下。
事故形态:非 TTY 下 /gsd:update 不可恢复
prompt-user 的设计前提是“交互模式下可以问用户”。但 GSD 的更新入口 /gsd:update 通常由 Claude Code 代为执行——这类运行没有 stdin TTY,无法交互式回答任何问题。回归测试 bug-3541-installer-migration-prompt-user-resolution.test.cjs 的头部注释精确还原了事故:
首次基线安装器迁移的
prompt-user动作直接硬抛异常且无解决路径。当残留的gsd-*文件被分类为stale-gsd-looking时,/gsd-update变得不可恢复。
一个典型的触发场景(测试 A 复刻的 1.41.2 → 1.42.2 升级)是:旧版本把 get-shit-done/sdk/{dist,src}/gsd-* 下的 SDK 构建产物写进了安装目录,而新版本 manifest 不再把这些路径分类为受管文件。基线扫描发现它们“长得像 GSD 产物但没有 manifest 证明”,于是对每个文件生成一个 prompt-user 阻塞项——非 TTY 运行时,整个安装流程停在 assertInstallerMigrationsUnblocked 抛出的异常上,用户没有任何文档化的出路。
修复方案 A:按分类给出安全默认值并记录日志
#3541 的核心修复是新增非 TTY 解决器,实现于 installer-migration-report.cjs 的 resolveInstallerMigrationPromptsForNonTty。它遍历 result.blocked 中的 prompt-user 动作,按以下优先级决定每个动作的归宿:
- 运维覆盖(operator-override):若环境变量
GSD_INSTALLER_MIGRATION_RESOLVE的值经 normalizeResolutionChoice 规范化(去空白、转小写)后是keep或remove之一,且该动作自身的choices数组允许此选项(actionSupportsChoice),则直接采用,来源标记为GSD_INSTALLER_MIGRATION_RESOLVE; - 分类默认(non-tty-default):否则调用 classifyPromptUserAction 按路径形状分类。#3541 引入的两个安全类别是:
| 类别 | 路径匹配规则 | 默认选择 | 理由(源码注释) |
|---|---|---|---|
stale-sdk-build-artifact |
`/^get-shit-done/sdk/(dist | src)//` | remove |
user-facing-skill |
/^skills\/gsd-[^/]+\/SKILL\.md$/ |
keep |
面向用户的 skill 锚点会作为命令暴露给用户,属于用户拥有的内容,必须保留 |
(后续 #3610/#3628 扩展)bundled-gsd-hook |
显式白名单 BUNDLED_GSD_HOOK_FILES(L111-L125)中的 13 个 hooks/gsd-* 文件 |
remove |
分发自带的 bundled hook,安装器即将写入全新副本;白名单之外同形状的文件(用户自写 hook、旧版本退役 hook)仍走阻塞流程 |
分类返回 null 的动作(无法安全默认)进入 unresolved,保持阻塞状态。
-
物化为具体计划动作:每个被解决的动作由 materializeResolution 转换——
keep变为baseline-preserve-user(幂等,文件本来就在磁盘上),remove变为backup-and-remove(安全删除:迁移日志gsd-migration-journal/<runId>-backups/下保留回滚副本)。转换后的动作会原地替换result.plan.actions中的原prompt-user项(找不到才追加),保证下游applyInstallerMigrationPlan永远不会遇到不支持的动作类型;result.blocked与result.plan.blocked两个面同步过滤为“仍无法解决”的集合。 -
结构化日志:每个被默认解决的动作追加一条 resolution 记录,字段为
{ relPath, category, choice, reason, resolvedActionType, source },其中source区分operator-override与non-tty-default,便于事后审计“这个决定是环境变量的决定还是分类器做出的”。
修复方案 B:残余阻塞的可操作性错误信息
无法安全默认的动作依然阻塞,但错误信息从“N 行路径刷屏”升级为按原因分组的可操作报告,由 buildBlockedErrorMessage 生成,格式如下:
installer migration blocked pending user choice: 2 files need a decision
choices: [keep, remove]
- 2 files: GSD-looking file is not proven manifest-managed and needs explicit user choice
e.g. get-shit-done/sdk/dist/gsd-a.js, get-shit-done/sdk/dist/gsd-b.js
resolve non-interactively by setting GSD_INSTALLER_MIGRATION_RESOLVE=<choice> (or run the installer in a TTY to be prompted per file).
关键设计点:
- 同一
reason的路径归并为一行摘要 + 数量(groupBlockedByReason),最多展示 3 条样例路径,避免 SDK 构建产物泄漏时打出千行报错; - 明确列出文档化的选项集合(动作自带
choices时取并集,否则回退为keep/remove,见 describeChoicesForActions); - 点名非交互解决面
GSD_INSTALLER_MIGRATION_RESOLVE,并提示另一条出路(在 TTY 中运行以获得逐文件提示); - 抛出的 Error 对象附带机器可读字段
blocked、blockedByReason、resolutionEnvVar(assertInstallerMigrationsUnblocked),调用方无需重新解析文本即可渲染自己的报告。
安装主流程中的调用链
在 bin/install.js 中,迁移运行被串在“回滚快照已建立之后、包实体化之前”这一安全窗口(L8197-L8266):
- 以
baselineScan: true调用runInstallerMigrations({ configDir, runtime, scope, migrations })生成计划; - 若
result.blocked非空,调用resolveInstallerMigrationPromptsForNonTty(result, { isTty: false });对每条 resolution 打印一行↪ installer-migration auto-resolved: <relPath> → <choice> (category=..., source=...),让每次自动裁决都在安装输出中可见(呼应 ADR 的“破坏性动作运行前必须可见”目标); - 若全部阻塞被解决(
plan.blocked.length === 0),立即用applyInstallerMigrationPlan应用已解除阻塞的计划,再reportInstallerMigrationResult汇报; - 最后
assertInstallerMigrationsUnblocked作为守门员:还有残余阻塞就抛出上面分组的错误,安装在新包文件写入之前失败。
从当前源码结构还可以推断出一次后续演进:install.js 的 #3610 注释 说明解决器后来改为无论 TTY 与否都运行分类器分支(此前按 !isTTY 门控导致交互式 npx 安装被 12 个 bundled hook 阻塞项硬中止),而 GSD_INSTALLER_MIGRATION_RESOLVE 环境变量覆盖分支仍只在非 TTY 模式生效。#3541 确立的“分类默认 + 环境变量兜底”双轨结构保持不变。
行为验证:三条回归测试
tests/bug-3541-installer-migration-prompt-user-resolution.test.cjs 通过公开入口 runInstallerMigrations + 解决器做了行为级验证:
- 测试 A(L73-L133):在临时配置目录写入一个空 manifest、一个
get-shit-done/sdk/dist/gsd-old-bundle.js和一个skills/gsd-roadmap/SKILL.md,两者都被基线迁移分类为prompt-user阻塞(验证事故前置条件);随后非 TTY 解决器必须输出 2 条 resolution——SDK 产物choice: 'remove'/category: 'stale-sdk-build-artifact',skillchoice: 'keep'/category: 'user-facing-skill',且assertInstallerMigrationsUnblocked不再抛出; - 测试 B(L135-L200):构造两条同 reason 的阻塞动作,断言错误信息同时包含
keep/remove选项、GSD_INSTALLER_MIGRATION_RESOLVE提示、按 reason 归并的2 files计数摘要,且抛出的错误携带blockedByReason映射(两条路径归入同一个 key); - 测试 C(L202-L238):注入
GSD_INSTALLER_MIGRATION_RESOLVE=keep环境变量,验证无法分类的skills/gsd-custom/SKILL.toml也能被解决,resolution 的source为环境变量名、category为operator-override,物化后的动作类型为baseline-preserve-user,且blocked/plan.blocked均清零。
实践指引:遇到迁移阻塞时怎么办
结合 changeset 与源码,运维者在 GSD 非交互升级(/gsd:update、CI 脚本等)中可遵循如下处置路径:
- 观察自动解决日志:安装输出中每行
↪ installer-migration auto-resolved: <path> → <choice> (category=..., source=...)都代表一个被分类器或环境变量解决的阻塞项,可核对source是non-tty-default还是GSD_INSTALLER_MIGRATION_RESOLVE; - 被自动移除的文件有回滚副本:分类为
remove的prompt-user动作物化为backup-and-remove,删除前在迁移日志gsd-migration-journal/<runId>-backups/下保留副本,可事后检查; - 面对残余阻塞:错误信息已按 reason 分组并给出选项集合。若你确认某类文件可保留或可删除,重新运行时设置
GSD_INSTALLER_MIGRATION_RESOLVE=keep或GSD_INSTALLER_MIGRATION_RESOLVE=remove(仅接受这两个值,大小写不敏感)即可非交互解决;注意该变量会作用于该次运行中所有支持该选项的阻塞动作,请谨慎评估范围; - 在 TTY 中运行安装器是错误信息给出的另一条出路,可获得逐文件提示。
相关资料
- 原始 changeset(本文主体):.changeset/3541-installer-migration-prompt-user-resolution.md,对应 PR #3547,随 RELEASE-v1.42.3.md 发布,该 release note 条目即“Prompt-user migration actions resolve in non-TTY runs”。
- 迁移层完整契约与动作类型定义:docs/installer-migrations.md(其中
prompt-user、preserve-user、首次基线迁移、安全策略各节是理解本修复的上下文)。 - 模块决策记录:docs/adr/0008-installer-migration-module.md。
- 核心实现:get-shit-done/bin/lib/installer-migration-report.cjs(解决器、分类器、错误构造)、get-shit-done/bin/lib/installer-migrations/000-first-time-baseline.cjs(首次基线扫描与
prompt-user产生点)、bin/install.js(安装主流程接线)。 - 回归测试:tests/bug-3541-installer-migration-prompt-user-resolution.test.cjs。
需要说明的适用前提:本修复针对的是 GSD 自身安装器(/gsd:update / bin/install.js 路径)下的迁移阻塞,依赖安装器迁移框架的 prompt-user 动作与 blocked 结果面;GSD_INSTALLER_MIGRATION_RESOLVE 是非交互解决面,不是通用配置项,且在当前代码中其覆盖分支仅在非 TTY 模式下生效。
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 StartedRust0622
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