universal-dev-standards 6.10.0 → 6.12.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 (39) hide show
  1. package/bin/uds.js +2 -0
  2. package/bundled/ai/standards/open-work-tracking.ai.yaml +216 -0
  3. package/bundled/core/open-work-tracking.md +333 -0
  4. package/bundled/locales/zh-CN/CHANGELOG.md +29 -3
  5. package/bundled/locales/zh-CN/CLAUDE.md +1 -1
  6. package/bundled/locales/zh-CN/README.md +2 -2
  7. package/bundled/locales/zh-CN/SECURITY.md +1 -1
  8. package/bundled/locales/zh-CN/docs/CHEATSHEET.md +2 -1
  9. package/bundled/locales/zh-CN/docs/CLI-INIT-OPTIONS.md +52 -5
  10. package/bundled/locales/zh-CN/docs/FEATURE-REFERENCE.md +6 -3
  11. package/bundled/locales/zh-TW/CHANGELOG.md +29 -3
  12. package/bundled/locales/zh-TW/CLAUDE.md +1 -1
  13. package/bundled/locales/zh-TW/README.md +2 -2
  14. package/bundled/locales/zh-TW/SECURITY.md +1 -1
  15. package/bundled/locales/zh-TW/core/open-work-tracking.md +255 -0
  16. package/bundled/locales/zh-TW/docs/CHEATSHEET.md +2 -1
  17. package/bundled/locales/zh-TW/docs/CLI-INIT-OPTIONS.md +52 -5
  18. package/bundled/locales/zh-TW/docs/FEATURE-REFERENCE.md +6 -3
  19. package/bundled/locales/zh-TW/integrations/claude-code/README.md +14 -5
  20. package/package.json +1 -1
  21. package/src/commands/check.js +253 -21
  22. package/src/commands/config.js +15 -9
  23. package/src/commands/init.js +24 -3
  24. package/src/commands/update.js +311 -47
  25. package/src/core/manifest.js +39 -1
  26. package/src/flows/init-flow.js +9 -1
  27. package/src/generators/layered-claudemd.js +13 -4
  28. package/src/i18n/messages.js +3 -3
  29. package/src/installers/integration-installer.js +13 -6
  30. package/src/installers/manifest-installer.js +4 -0
  31. package/src/reconciler/actual-state-scanner.js +29 -2
  32. package/src/reconciler/desired-state-calculator.js +51 -2
  33. package/src/reconciler/diff-engine.js +19 -3
  34. package/src/reconciler/plan-executor.js +17 -16
  35. package/src/utils/hasher.js +61 -5
  36. package/src/utils/integration-generator.js +239 -28
  37. package/src/utils/marker-locator.js +140 -0
  38. package/src/utils/reference-sync.js +53 -1
  39. package/standards-registry.json +19 -7
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  source: ../../../docs/CLI-INIT-OPTIONS.md
3
- source_version: 3.5.1
4
- translation_version: 3.5.1
5
- last_synced: 2026-01-15
3
+ source_version: 3.5.2
4
+ translation_version: 3.5.2
5
+ last_synced: 2026-09-18
6
6
  status: current
7
7
  ---
8
8
 
@@ -10,8 +10,8 @@ status: current
10
10
 
11
11
  > **語言**: [English](../../../docs/CLI-INIT-OPTIONS.md) | 繁體中文 | [简体中文](../../zh-CN/docs/CLI-INIT-OPTIONS.md)
12
12
  >
13
- > **版本**: 3.5.0
14
- > **最後更新**: 2026-01-09
13
+ > **版本**: 3.5.2
14
+ > **最後更新**: 2026-09-18
15
15
 
16
16
  本文件詳細說明 `uds init` 命令的每一個選項,包含使用情境、影響範圍和建議選擇。
17
17
 
@@ -835,8 +835,49 @@ uds init --experimental
835
835
  | 不生成 AGENTS.md | `--no-agents-md` | 跳過 AGENTS.md 生成 |
836
836
  | 強制執行 Hooks | `--with-hooks` | 安裝強制執行 hooks(commit-msg、security、logging) |
837
837
  | 內容佈局 | `--content-layout` | 內容佈局(`flat`、`layered`)- 預設:`flat` |
838
+ | Claude Code 目標檔 | `--claude-target` | Claude Code 整合內容要寫到哪裡:`project`(`CLAUDE.md`,預設)或 `local`(`CLAUDE.local.md`) |
838
839
  | 模式(已棄用) | `-m, --mode` | 安裝模式(skills, full)- 請改用 `--skills-location` |
839
840
 
841
+ ### Claude Code 整合目標檔(`--claude-target`)
842
+
843
+ UDS 預設把 Claude Code 內容寫進 `CLAUDE.md`——團隊共用、會進版控的那個檔案。
844
+ 若你是在一個**已有團隊 `CLAUDE.md`** 的 repo 裡**個人採用** UDS,改用
845
+ `--claude-target local`:UDS 會改寫入 `CLAUDE.local.md`,這是
846
+ [Claude Code 原生支援](https://code.claude.com/docs/en/memory.md)、
847
+ 緊接在 `CLAUDE.md` 之後讀入的檔案,且完全不動團隊的檔案。
848
+
849
+ ```bash
850
+ # 在有團隊 CLAUDE.md 的 repo 裡個人採用
851
+ uds init -y --claude-target local
852
+ ```
853
+
854
+ 使用前有三件事要知道:
855
+
856
+ 1. **要自己把它加進 gitignore。** UDS 不會寫 `.gitignore` 或
857
+ `.git/info/exclude`——請自行把 `CLAUDE.local.md` 加進其中一個,
858
+ 否則它會像任何新檔案一樣被 commit。
859
+ 2. **只存在於建立它的那個 worktree。** 因為(你 gitignore 之後)它是未受版控的檔案,
860
+ 在某個 `git worktree` 建立的 `CLAUDE.local.md` 在同一個 repo 的另一個 worktree
861
+ 看不到——每個 worktree 有自己的工作目錄,未受版控的檔案不會在 worktree 之間共享。
862
+ 若你使用多個 worktree,需要在每一個裡分別執行
863
+ `uds init --claude-target local`(或下方的 `uds update --claude-target local`)。
864
+ 3. **`AGENTS.md` 不受影響。** `--claude-target` 只改變 Claude Code 內容要寫到哪裡。
865
+ 若 `--agents-md` 生成了通用的 `AGENTS.md` 摘要,它仍照常寫進 `AGENTS.md`;
866
+ 若也不想讓它進版控,一樣要自己排除(例如透過 `.git/info/exclude`)。
867
+
868
+ 已經用預設目標檔裝好了,想不重裝就切換?`uds update` 支援同一個旗標:
869
+
870
+ ```bash
871
+ # 把既有安裝的 Claude Code 內容從 CLAUDE.md 搬到 CLAUDE.local.md
872
+ uds update --claude-target local
873
+
874
+ # 搬回去
875
+ uds update --claude-target project
876
+ ```
877
+
878
+ 這會從舊檔移除 UDS 區塊(保留你自己寫在裡面的其他內容)、寫進新檔,並更新
879
+ manifest——之後 `uds check` 驗的是新目標檔,不是舊的。
880
+
840
881
  ### 完整 CLI 範例
841
882
 
842
883
  ```bash
@@ -876,6 +917,12 @@ uds init -y --output-lang traditional-chinese --locale zh-tw
876
917
 
877
918
  # PHP 專案
878
919
  uds init -y --lang php --framework fat-free
920
+
921
+ # 在有團隊 CLAUDE.md 的 repo 裡個人採用
922
+ uds init -y --claude-target local
923
+
924
+ # 之後把既有安裝切換到 CLAUDE.local.md,不需重裝
925
+ uds update --claude-target local
879
926
  ```
880
927
 
881
928
  ---
@@ -1,7 +1,7 @@
1
1
  # UDS 功能參考手冊
2
2
 
3
3
  > Universal Development Standards - 完整功能文件
4
- > Auto-generated | Last updated: 2026-09-14
4
+ > Auto-generated | Last updated: 2026-09-23
5
5
 
6
6
  **Language**: [English](../../../docs/reference/FEATURE-REFERENCE.md) | 繁體中文 | [简体中文](../../zh-CN/docs/FEATURE-REFERENCE.md)
7
7
 
@@ -14,10 +14,10 @@
14
14
  3. [技能](#skills) (55)
15
15
  4. [代理](#agents) (5)
16
16
  5. [工作流程](#workflows) (5)
17
- 6. [核心規範](#core-standards) (152)
17
+ 6. [核心規範](#core-standards) (153)
18
18
  7. [腳本](#scripts) (59)
19
19
 
20
- **Total Features: 350**
20
+ **Total Features: 351**
21
21
 
22
22
  ---
23
23
 
@@ -54,6 +54,7 @@
54
54
  | `--no-agents-md` | Skip AGENTS.md generation |
55
55
  | `--with-hooks` | Install enforcement hooks declared by the installed standards |
56
56
  | `--content-layout` | Content layout (flat, layered) [default: flat] |
57
+ | `--claude-target` | Claude Code integration target: project (default, writes CLAUDE.md) or local (writes CLAUDE.local.md — not committed to git; gitignore it yourself) |
57
58
  | `-y, --yes` | Use defaults, skip interactive prompts |
58
59
  | `-E, --experimental` | Enable experimental features (methodology) |
59
60
  | `--force` | Bypass UDS source-repo self-adoption guard (DEC-044 / XSPEC-071) |
@@ -155,6 +156,7 @@
155
156
  | `--force` | Force update all files, ignoring hash comparison |
156
157
  | `--prune` | Delete .standards/ files UDS wrote but no longer ships (listed without this flag; never touches files UDS did not write) |
157
158
  | `--rollback` | Rollback to the most recent backup |
159
+ | `--claude-target` | Switch an existing install to a different Claude Code integration target: project (CLAUDE.md) or local (CLAUDE.local.md) — moves the UDS block, keeps your content, no reinstall |
158
160
  | `--locale` | Override locale for skills install (zh-tw, zh-cn, en); also reads .uds/install.yaml + UDS_LOCALE env |
159
161
 
160
162
  ### `uds skills`
@@ -514,6 +516,7 @@
514
516
  | `mutation-testing` | 1.1.0 | Mutation testing evaluates test suite effectiveness by injecting artificial bugs |
515
517
  | `no-cicd-deployment` | - | |
516
518
  | `observability-standards` | 1.0.0 | |
519
+ | `open-work-tracking` | 1.0.0 | The deferred-item-exit standard requires that a deferred item leave its document |
517
520
  | `packaging-standards` | 1.1.0 | This standard defines a Recipe-based packaging framework that enables user proje |
518
521
  | `performance-standards` | 1.2.0 | This standard defines comprehensive guidelines for software performance engineer |
519
522
  | `pii-classification` | 1.1.0 | **Status**: Active | **Updated**: 2026-06-19 | |
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  source: ../../../../integrations/claude-code/README.md
3
- source_version: 1.1.0
4
- translation_version: 1.1.0
5
- last_synced: 2026-08-12
3
+ source_version: 1.2.0
4
+ translation_version: 1.2.0
5
+ last_synced: 2026-09-18
6
6
  status: current
7
7
  ---
8
8
 
@@ -10,8 +10,8 @@ status: current
10
10
 
11
11
  > **語言**: English | [繁體中文](README.md)
12
12
 
13
- **版本**: 1.1.0
14
- **最後更新**: 2026-08-12
13
+ **版本**: 1.2.0
14
+ **最後更新**: 2026-09-18
15
15
 
16
16
  本目錄包含將通用開發標準 (Universal Development Standards) 與 [Claude Code](https://docs.anthropic.com/claude-code) 整合的資源。
17
17
 
@@ -63,6 +63,15 @@ npx universal-dev-standards init
63
63
  2. 確保專案中存在 `core/` 目錄。
64
64
  3. 如有需要,安裝技能(請參閱 `skills/README.md`)。
65
65
 
66
+ ### 在已有團隊 `CLAUDE.md` 的 repo 裡個人採用(XSPEC-418)
67
+
68
+ 若 repo 已有進版控、團隊共用的 `CLAUDE.md`,而你要為自己個人採用 UDS,
69
+ 對 `init`(或 `update`,用於切換既有安裝)加上 `--claude-target local`,
70
+ 讓 UDS 改寫進 `CLAUDE.local.md`——Claude Code 會緊接在 `CLAUDE.md` 之後讀取它。
71
+ 你必須自己把 `CLAUDE.local.md` 加進 `.gitignore`(或 `.git/info/exclude`),
72
+ 且注意未受版控的檔案只存在於建立它的那個 git worktree。
73
+ 完整說明見 [CLI-INIT-OPTIONS.md § Claude Code 整合目標檔](../../../../docs/CLI-INIT-OPTIONS.md)。
74
+
66
75
  ## 驗證
67
76
 
68
77
  要驗證整合是否運作正常:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-dev-standards",
3
- "version": "6.10.0",
3
+ "version": "6.12.0",
4
4
  "description": "CLI tool for adopting Universal Development Standards",
5
5
  "keywords": [
6
6
  "documentation",
@@ -12,7 +12,9 @@ import {
12
12
  computeFileHash,
13
13
  compareFileHash,
14
14
  hasFileHashes,
15
- compareIntegrationBlockHash
15
+ compareIntegrationBlockHash,
16
+ computeIntegrationBlockHash,
17
+ pruneIntegrationFileHashes
16
18
  } from '../utils/hasher.js';
17
19
  import { downloadFromGitHub, getMarketplaceSkillsInfo } from '../utils/github.js';
18
20
  import {
@@ -30,7 +32,9 @@ import {
30
32
  findBrokenPathMentions,
31
33
  compareStandardsWithReferences
32
34
  } from '../utils/reference-sync.js';
33
- import { extractMarkedContent, getToolFilePath, parseStandardsIndexCount, writeIntegrationFile } from '../utils/integration-generator.js';
35
+ import { extractMarkedContent, resolveIntegrationTargetFile, parseStandardsIndexCount, writeIntegrationFile } from '../utils/integration-generator.js';
36
+ import { AmbiguousMarkerError } from '../utils/marker-locator.js';
37
+ import { bumpManifestVersion } from '../core/manifest.js';
34
38
  import { INTEGRATION_MAPPINGS } from '../installers/integration-installer.js';
35
39
  import { getToolFormat } from '../core/constants.js';
36
40
  import { checkForUpdates } from '../utils/npm-registry.js';
@@ -137,6 +141,19 @@ function performFileIntegrityCheck(projectPath, manifest, msg) {
137
141
  if (hasFileHashes(manifest)) {
138
142
  // Hash-based integrity check
139
143
  for (const [relativePath, hashInfo] of Object.entries(manifest.fileHashes)) {
144
+ // XSPEC-418 R6 gap 2: a manifest written before this fix can still
145
+ // carry a whole-file entry for a path that is ALSO tracked by its UDS
146
+ // block (integrationBlockHashes) — the pruning added for R6 only runs
147
+ // on a write (uds update / check --restore / --migrate), so a project
148
+ // that has not run one of those yet stayed red on `uds check --ci`
149
+ // forever, for content outside the block that the block check itself
150
+ // says is fine. This is a READ-time skip — it does not touch the
151
+ // manifest, so a plain `uds check` alone cannot fix the underlying
152
+ // stale entry; the write paths still do that (see
153
+ // pruneIntegrationFileHashes in hasher.js).
154
+ if (manifest.integrationBlockHashes && relativePath in manifest.integrationBlockHashes) {
155
+ continue;
156
+ }
140
157
  const fullPath = join(projectPath, relativePath);
141
158
  const status = compareFileHash(fullPath, hashInfo);
142
159
 
@@ -401,12 +418,38 @@ export async function checkCommand(options = {}) {
401
418
  // Check Commands integrity if commandHashes exist
402
419
  checkCommandsIntegrity(manifest, projectPath, msg);
403
420
 
421
+ // XSPEC adopter-report Q3: neither of the two checks above (content-hash
422
+ // integrity) says anything about an installed Skills/Commands version
423
+ // being behind the latest UDS release — that was only ever computed by
424
+ // `uds update --plan --skills`/`--commands`. A plain `uds check` gave no
425
+ // signal at all that `uds update --skills` had anything to do.
426
+ checkSkillsCommandsVersionStaleness(manifest, projectPath, msg);
427
+
404
428
  // Check Integration blocks integrity if integrationBlockHashes exist
405
- checkIntegrationBlocksIntegrity(manifest, projectPath, msg);
429
+ // XSPEC-418 R1: the return value used to be discarded, so a removed/modified
430
+ // UDS block never affected the final verdict below — `uds check --ci` printed
431
+ // "compliant" and exited 0 with a visible ✗ still on screen.
432
+ const integrationBlockStatus = checkIntegrationBlocksIntegrity(manifest, projectPath, msg);
406
433
 
407
434
  // Handle --restore option
408
435
  if (options.restore) {
409
- await restoreFiles(projectPath, manifest, [...fileStatus.modified, ...fileStatus.missing]);
436
+ // XSPEC-418 R6 gap 1: integration files no longer live in `fileHashes`
437
+ // at all (that was the point of R6), so `fileStatus` — built entirely
438
+ // from `fileHashes` — never contains them any more. Without this,
439
+ // `--restore` silently did nothing for a damaged UDS block: it
440
+ // regenerated zero files and printed "Restored 0 file(s)" while the
441
+ // block-modified/markers-removed content sat untouched.
442
+ // `restoreSingleFile`'s integration-file branch (`manifest.integrationConfigs`)
443
+ // does not depend on fileHashes, so feeding these paths in is enough —
444
+ // it already regenerates just the block and leaves everything else alone.
445
+ const integrationFilesToRestore = [
446
+ ...integrationBlockStatus.modified,
447
+ ...integrationBlockStatus.noMarkers,
448
+ ...integrationBlockStatus.missing
449
+ ];
450
+ await restoreFiles(projectPath, manifest, [
451
+ ...new Set([...fileStatus.modified, ...fileStatus.missing, ...integrationFilesToRestore])
452
+ ]);
410
453
  return;
411
454
  }
412
455
 
@@ -470,8 +513,15 @@ export async function checkCommand(options = {}) {
470
513
  displayWorkflowStatus(projectPath);
471
514
 
472
515
  // Final status
516
+ // XSPEC-418 R1: integration block problems (UDS markers removed, block
517
+ // modified, or the tracked file missing) now feed the verdict — they used to
518
+ // be checked and printed above, then silently dropped here.
473
519
  const allGood = fileStatus.missing.length === 0 &&
474
- fileStatus.modified.length === 0;
520
+ fileStatus.modified.length === 0 &&
521
+ integrationBlockStatus.modified.length === 0 &&
522
+ integrationBlockStatus.missing.length === 0 &&
523
+ integrationBlockStatus.noMarkers.length === 0 &&
524
+ integrationBlockStatus.ambiguous.length === 0;
475
525
  if (allGood) {
476
526
  console.log(chalk.green(msg.projectCompliant));
477
527
  } else {
@@ -584,6 +634,11 @@ async function interactiveMode(projectPath, manifest, fileStatus, msg) {
584
634
  }
585
635
 
586
636
  if (manifestUpdated) {
637
+ // XSPEC-418 R6: interactive mode still has a "keep current content" branch
638
+ // that calls the whole-file updateFileHash(); if issue.file happens to be
639
+ // an integration file left in fileHashes by an older CLI, this is the
640
+ // write point that stops it from surviving another round.
641
+ pruneIntegrationFileHashes(manifest);
587
642
  writeManifest(manifest, projectPath);
588
643
  console.log(chalk.green(msg.manifestUpdated));
589
644
  console.log();
@@ -701,6 +756,9 @@ async function restoreFiles(projectPath, manifest, files) {
701
756
  }
702
757
 
703
758
  // Update manifest
759
+ // XSPEC-418 R6: catches any stale whole-file entry for an integration file
760
+ // this particular restore run didn't touch, not just the ones it did.
761
+ pruneIntegrationFileHashes(manifest);
704
762
  writeManifest(manifest, projectPath);
705
763
  console.log(chalk.gray(` ${msg.manifestUpdatedShort}`));
706
764
  console.log();
@@ -728,7 +786,13 @@ export async function restoreSingleFile(projectPath, manifest, relativePath, msg
728
786
  installedStandards: genConfig.installedStandards || manifest.standards || []
729
787
  }, projectPath);
730
788
  if (result.success) {
731
- updateFileHash(projectPath, manifest, relativePath);
789
+ // XSPEC-418 R6: this rewrites the UDS block, so the BLOCK hash is what
790
+ // must be refreshed — not a whole-file hash. Restoring used to call
791
+ // updateFileHash() here, which recorded the whole file (block +
792
+ // whatever the adopter wrote outside it) while leaving the stale block
793
+ // hash untouched; the next `uds check` then reported "UDS block
794
+ // modified" for a block that had just been correctly restored.
795
+ updateIntegrationBlockHash(manifest, relativePath, result.blockHashInfo);
732
796
  console.log(chalk.green(` ✓ ${relativePath}: ${msg.restored}`));
733
797
  return true;
734
798
  }
@@ -750,7 +814,32 @@ export async function restoreSingleFile(projectPath, manifest, relativePath, msg
750
814
  // Integration file - copy to root
751
815
  const result = await copyIntegration(sourcePath, relativePath, projectPath);
752
816
  if (result.success) {
753
- updateFileHash(projectPath, manifest, relativePath);
817
+ // XSPEC-418 R6: same rule as the genConfig branch above — track the
818
+ // block, not the whole file. This legacy static-copy fallback has no
819
+ // blockHashInfo returned to it, so compute one from what was just
820
+ // written; a template with no UDS markers at all (computeIntegration
821
+ // BlockHash returns null) falls back to the old whole-file behaviour,
822
+ // since there is no block to track instead.
823
+ // XSPEC adopter-report Q5: the file was just overwritten wholesale by
824
+ // copyIntegration (a full template copy, not a marker-preserving
825
+ // merge), so an ambiguous marker pair here cannot be this restore's
826
+ // own doing — fall back to whole-file hashing rather than fail a
827
+ // restore that already succeeded on disk.
828
+ let blockHashInfo;
829
+ try {
830
+ blockHashInfo = computeIntegrationBlockHash(join(projectPath, relativePath));
831
+ } catch (error) {
832
+ if (error instanceof AmbiguousMarkerError) {
833
+ blockHashInfo = null;
834
+ } else {
835
+ throw error;
836
+ }
837
+ }
838
+ if (blockHashInfo) {
839
+ updateIntegrationBlockHash(manifest, relativePath, blockHashInfo);
840
+ } else {
841
+ updateFileHash(projectPath, manifest, relativePath);
842
+ }
754
843
  console.log(chalk.green(` ✓ ${relativePath}: ${msg.restored}`));
755
844
  return true;
756
845
  } else {
@@ -770,6 +859,30 @@ export async function restoreSingleFile(projectPath, manifest, relativePath, msg
770
859
  }
771
860
  }
772
861
 
862
+ /**
863
+ * Update an integration file's UDS BLOCK hash in the manifest — not its
864
+ * whole-file hash (XSPEC-418 R6). Also drops any whole-file `fileHashes`
865
+ * entry for the same path, so restoring an integration file can never leave
866
+ * both records behind at once.
867
+ *
868
+ * @param {Object} manifest - Manifest object (mutated in place)
869
+ * @param {string} relativePath - Path as UDS reports it
870
+ * @param {Object|null|undefined} blockHashInfo - `{ blockHash, blockSize, fullHash, fullSize }`,
871
+ * e.g. from `writeIntegrationFile`'s result or `computeIntegrationBlockHash`
872
+ * @returns {boolean} Whether a block hash was recorded
873
+ */
874
+ export function updateIntegrationBlockHash(manifest, relativePath, blockHashInfo) {
875
+ if (!blockHashInfo) return false;
876
+ const normalizedPath = relativePath.replace(/\\/g, '/');
877
+ if (!manifest.integrationBlockHashes) manifest.integrationBlockHashes = {};
878
+ manifest.integrationBlockHashes[normalizedPath] = {
879
+ ...blockHashInfo,
880
+ installedAt: new Date().toISOString()
881
+ };
882
+ if (manifest.fileHashes) delete manifest.fileHashes[normalizedPath];
883
+ return true;
884
+ }
885
+
773
886
  /**
774
887
  * Update file hash in manifest
775
888
  */
@@ -918,21 +1031,54 @@ async function migrateToHashBasedTracking(projectPath, manifest) {
918
1031
  }
919
1032
  }
920
1033
 
921
- // Process integrations
1034
+ // Process integrations — tracked by their UDS BLOCK hash, not a whole-file
1035
+ // hash (XSPEC-418 R6). This loop used to write the whole file into the same
1036
+ // `fileHashes` map as standards/extensions above, which is the same defect
1037
+ // as every other write site R6 fixes, just reached by `--migrate` instead
1038
+ // of a normal update: any content the adopter had outside the block then
1039
+ // read as "modified" by standards-file integrity, contradicting the block
1040
+ // check in the same `uds check` run.
1041
+ const integrationBlockHashes = { ...(manifest.integrationBlockHashes || {}) };
922
1042
  for (const intEntry of manifest.integrations) {
923
1043
  const int = resolveIntegrationFile(intEntry) || intEntry;
924
1044
  const fullPath = join(projectPath, int);
925
1045
 
926
- const hashInfo = computeFileHash(fullPath);
927
- if (hashInfo) {
928
- fileHashes[int] = { ...hashInfo, installedAt: now };
1046
+ // XSPEC adopter-report Q5: this is a read-only manifest migration over
1047
+ // every tracked integration file at once — one file with an ambiguous
1048
+ // marker pair must not abort migrating the rest. It is simply left
1049
+ // without a block hash, same as a file with no markers at all; `uds
1050
+ // check`'s own block-integrity pass (below) is what surfaces the
1051
+ // ambiguity to the user, with line numbers.
1052
+ let blockHashInfo;
1053
+ try {
1054
+ blockHashInfo = computeIntegrationBlockHash(fullPath);
1055
+ } catch (error) {
1056
+ if (error instanceof AmbiguousMarkerError) {
1057
+ blockHashInfo = null;
1058
+ } else {
1059
+ throw error;
1060
+ }
1061
+ }
1062
+ if (blockHashInfo) {
1063
+ integrationBlockHashes[int] = { ...blockHashInfo, installedAt: now };
929
1064
  count++;
1065
+ } else {
1066
+ // No UDS markers found (plaintext template with none, or a file the
1067
+ // adopter fully rewrote) — fall back to whole-file tracking, same as
1068
+ // before this fix, rather than silently tracking nothing.
1069
+ const hashInfo = computeFileHash(fullPath);
1070
+ if (hashInfo) {
1071
+ fileHashes[int] = { ...hashInfo, installedAt: now };
1072
+ count++;
1073
+ }
930
1074
  }
931
1075
  }
932
1076
 
933
1077
  // Update manifest
934
1078
  manifest.fileHashes = fileHashes;
935
- manifest.version = '3.1.0';
1079
+ manifest.integrationBlockHashes = integrationBlockHashes;
1080
+ bumpManifestVersion(manifest);
1081
+ pruneIntegrationFileHashes(manifest);
936
1082
  writeManifest(manifest, projectPath);
937
1083
 
938
1084
  console.log(chalk.green(msg.migratedCount.replace('{count}', count)));
@@ -1220,7 +1366,9 @@ function checkIntegrationFiles(manifest, projectPath, msg) {
1220
1366
  // verdict — the tools that share it are named on the line instead.
1221
1367
  const toolsByFile = new Map();
1222
1368
  for (const tool of manifest.aiTools) {
1223
- const file = getToolFilePath(tool);
1369
+ // XSPEC-418 R3: check must inspect the actual target file (e.g. CLAUDE.local.md),
1370
+ // not the tool's hardcoded default.
1371
+ const file = resolveIntegrationTargetFile(tool, manifest);
1224
1372
  if (!file) continue;
1225
1373
  if (!toolsByFile.has(file)) toolsByFile.set(file, []);
1226
1374
  toolsByFile.get(file).push(tool);
@@ -1251,7 +1399,20 @@ function checkIntegrationFiles(manifest, projectPath, msg) {
1251
1399
 
1252
1400
  // Check for standards index marker
1253
1401
  const format = getToolFormat(tool);
1254
- const { content: markedContent } = extractMarkedContent(content, format);
1402
+ let markedContent;
1403
+ try {
1404
+ ({ content: markedContent } = extractMarkedContent(content, format));
1405
+ } catch (error) {
1406
+ if (error instanceof AmbiguousMarkerError) {
1407
+ // XSPEC adopter-report Q5: report explicitly with line numbers
1408
+ // instead of guessing which marker pair is real, or silently
1409
+ // treating the file as if it had no UDS block at all.
1410
+ console.log(chalk.red(` ✗ ${toolFile}: ${error.message}`));
1411
+ hasIssues = true;
1412
+ continue;
1413
+ }
1414
+ throw error;
1415
+ }
1255
1416
  const hasStandardsIndex = markedContent.length > 0 ||
1256
1417
  content.includes('Standards Index') ||
1257
1418
  content.includes('Standards Compliance');
@@ -1607,6 +1768,59 @@ async function checkCliVersion(bundledVersion) {
1607
1768
  // Enhanced Integrity Check Functions (v3.3.0+)
1608
1769
  // ============================================================
1609
1770
 
1771
+ /**
1772
+ * Warn when an installed Skills or Commands version is behind the latest
1773
+ * UDS release. This is deliberately modeled on the existing top-level
1774
+ * "Version: X → Y ⚠" row (see the `hasUpdate` check in the summary
1775
+ * dashboard below) rather than on `checkIntegrationBlocksIntegrity`
1776
+ * (XSPEC-418 R1): a version simply being behind the latest release is
1777
+ * ambient, expected state until the adopter chooses to update — not a
1778
+ * compliance defect the way a modified/missing/ambiguous UDS block is — so
1779
+ * it is reported but, unlike a block problem, does not fail `--ci`.
1780
+ *
1781
+ * Skills staleness is read per-installation from what is actually on disk
1782
+ * (`getInstalledSkillsInfoForAgent`), the same source `uds update --plan
1783
+ * --skills` uses, because different agents/levels can be out of sync
1784
+ * independently. Commands have no per-installation version on disk (only a
1785
+ * file count), so Commands staleness is read from the one version the
1786
+ * manifest itself records (`manifest.commands.version`).
1787
+ * // implements XSPEC adopter-report Q3
1788
+ *
1789
+ * @param {Object} manifest
1790
+ * @param {string} projectPath
1791
+ * @param {Object} msg - Localized messages (unused today; kept for symmetry
1792
+ * with the other Enhanced Integrity Check functions, which all take one)
1793
+ */
1794
+ function checkSkillsCommandsVersionStaleness(manifest, projectPath, msg) { // eslint-disable-line no-unused-vars
1795
+ const repoInfo = getRepositoryInfo();
1796
+ const latestVersion = repoInfo.skills.version;
1797
+ const stale = [];
1798
+
1799
+ const skillsInstallations = (manifest.skills?.installations || []).filter((i) => i.level !== 'marketplace');
1800
+ for (const inst of skillsInstallations) {
1801
+ const info = getInstalledSkillsInfoForAgent(inst.agent, inst.level, projectPath);
1802
+ const current = info?.version;
1803
+ if (current && current !== latestVersion) {
1804
+ stale.push(`${getAgentDisplayName(inst.agent)} (${inst.level}): Skills v${current} → v${latestVersion}`);
1805
+ }
1806
+ }
1807
+
1808
+ if (manifest.commands?.installed && (manifest.commands?.installations || []).length > 0) {
1809
+ const current = manifest.commands.version;
1810
+ if (current && current !== latestVersion) {
1811
+ stale.push(`Commands: v${current} → v${latestVersion}`);
1812
+ }
1813
+ }
1814
+
1815
+ if (stale.length === 0) return;
1816
+
1817
+ for (const line of stale) {
1818
+ console.log(chalk.yellow(` ⚠ ${line}`));
1819
+ }
1820
+ console.log(chalk.gray(' Run `uds update --plan --skills` / `--commands` for details, then `--apply` to update.'));
1821
+ console.log();
1822
+ }
1823
+
1610
1824
  /**
1611
1825
  * Check Skills files integrity against stored hashes
1612
1826
  * @param {Object} manifest - Manifest object
@@ -1789,19 +2003,19 @@ export function checkCommandsIntegrity(manifest, projectPath, msg) {
1789
2003
  * @param {Object} manifest - Manifest object
1790
2004
  * @param {string} projectPath - Project root path
1791
2005
  * @param {Object} msg - Localized messages
1792
- * @returns {Object} Status { unchanged: [], modified: [], missing: [], noMarkers: [] }
2006
+ * @returns {Object} Status { unchanged: [], modified: [], missing: [], noMarkers: [], ambiguous: [] }
1793
2007
  */
1794
2008
  function checkIntegrationBlocksIntegrity(manifest, projectPath, msg) {
1795
2009
  const blockHashes = manifest.integrationBlockHashes;
1796
2010
 
1797
2011
  // Skip if no block hashes tracked
1798
2012
  if (!blockHashes || Object.keys(blockHashes).length === 0) {
1799
- return { unchanged: [], modified: [], missing: [], noMarkers: [], tracked: false };
2013
+ return { unchanged: [], modified: [], missing: [], noMarkers: [], ambiguous: [], tracked: false };
1800
2014
  }
1801
2015
 
1802
2016
  console.log(chalk.cyan(msg.integrationBlocksCheck || 'Integration UDS Block Integrity'));
1803
2017
 
1804
- const status = { unchanged: [], modified: [], missing: [], noMarkers: [], tracked: true };
2018
+ const status = { unchanged: [], modified: [], missing: [], noMarkers: [], ambiguous: [], tracked: true };
1805
2019
 
1806
2020
  for (const [filePath, hashInfo] of Object.entries(blockHashes)) {
1807
2021
  const fullPath = join(projectPath, filePath);
@@ -1813,7 +2027,21 @@ function checkIntegrationBlocksIntegrity(manifest, projectPath, msg) {
1813
2027
  }
1814
2028
 
1815
2029
  // Compare block hash
1816
- const blockStatus = compareIntegrationBlockHash(fullPath, hashInfo);
2030
+ // XSPEC adopter-report Q5: a file with two real START or END marker
2031
+ // lines is not "modified" and not "no markers" — it is ambiguous, and
2032
+ // the whole point of this fix is to report that explicitly (with line
2033
+ // numbers) instead of silently picking one and guessing.
2034
+ let blockStatus;
2035
+ try {
2036
+ blockStatus = compareIntegrationBlockHash(fullPath, hashInfo);
2037
+ } catch (error) {
2038
+ if (error instanceof AmbiguousMarkerError) {
2039
+ status.ambiguous.push(filePath);
2040
+ console.log(chalk.red(` ✗ ${filePath}: ${error.message}`));
2041
+ continue;
2042
+ }
2043
+ throw error;
2044
+ }
1817
2045
 
1818
2046
  switch (blockStatus) {
1819
2047
  case 'unchanged':
@@ -1835,17 +2063,21 @@ function checkIntegrationBlocksIntegrity(manifest, projectPath, msg) {
1835
2063
  }
1836
2064
 
1837
2065
  // Summary
1838
- if (status.modified.length === 0 && status.missing.length === 0 && status.noMarkers.length === 0) {
2066
+ if (status.modified.length === 0 && status.missing.length === 0 && status.noMarkers.length === 0 && status.ambiguous.length === 0) {
1839
2067
  console.log(chalk.green(` ✓ ${msg.allBlocksIntact || 'All UDS blocks intact'} (${status.unchanged.length} files)`));
1840
2068
  console.log(chalk.gray(` ${msg.userContentPreserved || 'User customizations outside UDS blocks are preserved'}`));
1841
2069
  } else {
1842
2070
  console.log(chalk.gray(` ${(msg.blocksIntegritySummary || '{unchanged} intact, {modified} modified, {missing} missing')
1843
2071
  .replace('{unchanged}', status.unchanged.length)
1844
2072
  .replace('{modified}', status.modified.length)
1845
- .replace('{missing}', status.missing.length + status.noMarkers.length)}`));
2073
+ .replace('{missing}', status.missing.length + status.noMarkers.length + status.ambiguous.length)}`));
1846
2074
 
1847
2075
  if (status.modified.length > 0 || status.noMarkers.length > 0) {
1848
- console.log(chalk.yellow(` ${msg.runUpdateIntegrations || 'Run "uds update --integrations-only" to restore UDS content'}`));
2076
+ // XSPEC-418 R6 gap 1: `uds check --restore` now regenerates just the
2077
+ // UDS block for these files too (it used to do nothing for them — see
2078
+ // the --restore handler above), so it is named alongside `uds update
2079
+ // --integrations-only` rather than as a second-choice remedy.
2080
+ console.log(chalk.yellow(` ${msg.runUpdateIntegrations || 'Run "uds check --restore" or "uds update --integrations-only" to restore UDS content'}`));
1849
2081
  }
1850
2082
  }
1851
2083
 
@@ -45,11 +45,11 @@ import {
45
45
  import { displayLanguageToLocale } from '../utils/locale.js';
46
46
  import {
47
47
  writeIntegrationFile,
48
- getToolFilePath
48
+ resolveIntegrationTargetFile
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
+ import { mergeInstalledNames, bumpManifestVersion } from '../core/manifest.js';
53
53
 
54
54
  /**
55
55
  * Get localized message with fallback (for config-specific keys)
@@ -667,13 +667,16 @@ export async function runProjectConfiguration(options) {
667
667
  // Remove integration files for removed tools
668
668
  const spinner = createSpinner(msgObj.removingIntegrations).start();
669
669
  for (const tool of result.tools) {
670
- const filePath = join(projectPath, getToolFilePath(tool));
670
+ // XSPEC-418 R3: remove the file this tool actually targets (e.g.
671
+ // CLAUDE.local.md), not always its hardcoded default.
672
+ const removedToolFile = resolveIntegrationTargetFile(tool, manifest);
673
+ const filePath = join(projectPath, removedToolFile);
671
674
  if (existsSync(filePath)) {
672
675
  try {
673
676
  unlinkSync(filePath);
674
- console.log(chalk.gray(` ${msgObj.removed}: ${getToolFilePath(tool)}`));
677
+ console.log(chalk.gray(` ${msgObj.removed}: ${removedToolFile}`));
675
678
  } catch {
676
- console.log(chalk.yellow(` ${msgObj.couldNotRemove}: ${getToolFilePath(tool)}`));
679
+ console.log(chalk.yellow(` ${msgObj.couldNotRemove}: ${removedToolFile}`));
677
680
  }
678
681
  }
679
682
 
@@ -711,7 +714,7 @@ export async function runProjectConfiguration(options) {
711
714
  }
712
715
 
713
716
  // Remove from integrationBlockHashes (keyed by tool file path)
714
- const toolFileName = getToolFilePath(tool);
717
+ const toolFileName = removedToolFile;
715
718
  if (manifest.integrationBlockHashes?.[toolFileName]) {
716
719
  delete manifest.integrationBlockHashes[toolFileName];
717
720
  }
@@ -934,7 +937,8 @@ export async function runProjectConfiguration(options) {
934
937
  const generatedFiles = new Set();
935
938
 
936
939
  for (const tool of newAITools) {
937
- const targetFile = getToolFilePath(tool);
940
+ // XSPEC-418 R3
941
+ const targetFile = resolveIntegrationTargetFile(tool, manifest);
938
942
  if (generatedFiles.has(targetFile)) {
939
943
  continue; // Skip if already generated (AGENTS.md sharing)
940
944
  }
@@ -947,7 +951,9 @@ export async function runProjectConfiguration(options) {
947
951
  standardsFormat: manifest.format || 'ai',
948
952
  contentMode: newContentMode,
949
953
  // Pass output_language for dynamic commit standards generation
950
- outputLanguage: newOptions.output_language || 'english'
954
+ outputLanguage: newOptions.output_language || 'english',
955
+ // XSPEC-418 R2/R3
956
+ integrationTargets: manifest.integrationTargets
951
957
  };
952
958
 
953
959
  const result = writeIntegrationFile(tool, toolConfig, projectPath);
@@ -1052,7 +1058,7 @@ export async function runProjectConfiguration(options) {
1052
1058
  manifest.options = newOptions;
1053
1059
  manifest.contentMode = newContentMode;
1054
1060
  manifest.aiTools = newAITools;
1055
- manifest.version = '3.2.0';
1061
+ bumpManifestVersion(manifest);
1056
1062
 
1057
1063
  // Update methodology
1058
1064
  if (newMethodology) {