universal-dev-standards 6.13.0 → 6.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  source: ../../CHANGELOG.md
3
- source_version: 6.13.0
4
- translation_version: 6.13.0
5
- last_synced: 2026-09-27
3
+ source_version: 6.13.1
4
+ translation_version: 6.13.1
5
+ last_synced: 2026-09-28
6
6
  status: current
7
7
  ---
8
8
 
@@ -17,6 +17,20 @@ status: current
17
17
 
18
18
  ## [Unreleased]
19
19
 
20
+ ## [6.13.1] - 2026-09-28
21
+
22
+ > **修补版**:修复 6.13.0 暴露的两个“以 `uds update` 升级既有项目”的缺陷(繁中的提交消息语言段落变成英文;AGENTS.md 被改写成另一种格式且少了“这是索引”提醒),并新增一道发版前检查,实际从上一个正式版升级一次。**若你已用 `uds update` 升到 6.13.0,请在升级至 6.13.1 后再执行一次 `uds update`**,以还原那些段落。
23
+
24
+ ### 新增
25
+
26
+ - **`scripts/check-upgrade-fidelity.sh`,已接入 `pre-release-check.sh` 第 24 步:每次发版前跑一次真实的跨版本 `uds update` 升级检查。** 上面修复的两个缺陷有同一个共同形状——只有"既有项目、已经被上一个正式版更新过,再对更新版执行 `uds update`/`uds update --apply`"时才会发作——而 pre-release-check.sh 其余 31 步与 CI 全都没抓到,因为没有任何一步做真正的跨版本升级:`uds init` 是全新跑一次(没有东西可以回归);`uds update` 对照的 bundled registry 版本又跟 manifest 已记录的版本相同,会在走到出问题的那段代码前就提前返回。此检查用真实、已发布在 npm 上的上一个正式版建立三种项目形状(对应三个真的踩到这两个缺陷的项目),分别用一般 `uds update -y` 与 `uds update --apply` 让待检查的 CLI 升级,并断言:UDS 标记区块外的内容永不改变;区块内只允许一次真实升级该有的标准清单/数量变化;CLAUDE.md 的提交消息语言标题逐字不变(不是"有出现"就算过);"这只是索引"的提醒存在;一般 update 与 `--apply` 对"该用哪个生成器"意见一致;连续两次 `--apply`,第二次不再有任何变化。npm 连不上或上一版从未发布时**直接失败**(结束码 2,不是跳过)——这个检查唯一的价值就是跑一次真实的发布版,没有它却悄悄回报通过,正是要堵住的那个盲点重新打开。已双向验证:用 `--cli="npx -y universal-dev-standards@6.13.0"` 重现了上面两个缺陷(CLAUDE.md 语言被换掉、AGENTS.md 生成器被换掉),并打印出实际差异;本机开发版 CLI(已套用 commit `e1524872` 与 `8373207d`)在三种形状上全部干净通过。
27
+
28
+ ### 修复
29
+
30
+ - **一般(非 `--apply`)的 `uds update -y` 会把项目 CLAUDE.md 里繁体中文的提交消息标题悄悄换成英文版,`--plan --integrations-only` 的预演差异也可能忽略项目的显示语言设置。** `updateCommand` 主流程与其 `--plan` 预演各自独立地只用 `output_language`/`commit_language`(提交消息语言)推导集成区块的内容语言,完全忽略 `display_language`(`uds init` 与 reconciler 一直用来决定"你想读哪种语言"的设置)。一个以 `display_language: zh-tw` 与 `output_language: bilingual` 安装的项目——一种常见组合——因此从 `init` 拿到正确的"## 提交訊息語言"标题(繁体),却在下一次一般 `uds update` 拿到"## Commit Message Language",丢掉了原本正确的语言选择。已对两个真实采用者(asiaostrich-telemetry-server、EngramGraph)从 6.12.0 升到 6.13.0 实测验证。此缺陷早于 6.13.0 就存在(自 2026-03-25 的 commit `ad555d41` 起),且在 `uds update` 对一个已是最新版的项目执行时完全隐形——因为那种情况下它在走到这段代码前就提前返回——只有在真正跨版本升级时才会发作,这正是它看起来像 6.13.0 新回归的原因。`buildToolIntegrationConfig`(`--apply`/reconciler 使用)在 2026-09-16 已修好正确的推导逻辑;现在剩下的两个调用点都改用新增的 `resolveIntegrationLanguage(manifest)` 共用同一份逻辑。新增的回归测试对着真实临时项目与真正的生成器(无 mock)重现了确切的缺陷,另有一支静态守卫测试,只要那段旧的内嵌推导在 CLI 源码任何地方重新出现就会变红。
31
+ - **`uds update --apply`(reconciler)只要项目曾经由 `generateAgentsMd` 写出过 AGENTS.md,就会把它整份改写成完全不同、opencode 样式的文档——即使是在刚跑完 `uds init` 后紧接着的第一次 `--apply`,其他什么都没改也一样。** `desired-state-calculator.js` 的 `calculateIntegrations` 会把 `manifest.integrations` 里任何叫做 `AGENTS.md` 的条目,通过 `resolveToolKey` 解析成 `opencode` 这个工具——不论 opencode(或它的别名 `codex`)是否真的被选为 AI 工具。`uds init` 只要通用摘要写出过一次,就一定会把 `AGENTS.md` 记进 `manifest.integrations`(那只是一个追踪标记,不是工具选择的记录),所以每一个 `generateAgentsMd: true` 而且没选 codex/opencode 的项目都受影响。依生成器本身 2026-08-20 加入的设计说明:per-tool 模板本来就该只在真的选了 codex 或 opencode 时才取代通用摘要。已在三个真实项目(asiaostrich-telemetry-server、EngramGraph、dev-platform 自己的 `.standards/`)上确认,三者的 `aiTools` 都不含 codex/opencode。`calculateIntegrations`(与 `plan-executor.js` 的 `executeMigrateBlock`)现在会先检查工具是否真的被选,没有的话改用一般 `uds update` 本来就在用的同一个 `writeAgentsMdSummary`——`--apply` 与一般 `uds update` 现在会一致。新增的回归测试驱动真正的 `uds init`/`uds update`(无 mock);已对还原修正的版本确认会红,其中一支"第二次 `--apply`"幂等性检查还额外抓到了修正过程中自己引入的一个哈希不收敛问题(见下一条)。
32
+ - **通用 AGENTS.md 摘要"这个文件是索引,不是标准本文"的提醒(2026-08-18 加入)从未送到既有项目的受管区块——只到了文件头,而文件头在文件已存在时,标记式更新从来不会重新生成。** `writeAgentsMdSummary` 对已存在的文件,永远只替换 `<!-- UDS:STANDARDS:START/END -->` 两个标记之间的内容;提醒却写在标记外的文件头。已对两个真实采用者(asiaostrich-telemetry-server、EngramGraph)实测:不论更新前后,提醒在文件头和区块里都不存在。提醒现在也写进标记区块内(措辞与 per-tool 生成器自己的披露文字 `generateIndexDisclosure` 对齐,不另创第三种),于是每个项目在下一次 `uds update`/`--apply`(不论是新文件还是既有文件)都会收到它。顺手把这个区块也改用 per-tool 生成器本来就在用的同一个 `wrapWithMarkers` 辅助函数组出来(而非手写标记行),过程中也修掉一个相关缺口:全新的 AGENTS.md 文件原本也从未带有"请勿手动编辑"的警告注释,因为过去只有合并路径才会加上它。
33
+
20
34
  ## [6.13.0] - 2026-09-28
21
35
 
22
36
  > **正式版**:包含下方 6.13.0-beta.1 至 beta.5 的全部内容,beta.5 之后没有任何变更。重点:回合收尾关卡(agent 说了下一步却没做就不得结束回合)现在覆盖 **Claude Code 与 Codex**,两者都以从 npm 安装的版本在真实会话中验证过(Codex 要先用 `/hooks` 信任才会执行);Claude Code 关卡真的会拦(到 beta.4 为止每个回合都放行);Windows 上可用;`uds uninstall` 会移除它;`uds init` 让提交前检查真的执行;`developer-memory` 1.2.0。Gemini CLI 标为过时(Google 已对个人账号停用,改由 Antigravity CLI 取代,后者尚未支持)。
@@ -15,7 +15,7 @@ status: current
15
15
 
16
16
  > **语言**: [English](../../README.md) | [繁體中文](../zh-TW/README.md) | 简体中文
17
17
 
18
- **版本**: 6.13.0 | **发布日期**: 2026-09-28 | **授权**: [双重授权](../../LICENSE) (CC BY 4.0 + MIT)
18
+ **版本**: 6.13.1 | **发布日期**: 2026-09-28 | **授权**: [双重授权](../../LICENSE) (CC BY 4.0 + MIT)
19
19
 
20
20
  语言无关、框架无关的软件项目文档标准。通过 AI 原生工作流,确保不同技术栈之间的一致性、质量和可维护性。
21
21
 
@@ -13,7 +13,7 @@ status: current
13
13
  <!-- UDS_SUPPORTED_VERSIONS_START -->
14
14
  | 版本 | 支持状态 |
15
15
  |------|--------|
16
- | 6.13.0 | ✅ 最新正式版 |
16
+ | 6.13.1 | ✅ 最新正式版 |
17
17
  | < 6.0.0 | ❌ 已终止支持 |
18
18
  <!-- UDS_SUPPORTED_VERSIONS_END -->
19
19
 
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  source: ../../CHANGELOG.md
3
- source_version: 6.13.0
4
- translation_version: 6.13.0
5
- last_synced: 2026-09-27
3
+ source_version: 6.13.1
4
+ translation_version: 6.13.1
5
+ last_synced: 2026-09-28
6
6
  status: current
7
7
  ---
8
8
 
@@ -17,6 +17,20 @@ status: current
17
17
 
18
18
  ## [Unreleased]
19
19
 
20
+ ## [6.13.1] - 2026-09-28
21
+
22
+ > **修補版**:修正 6.13.0 暴露的兩個「以 `uds update` 升級既有專案」的缺陷(繁中的提交訊息語言段落變成英文;AGENTS.md 被改寫成另一種格式且少了「這是索引」提醒),並新增一道發版前檢查,實際從上一個正式版升級一次。**若你已用 `uds update` 升到 6.13.0,請在升級至 6.13.1 後再執行一次 `uds update`**,以還原那些段落。
23
+
24
+ ### 新增
25
+
26
+ - **`scripts/check-upgrade-fidelity.sh`,已接進 `pre-release-check.sh` 第 24 步:每次發版前跑一次真實的跨版本 `uds update` 升級檢查。** 上面修的兩個缺陷有同一個共同形狀——只有「既有專案、已經被上一個正式版更新過,再對更新版執行 `uds update`/`uds update --apply`」時才會發作——而 pre-release-check.sh 其餘 31 步與 CI 全都沒抓到,因為沒有任何一步做真正的跨版本升級:`uds init` 是全新跑一次(沒有東西可以回歸);`uds update` 對照的 bundled registry 版本又跟 manifest 已記錄的版本相同,會在走到出問題的那段程式碼前就提早返回。此檢查用真實、已發布在 npm 上的上一個正式版建立三種專案形狀(對應三個真的踩到這兩個缺陷的專案),分別用一般 `uds update -y` 與 `uds update --apply` 讓待檢查的 CLI 升級,並斷言:UDS 標記區塊外的內容永不改變;區塊內只允許一次真實升級該有的標準清單/數量變化;CLAUDE.md 的提交訊息語言標題逐字不變(不是「有出現」就算過);「這只是索引」的提醒存在;一般 update 與 `--apply` 對「該用哪個產生器」意見一致;連續兩次 `--apply`,第二次不再有任何變化。npm 連不上或上一版從未發布時**直接失敗**(結束碼 2,不是略過)——這個檢查唯一的價值就是跑一次真實的發布版,沒有它卻悄悄回報通過,正是要堵住的那個盲點重新打開。已雙向驗證:用 `--cli="npx -y universal-dev-standards@6.13.0"` 重現了上面兩個缺陷(CLAUDE.md 語言被換掉、AGENTS.md 產生器被換掉),並印出實際差異;本機開發版 CLI(已套用 commit `e1524872` 與 `8373207d`)在三種形狀上全部乾淨通過。
27
+
28
+ ### 修正
29
+
30
+ - **一般(非 `--apply`)的 `uds update -y` 會把專案 CLAUDE.md 裡繁體中文的提交訊息標題悄悄換成英文版,`--plan --integrations-only` 的預演差異也可能忽略專案的顯示語言設定。** `updateCommand` 主流程與其 `--plan` 預演各自獨立地只用 `output_language`/`commit_language`(提交訊息語言)推導整合區塊的內容語言,完全忽略 `display_language`(`uds init` 與 reconciler 一直用來決定「你想讀哪種語言」的設定)。一個以 `display_language: zh-tw` 與 `output_language: bilingual` 安裝的專案——一種常見組合——因此從 `init` 拿到正確的「## 提交訊息語言」標題,卻在下一次一般 `uds update` 拿到「## Commit Message Language」,丟掉了原本正確的語言選擇。已對兩個真實採用者(asiaostrich-telemetry-server、EngramGraph)從 6.12.0 升到 6.13.0 實測驗證。此缺陷早於 6.13.0 就存在(自 2026-03-25 的 commit `ad555d41` 起),且在 `uds update` 對一個已是最新版的專案執行時完全隱形——因為那種情況下它在走到這段程式碼前就提早返回——只有在真正跨版本升級時才會發作,這正是它看起來像 6.13.0 新回歸的原因。`buildToolIntegrationConfig`(`--apply`/reconciler 使用)在 2026-09-16 已修好正確的推導邏輯;現在剩下的兩個呼叫點都改用新增的 `resolveIntegrationLanguage(manifest)` 共用同一份邏輯。新增的回歸測試對著真實臨時專案與真正的產生器(無 mock)重現了確切的缺陷,另有一支靜態守衛測試,只要那段舊的內嵌推導在 CLI 原始碼任何地方重新出現就會變紅。
31
+ - **`uds update --apply`(reconciler)只要專案曾經由 `generateAgentsMd` 寫出過 AGENTS.md,就會把它整份改寫成完全不同、opencode 樣式的文件——即使是在剛跑完 `uds init` 後緊接著的第一次 `--apply`,其他什麼都沒改也一樣。** `desired-state-calculator.js` 的 `calculateIntegrations` 會把 `manifest.integrations` 裡任何叫做 `AGENTS.md` 的條目,透過 `resolveToolKey` 解析成 `opencode` 這個工具——不論 opencode(或它的別名 `codex`)是否真的被選為 AI 工具。`uds init` 只要通用摘要寫出過一次,就一定會把 `AGENTS.md` 記進 `manifest.integrations`(那只是一個追蹤標記,不是工具選擇的紀錄),所以每一個 `generateAgentsMd: true` 而且沒選 codex/opencode 的專案都受影響。依產生器本身 2026-08-20 加入的設計說明:per-tool 樣板本來就該只在真的選了 codex 或 opencode 時才取代通用摘要。已在三個真實專案(asiaostrich-telemetry-server、EngramGraph、dev-platform 自己的 `.standards/`)上確認,三者的 `aiTools` 都不含 codex/opencode。`calculateIntegrations`(與 `plan-executor.js` 的 `executeMigrateBlock`)現在會先檢查工具是否真的被選,沒有的話改用一般 `uds update` 本來就在用的同一個 `writeAgentsMdSummary`——`--apply` 與一般 `uds update` 現在會一致。新增的回歸測試驅動真正的 `uds init`/`uds update`(無 mock);已對還原修正的版本確認會紅,其中一支「第二次 `--apply`」冪等性檢查還額外抓到了修正過程中自己引入的一個雜湊不收斂問題(見下一條)。
32
+ - **通用 AGENTS.md 摘要「這個檔案是索引,不是標準本文」的提醒(2026-08-18 加入)從未送到既有專案的受管區塊——只到了檔頭,而檔頭在檔案已存在時,標記式更新從來不會重新產生。** `writeAgentsMdSummary` 對已存在的檔案,永遠只替換 `<!-- UDS:STANDARDS:START/END -->` 兩個標記之間的內容;提醒卻寫在標記外的檔頭。已對兩個真實採用者(asiaostrich-telemetry-server、EngramGraph)實測:不論更新前後,提醒在檔頭和區塊裡都不存在。提醒現在也寫進標記區塊內(措辭與 per-tool 產生器自己的揭露文字 `generateIndexDisclosure` 對齊,不另創第三種),於是每個專案在下一次 `uds update`/`--apply`(不論是新檔還是既有檔)都會收到它。順手把這個區塊也改用 per-tool 產生器本來就在用的同一個 `wrapWithMarkers` 輔助函式組出來(而非手寫標記行),過程中也修掉一個相關缺口:全新的 AGENTS.md 檔案原本也從未帶有「請勿手動編輯」的警告註解,因為過去只有合併路徑才會加上它。
33
+
20
34
  ## [6.13.0] - 2026-09-28
21
35
 
22
36
  > **正式版**:包含下方 6.13.0-beta.1 至 beta.5 的全部內容,beta.5 之後沒有任何變更。重點:回合收尾關卡(agent 說了下一步卻沒做就不得結束回合)現在涵蓋 **Claude Code 與 Codex**,兩者都以從 npm 安裝的版本在真實工作階段驗證過(Codex 要先用 `/hooks` 信任才會執行);Claude Code 關卡真的會擋(到 beta.4 為止每個回合都放行);Windows 上可用;`uds uninstall` 會移除它;`uds init` 讓提交前檢查真的執行;`developer-memory` 1.2.0。Gemini CLI 標為過時(Google 已對個人帳號停用,改由 Antigravity CLI 取代,後者尚未支援)。
@@ -15,7 +15,7 @@ status: current
15
15
 
16
16
  > **語言**: [English](../../README.md) | 繁體中文 | [简体中文](../zh-CN/README.md)
17
17
 
18
- **版本**: 6.13.0 | **發布日期**: 2026-09-28 | **授權**: [雙重授權](../../LICENSE) (CC BY 4.0 + MIT)
18
+ **版本**: 6.13.1 | **發布日期**: 2026-09-28 | **授權**: [雙重授權](../../LICENSE) (CC BY 4.0 + MIT)
19
19
 
20
20
  語言無關、框架無關的軟體專案文件標準。透過 AI 原生工作流,確保不同技術堆疊之間的一致性、品質和可維護性。
21
21
 
@@ -13,7 +13,7 @@ status: current
13
13
  <!-- UDS_SUPPORTED_VERSIONS_START -->
14
14
  | 版本 | 支援狀態 |
15
15
  |------|--------|
16
- | 6.13.0 | ✅ 最新正式版 |
16
+ | 6.13.1 | ✅ 最新正式版 |
17
17
  | < 6.0.0 | ❌ 已終止支援 |
18
18
  <!-- UDS_SUPPORTED_VERSIONS_END -->
19
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-dev-standards",
3
- "version": "6.13.0",
3
+ "version": "6.13.1",
4
4
  "description": "CLI tool for adopting Universal Development Standards",
5
5
  "keywords": [
6
6
  "documentation",
@@ -15,7 +15,8 @@ import {
15
15
  resolveContentModeForTool,
16
16
  generateIntegrationContent,
17
17
  extractMarkedContent,
18
- buildToolIntegrationConfig
18
+ buildToolIntegrationConfig,
19
+ resolveIntegrationLanguage
19
20
  } from '../utils/integration-generator.js';
20
21
  import {
21
22
  calculateCategoriesFromStandards,
@@ -782,13 +783,20 @@ export async function updateCommand(options) {
782
783
  // basename() removes both. See resolveStandardFilename.
783
784
  const installedStandardsList = manifest.standards || [];
784
785
 
785
- // Determine language setting
786
- let commonLanguage = 'en';
787
- if ((manifest.options?.output_language || manifest.options?.commit_language) === 'bilingual') {
788
- commonLanguage = 'bilingual';
789
- } else if ((manifest.options?.output_language || manifest.options?.commit_language) === 'traditional-chinese') {
790
- commonLanguage = 'zh-tw';
791
- }
786
+ // Determine language setting.
787
+ //
788
+ // Content language comes from `display_language`, falling back to
789
+ // `output_language`/`commit_language` only when there is no display
790
+ // setting to go on — see resolveIntegrationLanguage's docblock. This used
791
+ // to derive `commonLanguage` from output_language/commit_language alone,
792
+ // which is the commit-message language, not the display language: a
793
+ // project installed with `--locale zh-tw` and `output_language: bilingual`
794
+ // got the correct Chinese heading from `init`/the reconciler and an
795
+ // English one from the next plain `uds update`, silently overwriting the
796
+ // block that was already there. `buildToolIntegrationConfig` picked up
797
+ // the fix on 2026-09-16 (XSPEC-343 R2 follow-up); this call site — the
798
+ // plain `uds update` main flow — did not.
799
+ const commonLanguage = resolveIntegrationLanguage(manifest);
792
800
 
793
801
  // Track generated files to handle AGENTS.md sharing
794
802
  const generatedFiles = new Set();
@@ -1982,10 +1990,13 @@ async function updateIntegrationsOnly(projectPath, manifest, options = {}) {
1982
1990
  const next = generateIntegrationContent({
1983
1991
  tool,
1984
1992
  categories: ['anti-hallucination', 'commit-standards', 'code-review'],
1985
- language: (manifest.options?.output_language || manifest.options?.commit_language) === 'bilingual'
1986
- ? 'bilingual'
1987
- : (manifest.options?.output_language || manifest.options?.commit_language) === 'traditional-chinese'
1988
- ? 'zh-tw' : 'en',
1993
+ // See resolveIntegrationLanguage's docblock: content language comes
1994
+ // from `display_language`, not `output_language`/`commit_language`.
1995
+ // This inline derivation ignored `display_language` entirely, so
1996
+ // `--plan` reported "would change" (or "unchanged" against an
1997
+ // already-wrong file) using the wrong language for any project with
1998
+ // display_language != output_language (e.g. zh-tw + bilingual).
1999
+ language: resolveIntegrationLanguage(manifest),
1989
2000
  // Passed raw. `basename()` here destroyed the two things the
1990
2001
  // generator needs: a registry ID cannot be turned back into a
1991
2002
  // filename once it has been through basename() (it comes out
@@ -12,6 +12,7 @@ import { getAllStandards, getStandardSource, findOption, getOptionSource } from
12
12
  import {
13
13
  resolveToolKey,
14
14
  SUPPORTED_AI_TOOLS,
15
+ LEGACY_TOOL_MAPPINGS,
15
16
  MANIFEST_OPTION_BINDINGS,
16
17
  OPTIONS_INSTALL_DIR
17
18
  } from '../core/constants.js';
@@ -19,7 +20,9 @@ import {
19
20
  resolveIntegrationTargetFile,
20
21
  buildToolIntegrationConfig,
21
22
  generateIntegrationContent,
22
- extractMarkedContent
23
+ extractMarkedContent,
24
+ generateAgentsMdSummary,
25
+ buildAgentsMdSummaryConfig
23
26
  } from '../utils/integration-generator.js';
24
27
  import { PathResolver } from '../core/paths.js';
25
28
  import { computeFileHash } from '../utils/hasher.js';
@@ -299,6 +302,61 @@ function computeExpectedIntegrationBlockHash(manifest, toolName, format) {
299
302
  }
300
303
  }
301
304
 
305
+ /**
306
+ * The same block-hash computation as computeExpectedIntegrationBlockHash,
307
+ * for the OTHER AGENTS.md producer (the universal summary — see
308
+ * isAgentsMdGenuinelyToolOwned's docblock for why there are two).
309
+ *
310
+ * @param {Object} manifest
311
+ * @returns {string|null}
312
+ */
313
+ function computeExpectedAgentsMdSummaryHash(manifest) {
314
+ try {
315
+ const generated = generateAgentsMdSummary(buildAgentsMdSummaryConfig(manifest));
316
+ const { content: blockContent } = extractMarkedContent(generated, 'markdown');
317
+ if (!blockContent) return null;
318
+ return `sha256:${createHash('sha256').update(blockContent).digest('hex')}`;
319
+ } catch {
320
+ return null;
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Whether `toolName` is a tool the project actually selected, as opposed to
326
+ * one `resolveToolKey` happened to resolve an entry to because a filename
327
+ * collided.
328
+ *
329
+ * AGENTS.md is the one case in `SUPPORTED_AI_TOOLS` where this distinction
330
+ * matters: it is opencode's per-tool file, but it is ALSO the generic
331
+ * "universal AGENTS.md summary" target that `generateAgentsMd` writes for
332
+ * ANY project regardless of which AI tools are selected (`uds init` always
333
+ * pushes 'AGENTS.md' into `manifest.integrations` once that summary is
334
+ * written — see init.js's `generateUniversalAgentsMd` call site — purely as
335
+ * a "this file is UDS-managed" marker, not as a record of a tool choice).
336
+ * `resolveToolKey('AGENTS.md')` resolves to 'opencode' regardless, because
337
+ * that is simply the first (only) tool whose default file matches.
338
+ *
339
+ * Every other file in `SUPPORTED_AI_TOOLS` maps 1:1 to exactly one tool, so
340
+ * this check is only ever consulted for opencode/AGENTS.md; it is written
341
+ * generically in case that ever changes.
342
+ *
343
+ * Confirmed as a real bug, not a hypothetical: `uds init` (aiTools:
344
+ * ['claude-code'] only) followed immediately by `uds update --apply` — zero
345
+ * manifest edits, zero version change, nothing to reconcile — silently
346
+ * replaced AGENTS.md's content, because `calculateIntegrations` treated the
347
+ * bare tracking marker as evidence opencode was selected. Confirmed in
348
+ * production on two real adopters (asiaostrich-telemetry-server,
349
+ * EngramGraph) and dev-platform's own `.standards/` install, all three with
350
+ * `aiTools` containing no codex/opencode entry.
351
+ *
352
+ * @param {string} toolName - Tool key to check (e.g. 'opencode')
353
+ * @param {Object} manifest - Project manifest
354
+ * @returns {boolean}
355
+ */
356
+ function isToolGenuinelySelected(toolName, manifest) {
357
+ return (manifest.aiTools || []).some((t) => (LEGACY_TOOL_MAPPINGS[t] || t) === toolName);
358
+ }
359
+
302
360
  /**
303
361
  * Calculate expected integration files.
304
362
  * For integrations we track the UDS marker block, not the entire file.
@@ -325,6 +383,31 @@ function calculateIntegrations(state, manifest) {
325
383
  // detection) would treat that file as unmanaged and CLAUDE.md as desired.
326
384
  const relativePath = resolveIntegrationTargetFile(toolName, manifest) || toolConfig.file;
327
385
 
386
+ // See isToolGenuinelySelected's docblock. `relativePath === 'AGENTS.md'`
387
+ // rather than `toolName === 'opencode'`: correct either way today (only
388
+ // opencode's file is 'AGENTS.md'), but this is the actual thing that
389
+ // matters — which FILE is ambiguous — should another tool ever share it.
390
+ if (relativePath === 'AGENTS.md' && !isToolGenuinelySelected(toolName, manifest)) {
391
+ if (!manifest.generateAgentsMd) continue; // Not wanted at all; nothing to track.
392
+ state.integrations.set(relativePath, {
393
+ relativePath,
394
+ hash: computeExpectedAgentsMdSummaryHash(manifest),
395
+ size: null,
396
+ category: 'integration',
397
+ sourcePath: null,
398
+ metadata: {
399
+ toolName: null,
400
+ // Read by plan-executor.js's executeMigrateBlock to pick the
401
+ // matching writer (writeAgentsMdSummary, not writeIntegrationFile).
402
+ generator: 'summary',
403
+ format: 'markdown',
404
+ toolCategory: null,
405
+ supports: []
406
+ }
407
+ });
408
+ continue;
409
+ }
410
+
328
411
  state.integrations.set(relativePath, {
329
412
  relativePath,
330
413
  // XSPEC adopter-report Q6: this used to be a hardcoded `null` ("hashes
@@ -14,7 +14,12 @@
14
14
  import { existsSync, unlinkSync, mkdirSync, rmSync, copyFileSync, statSync } from 'fs';
15
15
  import { join, dirname } from 'path';
16
16
  import { copyStandard } from '../utils/copier.js';
17
- import { writeIntegrationFile, buildToolIntegrationConfig } from '../utils/integration-generator.js';
17
+ import {
18
+ writeIntegrationFile,
19
+ buildToolIntegrationConfig,
20
+ writeAgentsMdSummary,
21
+ buildAgentsMdSummaryConfig
22
+ } from '../utils/integration-generator.js';
18
23
  import {
19
24
  installSkillsToMultipleAgents,
20
25
  installCommandsToMultipleAgents
@@ -342,6 +347,34 @@ function executeDelete(projectPath, action, manifest) {
342
347
  * Uses writeIntegrationFile which handles marker-based section replacement.
343
348
  */
344
349
  function executeMigrateBlock(projectPath, action, manifest) {
350
+ // AGENTS.md tracked as the universal summary rather than a genuine
351
+ // per-tool integration — see desired-state-calculator.js's
352
+ // isToolGenuinelySelected docblock. Written through writeAgentsMdSummary,
353
+ // not writeIntegrationFile, so `--apply` produces the SAME content a plain
354
+ // `uds update` would for this case, instead of the opencode-shaped block a
355
+ // project with no codex/opencode selected never asked for.
356
+ // `diffIntegrations` (diff-engine.js) nests the desired-state metadata
357
+ // under `details.metadata`, not at the top level of `details` — only
358
+ // `toolName`/`format`/the hash fields are copied out. Checking
359
+ // `action.details?.generator` (rather than `action.details?.metadata?.generator`)
360
+ // is silently always false, and this branch would never fire; the
361
+ // fallback below then reads `action.details?.toolName` as `null` (correctly
362
+ // propagated from calculateIntegrations) and fails with "No tool name in
363
+ // action details" — reproduced live via `uds update --apply` E2E
364
+ // (tests/e2e/update-version-advances.test.js) before this fix.
365
+ if (action.details?.metadata?.generator === 'summary') {
366
+ const config = buildAgentsMdSummaryConfig(manifest);
367
+ const result = writeAgentsMdSummary(config, projectPath);
368
+ if (result.success) {
369
+ if (result.blockHashInfo) {
370
+ manifest.integrationBlockHashes[result.path] = result.blockHashInfo;
371
+ }
372
+ if (manifest.fileHashes) delete manifest.fileHashes[result.path];
373
+ return { action, success: true };
374
+ }
375
+ return { action, success: false, error: result.error };
376
+ }
377
+
345
378
  const toolName = action.details?.toolName;
346
379
  if (!toolName) {
347
380
  return { action, success: false, error: 'No tool name in action details' };
@@ -3462,21 +3462,41 @@ function withSelectedOptions(manifest) {
3462
3462
  return [...standards, ...extra];
3463
3463
  }
3464
3464
 
3465
- export function buildToolIntegrationConfig(manifest, tool) {
3465
+ /**
3466
+ * Resolve the content (display) language an integration block should be
3467
+ * written in, from a project's manifest.
3468
+ *
3469
+ * Content language comes from `display_language` — the setting whose entire
3470
+ * job is "what language do you want to read" — and only falls back to
3471
+ * `output_language`, which is the commit-message language, when the project
3472
+ * has no display setting to go on. They are different questions: `uds init`
3473
+ * has always used the first, and this was the single place that used the
3474
+ * second correctly (XSPEC-343 R2 / a Windows-adopter fix on 2026-09-16).
3475
+ *
3476
+ * `update.js`'s two other, older per-tool config builders (the plain
3477
+ * `uds update` main flow and its `--plan` dry-run) each derived `language`
3478
+ * independently and never picked up that fix — they read only
3479
+ * `output_language`/`commit_language`, so a project installed with
3480
+ * `display_language: zh-tw` and `output_language: bilingual` got the correct
3481
+ * Chinese heading from `init`/the reconciler and an English one the next
3482
+ * plain `uds update`, silently overwriting the block that was already there.
3483
+ * Both call sites now use this function instead of re-deriving the value.
3484
+ *
3485
+ * @param {Object} manifest - Project manifest
3486
+ * @returns {'en'|'zh-tw'|'bilingual'} Resolved content language
3487
+ */
3488
+ export function resolveIntegrationLanguage(manifest) {
3466
3489
  const selected = manifest.options?.output_language || manifest.options?.commit_language || 'english';
3467
-
3468
- // Content language comes from `display_language` — the setting whose entire
3469
- // job is "what language do you want to read" — and only falls back to
3470
- // `output_language`, which is the commit-message language, when the project
3471
- // has no display setting to go on. They are different questions: `uds init`
3472
- // has always used the first, every regeneration path used the second, and a
3473
- // project installed with `--locale zh-tw` therefore got Chinese instructions
3474
- // from `init` and English ones from the next `uds update`, silently.
3475
3490
  const display = manifest.options?.display_language;
3476
3491
  const fromOutput = selected === 'bilingual'
3477
3492
  ? 'bilingual'
3478
3493
  : selected === 'traditional-chinese' ? 'zh-tw' : 'en';
3479
- const language = ['en', 'zh-tw', 'bilingual'].includes(display) ? display : fromOutput;
3494
+ return ['en', 'zh-tw', 'bilingual'].includes(display) ? display : fromOutput;
3495
+ }
3496
+
3497
+ export function buildToolIntegrationConfig(manifest, tool) {
3498
+ const language = resolveIntegrationLanguage(manifest);
3499
+ const selected = manifest.options?.output_language || manifest.options?.commit_language || 'english';
3480
3500
  const resolved = resolveContentModeForTool(tool, manifest.contentMode || 'auto');
3481
3501
 
3482
3502
  return {
@@ -3502,6 +3522,33 @@ export function buildToolIntegrationConfig(manifest, tool) {
3502
3522
  };
3503
3523
  }
3504
3524
 
3525
+ /**
3526
+ * Build the generation config for the universal AGENTS.md summary
3527
+ * (`generateAgentsMdSummary`/`writeAgentsMdSummary`) from the manifest.
3528
+ *
3529
+ * This is the OTHER AGENTS.md producer — used when no genuinely-selected AI
3530
+ * tool (codex/opencode) claims AGENTS.md as its own file, i.e. the file is
3531
+ * present in `manifest.integrations` only because `uds init` tracks it there
3532
+ * once `generateAgentsMd` writes one, not because a tool was chosen. Mirrors
3533
+ * `commands/update.js`'s own construction of this same config exactly, so
3534
+ * the reconciler (`--apply`) and a plain `uds update` compute and write
3535
+ * IDENTICAL content for this case — see resolveIntegrationLanguage's and
3536
+ * this whole area's history for what happens when "the same config" is
3537
+ * derived independently in two places instead of shared.
3538
+ *
3539
+ * @param {Object} manifest - Project manifest
3540
+ * @returns {Object} Config for generateAgentsMdSummary/writeAgentsMdSummary
3541
+ */
3542
+ export function buildAgentsMdSummaryConfig(manifest) {
3543
+ return {
3544
+ installedStandards: manifest.standards || [],
3545
+ standardsFormat: manifest.format || 'ai',
3546
+ language: manifest.options?.display_language || 'en',
3547
+ outputLanguage: manifest.options?.output_language || manifest.options?.commit_language || 'english',
3548
+ standardOptions: manifest.options || {}
3549
+ };
3550
+ }
3551
+
3505
3552
  /**
3506
3553
  * Heading of the index-mode standards block. Not localised — both the English and
3507
3554
  * the Chinese body sit under this exact string, so it is a stable marker.
@@ -3835,13 +3882,45 @@ export function generateAgentsMdSummary(config = {}) {
3835
3882
  lines.push('');
3836
3883
 
3837
3884
  // Installed Standards Index
3838
- lines.push('<!-- UDS:STANDARDS:START -->');
3839
- lines.push('## Installed Standards');
3840
- lines.push('');
3885
+ //
3886
+ // Built as a separate block body and wrapped with wrapWithMarkers (the
3887
+ // same helper generateIntegrationContent's own block uses), rather than
3888
+ // hand-rolling the `<!-- UDS:STANDARDS:START/END -->` lines directly.
3889
+ // Hand-rolled, this block never carried the
3890
+ // `<!-- WARNING: This block is managed by UDS ... -->` line that
3891
+ // wrapWithMarkers adds — so a hash computed straight from this function's
3892
+ // output (desired-state-calculator.js's computeExpectedAgentsMdSummaryHash)
3893
+ // never matched a real on-disk file's hash (computeIntegrationBlockHash,
3894
+ // hasher.js — which always includes that line, since a written file only
3895
+ // ever gets it added by the merge path's own wrapWithMarkers call), and
3896
+ // `uds update --apply` reported "content differs" and re-wrote AGENTS.md
3897
+ // on EVERY run forever, settled state or not — a non-convergent
3898
+ // reconciliation loop, reproduced with a second `--apply` immediately
3899
+ // after a first that had already "fixed" everything.
3900
+ const blockLines = [];
3901
+ // XSPEC-357 R7's disclosure was added to this function's HEADER above
3902
+ // (2026-08-18) but never to the marker block itself. `writeAgentsMdSummary`'s
3903
+ // marker-based update for an EXISTING file only ever replaces the content
3904
+ // BETWEEN the markers — the header (including that reminder) is copied
3905
+ // verbatim from whatever the file already had. Every adopter whose
3906
+ // AGENTS.md predates 2026-08-18 therefore never received the reminder via
3907
+ // the header (nothing ever regenerates it) and never received it in the
3908
+ // block either, since it was never written there. Measured 2026-09-28
3909
+ // against two real adopters (asiaostrich-telemetry-server, EngramGraph):
3910
+ // reminder present at neither the file level nor the block level, 0 of 2.
3911
+ // Placed here, inside the block, it reaches every project on every
3912
+ // `uds update`/`--apply`, new or existing, regardless of the header's age.
3913
+ // Uses the SAME wording as the per-tool generator's index/minimal
3914
+ // templates (generateIndexDisclosure) rather than inventing a third
3915
+ // phrasing for a third producer of the same idea.
3916
+ blockLines.push(generateIndexDisclosure('markdown', language));
3917
+ blockLines.push('');
3918
+ blockLines.push('## Installed Standards');
3919
+ blockLines.push('');
3841
3920
 
3842
3921
  if (installedStandards.length > 0) {
3843
- lines.push('All standards are in `.standards/`. Installed standards:');
3844
- lines.push('');
3922
+ blockLines.push('All standards are in `.standards/`. Installed standards:');
3923
+ blockLines.push('');
3845
3924
 
3846
3925
  // Resolved before filtering, not after. The filter asks for an `.ai.yaml`
3847
3926
  // suffix and a manifest's core entries are registry IDs, which carry no
@@ -3867,14 +3946,13 @@ export function generateAgentsMdSummary(config = {}) {
3867
3946
  if (!filename?.endsWith(suffix)) continue;
3868
3947
  const isOption = entry.includes('/options/') || entry.includes('\\options\\');
3869
3948
  const dir = isOption ? '.standards/options' : '.standards';
3870
- lines.push(`- \`${dir}/${filename}\` — ${filename.replace(suffix, '')}`);
3949
+ blockLines.push(`- \`${dir}/${filename}\` — ${filename.replace(suffix, '')}`);
3871
3950
  }
3872
3951
  } else {
3873
- lines.push('No standards installed yet. Run `npx uds init` to install.');
3952
+ blockLines.push('No standards installed yet. Run `npx uds init` to install.');
3874
3953
  }
3875
3954
 
3876
- lines.push('');
3877
- lines.push('<!-- UDS:STANDARDS:END -->');
3955
+ lines.push(wrapWithMarkers(blockLines.join('\n'), 'markdown'));
3878
3956
  lines.push('');
3879
3957
 
3880
3958
  // Boundaries
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "version": "6.13.0",
3
+ "version": "6.13.1",
4
4
  "lastUpdated": "2026-05-13",
5
5
  "description": "Standards registry for universal-dev-standards with integrated skills and AI-optimized formats",
6
6
  "formats": {
@@ -58,14 +58,14 @@
58
58
  "standards": {
59
59
  "name": "universal-dev-standards",
60
60
  "url": "https://github.com/AsiaOstrich/universal-dev-standards",
61
- "version": "6.13.0"
61
+ "version": "6.13.1"
62
62
  },
63
63
  "skills": {
64
64
  "name": "universal-dev-standards",
65
65
  "url": "https://github.com/AsiaOstrich/universal-dev-standards",
66
66
  "localPath": "skills",
67
67
  "rawUrl": "https://raw.githubusercontent.com/AsiaOstrich/universal-dev-standards/main/skills",
68
- "version": "6.13.0",
68
+ "version": "6.13.1",
69
69
  "note": "Skills are now included in the main repository under skills/"
70
70
  }
71
71
  },
@@ -2295,7 +2295,7 @@
2295
2295
  "id": "license-compliance",
2296
2296
  "name": "License Compliance Standards",
2297
2297
  "nameZh": "授權合規標準",
2298
- "version": "6.13.0",
2298
+ "version": "6.13.1",
2299
2299
  "source": {
2300
2300
  "human": "core/license-compliance.md",
2301
2301
  "ai": "ai/standards/license-compliance.ai.yaml"
@@ -2307,7 +2307,7 @@
2307
2307
  "id": "verification-oracle",
2308
2308
  "name": "Verification Oracle Standards",
2309
2309
  "nameZh": "驗證 Oracle 標準",
2310
- "version": "6.13.0",
2310
+ "version": "6.13.1",
2311
2311
  "source": {
2312
2312
  "human": "core/verification-oracle.md",
2313
2313
  "ai": "ai/standards/verification-oracle.ai.yaml"
@@ -2319,7 +2319,7 @@
2319
2319
  "id": "model-provenance",
2320
2320
  "name": "Model Provenance Policy Standards",
2321
2321
  "nameZh": "模型來源政策標準",
2322
- "version": "6.13.0",
2322
+ "version": "6.13.1",
2323
2323
  "source": {
2324
2324
  "human": "core/model-provenance.md",
2325
2325
  "ai": "ai/standards/model-provenance.ai.yaml"
@@ -2331,7 +2331,7 @@
2331
2331
  "id": "resource-cost-boundary",
2332
2332
  "name": "Resource / Cost Boundary Declaration Standards",
2333
2333
  "nameZh": "資源/成本邊界宣告標準",
2334
- "version": "6.13.0",
2334
+ "version": "6.13.1",
2335
2335
  "source": {
2336
2336
  "human": "core/resource-cost-boundary.md",
2337
2337
  "ai": "ai/standards/resource-cost-boundary.ai.yaml"