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

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

2026-09-04 23:57:55作者:曹令琨Iris

本文基于 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.cjsresolveInstallerMigrationPromptsForNonTty。它遍历 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
user-facing-skill /^skills\/gsd-[^/]+\/SKILL\.md$/ keep 面向用户的 skill 锚点会作为命令暴露给用户,属于用户拥有的内容,必须保留
(后续 #3610/#3628 扩展)bundled-gsd-hook 显式白名单 BUNDLED_GSD_HOOK_FILESL111-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 对象附带机器可读字段 blockedblockedByReasonresolutionEnvVarassertInstallerMigrationsUnblocked),调用方无需重新解析文本即可渲染自己的报告。

安装主流程中的调用链

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',skill choice: '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 中运行安装器是错误信息给出的另一条出路,可获得逐文件提示。

相关资料

需要说明的适用前提:本修复针对的是 GSD 自身安装器(/gsd:update / bin/install.js 路径)下的迁移阻塞,依赖安装器迁移框架的 prompt-user 动作与 blocked 结果面;GSD_INSTALLER_MIGRATION_RESOLVE非交互解决面,不是通用配置项,且在当前代码中其覆盖分支仅在非 TTY 模式下生效。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
982
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384