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
@@ -3,6 +3,7 @@ import { dirname, join, basename } from 'path';
3
3
  import { getLanguageRules } from '../prompts/integrations.js';
4
4
  import { computeIntegrationBlockHash } from './hasher.js';
5
5
  import { UDS_MARKERS, SUPPORTED_AI_TOOLS, LEGACY_TOOL_MAPPINGS } from '../core/constants.js';
6
+ import { resolveSelectedOptionSources } from './registry.js';
6
7
  import { getAgentConfig, getAgentTier } from '../config/ai-agent-paths.js';
7
8
 
8
9
  /**
@@ -107,24 +108,13 @@ const STANDARD_TASK_MAPPING = {
107
108
  /**
108
109
  * Standard descriptions for index generation
109
110
  */
110
- const STANDARD_DESCRIPTIONS = {
111
- 'anti-hallucination.md': 'AI 協作防幻覺規範',
112
- 'commit-message.ai.yaml': '提交訊息格式',
113
- 'checkin-standards.md': '程式碼簽入檢查',
114
- 'logging-standards.md': '日誌記錄標準',
115
- 'error-code-standards.md': '錯誤碼標準',
116
- 'testing.ai.yaml': '測試標準',
117
- 'versioning.md': '語意化版本',
118
- 'changelog-standards.md': '變更日誌標準',
119
- 'code-review-checklist.md': '程式碼審查清單',
120
- 'spec-driven-development.md': '規格驅動開發',
121
- 'test-completeness-dimensions.md': '測試完整性維度',
122
- 'git-workflow.ai.yaml': 'Git 工作流程',
123
- 'developer-memory.ai.yaml': '開發者持久記憶',
124
- 'project-context-memory.ai.yaml': '專案情境記憶',
125
- 'zh-tw.md': '繁體中文本地化',
126
- 'workflow-enforcement.ai.yaml': '工作流程強制執行'
127
- };
111
+ // STANDARD_DESCRIPTIONS 已移除(XSPEC-358 R1 裁決為方案 A:索引區塊不再列名稱清單)。
112
+ // 移除前的實況值得記錄,因為它是一個**綠燈測試釘住生產環境不成立假設**的實例:
113
+ // 該表有 16 筆真實中文描述,鍵**帶副檔名**('anti-hallucination.md')。
114
+ // init 路徑傳入 copyStandard 的 sourcePath → basename **含副檔名** → 查表命中。
115
+ // update 路徑傳入 manifest.standards → **不含副檔名**(71/78 筆)→ 查表全 miss。
116
+ // 於是同一個區塊在 `uds init` 後有描述、在 `uds update` 後全是 `名稱 - 名稱`。
117
+ // 單元測試餵的是完整檔名,所以它一直是綠的;dev-platform 實測 78/78 皆 fallback。
128
118
 
129
119
  /**
130
120
  * Commit type templates for different output_language options
@@ -2908,6 +2898,11 @@ export function generateComplianceInstructions(installedStandards, mode, format,
2908
2898
  const sections = [];
2909
2899
 
2910
2900
  if (format === 'markdown') {
2901
+ // 三個清單全空時不得留下一個空標題(XSPEC-358 §1)——
2902
+ // 一個沒有內容的章節,讀起來像「這裡本來該有東西而它不見了」。
2903
+ if (mustFollow.length === 0 && shouldFollow.length === 0) {
2904
+ return '';
2905
+ }
2911
2906
  sections.push('## Standards Compliance Instructions');
2912
2907
  sections.push('');
2913
2908
 
@@ -2936,6 +2931,9 @@ export function generateComplianceInstructions(installedStandards, mode, format,
2936
2931
  }
2937
2932
  } else {
2938
2933
  // Plaintext format
2934
+ if (mustFollow.length === 0 && shouldFollow.length === 0) {
2935
+ return '';
2936
+ }
2939
2937
  sections.push('## Standards Compliance Instructions');
2940
2938
  sections.push('');
2941
2939
 
@@ -2975,71 +2973,108 @@ export function generateStandardsIndex(installedStandards, format, language = 'z
2975
2973
  return '';
2976
2974
  }
2977
2975
 
2978
- const coreStandards = [];
2979
- const optionStandards = [];
2980
-
2981
- // Separate core standards from options
2982
- for (const standardPath of installedStandards) {
2983
- const filename = basename(standardPath);
2984
- const isOption = standardPath.includes('/options/') || standardPath.includes('\\options\\');
2985
- const description = STANDARD_DESCRIPTIONS[filename] || filename;
2986
-
2987
- if (isOption) {
2988
- optionStandards.push({ filename, description, path: standardPath });
2989
- } else {
2990
- coreStandards.push({ filename, description, path: standardPath });
2991
- }
2992
- }
2993
-
2994
- const sections = [];
2976
+ const total = installedStandards.length;
2977
+ const optionCount = installedStandards.filter(
2978
+ (p) => p.includes('/options/') || p.includes('\\options\\')
2979
+ ).length;
2980
+ const coreCount = total - optionCount;
2981
+
2982
+ const heading = '## Installed Standards Index';
2983
+ const md = format === 'markdown';
2984
+ const body = language === 'en'
2985
+ ? [
2986
+ `This project has adopted **${total}** UDS standards (${coreCount} core, ${optionCount} options), installed in \`.standards/\`.`,
2987
+ '',
2988
+ 'The authoritative list is the `standards` field of `.standards/manifest.json`. Read a standard\'s own file to see what it requires — and check its first line, which is where a deprecated standard says so.'
2989
+ ]
2990
+ : [
2991
+ `本專案採用 UDS 標準共 **${total}** 條(core ${coreCount}、options ${optionCount}),安裝於 \`.standards/\`。`,
2992
+ '',
2993
+ '權威清單為 `.standards/manifest.json` 的 `standards` 欄位。要知道某標準要求什麼,讀它自己的檔案——並看第一行,已棄用的標準會在那裡寫明。'
2994
+ ];
2995
2995
 
2996
- if (format === 'markdown') {
2997
- sections.push('## Installed Standards Index');
2998
- sections.push('');
2999
- sections.push(language === 'en'
3000
- ? 'This project has adopted UDS standards. All standards are in `.standards/`:'
3001
- : '本專案採用 UDS 標準。所有規範位於 `.standards/`:');
3002
- sections.push('');
2996
+ const plainBody = body.map((l) => l.replace(/`/g, ''));
2997
+ return [heading, '', ...(md ? body : plainBody), ''].join('\n');
2998
+ }
3003
2999
 
3004
- if (coreStandards.length > 0) {
3005
- sections.push(`### Core (${coreStandards.length} standards)`);
3006
- for (const std of coreStandards) {
3007
- sections.push(`- \`${std.filename}\` - ${std.description}`);
3008
- }
3009
- sections.push('');
3010
- }
3000
+ /**
3001
+ * Build the generation config for one AI tool from the manifest.
3002
+ *
3003
+ * Both the normal `uds update` path and the reconciler regenerate integration
3004
+ * blocks, and they used to derive this config independently. The reconciler's
3005
+ * copy omitted `categories` entirely, defaulted `outputLanguage` to English, and
3006
+ * looked up `manifest.integrationConfigs` by **tool key** while the manifest
3007
+ * stores it by **file name** — so the lookup always missed. The result was a
3008
+ * strictly poorer block: reconciling machine-setup silently dropped its
3009
+ * "提交訊息語言" and "Standards Compliance Instructions" sections, which the
3010
+ * normal update path had always written. Two generators, one contract, one of
3011
+ * them quietly lossy. (XSPEC-343 R2)
3012
+ *
3013
+ * `integrationConfigs` is deliberately NOT merged in: its `installedStandards`
3014
+ * is a snapshot frozen at install time, and honouring it would reintroduce
3015
+ * exactly the staleness this whole XSPEC is about.
3016
+ *
3017
+ * @param {Object} manifest - Project manifest
3018
+ * @param {string} tool - Tool key (e.g. 'claude-code')
3019
+ * @returns {Object} Config for generateIntegrationContent / writeIntegrationFile
3020
+ */
3021
+ function withSelectedOptions(manifest) {
3022
+ const standards = manifest.standards || [];
3023
+ const seen = new Set(standards.map((p) => basename(p)));
3024
+ const extra = resolveSelectedOptionSources(manifest.options, manifest.format || 'ai')
3025
+ .filter((p) => !seen.has(basename(p)));
3026
+ return [...standards, ...extra];
3027
+ }
3011
3028
 
3012
- if (optionStandards.length > 0) {
3013
- sections.push('### Options');
3014
- for (const std of optionStandards) {
3015
- sections.push(`- \`options/${std.filename}\` - ${std.description}`);
3016
- }
3017
- sections.push('');
3018
- }
3019
- } else {
3020
- // Plaintext format
3021
- sections.push('## Installed Standards Index');
3022
- sections.push('');
3023
- sections.push(language === 'en'
3024
- ? 'UDS standards installed in .standards/:'
3025
- : 'UDS 標準已安裝於 .standards/:');
3026
- sections.push('');
3029
+ export function buildToolIntegrationConfig(manifest, tool) {
3030
+ const selected = manifest.options?.output_language || manifest.options?.commit_language || 'english';
3031
+ const language = selected === 'bilingual'
3032
+ ? 'bilingual'
3033
+ : selected === 'traditional-chinese' ? 'zh-tw' : 'en';
3034
+ const resolved = resolveContentModeForTool(tool, manifest.contentMode || 'auto');
3027
3035
 
3028
- for (const std of coreStandards) {
3029
- sections.push(`- ${std.filename} - ${std.description}`);
3030
- }
3036
+ return {
3037
+ tool,
3038
+ categories: ['anti-hallucination', 'commit-standards', 'code-review'],
3039
+ language,
3040
+ // Pass manifest entries UNCHANGED. Every consumer here documents this
3041
+ // parameter as "standard file paths" and basenames it for display, while
3042
+ // classifying options by `path.includes('/options/')`. Reducing to basenames
3043
+ // first destroys that signal: options silently count as core, so the index
3044
+ // block reported "(70 core, 0 options)" and the minimal block listed option
3045
+ // files under the wrong heading with a path that does not exist on disk.
3046
+ installedStandards: withSelectedOptions(manifest),
3047
+ contentMode: resolved.contentMode,
3048
+ level: resolved.level,
3049
+ outputLanguage: selected,
3050
+ methodology: manifest.methodology
3051
+ };
3052
+ }
3031
3053
 
3032
- if (optionStandards.length > 0) {
3033
- sections.push('');
3034
- sections.push('Options:');
3035
- for (const std of optionStandards) {
3036
- sections.push(`- options/${std.filename} - ${std.description}`);
3037
- }
3038
- }
3039
- sections.push('');
3040
- }
3054
+ /**
3055
+ * Heading of the index-mode standards block. Not localised — both the English and
3056
+ * the Chinese body sit under this exact string, so it is a stable marker.
3057
+ */
3058
+ export const STANDARDS_INDEX_HEADING = '## Installed Standards Index';
3041
3059
 
3042
- return sections.join('\n');
3060
+ /**
3061
+ * Read the standards count declared by an index-mode block.
3062
+ *
3063
+ * XSPEC-358 R1 replaced the enumerated list with a count plus a pointer to the
3064
+ * manifest. Two checks in `uds check` still asserted the old contract by grepping
3065
+ * the integration file for every standard name, so after the change they reported
3066
+ * `5/70` and `0/7` and told the user to run `uds update` — which regenerates the
3067
+ * same non-enumerating block. An unsatisfiable loop. Checks must assert what the
3068
+ * block claims now, not what it used to list. (XSPEC-343 R2 / XSPEC-358 R1)
3069
+ *
3070
+ * @param {string} content - Full integration file content
3071
+ * @returns {number|null} Declared count, or null if this is not an index block
3072
+ */
3073
+ export function parseStandardsIndexCount(content) {
3074
+ const at = content.indexOf(STANDARDS_INDEX_HEADING);
3075
+ if (at === -1) return null;
3076
+ const match = content.slice(at).match(/\*\*(\d+)\*\*/);
3077
+ return match ? Number(match[1]) : null;
3043
3078
  }
3044
3079
 
3045
3080
  /**
@@ -3053,7 +3088,15 @@ export function wrapWithMarkers(content, format) {
3053
3088
  const warning = format === 'plaintext'
3054
3089
  ? '# WARNING: This block is managed by UDS (universal-dev-standards). DO NOT manually edit. Use \'npx uds install\' or \'npx uds update\' to modify.'
3055
3090
  : '<!-- WARNING: This block is managed by UDS (universal-dev-standards). DO NOT manually edit. Use \'npx uds install\' or \'npx uds update\' to modify. -->';
3056
- return `${markers.start}\n${warning}\n${content}\n${markers.end}`;
3091
+ // 冪等:warning 位於 markers **內部**,而 extractMarkedContent 取出的內容也含它,
3092
+ // 於是重新包裝會疊出第二份(dev-platform CLAUDE.md 實測 178/179 兩行完全相同)。
3093
+ // 這裡先剝掉內容開頭既有的 warning,不論上游哪條路徑造成都能修掉。
3094
+ // ⚠️ 未隔離出上游那條路徑——這是防禦性修法,如實記錄其範圍。
3095
+ const stripped = content.replace(
3096
+ /^(?:<!--\s*WARNING: This block is managed by UDS[\s\S]*?-->|#\s*WARNING: This block is managed by UDS[^\n]*)\n?/,
3097
+ ''
3098
+ );
3099
+ return `${markers.start}\n${warning}\n${stripped}\n${markers.end}`;
3057
3100
  }
3058
3101
 
3059
3102
  /**
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from 'fs';
2
2
  import { fileURLToPath } from 'url';
3
3
  import { dirname, join } from 'path';
4
+ import { MANIFEST_OPTION_BINDINGS } from '../core/constants.js';
4
5
 
5
6
  const __filename = fileURLToPath(import.meta.url);
6
7
  const __dirname = dirname(__filename);
@@ -205,3 +206,41 @@ export function isMultiSelectOption(standard, categoryKey) {
205
206
  }
206
207
  return standard.options[categoryKey].multiSelect === true;
207
208
  }
209
+
210
+ /**
211
+ * Resolve the option source paths a manifest's `options` selection installs.
212
+ *
213
+ * `manifest.options` is the authoritative record of what the project chose;
214
+ * `manifest.standards` records option files inconsistently — most repos list
215
+ * them, machine-setup lists none — so anything counting options from
216
+ * `manifest.standards` alone reported "0 options" for a project that has seven
217
+ * of them on disk. (XSPEC-343 R2)
218
+ *
219
+ * @param {Object} manifestOptions - `manifest.options`
220
+ * @param {string} [format='ai'] - Source format
221
+ * @returns {string[]} Registry-relative source paths, e.g. ai/options/testing/unit-testing.ai.yaml
222
+ */
223
+ export function resolveSelectedOptionSources(manifestOptions, format = 'ai') {
224
+ if (!manifestOptions) return [];
225
+ const all = getAllStandards();
226
+ const paths = [];
227
+
228
+ for (const binding of MANIFEST_OPTION_BINDINGS) {
229
+ const key = binding.manifestKeys.find((k) => manifestOptions[k] != null);
230
+ if (!key) continue;
231
+
232
+ const standard = all.find((s) => s.id === binding.standardId);
233
+ if (!standard) continue;
234
+
235
+ const selection = manifestOptions[key];
236
+ for (const optionId of Array.isArray(selection) ? selection : [selection]) {
237
+ if (typeof optionId !== 'string') continue;
238
+ const option = findOption(standard, binding.categoryKey, optionId);
239
+ if (!option) continue;
240
+ const source = getOptionSource(option, format);
241
+ if (source) paths.push(source);
242
+ }
243
+ }
244
+
245
+ return paths;
246
+ }
@@ -15,10 +15,12 @@ import {
15
15
  getSkillsDirForAgent,
16
16
  getCommandsDirForAgent,
17
17
  getSkillsSupportedAgents,
18
- getCommandsSupportedAgents
18
+ getCommandsSupportedAgents,
19
+ getCommandFileExtension
19
20
  } from '../config/ai-agent-paths.js';
20
21
  import { computeDirectoryHashes, computeFileHash } from './hasher.js';
21
22
  import { isLocalizedLocale } from './locale.js';
23
+ import { getSkillsSourceDir } from './skills-source.js';
22
24
 
23
25
  // Get the CLI package root directory
24
26
  const __filename = fileURLToPath(import.meta.url);
@@ -26,19 +28,8 @@ const __dirname = dirname(__filename);
26
28
  const CLI_ROOT = join(__dirname, '..', '..');
27
29
  const BUNDLED_DIR = join(CLI_ROOT, 'bundled');
28
30
 
29
- /**
30
- * Get the Skills source directory.
31
- * Prioritizes bundled directory (npm install), falls back to development path.
32
- * @returns {string} Path to skills source directory
33
- */
34
- function getSkillsSourceDir() {
35
- const bundledPath = join(BUNDLED_DIR, 'skills');
36
- if (existsSync(bundledPath)) {
37
- return bundledPath;
38
- }
39
- // Development environment fallback
40
- return join(CLI_ROOT, '..', 'skills');
41
- }
31
+ // getSkillsSourceDir now lives in skills-source.js so github.js resolves the same path.
32
+ // Its previous private copy here was the half that worked; the copy in github.js was not.
42
33
 
43
34
  /**
44
35
  * Get the localized Skills source directory for a given locale.
@@ -108,23 +99,56 @@ export function getAvailableSkillNames() {
108
99
  return [];
109
100
  }
110
101
 
111
- const NON_SKILL_ITEMS = [
112
- 'README.md', 'CONTRIBUTING.template.md',
113
- 'commands', '.manifest.json', '.DS_Store'
114
- ];
115
-
116
102
  try {
117
103
  return readdirSync(SKILLS_LOCAL_DIR)
118
104
  .filter(item => {
119
- if (NON_SKILL_ITEMS.includes(item)) return false;
120
105
  const itemPath = join(SKILLS_LOCAL_DIR, item);
121
- return statSync(itemPath).isDirectory();
106
+ if (!statSync(itemPath).isDirectory()) return false;
107
+ // A skill is a directory containing SKILL.md. Defining it positively rather than
108
+ // by a deny-list matters: `skills/` also holds agents/, workflows/, ai/, tools/
109
+ // and _shared/, which belong to other installers. The old deny-list named only
110
+ // some of them, so the rest were treated as skills whose SKILL.md "failed to
111
+ // install" — 5 phantom failures that failed the whole install transaction.
112
+ // A new sibling directory now costs nothing; under a deny-list it would break
113
+ // installation until someone remembered to add it.
114
+ return existsSync(join(itemPath, 'SKILL.md'));
122
115
  });
123
116
  } catch {
124
117
  return [];
125
118
  }
126
119
  }
127
120
 
121
+ /**
122
+ * Every directory name in the skills source tree, including the ones that are not
123
+ * skills (`_shared`, `agents`, `ai`, `commands`, `tools`, `workflows`).
124
+ *
125
+ * Used as a provenance test: if a directory in an adopter's skills folder has a
126
+ * counterpart here, UDS put it there. If it does not, UDS did not — it is either
127
+ * an adopter's own skill or an artefact of a UDS version too old to reason about,
128
+ * and deleting it is not this tool's call to make.
129
+ *
130
+ * The distinction matters because an older CLI copied the non-skill siblings in
131
+ * by mistake, so a strict "is it a skill we ship" test would leave those behind
132
+ * while a naive "is it in the skills folder" test deletes hand-written skills.
133
+ * dev-platform has fourteen of those. (XSPEC-343 R2)
134
+ *
135
+ * @returns {Set<string>} Directory names present in the skills source tree
136
+ */
137
+ export function getSkillsSourceEntryNames() {
138
+ if (!existsSync(SKILLS_LOCAL_DIR)) {
139
+ return new Set();
140
+ }
141
+ try {
142
+ return new Set(
143
+ readdirSync(SKILLS_LOCAL_DIR, { withFileTypes: true })
144
+ .filter((e) => e.isDirectory())
145
+ .map((e) => e.name)
146
+ );
147
+ } catch {
148
+ return new Set();
149
+ }
150
+ }
151
+
128
152
  /**
129
153
  * Get list of available command names from local directory
130
154
  * @returns {string[]} Array of command names (without .md extension)
@@ -550,19 +574,10 @@ export async function installCommandsForAgent(agent, level = 'project', commandN
550
574
  return results;
551
575
  }
552
576
 
553
- /**
554
- * Get the appropriate file extension for commands based on agent
555
- * @param {string} agent - Agent identifier
556
- * @returns {string} File extension (including the dot)
557
- */
558
- function getCommandFileExtension(agent) {
559
- // Gemini CLI uses TOML format
560
- if (agent === 'gemini-cli') {
561
- return '.toml';
562
- }
563
- // Most agents use markdown
564
- return '.md';
565
- }
577
+ // getCommandFileExtension moved to config/ai-agent-paths.js — the reconciler's
578
+ // scanner needs the same mapping, and a second private copy is how the writer
579
+ // and the reader drift apart. It now reads `commandFormat` from the agent config
580
+ // instead of hard-coding one agent id. (XSPEC-343 R2)
566
581
 
567
582
  /**
568
583
  * Install a single command to target directory
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Skills source directory resolution — single source of truth.
3
+ *
4
+ * Two modules need to answer "where do the skill files live on this machine?", and they
5
+ * used to answer it separately. `github.js` had its own hardcoded copy pointing at
6
+ * `<cli>/../skills/claude-code`, a layout that no longer exists:
7
+ *
8
+ * - in a checkout, skills live at `<repo>/skills/<name>/`, with no `claude-code/` level
9
+ * - in an npm install, they are bundled at `<package>/bundled/skills/<name>/`
10
+ *
11
+ * So `hasLocalSkills()` was permanently false, every install silently fell through to the
12
+ * remote-download path, and that path then failed on files most skills do not have
13
+ * (only 5 of 61 ship a README.md), which failed the install transaction.
14
+ *
15
+ * Keeping the resolution in one module is the point: the bug was not the wrong path,
16
+ * it was the second copy of the path.
17
+ */
18
+
19
+ import { existsSync } from 'fs';
20
+ import { dirname, join } from 'path';
21
+ import { fileURLToPath } from 'url';
22
+
23
+ const __dirname = dirname(fileURLToPath(import.meta.url));
24
+
25
+ /** CLI package root — `cli/` in a checkout, the package root in an npm install. */
26
+ export const CLI_ROOT = join(__dirname, '..', '..');
27
+
28
+ /** Bundled assets directory, present in npm installs. */
29
+ export const BUNDLED_DIR = join(CLI_ROOT, 'bundled');
30
+
31
+ /**
32
+ * Resolve the skills source directory.
33
+ * Prefers the bundled copy (npm install), falls back to the repo layout (development).
34
+ * @returns {string} Absolute path to the skills source directory
35
+ */
36
+ export function getSkillsSourceDir() {
37
+ const bundledPath = join(BUNDLED_DIR, 'skills');
38
+ if (existsSync(bundledPath)) {
39
+ return bundledPath;
40
+ }
41
+ // Development environment fallback: <repo>/skills
42
+ return join(CLI_ROOT, '..', 'skills');
43
+ }
44
+
45
+ /**
46
+ * Whether skill files are available locally, making a remote download unnecessary.
47
+ * @returns {boolean}
48
+ */
49
+ export function hasLocalSkillsSource() {
50
+ return existsSync(getSkillsSourceDir());
51
+ }
@@ -1,6 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import yaml from 'js-yaml';
3
+ import * as yaml from 'js-yaml';
4
4
  import { exec } from 'child_process';
5
5
  import util from 'util';
6
6
  import { pathToFileURL } from 'node:url';
@@ -1,6 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import yaml from 'js-yaml';
3
+ import * as yaml from 'js-yaml';
4
4
  import { exec } from 'child_process';
5
5
  import util from 'util';
6
6
  import { pathToFileURL } from 'node:url';
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "version": "6.1.1",
3
+ "version": "6.2.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,18 +58,20 @@
58
58
  "standards": {
59
59
  "name": "universal-dev-standards",
60
60
  "url": "https://github.com/AsiaOstrich/universal-dev-standards",
61
- "version": "6.1.1"
61
+ "version": "6.2.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.1.1",
68
+ "version": "6.2.1",
69
69
  "note": "Skills are now included in the main repository under skills/"
70
70
  }
71
71
  },
72
72
  "supportedAITools": {
73
+ "_note": "INFORMATIONAL MIRROR. No code reads this block -- the CLI's actual install behaviour is driven by cli/src/config/ai-agent-paths.js, and integrations/REGISTRY.json is the SSOT for tier and deprecation. Kept in sync by scripts/check-integration-liveness.ts so the three cannot drift apart silently again (they did, for 5.5 months, on antigravity.supportsSkills).",
74
+ "_noteZh": "資訊性鏡像。沒有任何程式讀取此區塊——CLI 實際的安裝行為由 cli/src/config/ai-agent-paths.js 驅動,tier 與停用狀態的 SSOT 是 integrations/REGISTRY.json。由 scripts/check-integration-liveness.ts 維持同步,避免三者再次無聲漂移(antigravity.supportsSkills 曾漂移 5.5 個月)。",
73
75
  "claude-code": {
74
76
  "name": "Claude Code",
75
77
  "skillsPath": "skills",
@@ -197,8 +199,8 @@
197
199
  "skillsPath": "skills",
198
200
  "nativePaths": {
199
201
  "skills": {
200
- "project": ".codex/skills/",
201
- "user": "~/.codex/skills/"
202
+ "project": ".agents/skills/",
203
+ "user": "~/.agents/skills/"
202
204
  },
203
205
  "agents": {
204
206
  "project": ".codex/agents/",
@@ -210,7 +212,8 @@
210
212
  "supportsCommands": false,
211
213
  "supportsTask": false,
212
214
  "supportsAgents": true,
213
- "status": "partial"
215
+ "status": "partial",
216
+ "skillsPathNote": "Verified 2026-07-23 against developers.openai.com/codex/skills. Codex reads .agents/skills (plural). The previous '.codex/skills/' was a directory UDS invented and Codex never reads."
214
217
  },
215
218
  "copilot": {
216
219
  "name": "GitHub Copilot",
@@ -256,6 +259,10 @@
256
259
  },
257
260
  "gemini-cli": {
258
261
  "name": "Gemini CLI",
262
+ "deprecated": true,
263
+ "deprecationNote": "Google sunset Gemini CLI on 2026-06-18; succeeded by Antigravity CLI. The integrations/gemini-cli/ and .gemini/ trees are frozen -- see .gemini/DEPRECATED.md.",
264
+ "deprecationNoteZh": "Google 已於 2026-06-18 終止 Gemini CLI,由 Antigravity CLI 接手。integrations/gemini-cli/ 與 .gemini/ 兩棵樹已凍結——見 .gemini/DEPRECATED.md。",
265
+ "supersededBy": "antigravity",
259
266
  "skillsPath": "skills",
260
267
  "nativePaths": {
261
268
  "skills": {
@@ -284,12 +291,15 @@
284
291
  "antigravity": {
285
292
  "name": "Google Antigravity",
286
293
  "skillsPath": null,
294
+ "skillsPathStatus": "unverified",
295
+ "skillsPathNote": "Antigravity supports skills, but UDS has not verified where it reads them from. Candidates: '~/.gemini/antigravity-cli/plugins/<name>/skills/' (official plugin docs) and '.agent/skills/' (UDS's own 2026-02 spec, written in the Gemini CLI era). skillsPath stays null until one is confirmed against a real CLI -- an unverified path fails silently. Tracked as XSPEC-355 OQ6.",
296
+ "skillsPathNoteZh": "Antigravity 支援 skills,但 UDS 尚未驗證它實際從何處讀取。候選路徑:'~/.gemini/antigravity-cli/plugins/<name>/skills/'(官方 plugin 文件)與 '.agent/skills/'(UDS 自己 2026-02 的 spec,寫於 Gemini CLI 時代)。在對實際 CLI 確認之前 skillsPath 維持 null——路徑填錯會是靜默失敗。追蹤於 XSPEC-355 OQ6。",
287
297
  "nativePaths": {
288
298
  "config": {
289
299
  "project": "INSTRUCTIONS.md"
290
300
  }
291
301
  },
292
- "supportsSkills": false,
302
+ "supportsSkills": true,
293
303
  "supportsCommands": false,
294
304
  "supportsTask": false,
295
305
  "supportsAgents": false,
@@ -2226,7 +2236,7 @@
2226
2236
  "id": "license-compliance",
2227
2237
  "name": "License Compliance Standards",
2228
2238
  "nameZh": "授權合規標準",
2229
- "version": "6.1.1",
2239
+ "version": "6.2.1",
2230
2240
  "source": {
2231
2241
  "human": "core/license-compliance.md",
2232
2242
  "ai": "ai/standards/license-compliance.ai.yaml"
@@ -2238,7 +2248,7 @@
2238
2248
  "id": "verification-oracle",
2239
2249
  "name": "Verification Oracle Standards",
2240
2250
  "nameZh": "驗證 Oracle 標準",
2241
- "version": "6.1.1",
2251
+ "version": "6.2.1",
2242
2252
  "source": {
2243
2253
  "human": "core/verification-oracle.md",
2244
2254
  "ai": "ai/standards/verification-oracle.ai.yaml"
@@ -2250,7 +2260,7 @@
2250
2260
  "id": "model-provenance",
2251
2261
  "name": "Model Provenance Policy Standards",
2252
2262
  "nameZh": "模型來源政策標準",
2253
- "version": "6.1.1",
2263
+ "version": "6.2.1",
2254
2264
  "source": {
2255
2265
  "human": "core/model-provenance.md",
2256
2266
  "ai": "ai/standards/model-provenance.ai.yaml"
@@ -2262,7 +2272,7 @@
2262
2272
  "id": "resource-cost-boundary",
2263
2273
  "name": "Resource / Cost Boundary Declaration Standards",
2264
2274
  "nameZh": "資源/成本邊界宣告標準",
2265
- "version": "6.1.1",
2275
+ "version": "6.2.1",
2266
2276
  "source": {
2267
2277
  "human": "core/resource-cost-boundary.md",
2268
2278
  "ai": "ai/standards/resource-cost-boundary.ai.yaml"