universal-dev-standards 6.10.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 (34) hide show
  1. package/bin/uds.js +2 -0
  2. package/bundled/locales/zh-CN/CHANGELOG.md +23 -3
  3. package/bundled/locales/zh-CN/README.md +1 -1
  4. package/bundled/locales/zh-CN/SECURITY.md +1 -1
  5. package/bundled/locales/zh-CN/docs/CHEATSHEET.md +1 -1
  6. package/bundled/locales/zh-CN/docs/CLI-INIT-OPTIONS.md +52 -5
  7. package/bundled/locales/zh-CN/docs/FEATURE-REFERENCE.md +3 -1
  8. package/bundled/locales/zh-TW/CHANGELOG.md +23 -3
  9. package/bundled/locales/zh-TW/README.md +1 -1
  10. package/bundled/locales/zh-TW/SECURITY.md +1 -1
  11. package/bundled/locales/zh-TW/docs/CHEATSHEET.md +1 -1
  12. package/bundled/locales/zh-TW/docs/CLI-INIT-OPTIONS.md +52 -5
  13. package/bundled/locales/zh-TW/docs/FEATURE-REFERENCE.md +3 -1
  14. package/bundled/locales/zh-TW/integrations/claude-code/README.md +14 -5
  15. package/package.json +1 -1
  16. package/src/commands/check.js +253 -21
  17. package/src/commands/config.js +15 -9
  18. package/src/commands/init.js +24 -3
  19. package/src/commands/update.js +311 -47
  20. package/src/core/manifest.js +39 -1
  21. package/src/flows/init-flow.js +9 -1
  22. package/src/generators/layered-claudemd.js +13 -4
  23. package/src/i18n/messages.js +3 -3
  24. package/src/installers/integration-installer.js +13 -6
  25. package/src/installers/manifest-installer.js +4 -0
  26. package/src/reconciler/actual-state-scanner.js +29 -2
  27. package/src/reconciler/desired-state-calculator.js +51 -2
  28. package/src/reconciler/diff-engine.js +19 -3
  29. package/src/reconciler/plan-executor.js +17 -16
  30. package/src/utils/hasher.js +61 -5
  31. package/src/utils/integration-generator.js +239 -28
  32. package/src/utils/marker-locator.js +140 -0
  33. package/src/utils/reference-sync.js +53 -1
  34. package/standards-registry.json +7 -7
@@ -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) {
@@ -26,7 +26,7 @@ import { displayLanguageToLocale } from '../utils/locale.js';
26
26
  import { generateReleaseConfig, RELEASE_MODE_LABELS } from '../utils/release-config.js';
27
27
  import { guardAgainstSelfAdoption } from '../utils/detect-self-adoption.js';
28
28
  import { readInstallYaml } from '../utils/config-manager.js';
29
- import { getToolFilePath } from '../utils/integration-generator.js';
29
+ import { resolveIntegrationTargetFile } from '../utils/integration-generator.js';
30
30
  import { withFileTransaction } from '../utils/transaction.js';
31
31
 
32
32
  /**
@@ -107,7 +107,10 @@ export async function initCommand(options) {
107
107
  // `uds init` re-installs them idempotently.
108
108
  const ownedPaths = [join(projectPath, '.standards')];
109
109
  for (const tool of (config.integrations || config.aiTools || [])) {
110
- const file = getToolFilePath(tool);
110
+ // XSPEC-418 R2: honors --claude-target so a local-target install's owned
111
+ // path is CLAUDE.local.md, not CLAUDE.md — rollback must tear down the
112
+ // file actually written, not the tool's default.
113
+ const file = resolveIntegrationTargetFile(tool, { integrationTargets: config.integrationTargets });
111
114
  if (file) ownedPaths.push(join(projectPath, file));
112
115
  }
113
116
  if (config.generateAgentsMd) ownedPaths.push(join(projectPath, 'AGENTS.md'));
@@ -658,6 +661,23 @@ function buildNonInteractiveConfig(options, detected, projectPath) {
658
661
  ? false // codex/opencode handles AGENTS.md, skip universal output
659
662
  : (options.agentsMd !== undefined ? !!options.agentsMd : true);
660
663
 
664
+ // XSPEC-418 R2: --claude-target <project|local>, default 'project'. An
665
+ // unrecognized value is never substituted quietly (same policy as
666
+ // --content-mode above) — it falls back to 'project' with a warning rather
667
+ // than silently writing into CLAUDE.local.md or vice versa.
668
+ let claudeTargetFlag = options.claudeTarget || 'project';
669
+ if (claudeTargetFlag !== 'project' && claudeTargetFlag !== 'local') {
670
+ console.log(chalk.yellow(
671
+ `⚠ Unknown --claude-target '${claudeTargetFlag}'; using 'project'. Supported: project, local.`
672
+ ));
673
+ claudeTargetFlag = 'project';
674
+ }
675
+ // Only ever set when 'local' — 'project' or unspecified leaves the manifest
676
+ // shape completely unchanged (XSPEC-418 AC-5).
677
+ const integrationTargets = claudeTargetFlag === 'local'
678
+ ? { 'claude-code': 'CLAUDE.local.md' }
679
+ : undefined;
680
+
661
681
  return {
662
682
  languages: options.lang ? [options.lang] : Object.keys(detected.languages).filter(k => detected.languages[k]),
663
683
  frameworks: options.framework ? [options.framework] : Object.keys(detected.frameworks).filter(k => detected.frameworks[k]),
@@ -678,7 +698,8 @@ function buildNonInteractiveConfig(options, detected, projectPath) {
678
698
  generateAgentsMd,
679
699
  releaseMode: options.releaseMode || 'ci-cd',
680
700
  withHooks: !!options.withHooks,
681
- contentLayout: options.contentLayout || 'flat'
701
+ contentLayout: options.contentLayout || 'flat',
702
+ integrationTargets
682
703
  };
683
704
  }
684
705