get-shit-done 安装器迁移修复 3541 深度解析:非交互升级中 prompt-user 阻塞的分类解决机制
2026/9/5 19:34:55 网站建设 项目流程

get-shit-done 安装器迁移修复 #3541 深度解析:非交互升级中 prompt-user 阻塞的分类解决机制

【免费下载链接】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

本文基于 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):

  1. 已知用户拥有的路径(如get-shit-done/USER-PROFILE.mdskills/gsd-dev-preferences/SKILL.md)→baseline-preserve-user
  2. manifest 可证明的受管文件或已知生成 agent →record-baseline
  3. 看起来像 GSD 产物、但没有 manifest 证明的“stale-gsd-looking”文件 →prompt-userchoices: ['keep', 'remove'],reason 为 “GSD-looking file is not proven manifest-managed and needs explicit user choice”);
  4. 其余未知文件 →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动作,按以下优先级决定每个动作的归宿:

  1. 运维覆盖(operator-override):若环境变量GSD_INSTALLER_MIGRATION_RESOLVE的值经 normalizeResolutionChoice 规范化(去空白、转小写)后是keepremove之一,且该动作自身的choices数组允许此选项(actionSupportsChoice),则直接采用,来源标记为GSD_INSTALLER_MIGRATION_RESOLVE
  2. 分类默认(non-tty-default):否则调用 classifyPromptUserAction 按路径形状分类。#3541 引入的两个安全类别是:
类别路径匹配规则默认选择理由(源码注释)
stale-sdk-build-artifact/^get-shit-done\/sdk\/(dist|src)\//remove陈旧 SDK 构建产物每次安装都会重新生成,删除无损;新安装会提供替代物
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,保持阻塞状态。

  1. 物化为具体计划动作:每个被解决的动作由 materializeResolution 转换——keep变为baseline-preserve-user(幂等,文件本来就在磁盘上),remove变为backup-and-remove(安全删除:迁移日志gsd-migration-journal/<runId>-backups/下保留回滚副本)。转换后的动作会原地替换result.plan.actions中的原prompt-user项(找不到才追加),保证下游applyInstallerMigrationPlan永远不会遇到不支持的动作类型;result.blockedresult.plan.blocked两个面同步过滤为“仍无法解决”的集合。

  2. 结构化日志:每个被默认解决的动作追加一条 resolution 记录,字段为{ relPath, category, choice, reason, resolvedActionType, source },其中source区分operator-overridenon-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 对象附带机器可读字段blockedblockedByReasonresolutionEnvVar(assertInstallerMigrationsUnblocked),调用方无需重新解析文本即可渲染自己的报告。

安装主流程中的调用链

在 bin/install.js 中,迁移运行被串在“回滚快照已建立之后、包实体化之前”这一安全窗口(L8197-L8266):

  1. baselineScan: true调用runInstallerMigrations({ configDir, runtime, scope, migrations })生成计划;
  2. result.blocked非空,调用resolveInstallerMigrationPromptsForNonTty(result, { isTty: false });对每条 resolution 打印一行↪ installer-migration auto-resolved: <relPath> → <choice> (category=..., source=...),让每次自动裁决都在安装输出中可见(呼应 ADR 的“破坏性动作运行前必须可见”目标);
  3. 若全部阻塞被解决(plan.blocked.length === 0),立即用applyInstallerMigrationPlan应用已解除阻塞的计划,再reportInstallerMigrationResult汇报;
  4. 最后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为环境变量名、categoryoperator-override,物化后的动作类型为baseline-preserve-user,且blocked/plan.blocked均清零。

实践指引:遇到迁移阻塞时怎么办

结合 changeset 与源码,运维者在 GSD 非交互升级(/gsd:update、CI 脚本等)中可遵循如下处置路径:

  1. 观察自动解决日志:安装输出中每行↪ installer-migration auto-resolved: <path> → <choice> (category=..., source=...)都代表一个被分类器或环境变量解决的阻塞项,可核对sourcenon-tty-default还是GSD_INSTALLER_MIGRATION_RESOLVE
  2. 被自动移除的文件有回滚副本:分类为removeprompt-user动作物化为backup-and-remove,删除前在迁移日志gsd-migration-journal/<runId>-backups/下保留副本,可事后检查;
  3. 面对残余阻塞:错误信息已按 reason 分组并给出选项集合。若你确认某类文件可保留或可删除,重新运行时设置GSD_INSTALLER_MIGRATION_RESOLVE=keepGSD_INSTALLER_MIGRATION_RESOLVE=remove(仅接受这两个值,大小写不敏感)即可非交互解决;注意该变量会作用于该次运行中所有支持该选项的阻塞动作,请谨慎评估范围;
  4. 在 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-userpreserve-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 模式下生效。

【免费下载链接】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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询