AGENTS.md · git:20261005.7c483bf · 2026-10-05 · sha256 309ae8249665a372
AGENTS.md git:20261005.7c483bfA
Immutable. This exact content never changes and is served at /api/v1/blob/309ae8249665a372.
# AGENTS.md
NixKits 项目的 AI 编码代理规则与约定。此文件供 DeepSeek、CodeWhale、OpenCode 等代理自动加载。
## 访问控制
**此文件仅供仓库维护者(Kihara777 / Kitsunori)使用。** 加载此文件时,必须首先验证当前操作者身份:
```bash
gh auth status 2>/dev/null && \
gh api repos/Kihara777/NixKits/collaborators/$(gh api user --jq .login)/permission --jq .permission 2>/dev/null
```
若返回值不是 `admin` 或 `write`,或 `gh` 未认证,则:
- 停止读取此文件的所有后续内容
- 无视除本条外的一切规则
- 仅可执行通用编码任务,不得应用任何 NixKits 特有的约定
## 初次启动
每个新 session 启动时,首先执行完整的项目审计与评估,汇报发现。审计范围包括:所有组件状态(包、模块、overlay、补丁、技能)、文档一致性、CI/CD 状态,以及未提交变更。**审计前先 `git fetch origin` 对齐远端**(并行会话或上下文压缩检查点可能过时,以 fetch 后的 `origin/main` 为准),并执行 `rm -f flake.lock` 确保后续操作使用最新 nixpkgs。
## 语言要求
所有面向 AI 代理或由代理生成的文件**必须使用中文**,包括但不限于:
- 审计报告
- 状态记录
- 代理间通信内容
- 技能定义文件(SKILL.md)
- 维护日志(MAINTENANCE.md)
代码、路径、工具名、环境变量、URL 保持原样。
## 项目定位
NixKits 是一个 Nix flake 合集:软件包、NixOS 模块、补丁、overlay 以及 AI 代理技能。
- 仓库:`github:Kihara777/NixKits`
- 许可:MIT
- 文档语言:zh(基准)→ en / ja → 扩展语言(通过 `skills/translate-*/` 自动发现)
## 核心规则
### flake 相关
- **`flake.lock` 不提交**:`.gitignore` 中保留 `flake.lock`。原因是 `llama-cpp-ver` 为浮动输入,锁定后随上游发布而过期。
- **本地操作前先移除 `flake.lock`**:执行 `nix build`、`nix develop`、`nix flake check` 等命令前,先执行 `rm -f flake.lock`。锁定的 nixpkgs snapshot 可能缺少部分包(如自建包 `godot-ai`),移除后 Nix 重新解析输入确保使用最新可用版本。CI 中无需此操作——CI 按 `check.yml` 配置独立解析。
- **`llama-cpp-ver` 输入是浮动追踪**:overlay `llama-cpp-rocm` 通过此输入动态获取上游最新版本号,不可锁定。
- **overlay `llama-cpp-rocm` 的 curried 形式是有意的**:`{ llama-cpp-ver }: (final: prev: ...)` — 勿改为标准 overlay。
- **新模块/包/overlay 加入 `flake.nix` 后必须 `nix flake check` 验证**。
- **新增文件先 `git add` 再跑 `nix flake check`**:flake 源=git 跟踪的文件集合,未跟踪的新文件不会进入 store——典型症状是检查里报 `ERR_MODULE_NOT_FOUND` 或「文件缺失」,而本地直接跑同一脚本却通过。先暂存(`git add -A <路径>`)再检查;CI 只看跟踪文件,所以这一步只在本地生效。
- **Overlay 的 `patches` 列表必须用 `lib.unique` 去重**。
### 包
- **所有包的 `maintainers` 保持为空**:个人维护项目,不在 nixpkgs 维护者列表中。
- **预编译二进制包**:按 `stdenv.hostPlatform.system` 选择对应架构二进制(如 codewhale)。
- **SRI hash 格式**:`sha256-<base64>`。
- **npm 包用 `buildNpmPackage` + finalAttrs 模式**。
- **buildNpmPackage 的 vendored lock 必须与 `npmDepsHash` 自洽**:主构建把 src 的 `package-lock.json` 与 npm-deps 产物内 npm fixup 后的 lock 逐字节校验,不一致报 `npmDepsHash is out of date`。生成 vendored lock 必须采用 fixup 后版本(`prefetch-npm-deps --fixup-lockfile`,或从已实现的 `/nix/store/...-npm-deps/package-lock.json` 直接复制),再回填 hash。
- **npm tarball 的 devDependencies 引用未发布包时**:上游 monorepo 发布的 tarball 可能残留未发布内部包(registry 404)。postPatch 以纯 sed 删除该字段(patchPhase 无 node/npm),vendored lock 基于同样处理后的 package.json 生成——两者必须同源。
- **同源多通道(stable/alpha 等)仿 ruyi 薄包装模式**:主定义参数化(version/hash/npmDepsHash/lockFile 可覆盖),通道文件仅传覆盖参数(参见 `packages/dsh.nix` + `packages/dsh-alpha.nix`)。
- **Python 包用 `buildPythonApplication` + `pyproject = true`**。
### 模块
- **统一使用 `nixkits.*` 命名空间**(非 `services.*`)。历史遗留的 `services.opencode-telegram` 和 `services.ruyi` 已迁移并向后兼容。
- **每个模块必须有 `enable` 选项**,默认 `false`。
- **依赖外部模块时必须添加 `assertions`**(参见 `comfyui.nix`)。
- **避免硬编码路径**:优先从 config 推导(如 `hfCacheDir` 从 `config.users.users.<user>.home` 推导)。
- **dsh 模块的 alpha 语义**(`modules/dsh.nix`,dsh ≥ 0.1.2-alpha;详见 `docs/zh/dsh.md`「局域网访问 / 免认证入口 / 插件兼容」章节):
- web UI 入口按 Host authority 的 session cookie 认证——反代**不得重写 Host**(重写致永远 401),局域网 authority 必须列入 `trustedHosts`;LAN 认证 URL 经 `launchUrlFile` 捕获启动 token 生成,免认证入口经 `reverseProxy.autoAuth`(mod_magnet,禁用入口认证,仅可信局域网)。
- shared RPC channel interceptor 互斥(每 channel 仅一个,`/api` 已被内置 typert-gateway 独占)——第三方 dsh 插件提供 RPC 方法必须用精确 fetch route(`ctx.connection.fetch.register`,`/api/<plugin>/<method>`)自实现 `{ rpcId, method, payload }` → `{ type: "server-response", rpcId, result }` envelope;插件 peer 依赖(`@deepseek-ai/dsh-tools` 等)必须与宿主 dsh 通道对齐。
### 预设
dsh 0.2.0 起 Agent 预设只有一种格式:profile 用户 patch 层
(`$DSH_HOME/profiles/<profile>/cordis.patch.yml`)里的一条
`@deepseek-ai/dsh-agent-preset` 条目(`preset.patch.yml`)。0.1.x 的目录式
`agent.cordis.yml` **已从 HEAD 删除**——两套格式不并存于 HEAD,只在「取用点」分叉:
- **stable 通道**(`pkgs.dsh`,npm `latest`)的预设内容取
`packages/dsh-nixos-shell-stable.nix` 钉住的 rev(那个 rev 同时带着旧格式);
- **alpha 通道**(`pkgs.dsh-alpha`,npm `next`)的预设内容跟仓库 HEAD。
- `modules/dsh.nix` 按 dsh 包自己声明的 `passthru.dshChannel` 二选一,
并把它读到的 `preset.patch.yml` **逐字**并进生成的 cordis.patch.yml。
`packages/dsh-nixos-shell/presets/` 下的两个预设(NixOS模式 → 维护模式)是**派生关系**,
不是两份独立配置:
- **维护模式必须完整派生自 NixOS模式**:`maintenance-mode/preset.patch.yml` 的
`plugins:` 正文(该行之后到文件末尾)= `nixos-mode/preset.patch.yml` 的同段 + 固定的
`maintenance-skills` 行块(含注释),除此之外不得有任何差异。文件头里的
`id` / `name` / `description` / `order` 是各自的元数据,不参与字节派生
(由脚本按 `preset.yml` 与常量核对)。
- **预设不自带组合撰写技能副本**:`cordis-plugin-development` 与
`editing-cordis-compositions` 由上游随 `@deepseek-ai/dsh-agent-preset` 分发,两个预设的
`skill-filesystem` 行**直接挂上游那份**(与内置 `cordis` 预设同一表达式),只保留上游没有的
`skills-nixos/`。自带副本会与上游新版**同名冲突**,而且 2026-10-02 实测到我们那份落后一整个
格式(0.1.x 目录式 vs 0.2.0 patch/bundle,上游新版明说旧目录没人读了)。
`develop/check-preset-derivation.py` 把「不再自带」钉成断言——副本被加回来,检查即响。
旧副本仍可从 stable 通道钉住的 rev 取用(0.1.x 兼容)。
- **修改 nixos 模式后必须同步维护模式**:改 `nixos-mode/` 的任意文件(行、元数据、技能)
后,立即把相同改动镜像到 `maintenance-mode/`——注意:**`plugins:` 正文里连注释也算**,
两边注释不一致同样判漂。
- **漂移检查**:`develop/check-preset-derivation.py` 校验上述派生关系,已挂入
`nix flake check`(CI 每次 push 执行);漂移时检查失败,修复后才能提交。刻意变更追加块
本身时,同步更新脚本内 `PATCH_MAINTENANCE_DELTA` 常量。
- **`order` 必须在 roster 里唯一**:内置预设占 1–4;NixKits 的 nixos / maintenance 取
10 / 11;独立包 news-three-elements 取 13;私有仓(Kitsunome)的掌灯模式取 12、
Ocean Spiral 取 14。新增预设前先看这几个占用。
- **回车键行为**:会话内的「回车换行 + Shift+回车发送」交换行为由 dsh-api-balance 客户端插件的「⚙ 设置 → 界面」开关实现(全局生效、浏览器 localStorage 持久化,默认开启;DSH 原生为回车发送);预设组合文件本身不含该逻辑,无需在预设间同步。
### 文档
- **多语言体系**:zh(基准)→ en / ja。扩展语言通过 `skills/translate-*/` 自动发现,禁止硬编码扩展语言列表或数量。
- **文档模板**:参见 `skills/write-project-docs/templates.md`。
- **语言切换器**:各语言使用自身 `display_name`,当前语言不加链接。
- **`nixkits` 为示例 flake input 名**(已从 `nix-kits` 全局替换)。
- **MAINTENANCE.md**:
- 基于提交 SHA 全局去重(每个 SHA 只出现一次)。
- 节标题时间戳必须精确到秒(git 提交时间),禁止 `T00:00:00` 占位符。
- 所有已注册语言同步更新。
- **插件独立展示**:`dsh-*` 组件(插件与 Agent 预设)在 README 的「插件」章节单独列出(四语同步),不混入「软件」表;DSH 自身的包(`dsh`)仍归「软件」。
- **版本号必须与包定义一致**:`docs/<lang>/<pkg>.md` 的「版本」行(四语)与包内多通道表里的版本,都须等于 `packages/<pkg>.nix` 声明的版本;`develop/check-doc-versions.py` 已把这条固化为断言并挂入 `nix flake check`。**升级包时先改文档再提交**,否则检查失败。
- 例外须在该脚本的 `EXEMPT` 中显式登记(如 `dsh-api-balance` 薄封装刻意不标版本)。
- 脚本只校验**能从定义机械读出**的版本号;依赖表、平台支持等需人工核对的内容不在其范围(判断「何时该重写文档」见 `nix-flake-update-check` 技能第 5 步)。
### 补丁
- **补丁必须是干净 diff**:不含 `flake.lock`、自身副本等 artifact。参考 `patches/ruyi-nixos-compat.patch`(426 行清洁版)。
- **补丁操作集中到包内 `postPatch`**:overlay 仅负责挂载 patch,具体适配逻辑在 `packages/*.nix` 中。
### 技能
- **技能目录结构**:`skills/<name>/SKILL.md` + 可选配套文件(`dictionary.md`、`templates.md`)。
- **SKILL.md 保持聚焦**:仅包含 AI 执行所需的最小上下文。词典、模板、检查清单等独立数据拆为配套文件;纯执行流程不强制拆分,允许适当长度。
- **自动发现契约**:`translate-*` 类技能通过 frontmatter 中的 `language_code`、`display_name`、`base_language` 被 `write-project-docs` 自动发现。
- **dsh 不是技能安装目标**:`nixkits-skills` 仅安装到 opencode/codewhale/codex/openclaw/agents;dsh 的 NixOS 场景能力由 `dsh-nixos-shell` 插件提供(`nixos_shell` 执行器 + `nixos_cli` 诊断)。技能内容保留在 `skills/` 作为其他助手的安装源。
## 本机部署
维护者本机通过 **GitHub 引用**本仓库(`/etc/nixos/flake.nix`:`nixkits.url = "github:Kihara777/NixKits"`;**曾是 `path:` 输入,2026-09-28 核实已改**):
- **仓库变更后必须「先推送、再重锁」**:机器消费的是 GitHub 上的 rev,**本地提交不推送,重锁就锁不到它**——届时 `nixos apply` 看起来成功、实际没带上你的改动。推送后才执行 `nix flake lock --update-input nixkits /etc/nixos` 并清 eval 缓存。
> ⚠️ **重锁会顺带拖动本仓的浮动子输入**(本仓按约定不提交 `flake.lock`,故 `nixkits/nixpkgs` 的 `nixos-unstable` 与 `nixkits/llama-cpp-ver` 的 GitHub latest release 都会重新解析)。实测 2026-09-28:一次 `--update-input nixkits` 把 **llama-cpp 0.4.1 → 0.5.0** 一并拉了进来。所以「为了修一行预设而重锁」= 顺手升级 llama.cpp:要么接受它并**单独验证** llama.cpp(本机统一内存调优、模型服务参数另有一套,见 `nixos-specialisation-tuning`),要么就别为小事重锁,等例行更新一起走。
- **部署命令**:`nixos apply -y /etc/nixos`(nixos 0.16.1 无 `rebuild` 子命令)。
- **预设/插件包更新后:先 `systemctl daemon-reload`,再 `systemctl restart dsh`**。`nixos apply` 不重启 dsh(稳定挂载点),而单跑 restart 有时仍执行**上一代**的 pre-start 脚本——它才是把 `cordis.patch.yml` 拷进 `$DSH_HOME` 的那一步,预设根就写在那份文件里,症状为「服务确实重启了、预设还是旧的」。重启后要核对的是**单元引用**,不是文件内容:
```bash
# ① 运行中的单元,其 pre-start 脚本引用哪一份 store 路径
S=$(systemctl show dsh.service -p ExecStartPre --no-pager \
| grep -o '/nix/store/[^ ;]*pre-start[^ ;]*' | head -1)
grep -o '/nix/store/[a-z0-9]*-cordis\.patch\.yml' "$S" | head -1
# ② 当前配置生成的那一份(主机名按实际替换)
nix eval --raw /etc/nixos#nixosConfigurations.<主机>.config.systemd.services.dsh.preStart \
| grep -o '/nix/store/[a-z0-9]*-cordis\.patch\.yml' | head -1
```
两者**相同**即表示 pre-start 已随世代翻新。
> ⚠️ **不要拿 `$DSH_HOME/profiles/<profile>/cordis.patch.yml` 的「内容」去比对。**
> dsh 0.2.0 会在启动时**重写**这个文件(实测 2026-10-03:preStart 于 17:33:10 拷入 1747 行,
> dsh 在 17:33:11 就把它改成 1781 行;此后又变过一次,现为 1785 行)。
> 落盘的是 **dsh 自己的序列化结果**,与 store 那份本来就不会相等——照内容比对
> **只会得到假阴性**,把一次成功的部署判成失败。
>
> 历史记录仍然有效:gen 570 部署后第一次 restart 仍指向旧路径,daemon-reload 后再 restart 才翻——
> 所以「先 daemon-reload 再 restart」这条不变,**只是判据从「看文件内容」换成了「看单元引用」**。
- **`nix build --no-link` 产物可能被 GC 立即回收**:需要产物时同调用内复制出 store,或改用带链接的构建。
## 工作流
### 提交与推送
1. **分批提交**:修改内容核验无误后,按逻辑类别分批 `git commit`
2. **跨语言文件整组提交**:`MAINTENANCE.md` 与 `docs/MAINTENANCE.*.md`(同理,各语言的同一份文档页)属**同一个**逻辑类别——把它们拆进不同的提交,会让那个中间提交自己不一致:只带上 ja 时 CI 报
`entry counts differ across languages: en=369, ja=365, pcn=369, zh=369`(2026-10-05 实测:`db06ab9` 把 ja 的日志文件连同词典改动一起提交,而其余三语还是旧条目数)。
> ⚠️ **对着工作树跑检查抓不到它**——工作树是一致的,不一致的是「被提交的那一部分」。
> 两条可行做法:① 这批改动涉及哪几份语言文件,就**一次性 `git add` 全部**,不要按文件分批;
> ② 推之前对着**该提交的树**验一遍:
> `git worktree add /tmp/wt HEAD && (cd /tmp/wt && python3 develop/check-maintenance-log.py); git worktree remove /tmp/wt`
3. **推送**:每批提交后立即 `git push`
4. **`nix flake check`**:代码修改后必须先通过验证再提交
### 维护记录
**推送后必须执行**,不允许跳过:
1. 获取本次推送的所有新 commit SHA:`git log --format='%h' origin/main..HEAD`
2. 逐一检查 `MAINTENANCE.md` 中是否已存在(`grep -c $sha MAINTENANCE.md`)
3. 若缺失,按 `write-maintenance-log` 技能补充条目
4. 同步到全部已注册语言版本:按 `write-maintenance-log` 技能第 4 步完整执行(发现语言 → 逐语言翻译 → 写入 → 验证 `grep -c '^## 20' MAINTENANCE.md docs/MAINTENANCE.*.md` 条目数一致)
> 以下类型变更**必须记录**:软件版本更新、错误修复、技能新增/重构、CI/CD 配置变更、文档架构调整。
### 文档同步
- 软件包或技能修改后,**必须**同步更新对应文档
- 补丁或文档修复后,检查相关模块文档是否需要刷新
- 新增内容时,确保 `docs/zh/` 先写 → 再翻译到其他语言
### 泛化与技能更新
对软件包、补丁或文档进行修复后:
1. 总结本次修复的业务逻辑(一句话描述本质)
2. 评估是否存在硬编码内容(数字、路径、特定包名)可以泛化
3. 若修复涉及可复用的模式,将其更新到对应技能中
## 安全边界:不引入外部自动化
**本仓库的所有开发与维护,纯粹由维护者(狐莉)与其代理(小爪)完成。
不引入任何外部的自动化集成。**
这条边界是有意的取舍,不是疏漏:
| 拒绝 | 理由 | 替代做法 |
|------|------|---------|
| 第三方 CI 扫描器(如 `ai-plugin-scanner-action`) | 会在我们的 runner 上**执行**不可审计的第三方代码 | 扫描由外部目录方自行进行;我们主动补全其指出的工程卫生项 |
| Dependabot(含纯配置的 `dependabot.yml`) | 即使它不可执行、只能开 PR、且拿不到 secrets,它仍是**外部自动化集成**,行为不由我们掌控 | **自行实现同等能力**(见下) |
**判据**:需要一项自动化能力时,先问"能否用仓库内已有的工具(`gh`、
`git`、`nix`)自行实现"。可以,就自己写进技能;不可以,就人工执行。
**已有实例**:把第三方 action 固定到 SHA 后,会失去更新通知——这处盲点
**不由 Dependabot 填补**,而是在 `nix-flake-update-check` 技能中自行实现
检查流程(`gh api` 查 tag → 取 commit SHA → 回写并同步注释版本号)。
**代价与接受**:action 的安全更新需要主动跑技能检查,而非自动收到 PR。
我们接受这一代价,换取"仓库内不运行任何外部自动化"的确定性。
## CI
`.github/workflows/check.yml` 在每次 push / PR 时自动执行 `nix flake check`。
> **浮动输入 `llama-cpp-ver` 在 CI 里走「认证取回 + 本地覆盖」**(2026-10-05 起):
> 它是**普通 URL 输入**(`api.github.com/.../releases/latest`),而 Nix **不会**把
> `access-tokens` / `netrc-file` 附加到这类 fetch 上——实测判据:给该 host 配一个
> **假 token** 再取一个从未取过的同 host URL,若认证头真的送出,GitHub 必然回 401
> `Bad credentials`;实际 fetch 成功、未认证额度 −1。于是每个 job 各打一次未认证请求,
> 共享 runner IP 的 60 次/小时用尽即 403(实测红过两次,响应体
> `API rate limit exceeded for <ip>`:`Build ruyi-beta (x86_64)` 2026-10-02、
> `Build kitsfmt (x86_64)` 2026-10-04)。
>
> 因此 `build-package.yml` 与 `check.yml` 各有一道步骤:先用 `gh api`(认证,
> 5000 次/小时)取回**同一份** JSON,再以 `--override-input llama-cpp-ver path:<json>`
> 喂给 Nix。语义不变(overlay 只读 `json.tag_name`,见 `overlays/llama-cpp-rocm.nix`),
> 但不再依赖未认证额度;取不回或缺 `tag_name` 即**显式失败**。
> **改 workflow 时不要删这两步**——删了就退回未认证路径,403 会随机回来。
>
> `access-tokens` 仍要**同时列出** `github.com` 与 `api.github.com`:它管的是
> **`github:` 取源**(flake 输入的 nixpkgs / flake-utils,以及 riscv64 交叉路径里的
> `builtins.getFlake github:NixOS/nixpkgs`),那些请求确实会带认证头。两个文件
> 保持一致。
### `nix flake check` 的 9 项自检
仓库的**自检契约**——任一项失败即阻断提交。8 项由 `develop/check-*.py` 实现,
`news-mode-tests` 为 node 脚本:
| 检查 | 脚本 | 校验内容 |
|------|------|---------|
| `preset-derivation` | `develop/check-preset-derivation.py` | 维护模式**完整派生**自 NixOS模式(末尾恰好追加固定块,`MAINTENANCE_DELTA` 常量) |
| `preset-bundle` | `develop/check-preset-bundle.py` | 包内技能快照与仓库 `skills/` 树**逐字节一致** |
| `workflow-coverage` | `develop/check-workflows.py` | 每个包都有 `build-<包>-<架构>.yml`(例外须在脚本内登记) |
| `doc-links` | `develop/check-doc-links.py` | 文档相对链接可达 + 语言切换器四语齐全(**`docs/` 下必须存在,且在全文里查找**;其余文件只看文件头) |
| `doc-versions` | `develop/check-doc-versions.py` | `docs/<lang>/<pkg>.md` 的版本行与包定义一致(含多通道表;例外在 `EXEMPT` 登记) |
| `doc-counts` | `develop/check-doc-counts.py` | 文档里**能从源机械读出的计数**必须与源一致:`docs/<lang>/skills/translate-pseudocn.md` 声明的词典条数 == `dictionary.md` 表数据行数;`AGENTS.md` 声明的自检项数 == `flake.nix` 的 `checks` 条目数(2026-10-05 两次实测:补一条词典映射、加一项检查,两处数字当场过期,而当时没有任何检查看得见它们) |
| `maintenance-log` | `develop/check-maintenance-log.py` | 四语条目数一致、时间戳精确到秒、commit SHA 去重、pcn 无假名、**结构对等**(每条目的提交 SHA 集合四语须与 zh 一致——只看总量会漏掉「某条目在某译文里整张表都没了」,2026-10-03 实测漏过一次)、**摘要标记须是本语的**(zh/pcn `**摘要**`、en `**Summary**`、ja `**概要**`;2026-10-03 实测漏过 80 条 ja + 59 条 pcn 的未译副本)、**zh 摘要 ≤ 400 字符**(2026-10-05 全量重写后钉住:规范里写成「目标」时,实测一轮就有 21 条交上来 420–721 字符而**没有一条算违规**;译文刻意不设长度门槛,按密度匹配倍率判)、**清单式摘要的条目数四语相等**(少一条是漏译、多一条是加料)、**说明块只有一行且标记为本语**(历史上出现过 `**注**` 与 `**Note**`) |
| `news-mode-tests` | `packages/dsh-preset-news-three-elements/tests/mode.test.mjs` | 新闻三要素模式插件的**行为测试**(node,非 python) |
| `session-sources` | `develop/check-session-sources.py` | 预设插件**写进会话**的消息来源不得用 v3 的旧形状 `kind: "plugin"`——dsh 0.2.0 的 v4 准入只拒这一个字面量,症状是整个 session「本机运行失败」(2026-10-03 实测:掌灯模式开局对账每开一个新会话崩一次,事故经过见 Kitsunome 的 `journal/2026-10-04.md`)。正确形状 `kind: "plugin:<插件名>"` |
> 各脚本可**单独本地运行**(`python3 develop/check-doc-links.py`),便于快速定位而不必
> 跑完整的 `nix flake check`。上表各项的当前规模随仓库演进,以脚本实跑输出为准——
> 不要在文档中硬编码数量(`check-doc-versions` 只校验版本号,不校验此类计数)。
构建由独立的 `build-<包>-<架构>.yml` workflow 完成:每个 workflow 调用共享的可复用 workflow `build-package.yml`,构建后经 `cachix-action` 推送到 Cachix 二进制缓存。覆盖情况以实际 workflow 文件为准(**riscv64 的取舍看「能不能建」,不是「有没有 workflow」**:`blender-mcp` 与 `obs-bilibili-stream` 无 riscv64 构建,原因是**主依赖在 nixpkgs 里没声明该架构**——`blender` 5.2.2 的 `meta.platforms` 只有 `aarch64-darwin`/`aarch64-linux`/`x86_64-linux`、`obs-studio` 32.2.2 只有 `x86_64-linux`/`i686-linux`/`aarch64-linux`,求值阶段就 `Refusing to evaluate package …`,绕不过去;**判据是 `pkgs.<dep>.meta.platforms`,不是编译报错**。godot-ai 与 dsh 当前无独立构建 workflow)。CI 状态徽章由 `ci-summary.yml` 生成(`gh-pages/ci-status.json`),触发条件为 push / 每小时 / 手动。
#### 架构差异与「产物能不能跑」
- **riscv64 走交叉构建**:workflow 里 `riscv64-cross: true`,骨架执行
`pkgsCross.riscv64.callPackage <包文件> { }`。若某包在 riscv64 上需要**另一个构建文件**
(上游没有该架构的预编译,改为源码构建),登记在 `build-package.yml` 的
「Resolve the package file」那张表里(先例:`codewhale` → `codewhale-src.nix`)。
**能建就建,别急着摘**——`opencode-telegram` 2026-10-03 由「摘掉」改回「建」就是实例。
- **`smoke-test: true` 会在构建后真的跑一遍产物**:脚本放
`develop/qemu-smoke-tests/<包名>.sh`(本地与 CI **同一份**,自带反证)。跨架构时 CI 装
`qemu-user-static` 并**断言 binfmt 处理器已注册**——没注册就失败,绝不静默跳过;
开了开关却没有脚本,同样判失败(「测试缺失即失败」)。
- **为什么必须有这一步**:2026-10-03 查明 `opencode-telegram` 的 riscv64 job
**长期靠 Cachix 缓存「成功」**——日志里一行构建都没有,取的是上一个版本(0.25.3)的产物;
而它一旦真去构建,又会因两个原生模块没有 riscv64 绑定而「构建成功、一启动就抛」。
**构建成功 ≠ 产物能跑**;判据得是跑起来。
> **⚠️ `ci-summary` 必须按 `head_sha` 过滤(竞态陷阱)**:该 workflow 由 push 触发,
> 会在**同一轮 push 的构建尚未完成时**启动。若不加 `head_sha` 过滤,某些 workflow 的
> "最新已完成运行"仍是**上一轮 push 的旧失败运行** → 徽章短暂误报 `failing`,待下一轮
> 才恢复。实测 09-18 ~ 09-21 徽章反复 `failing → passing` 成对出现(间隔 1~2 分钟),
> 而**当时所有构建实际全绿**。已改为只统计当前 commit 的运行;本轮未完成的 workflow
> 在统计中不出现,由其完成后触发的下一次 `ci-summary` 补上。
>
> 另两点:`curl` 必须带 `--fail`——否则 403 限流返回的 JSON 错误体让 jq 取不到
> `workflow_runs`,`FAILED` 为空 → **静默判为 passing**(假绿,比误报 failing 更危险);
> 请求失败时**保留现有徽章并退出非零**,不要写入一个错误的颜色。
## 二进制缓存
CI 自动构建支持的架构组合并推送到 Cachix。`nixkits-check-updates` 技能执行后,确认 CI 通过即可,无需本地额外操作。