nixkits-check-updates · git:20261008.19cc335 · 2026-10-08 · sha256 2bb7ff04748db6db

nixkits-check-updates git:20261008.19cc335A

Immutable. This exact content never changes and is served at /api/v1/blob/2bb7ff04748db6db.

---
name: nixkits-check-updates
description: NixKits 仓库的软件包更新适配层——在通用技能 nix-flake-update-check 之上,补充本仓库特有的远端 issue / PR 形态(含与更新重叠的历史先例)、CI 结构(可复用 build-package.yml + 每包每架构 workflow)与实测失败形态(llama-cpp-ver 403 限流、codewhale-riscv64 hash、blender-mcp 取源 403、徽章竞态)、四语文档同步、dsh 内置插件清单同步(含插件改名令预设硬失败的排查)、同账户子仓(dsh-api-balance)的链式检查坐标、维护日志记录、历史事故教训(comfyui 漂移、codewhale-riscv64 CI 失败),以及收尾的流程复盘与规范校验(AGENTS.md / SECURITY.md 等)。
---

# NixKits 软件包更新(仓库适配层)

本技能是 **NixKits 仓库特有**的适配层。通用更新方法(包型分流的 hash 流程、
flake.lock 处置、补丁内版本检查、nixpkgs 漂移陷阱)定义在
**`nix-flake-update-check`** 技能中——**先加载它**,再用本技能补充下列
NixKits 特有环节。

```
→ 先加载技能: nix-flake-update-check   (通用流程)
→ 再加载技能: nixkits-check-updates   (本文件,仓库适配)
```

> 两个技能冲突时,**以本适配层为准**——它了解 NixKits 的真实约定。

## 本仓库的特有约定

| 项 | NixKits 的取值 |
|---|---|
| 包目录 | `packages/*.nix`(部分包在同名子目录内,如 `packages/ruyi/`) |
| 文档布局 | `docs/<lang>/<pkg>.md`,**四语**:`zh`(基准)/ `en` / `ja` / `pcn` |
| 变更记录 | `MAINTENANCE.md` + `docs/MAINTENANCE.{en,ja,pcn}.md`,由 `write-maintenance-log` 技能维护 |
| 不可锁定输入 | `llama-cpp-ver`(浮动追踪 llama.cpp 最新版),故 **`flake.lock` 不提交** |
| 额外同步 | 升级 `dsh` 时须同步内置插件清单(见下) |
| 远端信号 | 每轮开工前先核对未关闭 issue / PR(第 0 步)——本仓有与更新重叠的历史先例(见下) |
| 子项目 | `dsh-api-balance` 薄封装引用同账户子仓 `Kihara777/dsh-api-balance`(见下) |
| 固定 SHA 的 action | `actions/checkout` / `DeterminateSystems/nix-installer-action` / `cachix/cachix-action`——本仓**不启用任何外部依赖自动化**,全靠通用技能检查(见下) |
| 泛化要求 | 修复后评估可泛化内容,更新回 `nix-flake-update-check` |

## 第 0 步补充:远端 issue / PR(本仓坐标与形态)

通用技能第 0 步的流程(fetch → issue → PR → 判据)对本仓完全适用,
此处只补本仓特有的事实。

| 项 | NixKits 的取值 |
|---|---|
| 仓库坐标 | `Kihara777/NixKits`(仓库内可用 `gh repo view` 从 remote 自动推导,不必写死) |
| issue 功能 | **已启用**(`gh api repos/Kihara777/NixKits --jq .has_issues` → `true`) |
| 实测现状 | **2026-10-02:0 个未关闭 issue、0 个未关闭 PR** |
| 写权限校验 | AGENTS.md「访问控制」那条命令是**写操作**的前置;第 0 步只读,不重复它 |

### 为什么本仓尤其不能跳过这一步:已经发生过

下面全部是本仓**真实收到过**的外部信号——它们证明「issue / PR 与本轮更新重叠」
是**先例**,不是理论风险:

| 信号 | 与本轮工作的重叠点 |
|---|---|
| PR **#6**(`ci: bump actions/checkout 4.4.0 → 7.0.1`,已合并) | 正是**第 2 步末节**的 action 检查对象——若它当时仍在途,本轮就该先看它而不是重复升级 |
| PR **#7**(`chore: bump @deepseek-ai/dsh-tools …`,**已关闭**) | 正是一个**升级同一个包**的 PR。它注定过不了 CI(外部自动化不认识 `npmDepsHash`),最终关闭改手动——先看 PR 能省下这轮试错 |
| PR **#4** / **#5**(@anupamme / OrbisAI Security 的自动扫描报告,均关闭) | 两份**误报**,却**促成了 `/tts` 的 SSRF 真实修复**——外部信号的价值不在结论对错,在于它指向了哪里 |
| issue **#3**(awesome-ai-plugins 收录邀请,已关闭) | 成为一次技能拆分的契机(`nixkits-check-updates` 拆为通用核心 + 适配层) |

> ⚠️ **本仓已移除 Dependabot**(AGENTS.md「安全边界:不引入外部自动化」),
> 所以**不会再有机器人 PR**——这恰恰是**更需要人读**的理由:没有任何自动化
> 会在后台替你留意 PR 列表。外部贡献者与安全扫描仍会提 PR,而他们**看不到**
> 本轮打算改什么。

## 第 2 步补充:固定 SHA 的 Actions(本仓必查项)

本仓的软件包检查之外,**每轮还要查 `.github/workflows/` 里固定 SHA 的
第三方 action**——通用技能第 2 步末尾与 `traps.md`「检查 GitHub Actions 的
更新」有完整流程,此处只记 NixKits 特有的两点:

1. **这是本仓唯一的 action 更新途径**:AGENTS.md「安全边界:不引入外部自动化」
   明确拒绝任何外部依赖自动化(Dependabot 等),固定 SHA 后又天然收不到通知
   ——**漏掉这一步,这类更新就彻底无人过问**。这条对本仓的**任何经手人都成立**:
   维护者的例行检查、贡献者提交 CI 相关 PR 前的核对、以及评估接手成本的人,
   都只能靠主动跑这一条命令。
2. **改之前先看引用分布**:`build-package.yml` 是**可复用 workflow**,被 31 个
   `build-*.yml` 引用。若某 action 只在它里面出现,**只需改那一处**。
3. **推 workflow 改动要用仓库所有者账号**(2026-10-05 实测):本机 `gh` 里有两个账号,
   默认那个(`GrG41`)的 OAuth 令牌**没有 `workflow` 权限**,改 `.github/workflows/`
   下任何文件都会被拒:

   ```
   ! [remote rejected] main -> main (refusing to allow an OAuth App to create or
     update workflow `.github/workflows/build-package.yml` without `workflow` scope)
   ```

   处置:`gh auth switch --user Kihara777` → 推 → `gh auth switch --user GrG41` 还原。
   只改**非 workflow 文件**时默认账号够用,不必切换。

```bash
# 本仓当前固定的 action 及其引用处(改前先看这个)
grep -rhoE 'uses: [^ ]+@[0-9a-f]{40}' .github/workflows/*.yml | sort -u
grep -rc '<owner>/<repo>@' .github/workflows/*.yml | grep -v ':0'
```

> **本仓实测(2026-09-20 首次执行该检查)**:3 个 action 共 6 处引用,
> `actions/checkout` 分布在 `build-package.yml` / `check.yml` / `ci-summary.yml`
> **三个独立文件**(不是单一可复用 workflow),故改它是 3 处而非 1 处。

## 收尾:确认 CI 结果时不要被徽章误导

本仓的 CI 徽章由 `ci-summary.yml` 生成(写入 `gh-pages/ci-status.json`),
**它可能短暂显示 `failing` 而实际全绿**。判断真实状态请直接查运行记录:

```bash
# 以运行记录为准,不要只看 README 徽章
gh run list --limit 40 --json name,conclusion --jq '[.[]|.conclusion]|group_by(.)|map({(.[0]//"running"):length})|add'
```

**根因(2026-09-21 定位并修复)**:该 workflow 由 push 触发,会在**同一轮 push
的构建尚未完成时**启动。不加 `head_sha` 过滤时,某些 workflow 的"最新已完成
运行"仍是**上一轮 push 的旧失败运行** → 徽章翻成 `failing`,直到下一次 push
才恢复。实测 09-18~09-21 徽章反复 `failing → passing` **成对出现(间隔 1~2
分钟)**,而当时所有构建全绿。已改为按 `head_sha` 过滤。

> ⚠️ **反向陷阱更危险**:`curl` 不带 `--fail` 时,403 限流返回的 JSON 错误体
> 让 jq 取不到 `workflow_runs` → `FAILED` 为空 → **静默写成 `passing`**。
> 假绿比误报红更难发现:误报红会让人去查,假绿则无人察觉。

**技能执行后的正确收尾**:不依赖徽章,**直接确认本轮 workflow 的结论**;
若有失败,先区分**偶发**(如 `llama-cpp-ver` 的 403 限流,重跑即可)与**真失败**
(hash 不符、构建错误)。

## 推送后补充:本仓 CI 的结构与实测失败形态

通用技能「推送后:验证 CI 构建」一节给出的是仓库无关的方法;本仓的具体形态如下。

### 结构

| 文件 | 角色 |
|---|---|
| `check.yml` | 每次 push / PR 跑 `nix flake check`(七项自检) |
| `build-package.yml` | **可复用** workflow:构建 + `cachix-action` 推送 |
| `build-<包>-<架构>.yml` | 每个包每架构一个,调用上面那个可复用 workflow |
| `ci-summary.yml` | 渲染徽章(`gh-pages/ci-status.json`),push / 每小时 / 手动触发 |

**一次 push 触发约 34 个 workflow**,每个都要解析 flake 输入。其中浮动输入
`llama-cpp-ver`(普通 URL 输入,指向 `api.github.com/.../releases/latest`)**曾经**
每个 workflow 各打一次**未认证**请求,共享 runner IP 的 60 次/小时额度用尽即 403。

> **2026-10-05 更正(实测,不是推测)**:`access-tokens` **不覆盖这类 fetch**,
> 所以「双 host」那条治不了它。判据:给 `api.github.com` 配一个**假 token**,
> 再取一个从未取过的该 host URL——若认证头真的送出,GitHub 必然回 401
> `Bad credentials`;实测 fetch **成功**、且未认证额度 −1。`netrc-file` 同法实测,
> 同样不附加。(`access-tokens` 仍对 **`github:` 取源**有效——nixpkgs /
> flake-utils / 交叉路径的 `builtins.getFlake github:…`——那部分保留。)
>
> **现行处置已在仓库里**(`build-package.yml` + `check.yml` 各一步):
> 先用 `gh api`(**认证**,5000 次/小时)取回同一份 JSON,再以
> `--override-input llama-cpp-ver path:<json>` 作为本地输入喂给 Nix。
> 语义不变——overlay 只读 `json.tag_name`;取不到或字段缺失即**显式失败**。
> 看到 403 记录时先确认这两步还在(被删掉就会退回到未认证路径)。

### 本仓实测过的失败形态

| 形态 | 实测记录 | 性质与处置 |
|---|---|---|
| `llama-cpp-ver` 403 限流 | 两次实测:`Build ruyi-beta (x86_64)` 2026-10-02(IP 68.220.61.199)、`Build kitsfmt (x86_64)` 2026-10-04(IP 64.236.142.132);响应体均为 `API rate limit exceeded for <ip>`;**重跑即绿**(换 runner IP) | **偶发但成因结构性**:未认证请求 + 共享 IP 额度。**已根治**(认证取回 + 覆盖,见上)。仍见到时先按 `gh run rerun --failed` 处置,再查那两步是否还在 |
| codewhale-riscv64 hash | 用 `nix-prefetch-url` 取 archive tarball 的 hash 充当 `fetchFromGitHub` 的 hash → **连续失败**(两者算法不同) | **真失败**:改用构建报错里的 `got:` 值 |
| blender-mcp 取源 403 | `/archive/<rev>.tar.gz` 对全部 tag 403,`/api/v1/repos/.../archive/...` 200 | **真失败**:改 `fetchzip` 指向 API 端点 + `stripRoot = true` |
| 「绿但全是缓存」 | 改完取源方式后,CI 仍可能通篇 `copying path … from cache` | **可疑**:必须**确认走了 fetch 阶段**,否则那次绿没有验证力 |
| **靠缓存假绿一整版** | 2026-10-03 实测:`opencode-telegram (riscv64)` 连续 83 次 success,日志里**一行构建都没有**,取的是 `nixkits.cachix.org` 上**上一个版本 0.25.3** 的产物;包已升到 0.26.2,缓存未命中、真去构建,随即红。查 `gh api .../workflows/<wf>/runs` 的历史会以为「它本来能构建」 | **真失败(被缓存掩盖)**:判据是日志里有没有 `building '/nix/store/…'`。**版本一升,缓存必然失效**——所以「这个 job 一直是绿的」在版本更新后不构成任何证据 |
| **构建成功,产物跑不起来** | 2026-10-03 实测:riscv64 产物构建通过,但它**直接依赖**的原生模块(`better-sqlite3`)没有 riscv64 预编译、加载器两条路径全空 ⇒ 一启动就抛 | **真失败,且最危险**:构建绿是**没有验证力**的。处置见下「产物能不能跑」一节 |

### 产物能不能跑:`smoke-test` 与 `develop/qemu-smoke-tests/`

**构建成功 ≠ 产物能跑。** 本仓为此加了一条机制:

- workflow 传 `smoke-test: true` → `build-package.yml` 在构建之后**真的跑一遍产物**;
- 判据脚本放 `develop/qemu-smoke-tests/<包名>.sh`,**本地与 CI 是同一份**;
- 跨架构时 CI 装 `qemu-user-static` 并**断言 binfmt 处理器已注册**——没注册就失败,
  **绝不静默跳过**;开了开关却没有脚本,同样判失败(「测试缺失即失败」);
- 包定义里应记明**该架构为什么需要现编**(本仓实例:`packages/opencode-telegram.nix`
  头部写了两处 gyp 陷阱——交叉构建的 PATH 上没有裸 `gcc` 会让 `<!()` 命令展开成空串,
  以及 `force_build=1` 必须显式给,否则 target 退化成 `type: none`、`make` 只盖 stamp)。

**给判据脚本写反证**(本仓规矩):临时删掉被依赖的东西再跑一遍,必须红。
`opencode-telegram.sh` 当时的三个反证是:删 `better_sqlite3.node`、删整个模块目录、
把 CLI 换成假的——三个都应失败。

**新包加架构时**:`build-package.yml` 的「Resolve the package file」表是**唯一的出处**
(riscv64 需要另一个构建文件时登记在那儿,先例 `codewhale` → `codewhale-src.nix`);
**能建就建,别急着摘**。

#### ⚠️ 本机的构建条件比 CI **宽松**(交叉构建尤其)

本机注册了 `binfmt`(`boot/kernel.nix` 的 `binfmt.emulatedSystems`),
**riscv64 二进制在本机能直接跑**。后果是:构建脚本里那句 `#!/usr/bin/env node`
在本地会解析到 `buildInputs` 里的 riscv64 node 并**成功执行**,而在 x86_64 runner 上
`execve` 失败 ⇒ shell 把 `.js` 当脚本读 ⇒ 一串 `use strict: command not found`。
**2026-10-03 `opencode-telegram` 的 riscv64 构建就是这样「本地绿、CI 红」。**

**要复现 CI 的条件,先摘掉 binfmt:**

```bash
echo -1 | sudo tee /proc/sys/fs/binfmt_misc/riscv64-linux   # 摘掉
# …在这里跑构建…
sudo systemctl restart systemd-binfmt.service               # 还原
```

> **还原别手写 register。** 手工 `echo ':riscv64-linux:M:0:7f454c46…:…'` 会把 magic
> 当**ASCII 文本**注册进去(实测 magic 变成 `3766343534633436…`,一条永不匹配的规则)。
> 正确做法是删掉写坏的条目再让 systemd 按 `/etc/binfmt.d/` 重新注册;
> `binfmt.emulatedSystems` 是声明式的,**重启也一定会恢复**。


### 一次可用的检查流程

```bash
SHA=$(git rev-parse HEAD)
# 1. 本轮涉及的运行,按状态归类(未结束的不要当成"没问题")
gh run list --commit "$SHA" --limit 50 --json name,status,conclusion \
  --jq '.[] | "\(.status)\t\(.conclusion // "-")\t\(.name)"' | sort
# 2. 只看失败的,拿 id
gh run list --commit "$SHA" --limit 50 --json name,conclusion,databaseId \
  --jq '.[] | select(.conclusion == "failure") | "\(.databaseId)  \(.name)"'
# 3. 读日志原文再分类(偶发 vs 真失败)
gh run view <run-id> --log-failed | tail -60
# 4. 判定为偶发后重跑失败 job
gh run rerun <run-id> --failed
```

> ⚠️ **不要把徽章当判据**:本仓实测过徽章 `failing → passing` **成对出现、间隔 1~2 分钟**
> 而当时所有构建实际全绿。根因与修法见本节上方的「收尾」一段。

## 第 5 步补充:四语文档同步

NixKits 的文档是**四语**体系。版本号变更必须同步全部语言,且
**`docs/zh/` 先写,再翻译到其他语言**:

```bash
for lang in zh en ja pcn; do
  [ -f "docs/$lang/<pkg>.md" ] && sed -i "s/$OLD_VER/$NEW_VER/g" "docs/$lang/<pkg>.md"
done
```

同时更新 `README.md` 及 `docs/*/README.md` 中的版本表(四语同步)。

> `pcn`(偽中国語)的翻译规则见 `translate-pseudocn` 技能:**必须剥离全部
> 假名**。`check-maintenance-log` / `check-doc-links` 会拦截残留假名。

## 第 5 步补充:dsh 内置插件清单同步

升级 `dsh` 时,除版本号外还必须同步内置插件清单——dsh 的
`cordis.patch.yml` entry id 是 `nixkits.dsh.plugins.disabled` 的取值来源,
版本升级后插件可能增删。提取并写入 4 语言文档的「插件清单」章节:

```bash
DSH=$(nix build .#dsh --print-out-paths --no-link)
LIST=$(for f in "$DSH/lib/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai/"dsh-*/cordis.patch.yml; do
  awk '/^    - id: /{id=$3} /^      name: /{name=$2; gsub(/[.,\047]/, "", name); print id" -> "name}' "$f"
done | sort -u)
```

将提取的 `id -> name` 列表替换文档中「插件清单」代码块的内容
(标题保留各语言本地化,清单正文 id/name 跨语言一致)。

### ⚠️ 插件改名/删除会**真的坏掉**预设(不只是文档问题)

上面同步的是**文档**;同一件事还有一处会**功能性地**炸掉:本仓
`packages/dsh-nixos-shell/presets/` 下两个 Agent 预设的组合行,是按**包名**
引用内置插件的(`name: '@deepseek-ai/dsh-…'`)。实测 2026-09-28:dsh 0.1.6
把 `dsh-workflow-worker-thread` 改名为 `dsh-workflow-ptc`,而两预设仍写旧名——

| dsh 版本 | 对无法解析的插件行 | 症状 |
|---|---|---|
| ≤ 0.1.6-alpha.1 | **静默忽略** | 预设照常加载、**不留任何痕迹**(旧名可潜伏很久)|
| ≥ 0.1.6-alpha.2 | **硬失败** | 整份预设挂不起来:`preset "…" failed to mount: row "…" names a plugin that cannot be resolved`——界面上只表现为「预设消失了」|

**升级 dsh 前跑两层判据**(缺一不可;来源仓库的实现在 `/etc/nixos/tests/`):

1. **行解析**(廉价、离线):逐行查组合行里的包在插件树里存不存在 —— 抓改名/删除。
   `preset-rows-test.sh`
2. **真挂载**(权威):起一次性 dsh(独立 `DSH_HOME` + 备用端口),调
   `agentPresets/list`,逐条读**上游自己的** `broken` 字段 —— 这是唯一能抓住
   「包在、行也解析得了、加载时才炸」的判据(先例:0.1.5-alpha.2 把 `dsh-persona`
   的 `text` 改成必填 `prefix`,行扫描全绿而预设在建会话时死)。
   `dsh-preset-mount-probe.sh`(`--self-test` 会塞一份故意坏掉的夹具做反证)。

> ⚠️ **预设是 seed-once 播种的**(`modules/dsh.nix`:`if [ ! -e … ]`,并刻意
> `chmod -R u+w` 尊重用户那一份):修预设要修**两处**——包内源(管新机器与新 home)
> 与已部署的 `$DSH_HOME/.agent-presets/<id>`(管现在这台)。只修一处=半个修复。

> 判据自身的方法论(为什么判据必须先能失败)已上移通用技能第 7 步自检**第 9 问**。

## 第 8 步补充:记录维护日志

更新完成后**必须**调用 `write-maintenance-log` 技能:

```
→ 触发技能: write-maintenance-log
```

要点(完整规范以该技能为准):LIFO 插入、时间戳精确到秒(`git log` 获取)、
四语同步、提交表按时间升序、推送后核对各语言条目数一致
(`grep -c '^20' MAINTENANCE.md docs/MAINTENANCE.*.md`)。

## NixKits 专有排除项

通用排除规则之外,本仓库还有:

- **`llama-cpp-ver` 浮动输入**:overlay `llama-cpp-rocm` 通过该输入动态获取
  上游最新版本号,**不可锁定**——这是 `flake.lock` 不进 `.gitignore` 之外的
  必然结果:`flake.lock` 已被 `.gitignore` 排除,故第 4 步的 flake.lock
  前置检测会命中「情况 1」直接跳过。
- **`godot-ai`**:受上游依赖限制,无独立构建 workflow(详见 AGENTS.md)。
- **`codewhale-src`**:不是独立包,而是 `codewhale.nix` 在 riscv64 上的源构建分支。

## NixKits 历史事故教训

以下是本仓库实测踩过的坑,通用技能做不到(它不知道本仓库的具体配置)。
**更新相关包前先读**:

### comfyui / python 包(2026-08-09 事故)

nixpkgs 漂移到 `f13ff45` 后 `diffusers-0.38.0` 构建失败。根因是
`pythonRuntimeDepsCheckHook`(nixpkgs ≥ 2026-08-05)——修复方式见通用技能
「常见陷阱」第 3 条。

配套操作纪律:修复后**不要**执行无参数 `nix flake lock`,否则 nixpkgs
再次漂移、问题重演。用 `--update-input` 或固定 rev。详见通用技能陷阱第 4 条。

### codewhale-riscv64(CI 连续失败)

曾因用 `nix-prefetch-url` 预取 archive tarball 的 hash 作为
`fetchFromGitHub` 的 hash,导致 riscv64 CI **连续失败**——两者 hash 不一致。
正确姿势见通用技能「交叉编译注意」节。

### 匿名 curl 到 api.github.com 静默返回空(2026-09-20 实测)

本机用**匿名 `curl`** 请求 `api.github.com` **返回空响应**(不报错),而管道
下游的 `grep` 同样静默返回空——结果是**每个包都被判定为"已是最新"**,整轮
检查给出虚假的"全部正常"。认证的 `gh api` 正常(额度 5000/小时)。

**纪律**:本仓的版本检查一律走 `gh api`;**空结果必须当错误处理**,不能
当成"最新"。首次运行先用已知仓库自检取数链路:

```bash
gh api repos/Kihara777/NixKits --jq .full_name || echo "取数链路异常 —— 停止检查"
```

**为何危险**:这是**不报错、只撒谎**的失败形态——比构建失败更难发现,因为
报告看起来完全正常。已泛化为通用技能第 3 步的「用 `gh api` 而不是裸 `curl`」
与第 7 步十问自检第 7 问。

### Rust 包 Cargo.lock 同步

`codewhale-src` 的升级暴露出「只改 version + hash 会漏掉 Cargo.lock」的问题,
已泛化为通用技能中 Rust 包流程的第 3 步。

**实测复现(0.9.12 → 0.9.13)**:本地 lock 7073 行、上游 7347 行,上游新增
`wl-clipboard-rs` / `jobserver` 等依赖。**从上游 tag 直接取 lock 覆盖**即可:

```bash
gh api "repos/codewhale-hq/Codewhale/contents/Cargo.lock?ref=v<version>" \
  --jq '.content' | base64 -d > packages/codewhale-src-Cargo.lock
```

> ⚠️ `codewhale-src` **不是 flake 输出**(`flake.nix` 只在 riscv64 条件下经
> `codewhale` 引用它),故 `nix build .#codewhale-src` 会报
> `does not provide attribute`。取 hash 时用:

```bash
nix build --impure --no-link --expr '
let f = builtins.getFlake (toString ./.);
    pkgs = f.inputs.nixpkgs.legacyPackages.x86_64-linux;
in pkgs.callPackage ./packages/codewhale-src.nix { }'
```

#### ⚠️ codewhale 有**两个变体同名同输出**,升级必须两个都改

这是本仓最容易漏的一处——`flake.nix` 按架构分流,**两个文件都产出
`codewhale`**:

| 变体 | 适用架构 | 源 | 需改的字段 |
|---|---|---|---|
| `packages/codewhale.nix` | x86_64 / aarch64 | GitHub Releases 预编译二进制 | `version` + **4 个 hash**(cli/tui × x64/arm64) |
| `packages/codewhale-src.nix` | riscv64 | `fetchFromGitHub` + `Cargo.lock` | `version` + `hash` + **同步 Cargo.lock** |

```nix
codewhale = if pkgs.stdenv.hostPlatform.isRiscV
  then pkgs.callPackage ./packages/codewhale-src.nix { }   # ← riscv64
  else pkgs.callPackage ./packages/codewhale.nix { };      # ← x86_64/aarch64
```

**只改一个的症状**:本地 `nix build .#codewhale` 可能成功(取决于构建机架构),
但**部署后另一个架构上版本没变**。实测踩到:只改了 `-src` 变体,
系统上 `codewhale --version` 仍是 0.9.12。

> **核对方法**:部署后**逐个变体所在架构核对实际版本**,不要只看构建通过。
> 本机(x86_64)应验证 `codewhale.nix`;riscv64 的 CI 会验证 `-src`。
>
> 取预编译 hash:`nix store prefetch-file <url> --json`(0.9.13 实测 cli 与
> tui 资产 hash 相同,四值可两两相同,但**仍要分别填入**)。

### godot-ai v4 的 fail-closed 运行时校验(2026-09-17 实测,2026-10-02 更新)

godot-ai 4.x 启动时校验运行时包的精确版本,不匹配即拒绝启动。**4.1.0 是 9 项,
4.2.3 增至 14 项**(新增 `fastmcp-slim` / `httpx2` / `httpcore2` / `mcp-types` /
`sniffio`;`mcp` 1.29.1 → 2.2.0 跨大版本,wire types 拆成独立发行版
`mcp-types`,HTTP 客户端换成 `httpx2`)。本仓用**两个 overlay** 满足它
(通用方法见通用技能陷阱第 5 条):

| overlay | 作用 |
|---|---|
| `overlays/fastmcp.nix` | fastmcp / fastmcp-slim 抬到上游要求(4.0.5);4.0.5 的 fastmcp 本体是空壳发行版,代码全在 slim |
| `overlays/godot-ai-v4-deps.nix` | anyio / httpx2 / httpcore2 / mcp / **mcp-types(nixpkgs 无此包,源内新建定义)** / pydantic(+core) / starlette / uvicorn / websockets 抬到上游要求 |

> ⚠️ **两处必须同步**:`flake.nix` 的 `godotPkgs` 与 `overlays/default.nix` 的
> `godot-ai` 各自链了这两个 overlay。**只改一处会导致 `nix build .#godot-ai`
> 与经 overlay 消费的结果不一致**——本次就踩到:flake.nix 只链了 fastmcp,
> 构建产物仍用旧依赖,`--version` 直接 RuntimeError。
>
> 另:`pythonRuntimeDepsCheckHook` 相关的 `dontCheckRuntimeDeps = true` 只解决
> **构建期**;**运行期**校验必须靠 overlay 真正抬版本,不能靠它绕过。
> 4.2.3 升级时改回不设该开关(依赖表与闭包精确对齐时构建期钩子能过),
> 于是同一个 pin 表多了一道**更早**的判据——但两者判据不同层,运行期实跑仍不可省。

> ⚠️ **两个 overlay 必须真叠加,而不只是「都写了」**(2026-10-02 实测):
> 两文件原先各自 `python312.override { packageOverrides = …; }`,而链式 extend
> 下**后者替换前者**——fastmcp overlay 的覆盖被静默丢弃,`python312.pkgs.fastmcp-slim`
> 仍是 nixpkgs 旧版,**构建照样成功**。已改为 nixpkgs 的可叠加扩展点
> `pythonPackagesExtensions`(与顺序无关)。通用机制与自查判据见通用技能
> `traps.md` 第 8 条——那里同时给出了「一条 eval 就能判出覆盖是否被丢」的命令。

### blender-mcp 的 Gitea 取源(2026-09-17 实测)

`blender-mcp` 是**本仓唯一使用自托管 Gitea**(`projects.blender.org`)的包。
2026-09-17 升级 1.0.0 → 1.0.3 时发现:`fetchFromGitea` 生成的
`/archive/<rev>.tar.gz` 路径**对全部 tag 返回 403**,而
`/api/v1/repos/lab/blender_mcp/archive/<rev>.tar.gz` 返回 200。

**「1.0.0 还能构建」是假象**——它的 source 早在 Cachix / Nix store 里,
`nix build` 命中的是缓存,**根本没发请求**。用 `--rebuild` 才会暴露。

**本仓的处置**:改用 `fetchzip` 指向 API 端点 + `stripRoot = true`
(复现 fetchFromGitea 剥掉顶层目录的语义,使 `preConfigure = "cd mcp"` 仍成立)。
通用判据与验证方法见通用技能「非 GitHub 源」一节。

> ⚠️ 该包**有独立 workflow**(`build-blender-mcp-{x86_64,aarch64}.yml`,不在
> `check-workflows.py` 的 `EXEMPT` 登记中),故 CI 会构建它。但 **CI 同样可能
> 命中二进制缓存**——`--rebuild` 只是清本地缓存,不可靠。改完取源方式后
> **必须确认 CI 真的重新构建了**(看 workflow 日志是否有 fetch 阶段,
> 而非全部 `copying path ... from cache`)。
### dsh-alpha 的 vendored lock(2026-09-17 实测)

dsh tarball **不含** lock,需自行生成。踩到的坑:用
`npm install --package-lock-only --legacy-peer-deps` 生成的 lock **不含
`"peer": true` 条目**,构建报 `ENOTCACHED`。**去掉该 flag** 后 npm 才写入
peer 条目(与仓库既有可工作的 `dsh-package-lock.json` 结构一致,均为 24 条)。
详见通用技能 npm 节的「vendored lock 必须包含 peer 依赖条目」。

## 第 9 步补充:本仓的子项目引用关系

通用技能第 9 步给出「同账户子项目链式检查」的方法与前提判据。**本仓的实际
引用关系**只有这里知道:

| 包 | 引用的子仓 | 引用方式 | 子仓构建体系 | 可链式 |
|---|---|---|---|---|
| `dsh-api-balance` | `Kihara777/dsh-api-balance` | `fetchFromGitHub` 固定 `rev`(**不是** flake input) | 纯 JS npm 包(`package.json`,无 build script) | ✅ |
| `dsh-preset-news-three-elements` | 无(源码在本仓 `packages/dsh-preset-news-three-elements/`) | 自建包 | — | — |
| `dsh-nixos-shell` | 无(源码在本仓 `packages/dsh-nixos-shell/`) | 自建包 | — | — |

**判据**:`grep -rn 'owner = "Kihara777"' packages/*.nix` 是本仓的完整答案——
目前只有 `dsh-api-balance.nix` 一处。

### 为什么本仓的子仓链必然是单层

`dsh-api-balance` 是 **DSH 插件**,它的上游发布就是它自己(无更上游的同账户
子仓)。故本仓的链式检查实际为「1 层、1 个节点」,回环与深度上限都不会触发
——但**前提校验仍要跑**(见通用技能第 9 步),因为「目前只有一层」是现状而非
结构保证,将来新增子仓时判据必须自动成立。

### 子仓特有的注意点

- **子仓不是 nix flake**:`dsh-api-balance` 无 `flake.nix`,**不要**对它跑
  `nix flake check` 或 flake.lock 处置;按 npm 包处理(改 `package.json`
  的 `version`、必要时 `npm install` 刷新 `package-lock.json`)。
- **子仓有自己的四语文档体系**:与该子仓的 README 一样遵循 zh → en → ja → pcn。
  子仓条目写在其 `MAINTENANCE.md` + `docs/MAINTENANCE.{en,ja,pcn}.md`。
- **两个 hash 都要重算**:薄封装钉住 `src` hash 与 `npmDepsHash`,改 `rev` 后
  **两者都变**——按通用技能 npm 包流程走两次 `nix build`。
- **子仓发布渠道无 npm**:该子仓**不发布到 npm**(维护者视障、无法完成
  npm 的 2FA 流程),故不存在「子仓新版本是否已上 npm」这一步。安装走
  `dsh plugin add github:Kihara777/dsh-api-balance` 或本仓薄封装。
- **该子仓的依赖是 peer 性质**:它声明 `@deepseek-ai/dsh-credentials`
  这类 `@deepseek-ai/dsh-*` 依赖,**版本低于宿主 dsh 提供的版本是正常状态**
  (运行时从宿主树解析)。判据是「子仓要求是否**高于**宿主提供」,
  不是「两侧是否相等」——详见通用技能「依赖冲突检测」的 ⚠️ 说明。
  本仓薄封装为此使用 `npmFlags = [ "--legacy-peer-deps" ]`。

### 本仓薄封装的历史坐标(写条目时对照)

| 时间 | 薄封装 `rev` | 子仓 `version` |
|---|---|---|
| 迁出时 | `c47f857ccbd7ccefcce4d88c2e1c9a7d67c4b810` | `0.1.0` |

> 该子仓迁出后新增两个提交(`6e05c2b` 说明暂不提供 npm 打包并移除 npm 配置、
> `5d95480` 移除主 README 的「文档」章节),**版本号未变(均为 `0.1.0`)**。
> **实测变更清单**:`publish.yml` 删除、5 个 README/文档文件、`package.json`
> 仅移除 `publishConfig.access`——依赖段、`files` 白名单、`version` 均未动。
> 按通用技能「子仓何时需要在主仓侧跟进」的**字段级判据**(发布元数据不属
> 语义输入),**不触发薄封装重钉**。
>
> ⚠️ 注意不要说成「仅文档变更」——`package.json` **确实变了字节**。判据必须
> 落到**字段**而非文件:只看文件清单会把它误判为「清单文件变了 → 跟进」,
> 只看版本号则漏掉「依赖变更」这类同版本号的语义变更。

## 第 10 步(收尾):流程复盘与规范校验

**每次更新流程全部完成后**(含文档同步与维护日志推送)执行。这一步不看软件,
看的是**决定软件被如何更新的那套规范本身**——技能、`AGENTS.md`、`SECURITY.md`
以及相关的 develop 脚本。可以理解为**对更新流程自身做一次「检查更新」**。

```
→ 第 0 步:同步远端 + 确认活跃 issue / PR
→ 第 1~9 步:更新软件
→ 第 10 步:更新「决定软件如何被更新」的规范
```

### 10.1 复盘本次流程的经验教训

回看整个流程,逐条问:**这次哪里卡住了、哪里猜错了、哪里返工了?**

| 问题 | 追问 |
|---|---|
| 哪一步**第一次做就失败**? | 失败信息是什么?下次如何**一次做对**? |
| 哪一步**需要问用户**才继续? | 是信息不足(可自动化消除)还是真需决策(应固化为提问点)? |
| 哪一步**返工**了? | 返工的根因是流程缺失还是判断错误? |
| 有什么**原本不知道、现在知道了**的事实? | 它属于通用方法还是本仓特有? |

**产出必须落到具体位置**(按 10.3 的归属判据),**不允许「知道了但没写」**
——没写下来的教训在下一次流程中等于不存在。

#### ⚠️ 若本次在**测试分支**上作业:教训必须当场搬回 main

实测验证技能、或任何「改了不打算合并」的分支作业,都会产生**真实且通用**的
教训——但如果只在分支上写,这些教训会**随着分支一起被遗忘**。

**本仓的真实损失(2026-09-17 实测)**:blender-mcp 实测分支上写出了
70 行「自托管 forge(Gitea)取源」章节与 21 行适配层记录——`fetchFromGitea`
的 `/archive/` 路径可能整体 403、须改用 API 端点 + `stripRoot = true`——
这是**可复现、可追溯、任何自托管 forge 仓库都适用**的知识。但测试分支按
约定永不合并,该知识**险些永久滞留**,直到事后审计才发现。

| 情形 | 处置 |
|---|---|
| 测试分支产生了**通用**教训 | **当场**用第 10.3 判据归属,**手工写入 main**(禁止 cherry-pick——须核验证据在 main 上可复现) |
| 测试分支产生了**本仓特有**教训 | 同上,写入本适配层 |
| 纯属该次实验的临时产物(如回退基线) | 留在分支,不搬运 |

**判据(与 10.3 一致)**:把这条教训搬到另一个 nix flake 仓库还成立吗?
成立就搬——**「分支不会合并」不是「教训不重要」的理由**。

> **不要等到分支结束再搬**:分支可能永远不会「结束」,而上下文一旦丢失,
> 重建这条知识就要重新踩一遍坑。**在发现的那一刻搬运。**

### 10.2 校验规范文件的内容是否仍然成立

更新流程会**改变它自己所依赖的前提**。本步校验这些描述是否已过时:

| 文件 | 校验什么 |
|---|---|
| `AGENTS.md` | 包/模块/技能的清单与数量、部署命令、构建顺序、边界规则是否仍与仓库现状一致 |
| `SECURITY.md` | 支持版本段、已知设计边界、`supported versions` 是否随版本升级而过期 |
| 本技能与 `nix-flake-update-check` | 其中的路径、命令、包名、hash 形态是否仍可用 |
| `develop/check-*.py` | 检查脚本的断言是否仍反映真实约定(如 `MAINTENANCE_DELTA`、`EXEMPT` 登记) |
| `README.md` / `docs/*/` | 版本表、插件清单、组件列表是否与升级后的实际状态一致 |
| **文档中的外部链接** | 上游是否改名/迁移/归档——**方法见通用技能「审计文档中的外部链接」**。本仓实测:`SECURITY.md` 指向子仓的 `SECURITY.md` 死链(子仓根本没建该文件)、12 处 `Asus-linux/asusctl` 失效(项目已迁至 `OpenGamingCollective/asusctl`) |

**典型失配**:升级后某包不再是「最新」、某插件的库路径变了、某条边界描述的
前提消失了。发现即修,并在维护日志中说明。

> **优先用仓库自己的检查脚本**:`develop/check-doc-links.py`、
> `develop/check-preset-derivation.py`、`develop/check-workflows.py` 等已把部分
> 约定固化为断言,跑 `nix flake check` 比人工核对更可靠。
> **但它们的覆盖面有限**——外部链接可达性、跨仓文件是否存在、`SECURITY.md`
> 描述是否属实,脚本都查不了,仍需按上表手动核。

### 10.3 归属判据:教训该写到哪一层

按**可移植性**三分,与仓库适配层的既有分工一致:

| 教训的性质 | 归属 |
|---|---|
| 任何 nix flake 仓库都会遇到(如 `npmDepsHash` 机制、上游 fail-closed 校验) | **通用技能** `nix-flake-update-check` |
| 只有本仓成立(如 codewhale 双变体、godot-ai 的双 overlay 落点) | **本适配层** |
| 是**规则/边界**而非操作步骤(如「不引入外部自动化」) | `AGENTS.md` |
| 是**安全边界**的描述 | `SECURITY.md` |

> **判据**:把这条教训搬到另一个 nix flake 仓库,还成立吗?成立就上移,
> 不成立就留在适配层。**宁可留在适配层**——过早泛化会污染通用技能。

### 10.4 用户体验:基于上下文优化流程

复盘时同时问:**这套流程让用户等了几轮?** 目标是一次流程跑完。

- **能自己查证的不要问**:先查 release notes / 上游源码 / 构建报错,把问题
  收敛到真正需要人决策的少数几项(详见通用技能「交互式澄清」)
- **能合并的步骤合并**:同类构建(如多个 npm 包)可并行推进
- **把决策点前置**:开工前一次问全,而不是边做边问
- **减少重复验证**:同一 hash 不要反复取两次
- 若某轮**确实多花了一轮往返**,在 10.1 中记录**为什么**,而不是只记结果

### 10.5 ⚠️ 实践是检验真理的唯一标准

**任何对规范的修改都必须有实事求是的正当理由和确切证据**,并满足:

| 要求 | 含义 |
|---|---|
| **可复现** | 给出能重跑出相同结论的命令或步骤,不接受「我记得」「按理说」 |
| **可追溯** | 指明证据来源(构建报错原文、上游 release notes、实测输出、具体提交) |
| **允许质疑** | 结论要能被未来的自己或他人**用证据推翻**;只写结论不写判据即不合格 |

**硬性禁止**:

- ❌ **凭印象改规范**——「感觉这样更好」「一般应该……」不是理由
- ❌ **把一次偶发当成规律**——单次失败可能是限流/网络/缓存问题,须复现或
  说明为何认定是结构性的(参见通用技能对 `403` 限流与真实失败的区分)
- ❌ **为已经写对的内容「再优化」**——规范改动也应有成本意识
- ❌ **删掉看起来没用但仍有约束力的条目**——除非能证明其前提已消失

**验证方式**:若某条判断可用命令验证,就**跑一遍并附上输出**;无法验证的
(如「用户更希望 X」)应标明这是**判断**而非事实,并说明依据。

### 10.6 产出

复盘结束后:

1. 把教训写入 10.3 判定的归属位置
2. 规范文件(含 `AGENTS.md` / `SECURITY.md`)的修正一并提交
3. 在维护日志中记录**本轮流程改进**(属「技能与文档变更」或「CI/CD 变更」类型)
4. **不要**为了记录而记录——无实质发现的流程可以只记一行「本轮无新增教训」

## 泛化义务

按 AGENTS.md「泛化与技能更新」要求:修复后总结业务逻辑、评估是否存在硬编码
内容可泛化,**若发现通用性改进,更新回 `nix-flake-update-check`**,
并保持本适配层只承载 NixKits 特有内容。

> 本节是第 10 步的一个侧面(10.3 的归属判据)——第 10 步是完整的收尾流程,
> 本节只强调「不要忘记上移通用改进」。