universal-dev-standards 6.9.0 → 6.11.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 (63) hide show
  1. package/bin/uds.js +2 -0
  2. package/bundled/core/agent-communication-protocol.md +8 -0
  3. package/bundled/core/branch-completion.md +8 -0
  4. package/bundled/core/change-batching-standards.md +8 -0
  5. package/bundled/core/execution-history.md +8 -0
  6. package/bundled/core/pipeline-integration-standards.md +8 -0
  7. package/bundled/core/workflow-enforcement.md +8 -0
  8. package/bundled/core/workflow-state-protocol.md +8 -0
  9. package/bundled/locales/zh-CN/CHANGELOG.md +49 -3
  10. package/bundled/locales/zh-CN/README.md +1 -1
  11. package/bundled/locales/zh-CN/SECURITY.md +1 -1
  12. package/bundled/locales/zh-CN/core/agent-communication-protocol.md +7 -0
  13. package/bundled/locales/zh-CN/core/branch-completion.md +7 -0
  14. package/bundled/locales/zh-CN/core/change-batching-standards.md +7 -0
  15. package/bundled/locales/zh-CN/core/execution-history.md +7 -0
  16. package/bundled/locales/zh-CN/core/pipeline-integration-standards.md +7 -0
  17. package/bundled/locales/zh-CN/core/workflow-enforcement.md +7 -0
  18. package/bundled/locales/zh-CN/core/workflow-state-protocol.md +7 -0
  19. package/bundled/locales/zh-CN/docs/CHEATSHEET.md +1 -1
  20. package/bundled/locales/zh-CN/docs/CLI-INIT-OPTIONS.md +52 -5
  21. package/bundled/locales/zh-CN/docs/FEATURE-REFERENCE.md +3 -1
  22. package/bundled/locales/zh-CN/docs/MIGRATION-v6.md +8 -4
  23. package/bundled/locales/zh-TW/CHANGELOG.md +50 -3
  24. package/bundled/locales/zh-TW/README.md +1 -1
  25. package/bundled/locales/zh-TW/SECURITY.md +1 -1
  26. package/bundled/locales/zh-TW/core/agent-communication-protocol.md +7 -0
  27. package/bundled/locales/zh-TW/core/branch-completion.md +7 -0
  28. package/bundled/locales/zh-TW/core/change-batching-standards.md +7 -0
  29. package/bundled/locales/zh-TW/core/execution-history.md +7 -0
  30. package/bundled/locales/zh-TW/core/pipeline-integration-standards.md +7 -0
  31. package/bundled/locales/zh-TW/core/workflow-enforcement.md +7 -0
  32. package/bundled/locales/zh-TW/core/workflow-state-protocol.md +7 -0
  33. package/bundled/locales/zh-TW/docs/CHEATSHEET.md +1 -1
  34. package/bundled/locales/zh-TW/docs/CLI-INIT-OPTIONS.md +52 -5
  35. package/bundled/locales/zh-TW/docs/FEATURE-REFERENCE.md +3 -1
  36. package/bundled/locales/zh-TW/docs/MIGRATION-v6.md +8 -4
  37. package/bundled/locales/zh-TW/integrations/claude-code/README.md +14 -5
  38. package/package.json +7 -6
  39. package/src/commands/check.js +413 -44
  40. package/src/commands/config.js +34 -28
  41. package/src/commands/init.js +37 -7
  42. package/src/commands/spec.js +2 -2
  43. package/src/commands/update.js +560 -74
  44. package/src/core/manifest.js +39 -1
  45. package/src/flows/init-flow.js +9 -1
  46. package/src/generators/layered-claudemd.js +13 -4
  47. package/src/i18n/messages.js +39 -3
  48. package/src/installers/integration-installer.js +17 -10
  49. package/src/installers/manifest-installer.js +4 -0
  50. package/src/installers/skills-installer.js +4 -4
  51. package/src/installers/standards-installer.js +3 -3
  52. package/src/prompts/init.js +33 -4
  53. package/src/reconciler/actual-state-scanner.js +29 -2
  54. package/src/reconciler/desired-state-calculator.js +51 -2
  55. package/src/reconciler/diff-engine.js +76 -5
  56. package/src/reconciler/plan-executor.js +48 -27
  57. package/src/utils/hasher.js +61 -5
  58. package/src/utils/integration-generator.js +431 -92
  59. package/src/utils/marker-locator.js +140 -0
  60. package/src/utils/reference-sync.js +156 -8
  61. package/src/utils/registry.js +57 -0
  62. package/src/utils/spinner.js +31 -0
  63. package/standards-registry.json +8 -8
@@ -773,10 +773,48 @@ export function needsMigration(manifest) {
773
773
  if (!manifest) {
774
774
  return true;
775
775
  }
776
-
776
+
777
777
  return manifest.version !== CURRENT_SCHEMA_VERSION;
778
778
  }
779
779
 
780
+ /**
781
+ * Compare two `x.y.z` version strings numerically (not lexically — '3.10.0'
782
+ * must sort after '3.9.0'). Missing components count as 0.
783
+ * @returns {number} negative if a<b, 0 if equal, positive if a>b
784
+ */
785
+ function compareSemverStrings(a, b) {
786
+ const pa = String(a).split('.').map(Number);
787
+ const pb = String(b).split('.').map(Number);
788
+ for (let i = 0; i < 3; i++) {
789
+ const diff = (pa[i] || 0) - (pb[i] || 0);
790
+ if (diff !== 0) return diff;
791
+ }
792
+ return 0;
793
+ }
794
+
795
+ /**
796
+ * Set `manifest.version` to the CLI's current schema version — never lower.
797
+ *
798
+ * XSPEC adopter-report Q1: five write sites across update.js/check.js/config.js
799
+ * each hardcoded a schema version literal ('3.1.0'/'3.2.0'/'3.3.0') instead of
800
+ * CURRENT_SCHEMA_VERSION. Every one of those literals predates a later schema
801
+ * bump, so running the operation on an already-current manifest (3.4.0)
802
+ * downgraded it back to whatever the literal said. This is the single place
803
+ * that decision is made: always move toward CURRENT_SCHEMA_VERSION, never
804
+ * away from it, even if a caller is ever fixed incompletely again.
805
+ *
806
+ * @param {Object} manifest - Manifest object (mutated in place)
807
+ */
808
+ export function bumpManifestVersion(manifest) {
809
+ const current = manifest.version;
810
+ const isWellFormed = typeof current === 'string' && /^\d+\.\d+\.\d+$/.test(current);
811
+ if (!isWellFormed || compareSemverStrings(current, CURRENT_SCHEMA_VERSION) < 0) {
812
+ manifest.version = CURRENT_SCHEMA_VERSION;
813
+ }
814
+ // else: manifest.version is already at or ahead of CURRENT_SCHEMA_VERSION
815
+ // (e.g. a newer version written by a future CLI) — leave it alone.
816
+ }
817
+
780
818
  /**
781
819
  * Get all AI tools from manifest
782
820
  * @param {Object} manifest - Manifest
@@ -242,6 +242,13 @@ export async function runInitFlow(options, detected, projectPath) {
242
242
  // Optional step — skippable by answering "n" or pressing Ctrl+C.
243
243
  await promptProjectContractStep(projectPath);
244
244
 
245
+ // XSPEC-418 R2: --claude-target is a flag, not an interactive question, but a
246
+ // user running `uds init --claude-target local` without `-y` still expects it
247
+ // honored while answering the rest of the prompts.
248
+ const integrationTargets = options.claudeTarget === 'local'
249
+ ? { 'claude-code': 'CLAUDE.local.md' }
250
+ : undefined;
251
+
245
252
  return {
246
253
  languages,
247
254
  frameworks,
@@ -253,6 +260,7 @@ export async function runInitFlow(options, detected, projectPath) {
253
260
  integrations,
254
261
  contentMode,
255
262
  generateAgentsMd,
256
- releaseMode
263
+ releaseMode,
264
+ integrationTargets
257
265
  };
258
266
  }
@@ -11,26 +11,35 @@
11
11
  import { existsSync, readFileSync, writeFileSync } from 'fs';
12
12
  import { join } from 'path';
13
13
  import { mapStandardsToDirectories } from '../utils/directory-mapper.js';
14
+ import { locateMarkerBlock } from '../utils/marker-locator.js';
14
15
 
15
16
  const UDS_BEGIN = '<!-- UDS:STANDARDS:BEGIN -->';
16
17
  const UDS_END = '<!-- UDS:STANDARDS:END -->';
17
18
 
18
19
  /**
19
20
  * Replace content between UDS markers, preserving content outside markers.
21
+ *
22
+ * XSPEC adopter-report Q5: same defect class as integration-generator.js —
23
+ * raw indexOf matched the marker text anywhere in the file, not only on a
24
+ * standalone line. Routed through the shared locateMarkerBlock so a sentence
25
+ * mentioning the marker is never mistaken for the real boundary. Ambiguity
26
+ * (two real marker pairs) throws AmbiguousMarkerError, which this function
27
+ * intentionally does not catch — a write must refuse rather than guess.
28
+ *
20
29
  * @param {string} existing - Existing file content
21
30
  * @param {string} newUdsContent - New UDS content to insert between markers
22
31
  * @returns {string} Updated content
23
32
  */
24
33
  function replaceUdsBlock(existing, newUdsContent) {
25
- const beginIdx = existing.indexOf(UDS_BEGIN);
26
- const endIdx = existing.indexOf(UDS_END);
34
+ const block = locateMarkerBlock(existing, { start: UDS_BEGIN, end: UDS_END });
27
35
 
28
- if (beginIdx === -1 || endIdx === -1) {
36
+ if (!block) {
29
37
  // No markers found — append UDS block
30
38
  return existing + '\n\n' + wrapWithMarkers(newUdsContent);
31
39
  }
32
40
 
33
- const before = existing.substring(0, beginIdx);
41
+ const { startIdx, endIdx } = block;
42
+ const before = existing.substring(0, startIdx);
34
43
  const after = existing.substring(endIdx + UDS_END.length);
35
44
  return before + wrapWithMarkers(newUdsContent) + after;
36
45
  }
@@ -743,6 +743,9 @@ export const messages = {
743
743
  unchanged: 'unchanged',
744
744
  modified: 'modified',
745
745
  missing: 'missing',
746
+ retired: 'no longer shipped by UDS',
747
+ retiredHeader: '{count} tracked file(s) are gone because UDS stopped shipping them. That is the migration completed, not a fault:',
748
+ retiredHint: 'Run `uds update` to drop their manifest records. `--restore` cannot bring them back — there is no source upstream.',
746
749
  existsNoHash: 'exists, no hash',
747
750
  summary: 'Summary: {unchanged} unchanged, {modified} modified, {missing} missing',
748
751
  // Actions available
@@ -837,10 +840,17 @@ export const messages = {
837
840
  refSyncStatus: 'Reference Sync Status:',
838
841
  noRefsFound: 'No standard references found',
839
842
  refsNotInManifest: 'References not in manifest:',
840
- standardsNotReferenced: 'Standards not referenced (optional):',
843
+ standardsNotReferenced: 'Standards with no "Reference:"/"參考:" line in this file (optional):',
841
844
  refsInSync: '{path}: references in sync ({count} refs)',
842
845
  noIntegrationRefs: 'No integration files with standard references found.',
843
846
  runSyncRefs: 'Run `uds update --sync-refs` to fix reference issues.',
847
+ runIntegrationsOnly: 'Run `uds update --integrations-only` to regenerate integration files.',
848
+ danglingRefs: 'references files that are not on disk:',
849
+ brokenMentions: 'mentions paths that do not exist in this project:',
850
+ brokenMentionsFix: 'These are usually outside the UDS block, left by an older install. UDS does not rewrite text it did not write — correct them by hand or delete the line.',
851
+ danglingRefsFix: 'Run `uds update --integrations-only` to regenerate them.',
852
+ commandsUntracked: '{count} installed command file(s) are covered by no hash — integrity is not checked for them',
853
+ commandsUntrackedFix: 'Run `uds update --commands` to record them again.',
844
854
  // CLI update
845
855
  checkingCliUpdates: 'Checking for CLI updates...',
846
856
  couldNotCheckUpdates: 'ℹ Could not check for CLI updates (offline mode)',
@@ -1020,6 +1030,7 @@ export const messages = {
1020
1030
  syncingIntegrations: 'Syncing integration files...',
1021
1031
  syncedIntegrations: 'Synced {count} integration files',
1022
1032
  prunedOrphanedBlockHashes: 'Pruned {count} orphaned integration hash(es): {files}',
1033
+ droppedRetiredHashes: 'Dropped {count} manifest record(s) for files UDS no longer ships and that are already gone:',
1023
1034
  // Success
1024
1035
  updateSuccess: '✓ Standards updated successfully!',
1025
1036
  versionUpdated: 'Version: {current} → {latest}',
@@ -1047,6 +1058,7 @@ export const messages = {
1047
1058
  // Sync refs
1048
1059
  syncingRefs: 'Syncing integration references...',
1049
1060
  noIntegrationConfigs: '⚠ No integration configurations found in manifest.',
1061
+ integrationConfigsBackfilled: 'Rebuilt integration config for {count} file(s) from the manifest.',
1050
1062
  integrationConfigsRequired: 'Integration configs are required for reference sync.',
1051
1063
  thisHappensWhen: 'This happens when:',
1052
1064
  oldVersion: '- The project was initialized with an older version of UDS',
@@ -1971,6 +1983,9 @@ export const messages = {
1971
1983
  unchanged: '未變更',
1972
1984
  modified: '已修改',
1973
1985
  missing: '遺失',
1986
+ retired: 'UDS 已不再出貨',
1987
+ retiredHeader: '有 {count} 個被追蹤的檔案不見了,原因是 UDS 已不再出貨。那是遷移完成的樣子,不是故障:',
1988
+ retiredHint: '執行 `uds update` 清掉它們在 manifest 的紀錄。`--restore` 救不回來——上游已經沒有來源。',
1974
1989
  existsNoHash: '存在,無 hash',
1975
1990
  summary: '摘要:{unchanged} 未變更,{modified} 已修改,{missing} 遺失',
1976
1991
  // Actions available
@@ -2065,10 +2080,17 @@ export const messages = {
2065
2080
  refSyncStatus: '參考同步狀態:',
2066
2081
  noRefsFound: '找不到標準參考',
2067
2082
  refsNotInManifest: '未在 manifest 中的參考:',
2068
- standardsNotReferenced: '未參考的標準(選用):',
2083
+ standardsNotReferenced: '本檔案中沒有「Reference:」或「參考:」行提及的標準(選用):',
2069
2084
  refsInSync: '{path}:參考已同步({count} 個參考)',
2070
2085
  noIntegrationRefs: '找不到有標準參考的整合檔案。',
2071
2086
  runSyncRefs: '執行 `uds update --sync-refs` 修復參考問題。',
2087
+ runIntegrationsOnly: '執行 `uds update --integrations-only` 重新產生整合檔案。',
2088
+ danglingRefs: '引用了磁碟上不存在的檔案:',
2089
+ brokenMentions: '提到了本專案裡不存在的路徑:',
2090
+ brokenMentionsFix: '這些通常在 UDS 區塊之外,是舊版安裝留下的。UDS 不會改寫不是它寫的文字——請手動更正或刪掉那一行。',
2091
+ danglingRefsFix: '執行 `uds update --integrations-only` 重新產生。',
2092
+ commandsUntracked: '{count} 個已安裝的斜線命令沒有任何雜湊涵蓋——這些檔案的完整性沒有被檢查',
2093
+ commandsUntrackedFix: '執行 `uds update --commands` 重新記錄。',
2072
2094
  // CLI update
2073
2095
  checkingCliUpdates: '正在檢查 CLI 更新...',
2074
2096
  couldNotCheckUpdates: 'ℹ 無法檢查 CLI 更新(離線模式)',
@@ -2245,6 +2267,7 @@ export const messages = {
2245
2267
  syncingIntegrations: '同步整合檔案中...',
2246
2268
  syncedIntegrations: '已同步 {count} 個整合檔案',
2247
2269
  prunedOrphanedBlockHashes: '已清除 {count} 個孤兒整合雜湊:{files}',
2270
+ droppedRetiredHashes: '已清掉 {count} 筆 manifest 紀錄——這些檔案 UDS 已不再出貨,而且本來就已經不在了:',
2248
2271
  // Success
2249
2272
  updateSuccess: '✓ 標準更新成功!',
2250
2273
  versionUpdated: '版本:{current} → {latest}',
@@ -2272,6 +2295,7 @@ export const messages = {
2272
2295
  // Sync refs
2273
2296
  syncingRefs: '同步整合參考中...',
2274
2297
  noIntegrationConfigs: '⚠ manifest 中找不到整合設定。',
2298
+ integrationConfigsBackfilled: '已從 manifest 重建 {count} 個整合檔的設定。',
2275
2299
  integrationConfigsRequired: '參考同步需要整合設定。',
2276
2300
  thisHappensWhen: '這會發生在:',
2277
2301
  oldVersion: '- 專案使用較舊版本的 UDS 初始化',
@@ -3211,6 +3235,9 @@ export const messages = {
3211
3235
  unchanged: '未更改',
3212
3236
  modified: '已修改',
3213
3237
  missing: '缺失',
3238
+ retired: 'UDS 已不再发布',
3239
+ retiredHeader: '有 {count} 个被追踪的文件不见了,原因是 UDS 已不再发布。那是迁移完成的样子,不是故障:',
3240
+ retiredHint: '执行 `uds update` 清掉它们在 manifest 的记录。`--restore` 救不回来——上游已经没有来源。',
3214
3241
  existsNoHash: '存在,无哈希',
3215
3242
  summary: '摘要:{unchanged} 未更改,{modified} 已修改,{missing} 缺失',
3216
3243
  // Actions available
@@ -3305,10 +3332,17 @@ export const messages = {
3305
3332
  refSyncStatus: '参考同步状态:',
3306
3333
  noRefsFound: '未找到标准引用',
3307
3334
  refsNotInManifest: 'manifest 中无引用:',
3308
- standardsNotReferenced: '未引用的标准(可选):',
3335
+ standardsNotReferenced: '本文件中没有"Reference:"或"參考:"行提及的标准(可选):',
3309
3336
  refsInSync: '{path}:引用已同步({count} 个引用)',
3310
3337
  noIntegrationRefs: '未找到包含标准引用的集成文件。',
3311
3338
  runSyncRefs: '执行 `uds update --sync-refs` 修复引用问题。',
3339
+ runIntegrationsOnly: '执行 `uds update --integrations-only` 重新生成集成文件。',
3340
+ danglingRefs: '引用了磁盘上不存在的文件:',
3341
+ brokenMentions: '提到了本项目里不存在的路径:',
3342
+ brokenMentionsFix: '这些通常在 UDS 区块之外,是旧版安装留下的。UDS 不会改写不是它写的文字——请手动更正或删掉那一行。',
3343
+ danglingRefsFix: '执行 `uds update --integrations-only` 重新生成。',
3344
+ commandsUntracked: '{count} 个已安装的斜杠命令没有任何哈希覆盖——这些文件的完整性没有被检查',
3345
+ commandsUntrackedFix: '执行 `uds update --commands` 重新记录。',
3312
3346
  // CLI update
3313
3347
  checkingCliUpdates: '检查 CLI 更新...',
3314
3348
  couldNotCheckUpdates: 'ℹ 无法检查 CLI 更新(离线模式)',
@@ -3398,6 +3432,7 @@ export const messages = {
3398
3432
  syncingIntegrations: '正在同步集成文件...',
3399
3433
  syncedIntegrations: '已同步 {count} 个集成文件',
3400
3434
  prunedOrphanedBlockHashes: '已清除 {count} 个孤儿集成哈希:{files}',
3435
+ droppedRetiredHashes: '已清掉 {count} 笔 manifest 记录——这些文件 UDS 已不再发布,而且本来就已经不在了:',
3401
3436
  // Success
3402
3437
  updateSuccess: '✓ 标准更新成功!',
3403
3438
  versionUpdated: '版本:{current} → {latest}',
@@ -3425,6 +3460,7 @@ export const messages = {
3425
3460
  // Sync refs
3426
3461
  syncingRefs: '正在同步集成引用...',
3427
3462
  noIntegrationConfigs: '⚠ manifest 中未找到集成配置。',
3463
+ integrationConfigsBackfilled: '已从 manifest 重建 {count} 个集成文件的配置。',
3428
3464
  integrationConfigsRequired: '引用同步需要集成配置。',
3429
3465
  thisHappensWhen: '发生这种情况时:',
3430
3466
  oldVersion: '- 项目使用旧版 UDS 初始化',
@@ -1,9 +1,9 @@
1
- import ora from 'ora';
1
+ import { createSpinner } from '../utils/spinner.js';
2
2
  import { copyIntegration } from '../utils/copier.js';
3
3
  import {
4
4
  writeIntegrationFile,
5
5
  integrationFileExists,
6
- getToolFilePath,
6
+ resolveIntegrationTargetFile,
7
7
  writeAgentsMdSummary,
8
8
  resolveContentModeForTool
9
9
  } from '../utils/integration-generator.js';
@@ -99,14 +99,16 @@ export async function installIntegrations(config, projectPath) {
99
99
  }
100
100
 
101
101
  // Start installation spinner
102
- const intSpinner = ora(msg.generatingIntegrations).start();
102
+ const intSpinner = createSpinner(msg.generatingIntegrations).start();
103
103
 
104
104
  // Track generated files to handle AGENTS.md sharing (codex + opencode)
105
105
  const generatedFiles = new Set();
106
106
 
107
107
  for (const tool of integrations) {
108
108
  // Check if this file was already generated (for AGENTS.md sharing)
109
- const targetFile = getToolFilePath(tool);
109
+ // XSPEC-418 R2: honors config.integrationTargets (set from --claude-target at
110
+ // init time) rather than always assuming each tool's default file.
111
+ const targetFile = resolveIntegrationTargetFile(tool, { integrationTargets: config.integrationTargets });
110
112
  if (generatedFiles.has(targetFile)) {
111
113
  // Skip - file already generated by another tool (e.g., codex generated AGENTS.md, skip opencode)
112
114
  continue;
@@ -129,7 +131,9 @@ export async function installIntegrations(config, projectPath) {
129
131
  contentMode: resolved.contentMode,
130
132
  level: resolved.level ?? level,
131
133
  // Pass output_language for dynamic commit standards generation
132
- outputLanguage
134
+ outputLanguage,
135
+ // XSPEC-418 R2: forwarded so writeIntegrationFile resolves the actual target.
136
+ integrationTargets: config.integrationTargets
133
137
  };
134
138
 
135
139
  // Use dynamic generator
@@ -218,13 +222,14 @@ export async function generateClaudeMd(config, projectPath) {
218
222
 
219
223
  const msg = t().commands.init;
220
224
 
221
- // Check if Claude Code is selected and CLAUDE.md doesn't exist
225
+ // Check if Claude Code is selected and its target file doesn't already exist
226
+ // XSPEC-418 R2: the target may be CLAUDE.local.md, not CLAUDE.md.
222
227
  const claudeCodeSelected = aiTools.includes('claude-code');
223
- if (!claudeCodeSelected || integrationFileExists('claude-code', projectPath)) {
228
+ if (!claudeCodeSelected || integrationFileExists('claude-code', projectPath, { integrationTargets: config.integrationTargets })) {
224
229
  return { path: null, error: null };
225
230
  }
226
231
 
227
- const claudeSpinner = ora(msg.generatingClaudeMd).start();
232
+ const claudeSpinner = createSpinner(msg.generatingClaudeMd).start();
228
233
 
229
234
  // Derive categories from installedStandards to avoid referencing non-installed standards
230
235
  const categories = installedStandards.length > 0
@@ -245,7 +250,9 @@ export async function generateClaudeMd(config, projectPath) {
245
250
  contentMode: claudeResolved.contentMode,
246
251
  level: claudeResolved.level ?? level,
247
252
  // Pass output_language for dynamic commit standards generation
248
- outputLanguage
253
+ outputLanguage,
254
+ // XSPEC-418 R2
255
+ integrationTargets: config.integrationTargets
249
256
  };
250
257
 
251
258
  const result = writeIntegrationFile('claude-code', claudeConfig, projectPath);
@@ -285,7 +292,7 @@ export async function generateUniversalAgentsMd(config, integrationResults, proj
285
292
  }
286
293
 
287
294
  const msg = t().commands.init;
288
- const spinner = ora(msg.generatingAgentsMd || 'Generating AGENTS.md...').start();
295
+ const spinner = createSpinner(msg.generatingAgentsMd || 'Generating AGENTS.md...').start();
289
296
 
290
297
  const outputLanguage = config.outputLanguage || config.commitLanguage ||
291
298
  config.standardOptions?.output_language || config.standardOptions?.commit_language ||
@@ -31,6 +31,10 @@ export function writeFinalManifest(config, results, projectPath) {
31
31
  extensions: results.extensions || [],
32
32
  integrations: results.integrations || [],
33
33
  integrationConfigs: results.manifestIntegrationConfigs || {},
34
+ // XSPEC-418 R2: only ever present when --claude-target local was chosen.
35
+ // Omitted entirely otherwise so a manifest with no override is byte-for-byte
36
+ // what it was before this feature existed (AC-5).
37
+ ...(config.integrationTargets ? { integrationTargets: config.integrationTargets } : {}),
34
38
  options: {
35
39
  display_language: config.displayLanguage || 'en',
36
40
  workflow: config.standardOptions?.workflow || null,
@@ -13,7 +13,7 @@
13
13
  * @version 1.0.0
14
14
  */
15
15
 
16
- import ora from 'ora';
16
+ import { createSpinner } from '../utils/spinner.js';
17
17
  import chalk from 'chalk';
18
18
  import {
19
19
  installSkillsToMultipleAgents,
@@ -115,7 +115,7 @@ function getSkillFiles() {
115
115
  export async function installSkills(skillsConfig, projectPath, messages, results) {
116
116
  // Modern unified multi-agent installation
117
117
  if (skillsConfig.needsInstall && skillsConfig.skillsInstallations?.length > 0) {
118
- const skillSpinner = ora(messages.installingSkills).start();
118
+ const skillSpinner = createSpinner(messages.installingSkills).start();
119
119
 
120
120
  // Use new unified installer for multi-agent support
121
121
  const installResult = await installSkillsToMultipleAgents(
@@ -186,7 +186,7 @@ export async function installSkills(skillsConfig, projectPath, messages, results
186
186
  * @returns {Promise<void>}
187
187
  */
188
188
  async function installSkillsLegacy(skillsConfig, projectPath, messages, results) {
189
- const skillSpinner = ora(messages.installingSkills).start();
189
+ const skillSpinner = createSpinner(messages.installingSkills).start();
190
190
 
191
191
  const skillFiles = getSkillFiles();
192
192
  let successCount = 0;
@@ -255,7 +255,7 @@ export async function installCommands(skillsConfig, projectPath, messages, resul
255
255
  return;
256
256
  }
257
257
 
258
- const cmdSpinner = ora(messages.installingCommands || 'Installing slash commands...').start();
258
+ const cmdSpinner = createSpinner(messages.installingCommands || 'Installing slash commands...').start();
259
259
 
260
260
  const cmdResult = await installCommandsToMultipleAgents(
261
261
  skillsConfig.commandsInstallations,
@@ -1,5 +1,5 @@
1
1
  import chalk from 'chalk';
2
- import ora from 'ora';
2
+ import { createSpinner } from '../utils/spinner.js';
3
3
  import { basename, join } from 'path';
4
4
  import {
5
5
  getAllStandards,
@@ -75,7 +75,7 @@ export async function installStandards(config, projectPath) {
75
75
  };
76
76
 
77
77
  // Start copying standards
78
- const copySpinner = ora(msg.copyingStandards).start();
78
+ const copySpinner = createSpinner(msg.copyingStandards).start();
79
79
 
80
80
  // Copy standards based on format
81
81
  const formatsToUse = config.format === 'both' ? ['ai', 'human'] : [config.format];
@@ -113,7 +113,7 @@ export async function installStandards(config, projectPath) {
113
113
  const localeExtension = (config.displayLanguage === 'zh-tw' || config.displayLanguage === 'zh-cn') ? config.displayLanguage : null;
114
114
 
115
115
  if (config.languages.length > 0 || config.frameworks.length > 0 || localeExtension) {
116
- const extSpinner = ora(msg.copyingExtensions).start();
116
+ const extSpinner = createSpinner(msg.copyingExtensions).start();
117
117
 
118
118
  for (const lang of config.languages) {
119
119
  if (EXTENSION_MAPPINGS[lang]) {
@@ -1,5 +1,5 @@
1
1
  import { select, checkbox, confirm as inquirerConfirm, Separator, input } from '@inquirer/prompts';
2
- import { existsSync, writeFileSync, readdirSync } from 'fs';
2
+ import { existsSync, readFileSync, writeFileSync, readdirSync } from 'fs';
3
3
  import { join } from 'path';
4
4
  import chalk from 'chalk';
5
5
  import os from 'os';
@@ -1385,11 +1385,40 @@ export function suggestCommands(projectPath) {
1385
1385
  if (existsSync(p('Gemfile')))
1386
1386
  return { test: 'bundle exec rspec', lint: 'bundle exec rubocop', build: 'gem build *.gemspec', security: 'bundle audit' };
1387
1387
 
1388
- // C# — glob for .csproj / .sln
1388
+ // C# — glob for .csproj / .sln.
1389
+ //
1390
+ // `.csproj` has named two incompatible build systems since 2017. An SDK-style
1391
+ // project (`<Project Sdk="…">`, `<TargetFramework>`) builds with the `dotnet`
1392
+ // CLI. An old-style one (`<TargetFrameworkVersion>`, no Sdk attribute) needs
1393
+ // MSBuild and Visual Studio's targets, and handing it `dotnet build` produces
1394
+ // `MSB4019: could not find Import project …Microsoft.WebApplication.targets`
1395
+ // — reported 2026-09-16 from a .NET Framework 4.8 Web App, which also got
1396
+ // `dotnet list package --vulnerable`, a command that silently reports nothing
1397
+ // for a `packages.config` project because it only reads `PackageReference`.
1398
+ //
1399
+ // For the old style only `build` gets an answer. `dotnet format` requires an
1400
+ // SDK-style project and there is no drop-in equivalent; test discovery
1401
+ // depends on which runner the solution uses. The unknown-ecosystem branch
1402
+ // below already returns blanks "so user fills them in", and a blank a human
1403
+ // completes beats a command that looks authoritative and fails on first use.
1389
1404
  try {
1390
1405
  const files = readdirSync(projectPath);
1391
- if (files.some(f => f.endsWith('.csproj') || f.endsWith('.sln')))
1392
- return { test: 'dotnet test', lint: 'dotnet format --verify-no-changes', build: 'dotnet build', security: 'dotnet list package --vulnerable' };
1406
+ const projectFiles = files.filter(f => f.endsWith('.csproj') || f.endsWith('.vbproj'));
1407
+ if (projectFiles.length > 0 || files.some(f => f.endsWith('.sln'))) {
1408
+ const oldStyle = projectFiles.some(f => {
1409
+ try {
1410
+ const xml = readFileSync(join(projectPath, f), 'utf-8');
1411
+ if (/<Project[^>]*\sSdk\s*=/i.test(xml)) return false;
1412
+ return /<TargetFrameworkVersion\s*>/i.test(xml);
1413
+ } catch {
1414
+ return false;
1415
+ }
1416
+ });
1417
+
1418
+ return oldStyle
1419
+ ? { test: '', lint: '', build: 'msbuild /t:Build /p:Configuration=Release', security: '' }
1420
+ : { test: 'dotnet test', lint: 'dotnet format --verify-no-changes', build: 'dotnet build', security: 'dotnet list package --vulnerable' };
1421
+ }
1393
1422
  } catch {
1394
1423
  // ignore read errors
1395
1424
  }
@@ -13,6 +13,7 @@ import { join, relative } from 'path';
13
13
  import { readManifest } from '../core/manifest.js';
14
14
  import { computeFileHash, computeIntegrationBlockHash, normalizeLineEndings } from '../utils/hasher.js';
15
15
  import { SUPPORTED_AI_TOOLS, UDS_MARKERS } from '../core/constants.js';
16
+ import { locateMarkerBlock, AmbiguousMarkerError } from '../utils/marker-locator.js';
16
17
  import { getSkillsDirForAgent, getCommandsDirForAgent, getCommandFileExtension } from '../config/ai-agent-paths.js';
17
18
  import { getSkillsSourceEntryNames, getAvailableCommandNames } from '../utils/skills-installer.js';
18
19
 
@@ -230,7 +231,21 @@ function scanIntegrations(state, projectPath) {
230
231
  const filePath = join(projectPath, toolConfig.file);
231
232
  if (!existsSync(filePath)) continue;
232
233
 
233
- const blockHash = computeIntegrationBlockHash(filePath);
234
+ // XSPEC adopter-report Q5: this scan walks every configured tool's file
235
+ // in one pass to build a full picture of the project — one file with an
236
+ // ambiguous marker pair must not abort the whole scan. `uds check`'s
237
+ // dedicated block-integrity pass is what surfaces the ambiguity to the
238
+ // user explicitly, with line numbers; this scan just records "unknown".
239
+ let blockHash;
240
+ try {
241
+ blockHash = computeIntegrationBlockHash(filePath);
242
+ } catch (error) {
243
+ if (error instanceof AmbiguousMarkerError) {
244
+ blockHash = null;
245
+ } else {
246
+ throw error;
247
+ }
248
+ }
234
249
  const fileHash = computeFileHash(filePath);
235
250
 
236
251
  state.integrations.set(toolConfig.file, {
@@ -543,7 +558,19 @@ function hasUDSMarkers(filePath, format) {
543
558
  try {
544
559
  const content = readFileSync(filePath, 'utf-8');
545
560
  const markers = UDS_MARKERS[format] || UDS_MARKERS.markdown;
546
- return content.includes(markers.start) && content.includes(markers.end);
561
+ // XSPEC adopter-report Q5: `.includes()` matched the marker text
562
+ // anywhere in the file, including inside a sentence that merely mentions
563
+ // it — locateMarkerBlock requires a standalone, non-fenced line. An
564
+ // ambiguous file (two real marker lines) still "has UDS markers" for
565
+ // this boolean's purpose, so that is also true, not an error here.
566
+ try {
567
+ return locateMarkerBlock(content, markers) !== null;
568
+ } catch (error) {
569
+ if (error instanceof AmbiguousMarkerError) {
570
+ return true;
571
+ }
572
+ throw error;
573
+ }
547
574
  } catch {
548
575
  return false;
549
576
  }
@@ -15,8 +15,15 @@ import {
15
15
  MANIFEST_OPTION_BINDINGS,
16
16
  OPTIONS_INSTALL_DIR
17
17
  } from '../core/constants.js';
18
+ import {
19
+ resolveIntegrationTargetFile,
20
+ buildToolIntegrationConfig,
21
+ generateIntegrationContent,
22
+ extractMarkedContent
23
+ } from '../utils/integration-generator.js';
18
24
  import { PathResolver } from '../core/paths.js';
19
25
  import { computeFileHash } from '../utils/hasher.js';
26
+ import { createHash } from 'crypto';
20
27
  import {
21
28
  getSkillsDirForAgent,
22
29
  getCommandsDirForAgent,
@@ -261,6 +268,37 @@ function calculateExtensions(state, manifest) {
261
268
  }
262
269
  }
263
270
 
271
+ /**
272
+ * Compute the block hash generation would produce for this tool right now,
273
+ * using the same builder (`buildToolIntegrationConfig`) and generator
274
+ * (`generateIntegrationContent`) `--apply`/`uds update` themselves use.
275
+ *
276
+ * Hashed the same way `computeIntegrationBlockHash` (hasher.js) hashes an
277
+ * actual on-disk file — extract the marker block, trim, sha256 — so the two
278
+ * are directly comparable. Never throws: generation can fail for reasons
279
+ * outside this module's control (an incomplete/mocked registry, a tool this
280
+ * process cannot resolve, etc.), and the correct response to "cannot compute
281
+ * an expected hash" is `null` — diffIntegrations already knows how to treat
282
+ * that the same as before this fix (always migrate_block), not to crash the
283
+ * whole state calculation over one tool.
284
+ *
285
+ * @param {Object} manifest
286
+ * @param {string} toolName
287
+ * @param {string} format
288
+ * @returns {string|null}
289
+ */
290
+ function computeExpectedIntegrationBlockHash(manifest, toolName, format) {
291
+ try {
292
+ const config = { ...buildToolIntegrationConfig(manifest, toolName), format: manifest.format || 'ai' };
293
+ const generated = generateIntegrationContent({ ...config, tool: toolName });
294
+ const { content: blockContent } = extractMarkedContent(generated, format);
295
+ if (!blockContent) return null;
296
+ return `sha256:${createHash('sha256').update(blockContent).digest('hex')}`;
297
+ } catch {
298
+ return null;
299
+ }
300
+ }
301
+
264
302
  /**
265
303
  * Calculate expected integration files.
266
304
  * For integrations we track the UDS marker block, not the entire file.
@@ -281,11 +319,22 @@ function calculateIntegrations(state, manifest) {
281
319
  const toolConfig = toolName ? SUPPORTED_AI_TOOLS[toolName] : null;
282
320
  if (!toolConfig) continue;
283
321
 
284
- const relativePath = toolConfig.file;
322
+ // XSPEC-418 R2/R3: the desired path is the tool's ACTUAL target, not always
323
+ // its default file — a local-target claude-code install's desired state
324
+ // must say CLAUDE.local.md, or `--apply`/`--plan` (and their orphan
325
+ // detection) would treat that file as unmanaged and CLAUDE.md as desired.
326
+ const relativePath = resolveIntegrationTargetFile(toolName, manifest) || toolConfig.file;
285
327
 
286
328
  state.integrations.set(relativePath, {
287
329
  relativePath,
288
- hash: null, // Integration hashes are computed after generation
330
+ // XSPEC adopter-report Q6: this used to be a hardcoded `null` ("hashes
331
+ // are computed after generation"), which is why diffIntegrations could
332
+ // never tell "content already matches" from "content differs" and
333
+ // unconditionally produced migrate_block — `--plan` never converged,
334
+ // even immediately after `--apply`. Actually generating the content
335
+ // ahead of time (the same generation `--apply` itself runs) gives a
336
+ // real hash to compare against the actual file's block hash.
337
+ hash: computeExpectedIntegrationBlockHash(manifest, toolName, toolConfig.format),
289
338
  size: null,
290
339
  category: 'integration',
291
340
  sourcePath: null, // Generated, not copied from source