universal-dev-standards 6.1.0 → 6.2.0

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.
Files changed (78) hide show
  1. package/bundled/locales/zh-CN/CHANGELOG.md +46 -4
  2. package/bundled/locales/zh-CN/README.md +75 -33
  3. package/bundled/locales/zh-CN/SECURITY.md +1 -1
  4. package/bundled/locales/zh-CN/core/behavior-snapshot.md +2 -2
  5. package/bundled/locales/zh-CN/core/data-migration-testing.md +2 -2
  6. package/bundled/locales/zh-CN/docs/CHEATSHEET.md +4 -4
  7. package/bundled/locales/zh-CN/docs/CLI-INIT-OPTIONS.md +9 -3
  8. package/bundled/locales/zh-CN/docs/FEATURE-REFERENCE.md +10 -11
  9. package/bundled/locales/zh-CN/docs/USER-MANUAL.md +40 -21
  10. package/bundled/locales/zh-CN/integrations/gemini-cli/README.md +12 -0
  11. package/bundled/locales/zh-CN/skills/ac-coverage/SKILL.md +11 -5
  12. package/bundled/locales/zh-CN/skills/adr-assistant/SKILL.md +2 -2
  13. package/bundled/locales/zh-CN/skills/commands/brainstorm.md +2 -2
  14. package/bundled/locales/zh-CN/skills/commit-standards/SKILL.md +2 -2
  15. package/bundled/locales/zh-CN/skills/contract-test-assistant/SKILL.md +2 -2
  16. package/bundled/locales/zh-CN/skills/deploy-assistant/SKILL.md +2 -2
  17. package/bundled/locales/zh-CN/skills/dev-methodology/SKILL.md +2 -2
  18. package/bundled/locales/zh-CN/skills/dev-workflow-guide/SKILL.md +2 -2
  19. package/bundled/locales/zh-CN/skills/journey-test-assistant/SKILL.md +2 -2
  20. package/bundled/locales/zh-CN/skills/knowledge-graph/SKILL.md +2 -2
  21. package/bundled/locales/zh-CN/skills/knowledge-graph/guide.md +2 -2
  22. package/bundled/locales/zh-CN/skills/migration-assistant/SKILL.md +188 -2
  23. package/bundled/locales/zh-CN/skills/observability-assistant/guide.md +2 -2
  24. package/bundled/locales/zh-CN/skills/orchestrate/SKILL.md +2 -2
  25. package/bundled/locales/zh-CN/skills/plan/SKILL.md +2 -2
  26. package/bundled/locales/zh-CN/skills/push/SKILL.md +2 -2
  27. package/bundled/locales/zh-CN/skills/retrospective-assistant/SKILL.md +2 -2
  28. package/bundled/locales/zh-CN/skills/reverse-engineer/SKILL.md +2 -2
  29. package/bundled/locales/zh-CN/skills/runbook-assistant/guide.md +2 -2
  30. package/bundled/locales/zh-CN/skills/skill-builder/SKILL.md +2 -2
  31. package/bundled/locales/zh-CN/skills/slo-assistant/guide.md +2 -2
  32. package/bundled/locales/zh-CN/skills/spec-derivation/SKILL.md +2 -2
  33. package/bundled/locales/zh-CN/skills/spec-driven-dev/SKILL.md +40 -2
  34. package/bundled/locales/zh-CN/skills/sweep/SKILL.md +2 -2
  35. package/bundled/locales/zh-CN/skills/testing-guide/SKILL.md +2 -2
  36. package/bundled/locales/zh-TW/CHANGELOG.md +47 -4
  37. package/bundled/locales/zh-TW/README.md +75 -33
  38. package/bundled/locales/zh-TW/SECURITY.md +1 -1
  39. package/bundled/locales/zh-TW/core/audit-trail.md +2 -2
  40. package/bundled/locales/zh-TW/core/behavior-snapshot.md +2 -2
  41. package/bundled/locales/zh-TW/core/browser-compatibility-standards.md +18 -7
  42. package/bundled/locales/zh-TW/docs/CHEATSHEET.md +4 -4
  43. package/bundled/locales/zh-TW/docs/CLI-INIT-OPTIONS.md +9 -3
  44. package/bundled/locales/zh-TW/docs/FEATURE-REFERENCE.md +10 -11
  45. package/bundled/locales/zh-TW/docs/USER-MANUAL.md +40 -21
  46. package/bundled/locales/zh-TW/integrations/gemini-cli/README.md +12 -0
  47. package/bundled/skills/commands/journey-test.md +80 -6
  48. package/bundled/skills/commands/skill-builder.md +75 -6
  49. package/package.json +3 -3
  50. package/src/commands/check.js +77 -20
  51. package/src/commands/config.js +9 -0
  52. package/src/commands/init.js +15 -1
  53. package/src/commands/release.js +1 -1
  54. package/src/commands/update.js +82 -31
  55. package/src/config/ai-agent-paths.js +47 -7
  56. package/src/core/constants.js +61 -1
  57. package/src/core/manifest.js +56 -0
  58. package/src/flow/flow-parser.js +1 -1
  59. package/src/flow/gate-loader.js +1 -1
  60. package/src/i18n/messages.js +12 -6
  61. package/src/installers/standards-installer.js +10 -20
  62. package/src/reconciler/actual-state-scanner.js +118 -43
  63. package/src/reconciler/desired-state-calculator.js +157 -43
  64. package/src/reconciler/diff-engine.js +7 -2
  65. package/src/reconciler/manifest-migrator.js +5 -2
  66. package/src/reconciler/plan-executor.js +53 -14
  67. package/src/uninstallers/integration-uninstaller.js +7 -1
  68. package/src/utils/config-loader.js +1 -1
  69. package/src/utils/config-manager.js +1 -1
  70. package/src/utils/github.js +5 -1
  71. package/src/utils/hasher.js +7 -4
  72. package/src/utils/integration-generator.js +121 -78
  73. package/src/utils/registry.js +39 -0
  74. package/src/utils/skills-installer.js +49 -34
  75. package/src/utils/skills-source.js +51 -0
  76. package/src/utils/standard-fixer.js +1 -1
  77. package/src/utils/standard-validator.js +1 -1
  78. package/standards-registry.json +21 -11
@@ -33,10 +33,79 @@ It is backed by the `skill-builder` skill (`skills/skill-builder/SKILL.md`).
33
33
  | Complex | any of the above is exceeded | Create an XSPEC first (`/sdd`), then build |
34
34
  | Delta | modifying an existing Skill | Append `## MODIFIED` / `## ADDED`, bump version |
35
35
 
36
- ## Implementation Note for AI
36
+ ## AI Agent Behavior | AI 代理行為
37
37
 
38
- When the user invokes `/skill-builder`:
39
- 1. Read `skills/skill-builder/SKILL.md` for the full decision tree, placement
40
- rules, workflow steps, and output checklist.
41
- 2. Walk the user through path selection (Simple / Complex / Delta) and produce
42
- the `SKILL.md` (plus `SKILL-CANDIDATES.md` update) per that skill.
38
+ > Follows [AI Command Behavior Standards](../../core/ai-command-behavior.md)
39
+
40
+ ### Entry Router | 進入路由
41
+
42
+ | Input | AI Action |
43
+ |-------|-----------|
44
+ | `/skill-builder` | 讀取 `SKILL-CANDIDATES.md`:有已達 3 次的候選 → 列出供選擇;無 → 詢問重複流程的描述 |
45
+ | `/skill-builder <process description>` | 以該描述進入 Step 1,直接開始擷取流程 |
46
+
47
+ ### Interaction Script | 互動腳本
48
+
49
+ 先讀取 `skills/skill-builder/SKILL.md`,取得完整決策樹、放置規則、工作流程與輸出檢查表。
50
+
51
+ #### Step 1–2: 擷取流程並登記候選
52
+
53
+ 1. 擷取重複序列:步驟與順序、目前手動執行過幾次、觸及哪些工具或檔案
54
+ 2. 開啟專案的 `SKILL-CANDIDATES.md`(首次則從 `templates/SKILL-CANDIDATES.md` 複製)
55
+
56
+ **Decision: 觸發次數**
57
+ - IF 尚未登記 → 新增列,填入目前次數
58
+ - IF 已登記 → 次數 +1
59
+ - IF 次數 < 3 → **告知尚未達觸發門檻,登記後停止,不建立 SKILL.md**
60
+ - IF 次數 ≥ 3 → 標記觸發 ✅,繼續 Step 3
61
+
62
+ 🛑 **STOP**: 次數未達 3 時停止於此,等待使用者明確要求才破例
63
+
64
+ #### Step 3: 路徑選擇
65
+
66
+ 回答 4 個判斷問題,判定路徑:
67
+
68
+ - IF ≤7 步、無分支、<3 個標準、無原始碼產出 → **Simple**(Step 4a)
69
+ - IF 上述任一超出 → **Complex**(Step 4b)
70
+ - IF 修改既有 Skill → **Delta**(Step 4c)
71
+
72
+ 🛑 **STOP**: 展示判定結果與理由後等待使用者確認路徑
73
+
74
+ #### Step 4: 依路徑分流
75
+
76
+ - **4a Simple** — 以 `templates/SKILL-BRIEF-TEMPLATE.md` 填寫 Brief:觸發情境、3–7 個核心步驟、2–3 條 AC、明確的 out of scope
77
+ - **4b Complex** — 執行 `/sdd` 建立 XSPEC;**XSPEC 未 Approved 前不進入 Step 5**
78
+ - **4c Delta** — 標定既有 SKILL.md 受影響章節,於檔末加 `## MODIFIED Requirements` 或 `## ADDED Requirements`
79
+
80
+ #### Step 5–7: 產出與登記
81
+
82
+ 1. **放置決策**:步驟引用專案特有路徑(如 `TECH-RADAR.md`、`DEC-*.md`)→ `{project}/.claude/skills/`;步驟為通用 → UDS `skills/{name}/` **並建立 zh-TW locale**
83
+ 2. 由 Brief 或 XSPEC 生成 SKILL.md,核對 frontmatter:`name`、`scope`、`description`、`allowed-tools`
84
+ 3. 回填 `SKILL-CANDIDATES.md`:觸發 ✅、Skill 欄填入
85
+ 4. 依 skill 的 commit 格式提交
86
+
87
+ 🛑 **STOP**: commit 前展示輸出檢查表結果,等待使用者確認
88
+
89
+ ### Stop Points | 停止點
90
+
91
+ | Stop Point | 等待內容 |
92
+ |-----------|---------|
93
+ | 觸發次數 < 3 | 使用者是否明確要求破例建立 |
94
+ | 路徑判定後 | 使用者確認 Simple / Complex / Delta |
95
+ | Complex 路徑 | XSPEC 經 `/sdd` 建立並 Approved |
96
+ | commit 前 | 使用者確認輸出檢查表 |
97
+
98
+ ### Error Handling | 錯誤處理
99
+
100
+ | Error Condition | AI Action |
101
+ |-----------------|-----------|
102
+ | 未提供流程描述且無候選 | 詢問重複流程內容,**不得臆造流程** |
103
+ | `SKILL-CANDIDATES.md` 不存在 | 從 `templates/SKILL-CANDIDATES.md` 複製後繼續 |
104
+ | 觸發次數不足 3 次 | 登記候選後停止,說明門檻理由 |
105
+ | 同名 Skill 已存在 | 改走 Delta 路徑,不覆蓋既有 SKILL.md |
106
+ | 放置位置無法判定 | 列出「專案特有 vs 通用」判準請使用者裁定 |
107
+
108
+ ## References | 參考
109
+
110
+ - [Skill Builder Skill](../skill-builder/SKILL.md)
111
+ - Related: [/sdd](./sdd.md) (Complex path creates an XSPEC first)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-dev-standards",
3
- "version": "6.1.0",
3
+ "version": "6.2.0",
4
4
  "description": "CLI tool for adopting Universal Development Standards",
5
5
  "keywords": [
6
6
  "documentation",
@@ -62,7 +62,7 @@
62
62
  "@inquirer/prompts": "^8.4.2",
63
63
  "chalk": "^5.3.0",
64
64
  "commander": "^15.0.0",
65
- "js-yaml": "^4.1.0",
65
+ "js-yaml": "^5.2.1",
66
66
  "ora": "^9.4.0"
67
67
  },
68
68
  "devDependencies": {
@@ -70,7 +70,7 @@
70
70
  "@vitest/coverage-v8": "^4.1.5",
71
71
  "ajv": "^8.20.0",
72
72
  "ajv-formats": "^3.0.1",
73
- "eslint": "10.6.0",
73
+ "eslint": "10.7.0",
74
74
  "glob": "^13.0.1",
75
75
  "globals": "17.7.0",
76
76
  "husky": "^9.1.7",
@@ -30,7 +30,7 @@ import {
30
30
  parseReferences,
31
31
  compareStandardsWithReferences
32
32
  } from '../utils/reference-sync.js';
33
- import { extractMarkedContent, getToolFilePath } from '../utils/integration-generator.js';
33
+ import { extractMarkedContent, getToolFilePath, parseStandardsIndexCount } from '../utils/integration-generator.js';
34
34
  import { getToolFormat } from '../core/constants.js';
35
35
  import { checkForUpdates } from '../utils/npm-registry.js';
36
36
  import { writeUpdateCache } from '../utils/update-checker.js';
@@ -39,17 +39,15 @@ import { WorkflowGate } from '../utils/workflow-gate.js';
39
39
  import { t, setLanguage, isLanguageExplicitlySet } from '../i18n/messages.js';
40
40
  import { guardAgainstSelfAdoption } from '../utils/detect-self-adoption.js';
41
41
  import { lintAll as lintI18nAll, partitionFindings as partitionI18nFindings } from '../lint/i18n.js';
42
+ import { resolveIntegrationFile } from '../core/constants.js';
42
43
 
43
44
  /**
44
45
  * Display the summary of file integrity status
45
46
  */
46
47
  function displayFileIntegritySummary(fileStatus, msg) {
47
- if (fileStatus.unchanged.length > 0) {
48
- for (const file of fileStatus.unchanged) {
49
- console.log(chalk.green(` ✓ ${file} (${msg.unchanged})`));
50
- }
51
- }
52
-
48
+ // XSPEC-342 R4(安靜通過):不再逐檔列印「✓ 未變更」——實測佔 `uds check` 輸出約 70%
49
+ // (121 行中 85 行),把真正該讀的訊息(如標準落後)淹沒,也讓輸出大到被 agent 截斷。
50
+ // 未變更數仍保留在下方 summary 一行;此處只列印「需要注意」的檔(modified/missing/noHash)。
53
51
  if (fileStatus.modified.length > 0) {
54
52
  for (const file of fileStatus.modified) {
55
53
  console.log(chalk.yellow(` ⚠ ${file} (${msg.modified})`));
@@ -164,7 +162,9 @@ function performFileIntegrityCheck(projectPath, manifest, msg) {
164
162
  }
165
163
 
166
164
  // Check integrations
167
- for (const int of manifest.integrations) {
165
+ for (const intEntry of manifest.integrations) {
166
+ // 兩種形狀都要能解出路徑(XSPEC-343 R1)
167
+ const int = resolveIntegrationFile(intEntry) || intEntry;
168
168
  const filePath = join(projectPath, int);
169
169
  if (existsSync(filePath)) {
170
170
  fileStatus.noHash.push(int);
@@ -221,7 +221,7 @@ function initializeCheckContext(projectPath) {
221
221
  /**
222
222
  * Display standards adoption status and update information
223
223
  */
224
- function displayAdoptionStatus(manifest, msg, common, repoInfo) {
224
+ function displayAdoptionStatus(manifest, msg, common, repoInfo, standardsUpdate) {
225
225
  console.log(chalk.green(msg.standardsInitialized));
226
226
  console.log();
227
227
  console.log(chalk.cyan(msg.adoptionStatus));
@@ -229,9 +229,15 @@ function displayAdoptionStatus(manifest, msg, common, repoInfo) {
229
229
  console.log(chalk.gray(` ${common.version}: ${manifest.upstream.version}`));
230
230
  console.log();
231
231
 
232
- // Check for updates (bundled registry)
233
- if (manifest.upstream.version !== repoInfo.standards.version) {
234
- console.log(chalk.yellow(msg.updateAvailable.replace('{current}', manifest.upstream.version).replace('{latest}', repoInfo.standards.version)));
232
+ // XSPEC-342 R1:判斷「已裝標準」是否落後 **npm 最新版**,而非 CLI 自己 bundled 的副本。
233
+ // 舊 bug:拿 manifest.upstream.version 比 repoInfo.standards.version(=跑這支 CLI 帶的
234
+ // 標準副本)。CLI 一舊,bundled < npm 最新,就會吐出「6.1.0 → 5.12.1」這種倒退訊息,
235
+ // 且永遠說不出「你的標準過期了」。standardsUpdate = checkForUpdates(已裝標準版本):
236
+ // available = 已裝 < npm 最新。離線時為 null → 靜默略過(不誤用 bundled 版本比對)。
237
+ if (standardsUpdate && !standardsUpdate.offline && standardsUpdate.available) {
238
+ console.log(chalk.yellow(msg.updateAvailable
239
+ .replace('{current}', manifest.upstream.version)
240
+ .replace('{latest}', standardsUpdate.latestVersion)));
235
241
  console.log(chalk.gray(` ${msg.runUpdate}`));
236
242
  console.log();
237
243
  }
@@ -308,10 +314,17 @@ export async function checkCommand(options = {}) {
308
314
  console.log(chalk.bold(msg.title));
309
315
  console.log(chalk.gray('─'.repeat(50)));
310
316
 
311
- // Display adoption info
312
- displayAdoptionStatus(manifest, msg, common, repoInfo);
317
+ // XSPEC-342 R1:先問 npm 最新,判斷「已裝標準」是否落後(離線則跳過,不誤用 CLI bundled 版本)。
318
+ // fetchLatestVersion 有 60s 模組級快取,故此處與下方 checkCliVersion 共兩次呼叫只打一次網路。
319
+ let standardsUpdate = null;
320
+ if (!options.offline) {
321
+ standardsUpdate = await checkForUpdates(manifest.upstream.version, {
322
+ checkBeta: manifest.upstream.version.includes('-')
323
+ });
324
+ }
313
325
 
314
- // Check for CLI updates from npm registry (unless --offline)
326
+ // Display adoption info
327
+ displayAdoptionStatus(manifest, msg, common, repoInfo, standardsUpdate);
315
328
 
316
329
  // Check for CLI updates from npm registry (unless --offline)
317
330
  if (!options.offline) {
@@ -717,7 +730,9 @@ function removeFromManifest(manifest, relativePath) {
717
730
  const fileName = basename(relativePath);
718
731
  manifest.standards = manifest.standards.filter(s => !s.endsWith(fileName));
719
732
  manifest.extensions = manifest.extensions.filter(e => typeof e !== 'string' || !e.endsWith(fileName));
720
- manifest.integrations = manifest.integrations.filter(i => i !== relativePath);
733
+ manifest.integrations = manifest.integrations.filter(
734
+ i => (resolveIntegrationFile(i) || i) !== relativePath
735
+ );
721
736
 
722
737
  // Remove from integrationBlockHashes
723
738
  if (manifest.integrationBlockHashes && manifest.integrationBlockHashes[relativePath]) {
@@ -751,7 +766,7 @@ export function getSourcePathFromRelative(manifest, relativePath) {
751
766
  }
752
767
 
753
768
  // Check integrations - these might need special handling
754
- if (manifest.integrations.includes(relativePath)) {
769
+ if (manifest.integrations.some(i => (resolveIntegrationFile(i) || i) === relativePath)) {
755
770
  // Integration files have different source paths
756
771
  const integrationMappings = {
757
772
  '.cursorrules': 'integrations/cursor/.cursorrules',
@@ -821,7 +836,8 @@ async function migrateToHashBasedTracking(projectPath, manifest) {
821
836
  }
822
837
 
823
838
  // Process integrations
824
- for (const int of manifest.integrations) {
839
+ for (const intEntry of manifest.integrations) {
840
+ const int = resolveIntegrationFile(intEntry) || intEntry;
825
841
  const fullPath = join(projectPath, int);
826
842
 
827
843
  const hashInfo = computeFileHash(fullPath);
@@ -1138,6 +1154,27 @@ function checkIntegrationFiles(manifest, projectPath, msg) {
1138
1154
  // Report status - use all installed standards as the total
1139
1155
  const totalTrackable = standardsFiles.length;
1140
1156
 
1157
+ // Index-mode blocks deliberately do NOT enumerate standards (XSPEC-358 R1);
1158
+ // grepping for each name reports a false failure and points at `uds update`,
1159
+ // which regenerates the identical block. Assert the declared count instead.
1160
+ const declaredCount = parseStandardsIndexCount(content);
1161
+ if (declaredCount !== null) {
1162
+ if (declaredCount === totalTrackable) {
1163
+ console.log(chalk.green(` ✓ ${toolFile}:`));
1164
+ console.log(chalk.gray(` ${msg.standardsIndexPresent}`));
1165
+ console.log(chalk.gray(` ${msg.standardsIndexCount
1166
+ ? msg.standardsIndexCount.replace('{count}', declaredCount)
1167
+ : `Index declares ${declaredCount} standards (matches manifest)`}`));
1168
+ } else {
1169
+ console.log(chalk.yellow(` ⚠ ${toolFile}:`));
1170
+ console.log(chalk.yellow(` ${msg.standardsIndexCountMismatch
1171
+ ? msg.standardsIndexCountMismatch.replace('{declared}', declaredCount).replace('{actual}', totalTrackable)
1172
+ : `Index declares ${declaredCount} standards but the manifest has ${totalTrackable}`}`));
1173
+ hasIssues = true;
1174
+ }
1175
+ continue;
1176
+ }
1177
+
1141
1178
  if (hasStandardsIndex && missingStandards.length === 0) {
1142
1179
  console.log(chalk.green(` ✓ ${toolFile}:`));
1143
1180
  console.log(chalk.gray(` ${msg.standardsIndexPresent}`));
@@ -1209,6 +1246,25 @@ function checkAgentsMdSync(manifest, projectPath, msg) {
1209
1246
  const content = readFileSync(agentsMdPath, 'utf-8');
1210
1247
  const installedStandards = (manifest.standards || []).map(s => basename(s));
1211
1248
 
1249
+ // Same as the AI-tool integration check above: an index-mode block declares a
1250
+ // count instead of listing names, so the name grep below cannot apply to it.
1251
+ const declaredCount = parseStandardsIndexCount(content);
1252
+ if (declaredCount !== null) {
1253
+ const actual = installedStandards.length;
1254
+ if (declaredCount === actual) {
1255
+ console.log(chalk.green(` ✓ AGENTS.md ${msg.standardsSynced || 'standards synced'} (${declaredCount})`));
1256
+ } else {
1257
+ console.log(chalk.yellow(` ⚠ AGENTS.md ${msg.standardsOutOfSync || 'standards out of sync'} (${declaredCount} != ${actual})`));
1258
+ console.log(chalk.gray(` ${msg.runUpdateToSync || 'Run "uds update" to sync'}`));
1259
+ }
1260
+ const idxLineCount = content.split('\n').length;
1261
+ if (idxLineCount > 150) {
1262
+ console.log(chalk.yellow(` ⚠ AGENTS.md ${msg.exceedsLineLimit || 'exceeds 150 line limit'} (${idxLineCount} lines)`));
1263
+ }
1264
+ console.log();
1265
+ return;
1266
+ }
1267
+
1212
1268
  // Check standards listed in AGENTS.md vs manifest
1213
1269
  const aiYamlStandards = installedStandards.filter(s => s.endsWith('.ai.yaml'));
1214
1270
  let listedCount = 0;
@@ -1254,7 +1310,8 @@ function checkReferenceSync(manifest, projectPath, msg) {
1254
1310
 
1255
1311
  // Pre-scan: collect results for files that have references
1256
1312
  const results = [];
1257
- for (const integrationPath of manifest.integrations) {
1313
+ for (const integrationEntry of manifest.integrations) {
1314
+ const integrationPath = resolveIntegrationFile(integrationEntry) || integrationEntry;
1258
1315
  const fullPath = join(projectPath, integrationPath);
1259
1316
 
1260
1317
  if (!existsSync(fullPath)) continue;
@@ -1690,7 +1747,7 @@ function getFileStatusCounts(manifest, projectPath) {
1690
1747
  const allFiles = [
1691
1748
  ...manifest.standards.map(s => `.standards/${basename(s)}`),
1692
1749
  ...manifest.extensions.filter(e => typeof e === 'string').map(e => `.standards/${basename(e)}`),
1693
- ...manifest.integrations
1750
+ ...manifest.integrations.map(i => resolveIntegrationFile(i) || i)
1694
1751
  ];
1695
1752
  for (const relativePath of allFiles) {
1696
1753
  const fullPath = join(projectPath, relativePath);
@@ -49,6 +49,7 @@ import {
49
49
  } from '../utils/integration-generator.js';
50
50
  import { getMarketplaceSkillsInfo } from '../utils/github.js';
51
51
  import { regenerateIntegrations } from './update.js';
52
+ import { mergeInstalledNames } from '../core/manifest.js';
52
53
 
53
54
  /**
54
55
  * Get localized message with fallback (for config-specific keys)
@@ -995,6 +996,7 @@ export async function runProjectConfiguration(options) {
995
996
  manifest.skills.installations.push(inst);
996
997
  }
997
998
  }
999
+ manifest.skills.names = mergeInstalledNames(manifest.skills.names, skillResult);
998
1000
  if (skillResult.allFileHashes) {
999
1001
  manifest.skillHashes = { ...(manifest.skillHashes || {}), ...skillResult.allFileHashes };
1000
1002
  }
@@ -1031,6 +1033,7 @@ export async function runProjectConfiguration(options) {
1031
1033
  manifest.commands.installations.push(inst);
1032
1034
  }
1033
1035
  }
1036
+ manifest.commands.names = mergeInstalledNames(manifest.commands.names, cmdResult);
1034
1037
  if (cmdResult.allFileHashes) {
1035
1038
  manifest.commandHashes = { ...(manifest.commandHashes || {}), ...cmdResult.allFileHashes };
1036
1039
  }
@@ -1234,6 +1237,7 @@ async function handleSkillsConfiguration(manifest, projectPath, msgObj, common,
1234
1237
  } else {
1235
1238
  manifest.skills.installations.push(installations[0]);
1236
1239
  }
1240
+ manifest.skills.names = mergeInstalledNames(manifest.skills.names, result);
1237
1241
  writeManifest(manifest, projectPath);
1238
1242
  return;
1239
1243
  }
@@ -1368,6 +1372,7 @@ async function handleSkillsConfiguration(manifest, projectPath, msgObj, common,
1368
1372
  // Update manifest - clear declined status for installed tools
1369
1373
  manifest.skills = manifest.skills || {};
1370
1374
  manifest.skills.installations = installations;
1375
+ manifest.skills.names = mergeInstalledNames(manifest.skills.names, result);
1371
1376
  if (manifest.declinedFeatures?.skills) {
1372
1377
  manifest.declinedFeatures.skills = manifest.declinedFeatures.skills.filter(
1373
1378
  tool => !declinedToolsWithSupport.includes(tool)
@@ -1402,6 +1407,7 @@ async function handleSkillsConfiguration(manifest, projectPath, msgObj, common,
1402
1407
  // Update manifest
1403
1408
  manifest.skills = manifest.skills || {};
1404
1409
  manifest.skills.installations = installations;
1410
+ manifest.skills.names = mergeInstalledNames(manifest.skills.names, result);
1405
1411
  writeManifest(manifest, projectPath);
1406
1412
  }
1407
1413
 
@@ -1465,6 +1471,7 @@ async function handleCommandsConfiguration(manifest, projectPath, msgObj, common
1465
1471
  } else {
1466
1472
  manifest.commands.installations.push(installations[0]);
1467
1473
  }
1474
+ manifest.commands.names = mergeInstalledNames(manifest.commands.names, result);
1468
1475
  writeManifest(manifest, projectPath);
1469
1476
  return;
1470
1477
  }
@@ -1566,6 +1573,7 @@ async function handleCommandsConfiguration(manifest, projectPath, msgObj, common
1566
1573
  // Update manifest - clear declined status for installed tools
1567
1574
  manifest.commands = manifest.commands || {};
1568
1575
  manifest.commands.installations = declinedCommandsWithSupport;
1576
+ manifest.commands.names = mergeInstalledNames(manifest.commands.names, result);
1569
1577
  if (manifest.declinedFeatures?.commands) {
1570
1578
  manifest.declinedFeatures.commands = manifest.declinedFeatures.commands.filter(
1571
1579
  tool => !declinedCommandsWithSupport.includes(tool)
@@ -1601,5 +1609,6 @@ async function handleCommandsConfiguration(manifest, projectPath, msgObj, common
1601
1609
  // Update manifest
1602
1610
  manifest.commands = manifest.commands || {};
1603
1611
  manifest.commands.installations = selectedAgents;
1612
+ manifest.commands.names = mergeInstalledNames(manifest.commands.names, result);
1604
1613
  writeManifest(manifest, projectPath);
1605
1614
  }
@@ -240,7 +240,21 @@ export async function initCommand(options) {
240
240
  * - Node.js projects: use husky
241
241
  * - Non-Node.js projects: write native .git/hooks/pre-commit
242
242
  */
243
- export async function setupHuskyHook(projectPath) {
243
+ export async function setupHuskyHook(projectPath, { allowInTest = false } = {}) {
244
+ // 2026-07-30:本函式在跑測試時改寫了 universal-dev-standards 自己
245
+ // ——`npm install --save-dev husky`、改 package.json、建 .husky/pre-commit。
246
+ //
247
+ // 路徑是:測試呼叫 `initCommand({...})`(三個 init 測試檔都沒帶路徑、
248
+ // 也沒 mock child_process)→ `init.js` 取 `process.cwd()` → 走到這裡。
249
+ // 而下面那個 `.git` 是唯一的守衛,它只問「cwd 是不是一個 repo」:
250
+ // 從 `cli/` 跑 → cwd 沒有 .git → 提早 return,無害;
251
+ // 從 repo root 跑 → cwd **有** .git → 照跑,動到開發者自己的 repo。
252
+ // 同一批測試、不同 cwd,一個無害一個會寫檔。
253
+ //
254
+ // 守衛用顯式 opt-in 而非「偵測到測試就一律跳過」:`init.husky.test.js` 有十幾條
255
+ // 測試是**刻意**在暫存目錄裡驗這個函式的行為,一律跳過會把它們變成永遠通過的空測試。
256
+ if (process.env.VITEST && !allowInTest) return;
257
+
244
258
  const hasGit = existsSync(join(projectPath, '.git'));
245
259
  if (!hasGit) return;
246
260
 
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import chalk from 'chalk';
13
- import yaml from 'js-yaml';
13
+ import * as yaml from 'js-yaml';
14
14
  import { existsSync, readFileSync, writeFileSync } from 'fs';
15
15
  import { join } from 'path';
16
16
  import { execSync } from 'child_process';
@@ -2,7 +2,7 @@ import chalk from 'chalk';
2
2
  import ora from 'ora';
3
3
  import { select, confirm as inquirerConfirm, checkbox, Separator } from '@inquirer/prompts';
4
4
  import { execSync } from 'child_process';
5
- import { existsSync, unlinkSync } from 'fs';
5
+ import { existsSync, readFileSync, unlinkSync } from 'fs';
6
6
  import { join, basename } from 'path';
7
7
  import { readManifest, writeManifest, copyStandard, isInitialized } from '../utils/copier.js';
8
8
  import { getRepositoryInfo, getAllStandards, getStandardSource } from '../utils/registry.js';
@@ -11,7 +11,10 @@ import {
11
11
  writeIntegrationFile,
12
12
  getToolFilePath,
13
13
  writeAgentsMdSummary,
14
- resolveContentModeForTool
14
+ resolveContentModeForTool,
15
+ generateIntegrationContent,
16
+ extractMarkedContent,
17
+ buildToolIntegrationConfig
15
18
  } from '../utils/integration-generator.js';
16
19
  import {
17
20
  calculateCategoriesFromStandards,
@@ -51,6 +54,8 @@ import {
51
54
  } from '../reconciler/index.js';
52
55
  import { restoreSingleFile } from './check.js';
53
56
  import { guardAgainstSelfAdoption } from '../utils/detect-self-adoption.js';
57
+ import { resolveIntegrationFile } from '../core/constants.js';
58
+ import { mergeInstalledNames } from '../core/manifest.js';
54
59
 
55
60
  /**
56
61
  * Determine the correct target directory for a standard file.
@@ -335,8 +340,12 @@ export async function updateCommand(options) {
335
340
  }
336
341
 
337
342
  // Handle --integrations-only option
343
+ // ⚠️ 這個分支在 --plan 檢查(見下)**之前**,所以必須自己處理 --plan——
344
+ // 否則 `--plan --integrations-only` 會靜默寫檔。2026-07-30 實測踩到:
345
+ // 以該組合驗證 XSPEC-358 的區塊變更時,它改了 machine-setup 的
346
+ // CLAUDE.md 與 .standards/manifest.json,而旗標文件寫的是 "without executing"。
338
347
  if (options.integrationsOnly) {
339
- await updateIntegrationsOnly(projectPath, manifest);
348
+ await updateIntegrationsOnly(projectPath, manifest, options);
340
349
  return;
341
350
  }
342
351
 
@@ -847,8 +856,9 @@ export async function updateCommand(options) {
847
856
  const fileName = basename(ext);
848
857
  allTrackedFiles.push(join('.standards', fileName));
849
858
  }
850
- for (const int of (manifest.integrations || [])) {
851
- const filePath = getToolFilePath(int);
859
+ for (const intEntry of (manifest.integrations || [])) {
860
+ // 兩種形狀都要能解出路徑(XSPEC-343 R1)
861
+ const filePath = resolveIntegrationFile(intEntry) || getToolFilePath(intEntry);
852
862
  if (filePath) {
853
863
  allTrackedFiles.push(filePath);
854
864
  }
@@ -951,6 +961,7 @@ export async function updateCommand(options) {
951
961
  ...(manifest.skills.installations || []),
952
962
  ...installSkills
953
963
  ];
964
+ manifest.skills.names = mergeInstalledNames(manifest.skills.names, skillResult);
954
965
 
955
966
  // Derive location from installations if not set
956
967
  if (!manifest.skills.location) {
@@ -987,6 +998,7 @@ export async function updateCommand(options) {
987
998
  // Update manifest version
988
999
  if (!manifest.skills) manifest.skills = {};
989
1000
  manifest.skills.version = repoInfo.skills.version;
1001
+ manifest.skills.names = mergeInstalledNames(manifest.skills.names, updateResult);
990
1002
 
991
1003
  // Derive location from installations if not set
992
1004
  if (!manifest.skills.location && manifest.skills.installations?.length > 0) {
@@ -1028,6 +1040,7 @@ export async function updateCommand(options) {
1028
1040
  ...(manifest.commands.installations || []),
1029
1041
  ...installCommands
1030
1042
  ];
1043
+ manifest.commands.names = mergeInstalledNames(manifest.commands.names, cmdResult);
1031
1044
 
1032
1045
  // Update command hashes for integrity tracking
1033
1046
  if (cmdResult.allFileHashes) {
@@ -1053,6 +1066,7 @@ export async function updateCommand(options) {
1053
1066
  // Update manifest version
1054
1067
  if (!manifest.commands) manifest.commands = {};
1055
1068
  manifest.commands.version = repoInfo.skills.version;
1069
+ manifest.commands.names = mergeInstalledNames(manifest.commands.names, updateCmdResult);
1056
1070
 
1057
1071
  // Update command hashes for integrity tracking
1058
1072
  if (updateCmdResult.allFileHashes) {
@@ -1127,6 +1141,7 @@ export async function updateCommand(options) {
1127
1141
  manifest.skills.installed = true;
1128
1142
  manifest.skills.version = repoInfo.skills.version;
1129
1143
  manifest.skills.installations = [...(manifest.skills.installations || []), ...missingSkills];
1144
+ manifest.skills.names = mergeInstalledNames(manifest.skills.names, result);
1130
1145
  if (result.allFileHashes) {
1131
1146
  if (!manifest.skillHashes) manifest.skillHashes = {};
1132
1147
  Object.assign(manifest.skillHashes, result.allFileHashes);
@@ -1144,6 +1159,7 @@ export async function updateCommand(options) {
1144
1159
  const result = await installSkillsToMultipleAgents(outdatedSkills, null, projectPath, skillsLocale);
1145
1160
  if (!manifest.skills) manifest.skills = {};
1146
1161
  manifest.skills.version = repoInfo.skills.version;
1162
+ manifest.skills.names = mergeInstalledNames(manifest.skills.names, result);
1147
1163
  if (!manifest.skills.location && manifest.skills.installations?.length > 0) {
1148
1164
  const levels = manifest.skills.installations.map(s => s.level).filter(Boolean);
1149
1165
  const uniqueLevels = [...new Set(levels)];
@@ -1168,6 +1184,7 @@ export async function updateCommand(options) {
1168
1184
  manifest.commands.installed = true;
1169
1185
  manifest.commands.version = repoInfo.skills.version;
1170
1186
  manifest.commands.installations = [...(manifest.commands.installations || []), ...missingCommands];
1187
+ manifest.commands.names = mergeInstalledNames(manifest.commands.names, result);
1171
1188
  if (result.allFileHashes) {
1172
1189
  if (!manifest.commandHashes) manifest.commandHashes = {};
1173
1190
  replaceCommandHashesForUpdatedAgents(manifest.commandHashes, result.allFileHashes);
@@ -1185,6 +1202,7 @@ export async function updateCommand(options) {
1185
1202
  const result = await installCommandsToMultipleAgents(outdatedCommands, null, projectPath, skillsLocale);
1186
1203
  if (!manifest.commands) manifest.commands = {};
1187
1204
  manifest.commands.version = repoInfo.skills.version;
1205
+ manifest.commands.names = mergeInstalledNames(manifest.commands.names, result);
1188
1206
  if (result.allFileHashes) {
1189
1207
  if (!manifest.commandHashes) manifest.commandHashes = {};
1190
1208
  replaceCommandHashesForUpdatedAgents(manifest.commandHashes, result.allFileHashes);
@@ -1319,17 +1337,6 @@ export function regenerateIntegrations(projectPath, manifest) {
1319
1337
  return { success: true, updated: [], errors: [] };
1320
1338
  }
1321
1339
 
1322
- // Build installed standards list
1323
- const installedStandardsList = manifest.standards?.map(s => basename(s)) || [];
1324
-
1325
- // Determine language setting
1326
- let commonLanguage = 'en';
1327
- if ((manifest.options?.output_language || manifest.options?.commit_language) === 'bilingual') {
1328
- commonLanguage = 'bilingual';
1329
- } else if ((manifest.options?.output_language || manifest.options?.commit_language) === 'traditional-chinese') {
1330
- commonLanguage = 'zh-tw';
1331
- }
1332
-
1333
1340
  const results = {
1334
1341
  updated: [],
1335
1342
  errors: []
@@ -1345,19 +1352,8 @@ export function regenerateIntegrations(projectPath, manifest) {
1345
1352
  continue; // Skip if already generated (AGENTS.md sharing)
1346
1353
  }
1347
1354
 
1348
- // Resolve contentMode per tool based on tier
1349
- const savedMode = manifest.contentMode || 'auto';
1350
- const resolvedMode = resolveContentModeForTool(tool, savedMode);
1351
- const toolConfig = {
1352
- tool,
1353
- categories: ['anti-hallucination', 'commit-standards', 'code-review'],
1354
- language: commonLanguage,
1355
- installedStandards: installedStandardsList,
1356
- contentMode: resolvedMode.contentMode,
1357
- level: resolvedMode.level,
1358
- // Pass output_language for dynamic commit standards generation
1359
- outputLanguage: manifest.options?.output_language || manifest.options?.commit_language || 'english'
1360
- };
1355
+ // Shared with the reconciler so both paths emit the identical block.
1356
+ const toolConfig = buildToolIntegrationConfig(manifest, tool);
1361
1357
 
1362
1358
  const result = writeIntegrationFile(tool, toolConfig, projectPath);
1363
1359
  if (result.success) {
@@ -1392,7 +1388,7 @@ export function regenerateIntegrations(projectPath, manifest) {
1392
1388
  // Regenerate universal AGENTS.md if enabled and not already covered
1393
1389
  if (manifest.generateAgentsMd && !generatedFiles.has('AGENTS.md')) {
1394
1390
  const summaryConfig = {
1395
- installedStandards: installedStandardsList,
1391
+ installedStandards: (manifest.standards || []).map(s => basename(s)),
1396
1392
  language: manifest.options?.display_language || 'en',
1397
1393
  outputLanguage: manifest.options?.output_language || manifest.options?.commit_language || 'english',
1398
1394
  standardOptions: manifest.options || {}
@@ -1422,7 +1418,7 @@ export function regenerateIntegrations(projectPath, manifest) {
1422
1418
  * @param {string} projectPath - Project path
1423
1419
  * @param {Object} manifest - Manifest object
1424
1420
  */
1425
- async function updateIntegrationsOnly(projectPath, manifest) {
1421
+ async function updateIntegrationsOnly(projectPath, manifest, options = {}) {
1426
1422
  const msg = t().commands.update;
1427
1423
 
1428
1424
  console.log(chalk.cyan(msg.updatingIntegrationsOnly));
@@ -1436,6 +1432,59 @@ async function updateIntegrationsOnly(projectPath, manifest) {
1436
1432
  return;
1437
1433
  }
1438
1434
 
1435
+ // --plan:只比對、不寫任何檔(連 manifest 也不寫)。
1436
+ // 這一段刻意不呼叫 regenerateIntegrations——那個函式會寫檔,
1437
+ // 「先寫再還原」的作法在中途掛掉時會留下壞狀態,比沒有 dry-run 更糟。
1438
+ if (options.plan) {
1439
+ console.log(chalk.bold('=== Integration Plan (dry run — nothing is written) ==='));
1440
+ const wouldChange = [];
1441
+ const unchanged = [];
1442
+ const seen = new Set();
1443
+ for (const tool of aiTools) {
1444
+ const targetFile = getToolFilePath(tool);
1445
+ if (seen.has(targetFile)) continue;
1446
+ seen.add(targetFile);
1447
+ const savedMode = manifest.contentMode || 'auto';
1448
+ const resolvedMode = resolveContentModeForTool(tool, savedMode);
1449
+ const next = generateIntegrationContent({
1450
+ tool,
1451
+ categories: ['anti-hallucination', 'commit-standards', 'code-review'],
1452
+ language: (manifest.options?.output_language || manifest.options?.commit_language) === 'bilingual'
1453
+ ? 'bilingual'
1454
+ : (manifest.options?.output_language || manifest.options?.commit_language) === 'traditional-chinese'
1455
+ ? 'zh-tw' : 'en',
1456
+ installedStandards: manifest.standards?.map((x) => basename(x)) || [],
1457
+ contentMode: resolvedMode.contentMode,
1458
+ level: resolvedMode.level,
1459
+ outputLanguage: manifest.options?.output_language || manifest.options?.commit_language || 'english'
1460
+ });
1461
+ const full = join(projectPath, targetFile);
1462
+ const current = existsSync(full) ? readFileSync(full, 'utf8') : '';
1463
+ const format = targetFile.endsWith('.md') ? 'markdown' : 'plaintext';
1464
+ const cur = extractMarkedContent(current, format).content || '';
1465
+ // 比對正規化過的內容:只關心「受管區塊會不會變」,不關心尾端空白。
1466
+ if (cur.trim() === String(next).trim()) {
1467
+ unchanged.push(targetFile);
1468
+ } else {
1469
+ const delta = String(next).trim().length - cur.trim().length;
1470
+ wouldChange.push(`${targetFile} (${delta >= 0 ? '+' : ''}${delta} bytes in the UDS block)`);
1471
+ }
1472
+ }
1473
+ if (wouldChange.length > 0) {
1474
+ console.log(chalk.yellow(` ~ Would update (${wouldChange.length}):`));
1475
+ for (const f of wouldChange) console.log(chalk.gray(` ${f}`));
1476
+ }
1477
+ if (unchanged.length > 0) {
1478
+ console.log(chalk.gray(` = Unchanged (${unchanged.length}): ${unchanged.join(', ')}`));
1479
+ }
1480
+ console.log();
1481
+ console.log(chalk.gray(' Also NOT written in plan mode: .standards/manifest.json'));
1482
+ console.log(chalk.gray(' (a real run rewrites it — version, fileHashes, integrationBlockHashes,'));
1483
+ console.log(chalk.gray(' and it normalises any standards entries stored as full paths)'));
1484
+ console.log();
1485
+ return;
1486
+ }
1487
+
1439
1488
  const spinner = ora(msg.regeneratingIntegrations).start();
1440
1489
 
1441
1490
  // Use reusable regeneration function
@@ -1698,6 +1747,7 @@ async function updateSkillsOnly(projectPath, manifest, options) {
1698
1747
  // Update manifest
1699
1748
  manifest.skills.version = latestVersion;
1700
1749
  manifest.skills.installations = skillsInstallations;
1750
+ manifest.skills.names = mergeInstalledNames(manifest.skills.names, result);
1701
1751
 
1702
1752
  // Update skill hashes for integrity tracking
1703
1753
  if (result.allFileHashes) {
@@ -1804,6 +1854,7 @@ async function updateCommandsOnly(projectPath, manifest, options) {
1804
1854
  }
1805
1855
  return inst;
1806
1856
  });
1857
+ manifest.commands.names = mergeInstalledNames(manifest.commands.names, result);
1807
1858
 
1808
1859
  // Update command hashes for integrity tracking
1809
1860
  if (result.allFileHashes) {