universal-dev-standards 6.1.1 → 6.2.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.
Files changed (78) hide show
  1. package/bundled/locales/zh-CN/CHANGELOG.md +40 -3
  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 +41 -3
  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 +54 -7
  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 +87 -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 +6 -0
  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.1",
3
+ "version": "6.2.1",
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,6 +39,7 @@ 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
@@ -161,7 +162,9 @@ function performFileIntegrityCheck(projectPath, manifest, msg) {
161
162
  }
162
163
 
163
164
  // Check integrations
164
- for (const int of manifest.integrations) {
165
+ for (const intEntry of manifest.integrations) {
166
+ // 兩種形狀都要能解出路徑(XSPEC-343 R1)
167
+ const int = resolveIntegrationFile(intEntry) || intEntry;
165
168
  const filePath = join(projectPath, int);
166
169
  if (existsSync(filePath)) {
167
170
  fileStatus.noHash.push(int);
@@ -727,7 +730,9 @@ function removeFromManifest(manifest, relativePath) {
727
730
  const fileName = basename(relativePath);
728
731
  manifest.standards = manifest.standards.filter(s => !s.endsWith(fileName));
729
732
  manifest.extensions = manifest.extensions.filter(e => typeof e !== 'string' || !e.endsWith(fileName));
730
- manifest.integrations = manifest.integrations.filter(i => i !== relativePath);
733
+ manifest.integrations = manifest.integrations.filter(
734
+ i => (resolveIntegrationFile(i) || i) !== relativePath
735
+ );
731
736
 
732
737
  // Remove from integrationBlockHashes
733
738
  if (manifest.integrationBlockHashes && manifest.integrationBlockHashes[relativePath]) {
@@ -761,7 +766,7 @@ export function getSourcePathFromRelative(manifest, relativePath) {
761
766
  }
762
767
 
763
768
  // Check integrations - these might need special handling
764
- if (manifest.integrations.includes(relativePath)) {
769
+ if (manifest.integrations.some(i => (resolveIntegrationFile(i) || i) === relativePath)) {
765
770
  // Integration files have different source paths
766
771
  const integrationMappings = {
767
772
  '.cursorrules': 'integrations/cursor/.cursorrules',
@@ -831,7 +836,8 @@ async function migrateToHashBasedTracking(projectPath, manifest) {
831
836
  }
832
837
 
833
838
  // Process integrations
834
- for (const int of manifest.integrations) {
839
+ for (const intEntry of manifest.integrations) {
840
+ const int = resolveIntegrationFile(intEntry) || intEntry;
835
841
  const fullPath = join(projectPath, int);
836
842
 
837
843
  const hashInfo = computeFileHash(fullPath);
@@ -1148,6 +1154,27 @@ function checkIntegrationFiles(manifest, projectPath, msg) {
1148
1154
  // Report status - use all installed standards as the total
1149
1155
  const totalTrackable = standardsFiles.length;
1150
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
+
1151
1178
  if (hasStandardsIndex && missingStandards.length === 0) {
1152
1179
  console.log(chalk.green(` ✓ ${toolFile}:`));
1153
1180
  console.log(chalk.gray(` ${msg.standardsIndexPresent}`));
@@ -1219,6 +1246,25 @@ function checkAgentsMdSync(manifest, projectPath, msg) {
1219
1246
  const content = readFileSync(agentsMdPath, 'utf-8');
1220
1247
  const installedStandards = (manifest.standards || []).map(s => basename(s));
1221
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
+
1222
1268
  // Check standards listed in AGENTS.md vs manifest
1223
1269
  const aiYamlStandards = installedStandards.filter(s => s.endsWith('.ai.yaml'));
1224
1270
  let listedCount = 0;
@@ -1264,7 +1310,8 @@ function checkReferenceSync(manifest, projectPath, msg) {
1264
1310
 
1265
1311
  // Pre-scan: collect results for files that have references
1266
1312
  const results = [];
1267
- for (const integrationPath of manifest.integrations) {
1313
+ for (const integrationEntry of manifest.integrations) {
1314
+ const integrationPath = resolveIntegrationFile(integrationEntry) || integrationEntry;
1268
1315
  const fullPath = join(projectPath, integrationPath);
1269
1316
 
1270
1317
  if (!existsSync(fullPath)) continue;
@@ -1700,7 +1747,7 @@ function getFileStatusCounts(manifest, projectPath) {
1700
1747
  const allFiles = [
1701
1748
  ...manifest.standards.map(s => `.standards/${basename(s)}`),
1702
1749
  ...manifest.extensions.filter(e => typeof e === 'string').map(e => `.standards/${basename(e)}`),
1703
- ...manifest.integrations
1750
+ ...manifest.integrations.map(i => resolveIntegrationFile(i) || i)
1704
1751
  ];
1705
1752
  for (const relativePath of allFiles) {
1706
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
  }
@@ -947,10 +957,12 @@ export async function updateCommand(options) {
947
957
  if (!manifest.skills) manifest.skills = {};
948
958
  manifest.skills.installed = true;
949
959
  manifest.skills.version = repoInfo.skills.version;
960
+ manifest.skills.locale = skillsLocale;
950
961
  manifest.skills.installations = [
951
962
  ...(manifest.skills.installations || []),
952
963
  ...installSkills
953
964
  ];
965
+ manifest.skills.names = mergeInstalledNames(manifest.skills.names, skillResult);
954
966
 
955
967
  // Derive location from installations if not set
956
968
  if (!manifest.skills.location) {
@@ -987,6 +999,8 @@ export async function updateCommand(options) {
987
999
  // Update manifest version
988
1000
  if (!manifest.skills) manifest.skills = {};
989
1001
  manifest.skills.version = repoInfo.skills.version;
1002
+ manifest.skills.locale = updateLocale;
1003
+ manifest.skills.names = mergeInstalledNames(manifest.skills.names, updateResult);
990
1004
 
991
1005
  // Derive location from installations if not set
992
1006
  if (!manifest.skills.location && manifest.skills.installations?.length > 0) {
@@ -1028,6 +1042,7 @@ export async function updateCommand(options) {
1028
1042
  ...(manifest.commands.installations || []),
1029
1043
  ...installCommands
1030
1044
  ];
1045
+ manifest.commands.names = mergeInstalledNames(manifest.commands.names, cmdResult);
1031
1046
 
1032
1047
  // Update command hashes for integrity tracking
1033
1048
  if (cmdResult.allFileHashes) {
@@ -1053,6 +1068,7 @@ export async function updateCommand(options) {
1053
1068
  // Update manifest version
1054
1069
  if (!manifest.commands) manifest.commands = {};
1055
1070
  manifest.commands.version = repoInfo.skills.version;
1071
+ manifest.commands.names = mergeInstalledNames(manifest.commands.names, updateCmdResult);
1056
1072
 
1057
1073
  // Update command hashes for integrity tracking
1058
1074
  if (updateCmdResult.allFileHashes) {
@@ -1126,7 +1142,9 @@ export async function updateCommand(options) {
1126
1142
  if (!manifest.skills) manifest.skills = {};
1127
1143
  manifest.skills.installed = true;
1128
1144
  manifest.skills.version = repoInfo.skills.version;
1145
+ manifest.skills.locale = skillsLocale;
1129
1146
  manifest.skills.installations = [...(manifest.skills.installations || []), ...missingSkills];
1147
+ manifest.skills.names = mergeInstalledNames(manifest.skills.names, result);
1130
1148
  if (result.allFileHashes) {
1131
1149
  if (!manifest.skillHashes) manifest.skillHashes = {};
1132
1150
  Object.assign(manifest.skillHashes, result.allFileHashes);
@@ -1144,6 +1162,8 @@ export async function updateCommand(options) {
1144
1162
  const result = await installSkillsToMultipleAgents(outdatedSkills, null, projectPath, skillsLocale);
1145
1163
  if (!manifest.skills) manifest.skills = {};
1146
1164
  manifest.skills.version = repoInfo.skills.version;
1165
+ manifest.skills.locale = skillsLocale;
1166
+ manifest.skills.names = mergeInstalledNames(manifest.skills.names, result);
1147
1167
  if (!manifest.skills.location && manifest.skills.installations?.length > 0) {
1148
1168
  const levels = manifest.skills.installations.map(s => s.level).filter(Boolean);
1149
1169
  const uniqueLevels = [...new Set(levels)];
@@ -1168,6 +1188,7 @@ export async function updateCommand(options) {
1168
1188
  manifest.commands.installed = true;
1169
1189
  manifest.commands.version = repoInfo.skills.version;
1170
1190
  manifest.commands.installations = [...(manifest.commands.installations || []), ...missingCommands];
1191
+ manifest.commands.names = mergeInstalledNames(manifest.commands.names, result);
1171
1192
  if (result.allFileHashes) {
1172
1193
  if (!manifest.commandHashes) manifest.commandHashes = {};
1173
1194
  replaceCommandHashesForUpdatedAgents(manifest.commandHashes, result.allFileHashes);
@@ -1185,6 +1206,7 @@ export async function updateCommand(options) {
1185
1206
  const result = await installCommandsToMultipleAgents(outdatedCommands, null, projectPath, skillsLocale);
1186
1207
  if (!manifest.commands) manifest.commands = {};
1187
1208
  manifest.commands.version = repoInfo.skills.version;
1209
+ manifest.commands.names = mergeInstalledNames(manifest.commands.names, result);
1188
1210
  if (result.allFileHashes) {
1189
1211
  if (!manifest.commandHashes) manifest.commandHashes = {};
1190
1212
  replaceCommandHashesForUpdatedAgents(manifest.commandHashes, result.allFileHashes);
@@ -1319,17 +1341,6 @@ export function regenerateIntegrations(projectPath, manifest) {
1319
1341
  return { success: true, updated: [], errors: [] };
1320
1342
  }
1321
1343
 
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
1344
  const results = {
1334
1345
  updated: [],
1335
1346
  errors: []
@@ -1345,19 +1356,8 @@ export function regenerateIntegrations(projectPath, manifest) {
1345
1356
  continue; // Skip if already generated (AGENTS.md sharing)
1346
1357
  }
1347
1358
 
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
- };
1359
+ // Shared with the reconciler so both paths emit the identical block.
1360
+ const toolConfig = buildToolIntegrationConfig(manifest, tool);
1361
1361
 
1362
1362
  const result = writeIntegrationFile(tool, toolConfig, projectPath);
1363
1363
  if (result.success) {
@@ -1392,7 +1392,7 @@ export function regenerateIntegrations(projectPath, manifest) {
1392
1392
  // Regenerate universal AGENTS.md if enabled and not already covered
1393
1393
  if (manifest.generateAgentsMd && !generatedFiles.has('AGENTS.md')) {
1394
1394
  const summaryConfig = {
1395
- installedStandards: installedStandardsList,
1395
+ installedStandards: (manifest.standards || []).map(s => basename(s)),
1396
1396
  language: manifest.options?.display_language || 'en',
1397
1397
  outputLanguage: manifest.options?.output_language || manifest.options?.commit_language || 'english',
1398
1398
  standardOptions: manifest.options || {}
@@ -1422,7 +1422,7 @@ export function regenerateIntegrations(projectPath, manifest) {
1422
1422
  * @param {string} projectPath - Project path
1423
1423
  * @param {Object} manifest - Manifest object
1424
1424
  */
1425
- async function updateIntegrationsOnly(projectPath, manifest) {
1425
+ async function updateIntegrationsOnly(projectPath, manifest, options = {}) {
1426
1426
  const msg = t().commands.update;
1427
1427
 
1428
1428
  console.log(chalk.cyan(msg.updatingIntegrationsOnly));
@@ -1436,6 +1436,59 @@ async function updateIntegrationsOnly(projectPath, manifest) {
1436
1436
  return;
1437
1437
  }
1438
1438
 
1439
+ // --plan:只比對、不寫任何檔(連 manifest 也不寫)。
1440
+ // 這一段刻意不呼叫 regenerateIntegrations——那個函式會寫檔,
1441
+ // 「先寫再還原」的作法在中途掛掉時會留下壞狀態,比沒有 dry-run 更糟。
1442
+ if (options.plan) {
1443
+ console.log(chalk.bold('=== Integration Plan (dry run — nothing is written) ==='));
1444
+ const wouldChange = [];
1445
+ const unchanged = [];
1446
+ const seen = new Set();
1447
+ for (const tool of aiTools) {
1448
+ const targetFile = getToolFilePath(tool);
1449
+ if (seen.has(targetFile)) continue;
1450
+ seen.add(targetFile);
1451
+ const savedMode = manifest.contentMode || 'auto';
1452
+ const resolvedMode = resolveContentModeForTool(tool, savedMode);
1453
+ const next = generateIntegrationContent({
1454
+ tool,
1455
+ categories: ['anti-hallucination', 'commit-standards', 'code-review'],
1456
+ language: (manifest.options?.output_language || manifest.options?.commit_language) === 'bilingual'
1457
+ ? 'bilingual'
1458
+ : (manifest.options?.output_language || manifest.options?.commit_language) === 'traditional-chinese'
1459
+ ? 'zh-tw' : 'en',
1460
+ installedStandards: manifest.standards?.map((x) => basename(x)) || [],
1461
+ contentMode: resolvedMode.contentMode,
1462
+ level: resolvedMode.level,
1463
+ outputLanguage: manifest.options?.output_language || manifest.options?.commit_language || 'english'
1464
+ });
1465
+ const full = join(projectPath, targetFile);
1466
+ const current = existsSync(full) ? readFileSync(full, 'utf8') : '';
1467
+ const format = targetFile.endsWith('.md') ? 'markdown' : 'plaintext';
1468
+ const cur = extractMarkedContent(current, format).content || '';
1469
+ // 比對正規化過的內容:只關心「受管區塊會不會變」,不關心尾端空白。
1470
+ if (cur.trim() === String(next).trim()) {
1471
+ unchanged.push(targetFile);
1472
+ } else {
1473
+ const delta = String(next).trim().length - cur.trim().length;
1474
+ wouldChange.push(`${targetFile} (${delta >= 0 ? '+' : ''}${delta} bytes in the UDS block)`);
1475
+ }
1476
+ }
1477
+ if (wouldChange.length > 0) {
1478
+ console.log(chalk.yellow(` ~ Would update (${wouldChange.length}):`));
1479
+ for (const f of wouldChange) console.log(chalk.gray(` ${f}`));
1480
+ }
1481
+ if (unchanged.length > 0) {
1482
+ console.log(chalk.gray(` = Unchanged (${unchanged.length}): ${unchanged.join(', ')}`));
1483
+ }
1484
+ console.log();
1485
+ console.log(chalk.gray(' Also NOT written in plan mode: .standards/manifest.json'));
1486
+ console.log(chalk.gray(' (a real run rewrites it — version, fileHashes, integrationBlockHashes,'));
1487
+ console.log(chalk.gray(' and it normalises any standards entries stored as full paths)'));
1488
+ console.log();
1489
+ return;
1490
+ }
1491
+
1439
1492
  const spinner = ora(msg.regeneratingIntegrations).start();
1440
1493
 
1441
1494
  // Use reusable regeneration function
@@ -1697,7 +1750,9 @@ async function updateSkillsOnly(projectPath, manifest, options) {
1697
1750
 
1698
1751
  // Update manifest
1699
1752
  manifest.skills.version = latestVersion;
1753
+ manifest.skills.locale = skillsLocaleForUpdate;
1700
1754
  manifest.skills.installations = skillsInstallations;
1755
+ manifest.skills.names = mergeInstalledNames(manifest.skills.names, result);
1701
1756
 
1702
1757
  // Update skill hashes for integrity tracking
1703
1758
  if (result.allFileHashes) {
@@ -1804,6 +1859,7 @@ async function updateCommandsOnly(projectPath, manifest, options) {
1804
1859
  }
1805
1860
  return inst;
1806
1861
  });
1862
+ manifest.commands.names = mergeInstalledNames(manifest.commands.names, result);
1807
1863
 
1808
1864
  // Update command hashes for integrity tracking
1809
1865
  if (result.allFileHashes) {