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.
- package/bin/uds.js +2 -0
- package/bundled/ai/standards/open-work-tracking.ai.yaml +216 -0
- package/bundled/core/open-work-tracking.md +333 -0
- package/bundled/locales/zh-CN/CHANGELOG.md +29 -3
- package/bundled/locales/zh-CN/CLAUDE.md +1 -1
- package/bundled/locales/zh-CN/README.md +2 -2
- package/bundled/locales/zh-CN/SECURITY.md +1 -1
- package/bundled/locales/zh-CN/docs/CHEATSHEET.md +2 -1
- package/bundled/locales/zh-CN/docs/CLI-INIT-OPTIONS.md +52 -5
- package/bundled/locales/zh-CN/docs/FEATURE-REFERENCE.md +6 -3
- package/bundled/locales/zh-TW/CHANGELOG.md +29 -3
- package/bundled/locales/zh-TW/CLAUDE.md +1 -1
- package/bundled/locales/zh-TW/README.md +2 -2
- package/bundled/locales/zh-TW/SECURITY.md +1 -1
- package/bundled/locales/zh-TW/core/open-work-tracking.md +255 -0
- package/bundled/locales/zh-TW/docs/CHEATSHEET.md +2 -1
- package/bundled/locales/zh-TW/docs/CLI-INIT-OPTIONS.md +52 -5
- package/bundled/locales/zh-TW/docs/FEATURE-REFERENCE.md +6 -3
- package/bundled/locales/zh-TW/integrations/claude-code/README.md +14 -5
- package/package.json +1 -1
- package/src/commands/check.js +253 -21
- package/src/commands/config.js +15 -9
- package/src/commands/init.js +24 -3
- package/src/commands/update.js +311 -47
- package/src/core/manifest.js +39 -1
- package/src/flows/init-flow.js +9 -1
- package/src/generators/layered-claudemd.js +13 -4
- package/src/i18n/messages.js +3 -3
- package/src/installers/integration-installer.js +13 -6
- package/src/installers/manifest-installer.js +4 -0
- package/src/reconciler/actual-state-scanner.js +29 -2
- package/src/reconciler/desired-state-calculator.js +51 -2
- package/src/reconciler/diff-engine.js +19 -3
- package/src/reconciler/plan-executor.js +17 -16
- package/src/utils/hasher.js +61 -5
- package/src/utils/integration-generator.js +239 -28
- package/src/utils/marker-locator.js +140 -0
- package/src/utils/reference-sync.js +53 -1
- package/standards-registry.json +19 -7
package/src/commands/init.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
|
package/src/commands/update.js
CHANGED
|
@@ -6,10 +6,11 @@ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from '
|
|
|
6
6
|
import { join, basename, dirname, relative } from 'path';
|
|
7
7
|
import { readManifest, writeManifest, copyStandard, isInitialized, getRepoRoot } from '../utils/copier.js';
|
|
8
8
|
import { getRepositoryInfo, getAllStandards, getShippableFilenames, getStandardSource } from '../utils/registry.js';
|
|
9
|
-
import { computeFileHash, planStandardsRemovals, refreshIntegrationBlockHashes } from '../utils/hasher.js';
|
|
9
|
+
import { computeFileHash, planStandardsRemovals, refreshIntegrationBlockHashes, pruneIntegrationFileHashes } from '../utils/hasher.js';
|
|
10
|
+
import { AmbiguousMarkerError } from '../utils/marker-locator.js';
|
|
10
11
|
import {
|
|
11
12
|
writeIntegrationFile,
|
|
12
|
-
|
|
13
|
+
resolveIntegrationTargetFile,
|
|
13
14
|
writeAgentsMdSummary,
|
|
14
15
|
resolveContentModeForTool,
|
|
15
16
|
generateIntegrationContent,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
} from '../utils/integration-generator.js';
|
|
19
20
|
import {
|
|
20
21
|
calculateCategoriesFromStandards,
|
|
22
|
+
repairIntegrationConfigCategories,
|
|
21
23
|
arraysEqual,
|
|
22
24
|
getToolFromPath
|
|
23
25
|
} from '../utils/reference-sync.js';
|
|
@@ -56,13 +58,14 @@ import {
|
|
|
56
58
|
} from '../reconciler/index.js';
|
|
57
59
|
import { restoreSingleFile } from './check.js';
|
|
58
60
|
import { guardAgainstSelfAdoption } from '../utils/detect-self-adoption.js';
|
|
59
|
-
import { resolveIntegrationFile, SUPPORTED_AI_TOOLS } from '../core/constants.js';
|
|
61
|
+
import { resolveIntegrationFile, SUPPORTED_AI_TOOLS, getToolFormat } from '../core/constants.js';
|
|
60
62
|
import {
|
|
61
63
|
mergeInstalledNames,
|
|
62
64
|
recordFileProvenance,
|
|
63
65
|
forgetFileProvenance,
|
|
64
66
|
establishProvenance,
|
|
65
|
-
isProvenanceEstablished
|
|
67
|
+
isProvenanceEstablished,
|
|
68
|
+
bumpManifestVersion
|
|
66
69
|
} from '../core/manifest.js';
|
|
67
70
|
|
|
68
71
|
/**
|
|
@@ -438,6 +441,15 @@ export async function updateCommand(options) {
|
|
|
438
441
|
console.log(chalk.bold(msg.title));
|
|
439
442
|
console.log(chalk.gray('─'.repeat(50)));
|
|
440
443
|
|
|
444
|
+
// Handle --claude-target option (XSPEC-418 R4): switch an EXISTING
|
|
445
|
+
// installation's claude-code integration target between CLAUDE.md and
|
|
446
|
+
// CLAUDE.local.md, without a full reinstall. Standalone, like --sync-refs
|
|
447
|
+
// below — it does not compose with other update modes/scopes.
|
|
448
|
+
if (options.claudeTarget) {
|
|
449
|
+
await switchClaudeTarget(projectPath, manifest, options.claudeTarget, options);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
441
453
|
// Handle --sync-refs option.
|
|
442
454
|
// `--plan` is honoured here too. This branch is above the mode dispatch
|
|
443
455
|
// because sync-refs is its own operation rather than a scope of the
|
|
@@ -489,7 +501,7 @@ export async function updateCommand(options) {
|
|
|
489
501
|
// Handle --plan option (DSR dry-run). Nothing below this line writes.
|
|
490
502
|
if (options.plan) {
|
|
491
503
|
if (!scopedToSkills && !scopedToCommands) {
|
|
492
|
-
await handlePlan(projectPath, options);
|
|
504
|
+
await handlePlan(projectPath, options, manifest);
|
|
493
505
|
}
|
|
494
506
|
if (scopedToSkills) await planSkills(projectPath, manifest, options);
|
|
495
507
|
if (scopedToCommands) await planCommands(projectPath, manifest, options);
|
|
@@ -752,6 +764,16 @@ export async function updateCommand(options) {
|
|
|
752
764
|
|
|
753
765
|
// Update integrations (unless --standards-only)
|
|
754
766
|
if (!options.standardsOnly && manifest.integrations && manifest.integrations.length > 0) {
|
|
767
|
+
// XSPEC adopter-report Q1 follow-up: this block writes
|
|
768
|
+
// integrationBlockHashes but never touched manifest.integrationConfigs at
|
|
769
|
+
// all, so a manifest that picked up a broken (empty/unrecognized)
|
|
770
|
+
// categories array from an older buggy `--sync-refs` run stayed broken
|
|
771
|
+
// through every subsequent plain `uds update` — the only path that
|
|
772
|
+
// repaired it was `--sync-refs` itself. Self-heal corruption here too, not
|
|
773
|
+
// just there; an already-valid list is left alone (see
|
|
774
|
+
// repairIntegrationConfigCategories's docblock for why).
|
|
775
|
+
repairIntegrationConfigCategories(manifest);
|
|
776
|
+
|
|
755
777
|
const intSpinner = createSpinner(msg.syncingIntegrations).start();
|
|
756
778
|
|
|
757
779
|
// Build installed standards list
|
|
@@ -773,7 +795,9 @@ export async function updateCommand(options) {
|
|
|
773
795
|
const aiTools = manifest.aiTools || [];
|
|
774
796
|
|
|
775
797
|
for (const tool of aiTools) {
|
|
776
|
-
|
|
798
|
+
// XSPEC-418 R3: honors manifest.integrationTargets — the tool's file may
|
|
799
|
+
// be CLAUDE.local.md, not the hardcoded default.
|
|
800
|
+
const targetFile = resolveIntegrationTargetFile(tool, manifest);
|
|
777
801
|
if (generatedFiles.has(targetFile)) {
|
|
778
802
|
continue; // Skip if already generated (AGENTS.md sharing)
|
|
779
803
|
}
|
|
@@ -790,7 +814,9 @@ export async function updateCommand(options) {
|
|
|
790
814
|
contentMode: resolved.contentMode,
|
|
791
815
|
level: resolved.level,
|
|
792
816
|
// Pass output_language for dynamic commit standards generation
|
|
793
|
-
outputLanguage: manifest.options?.output_language || manifest.options?.commit_language || 'english'
|
|
817
|
+
outputLanguage: manifest.options?.output_language || manifest.options?.commit_language || 'english',
|
|
818
|
+
// XSPEC-418 R2/R3: so writeIntegrationFile resolves the actual target.
|
|
819
|
+
integrationTargets: manifest.integrationTargets
|
|
794
820
|
};
|
|
795
821
|
|
|
796
822
|
const result = writeIntegrationFile(tool, toolConfig, projectPath);
|
|
@@ -849,7 +875,11 @@ export async function updateCommand(options) {
|
|
|
849
875
|
if (manifest.integrationBlockHashes) {
|
|
850
876
|
const expectedFiles = new Set();
|
|
851
877
|
for (const tool of (manifest.aiTools || [])) {
|
|
852
|
-
|
|
878
|
+
// XSPEC-418 R3: an expected set built from the hardcoded default file
|
|
879
|
+
// pruned CLAUDE.local.md's own hash the moment a local-target install
|
|
880
|
+
// ran this cleanup — it looked orphaned because nothing here knew the
|
|
881
|
+
// real target had moved.
|
|
882
|
+
const targetFile = resolveIntegrationTargetFile(tool, manifest);
|
|
853
883
|
if (targetFile) expectedFiles.add(targetFile);
|
|
854
884
|
}
|
|
855
885
|
// Universal AGENTS.md is tracked when generateAgentsMd is enabled.
|
|
@@ -880,8 +910,14 @@ export async function updateCommand(options) {
|
|
|
880
910
|
if (!layeredResult.fallback) {
|
|
881
911
|
console.log(chalk.green(` ✓ Layered CLAUDE.md updated (${layeredResult.generatedFiles.length} files)`));
|
|
882
912
|
}
|
|
883
|
-
} catch {
|
|
884
|
-
//
|
|
913
|
+
} catch (error) {
|
|
914
|
+
// XSPEC adopter-report Q5: an ambiguous marker pair is a real problem
|
|
915
|
+
// the adopter needs to see — refuse that one write and say why,
|
|
916
|
+
// rather than folding it into "generator not available".
|
|
917
|
+
if (error instanceof AmbiguousMarkerError) {
|
|
918
|
+
console.log(chalk.red(` ✗ Layered CLAUDE.md not updated: ${error.message}`));
|
|
919
|
+
}
|
|
920
|
+
// Otherwise: silently skip if generator not available
|
|
885
921
|
}
|
|
886
922
|
}
|
|
887
923
|
|
|
@@ -951,14 +987,14 @@ export async function updateCommand(options) {
|
|
|
951
987
|
}
|
|
952
988
|
}
|
|
953
989
|
|
|
954
|
-
//
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
990
|
+
// XSPEC-418 R6: integration files (results.integrations — CLAUDE.md,
|
|
991
|
+
// CLAUDE.local.md, AGENTS.md, etc.) are tracked by their UDS block in
|
|
992
|
+
// integrationBlockHashes, not by whole-file hash here. A whole-file entry
|
|
993
|
+
// for one of these disagrees the moment the adopter edits anything outside
|
|
994
|
+
// the block — exactly the customization the marker-based update preserves —
|
|
995
|
+
// and made `uds check` report "modified" while its own block-integrity
|
|
996
|
+
// check said the block was intact. See pruneIntegrationFileHashes below for
|
|
997
|
+
// cleanup of any such entry an older CLI already wrote.
|
|
962
998
|
|
|
963
999
|
// Record what UDS wrote this run, then decide what (if anything) may go.
|
|
964
1000
|
// (XSPEC-384 R1/R2/R3)
|
|
@@ -1086,7 +1122,7 @@ export async function updateCommand(options) {
|
|
|
1086
1122
|
// date. The manifest is still written so that hash/migration bookkeeping for
|
|
1087
1123
|
// the files that DID succeed is persisted.
|
|
1088
1124
|
const updateIncomplete = results.errors.length > 0;
|
|
1089
|
-
manifest
|
|
1125
|
+
bumpManifestVersion(manifest);
|
|
1090
1126
|
if (!updateIncomplete) {
|
|
1091
1127
|
manifest.upstream.version = latestVersion;
|
|
1092
1128
|
manifest.upstream.installed = new Date().toISOString().split('T')[0];
|
|
@@ -1119,8 +1155,9 @@ export async function updateCommand(options) {
|
|
|
1119
1155
|
allTrackedFiles.push(join('.standards', fileName));
|
|
1120
1156
|
}
|
|
1121
1157
|
for (const intEntry of (manifest.integrations || [])) {
|
|
1122
|
-
// 兩種形狀都要能解出路徑(XSPEC-343 R1
|
|
1123
|
-
|
|
1158
|
+
// 兩種形狀都要能解出路徑(XSPEC-343 R1),且要尊重 manifest.integrationTargets
|
|
1159
|
+
// 的目標覆寫(XSPEC-418 R2/R3)——resolveIntegrationTargetFile 已同時處理兩者。
|
|
1160
|
+
const filePath = resolveIntegrationTargetFile(intEntry, manifest);
|
|
1124
1161
|
if (filePath) {
|
|
1125
1162
|
allTrackedFiles.push(filePath);
|
|
1126
1163
|
}
|
|
@@ -1641,6 +1678,140 @@ async function offerErrorExitGate(projectPath, options) {
|
|
|
1641
1678
|
console.log();
|
|
1642
1679
|
}
|
|
1643
1680
|
|
|
1681
|
+
/**
|
|
1682
|
+
* Switch the claude-code integration's target file between CLAUDE.md and
|
|
1683
|
+
* CLAUDE.local.md for an EXISTING installation, without a full reinstall.
|
|
1684
|
+
* // implements XSPEC-418 R4
|
|
1685
|
+
*
|
|
1686
|
+
* The scenario this exists for: an adopter installed with the default target
|
|
1687
|
+
* (CLAUDE.md), then either hand-moved the UDS block to CLAUDE.local.md (it now
|
|
1688
|
+
* exists there with markers already) or wants to move it there for the first
|
|
1689
|
+
* time. Either way `uds update --claude-target local` should leave exactly one
|
|
1690
|
+
* up-to-date block in the new target and none in the old one.
|
|
1691
|
+
*
|
|
1692
|
+
* @param {string} projectPath
|
|
1693
|
+
* @param {Object} manifest - Mutated and written to disk on success
|
|
1694
|
+
* @param {string} target - 'project' | 'local'
|
|
1695
|
+
* @param {Object} options - CLI options (unused today; kept for symmetry with
|
|
1696
|
+
* the other option handlers and so a future confirmation prompt has somewhere
|
|
1697
|
+
* to read --yes from)
|
|
1698
|
+
*/
|
|
1699
|
+
async function switchClaudeTarget(projectPath, manifest, target, options) { // eslint-disable-line no-unused-vars
|
|
1700
|
+
if (target !== 'project' && target !== 'local') {
|
|
1701
|
+
console.log(chalk.red(` ✗ --claude-target must be "project" or "local", got "${target}"`));
|
|
1702
|
+
console.log();
|
|
1703
|
+
process.exitCode = 1;
|
|
1704
|
+
return;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
if (!(manifest.aiTools || []).includes('claude-code')) {
|
|
1708
|
+
console.log(chalk.yellow(' claude-code is not among the configured AI tools — nothing to switch.'));
|
|
1709
|
+
console.log();
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
const oldFile = resolveIntegrationTargetFile('claude-code', manifest);
|
|
1714
|
+
const newFile = target === 'local' ? 'CLAUDE.local.md' : 'CLAUDE.md';
|
|
1715
|
+
|
|
1716
|
+
if (oldFile === newFile) {
|
|
1717
|
+
console.log(chalk.gray(` claude-code integration already targets ${newFile}. Nothing to do.`));
|
|
1718
|
+
console.log();
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
console.log(chalk.cyan(` Switching claude-code integration target: ${oldFile} → ${newFile}`));
|
|
1723
|
+
|
|
1724
|
+
// 1. Remove the UDS block from the OLD file, preserving any user content —
|
|
1725
|
+
// the same rule uninstallIntegrations already applies. A file left 100%
|
|
1726
|
+
// UDS-generated once the block is gone is deleted outright.
|
|
1727
|
+
const oldPath = join(projectPath, oldFile);
|
|
1728
|
+
if (existsSync(oldPath)) {
|
|
1729
|
+
const format = getToolFormat('claude-code');
|
|
1730
|
+
const content = readFileSync(oldPath, 'utf-8');
|
|
1731
|
+
// XSPEC adopter-report Q5: an ambiguous marker pair means this write
|
|
1732
|
+
// must refuse rather than guess which block to move — report and leave
|
|
1733
|
+
// the old file untouched instead of switching targets on a bad guess.
|
|
1734
|
+
let parts;
|
|
1735
|
+
try {
|
|
1736
|
+
parts = extractMarkedContent(content, format);
|
|
1737
|
+
} catch (error) {
|
|
1738
|
+
if (error instanceof AmbiguousMarkerError) {
|
|
1739
|
+
console.log(chalk.red(` ✗ ${oldFile}: ${error.message}`));
|
|
1740
|
+
console.log(chalk.gray(` Not switching — fix the marker pair in ${oldFile} first.`));
|
|
1741
|
+
process.exitCode = 1;
|
|
1742
|
+
return;
|
|
1743
|
+
}
|
|
1744
|
+
throw error;
|
|
1745
|
+
}
|
|
1746
|
+
if (parts.content) {
|
|
1747
|
+
const userContent = (parts.before.trim() + parts.after.trim()).trim();
|
|
1748
|
+
if (userContent.length > 0) {
|
|
1749
|
+
const cleaned = (parts.before + parts.after).trim() + '\n';
|
|
1750
|
+
writeFileSync(oldPath, cleaned, 'utf-8');
|
|
1751
|
+
console.log(chalk.gray(` ${oldFile}: UDS block removed, your content kept`));
|
|
1752
|
+
} else {
|
|
1753
|
+
unlinkSync(oldPath);
|
|
1754
|
+
console.log(chalk.gray(` ${oldFile}: deleted (was 100% UDS-generated)`));
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
// else: no UDS markers found in the old file (already moved by hand) —
|
|
1758
|
+
// nothing UDS-owned to remove.
|
|
1759
|
+
}
|
|
1760
|
+
if (manifest.integrationBlockHashes) delete manifest.integrationBlockHashes[oldFile];
|
|
1761
|
+
if (manifest.fileHashes) delete manifest.fileHashes[oldFile];
|
|
1762
|
+
|
|
1763
|
+
// 2. Point the manifest at the new target. Switching back to 'project'
|
|
1764
|
+
// removes the override entirely rather than writing it as 'CLAUDE.md', so a
|
|
1765
|
+
// round-tripped manifest is shaped exactly as if local had never been chosen
|
|
1766
|
+
// (XSPEC-418 AC-5).
|
|
1767
|
+
if (target === 'local') {
|
|
1768
|
+
manifest.integrationTargets = { ...(manifest.integrationTargets || {}), 'claude-code': 'CLAUDE.local.md' };
|
|
1769
|
+
} else if (manifest.integrationTargets) {
|
|
1770
|
+
delete manifest.integrationTargets['claude-code'];
|
|
1771
|
+
if (Object.keys(manifest.integrationTargets).length === 0) delete manifest.integrationTargets;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
// 3. Write (or update in place) the new target. writeIntegrationFile already
|
|
1775
|
+
// does a marker-based UPDATE when the file exists and has UDS markers — the
|
|
1776
|
+
// "hand-moved to CLAUDE.local.md already" arm of R4 — and only appends when
|
|
1777
|
+
// it does not, so this one call covers both AC-4 arms.
|
|
1778
|
+
const toolConfig = buildToolIntegrationConfig(manifest, 'claude-code');
|
|
1779
|
+
const result = writeIntegrationFile('claude-code', toolConfig, projectPath);
|
|
1780
|
+
if (!result.success) {
|
|
1781
|
+
console.log(chalk.red(` ✗ Failed to write ${newFile}: ${result.error}`));
|
|
1782
|
+
console.log();
|
|
1783
|
+
process.exitCode = 1;
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
if (result.blockHashInfo) {
|
|
1788
|
+
if (!manifest.integrationBlockHashes) manifest.integrationBlockHashes = {};
|
|
1789
|
+
manifest.integrationBlockHashes[result.path] = {
|
|
1790
|
+
...result.blockHashInfo,
|
|
1791
|
+
installedAt: new Date().toISOString()
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
// XSPEC-418 R6: no whole-file fileHashes entry for the new target either —
|
|
1795
|
+
// only its UDS block is tracked, above.
|
|
1796
|
+
|
|
1797
|
+
// 4. manifest.integrations may record the old file path (XSPEC-343 shapes) —
|
|
1798
|
+
// point it at the new one instead of leaving a stale entry check would then
|
|
1799
|
+
// report as missing.
|
|
1800
|
+
manifest.integrations = (manifest.integrations || []).map(entry => (entry === oldFile ? newFile : entry));
|
|
1801
|
+
if (!manifest.integrations.includes(newFile) && !manifest.integrations.includes('claude-code')) {
|
|
1802
|
+
manifest.integrations.push(newFile);
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
// XSPEC-418 R6: also catches a stale whole-file entry for either file left
|
|
1806
|
+
// by an older CLI, not just the one this run might otherwise have added.
|
|
1807
|
+
pruneIntegrationFileHashes(manifest);
|
|
1808
|
+
|
|
1809
|
+
writeManifest(manifest, projectPath);
|
|
1810
|
+
|
|
1811
|
+
console.log(chalk.green(` ✓ claude-code now targets ${newFile}`));
|
|
1812
|
+
console.log();
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1644
1815
|
/**
|
|
1645
1816
|
* Regenerate integration files for all configured AI tools
|
|
1646
1817
|
* Reusable core logic that can be called from both updateIntegrationsOnly and configureCommand
|
|
@@ -1665,12 +1836,15 @@ export function regenerateIntegrations(projectPath, manifest) {
|
|
|
1665
1836
|
const now = new Date().toISOString();
|
|
1666
1837
|
|
|
1667
1838
|
for (const tool of aiTools) {
|
|
1668
|
-
|
|
1839
|
+
// XSPEC-418 R3
|
|
1840
|
+
const targetFile = resolveIntegrationTargetFile(tool, manifest);
|
|
1669
1841
|
if (generatedFiles.has(targetFile)) {
|
|
1670
1842
|
continue; // Skip if already generated (AGENTS.md sharing)
|
|
1671
1843
|
}
|
|
1672
1844
|
|
|
1673
1845
|
// Shared with the reconciler so both paths emit the identical block.
|
|
1846
|
+
// buildToolIntegrationConfig already carries manifest.integrationTargets
|
|
1847
|
+
// through to writeIntegrationFile (XSPEC-418 R2).
|
|
1674
1848
|
const toolConfig = buildToolIntegrationConfig(manifest, tool);
|
|
1675
1849
|
|
|
1676
1850
|
const result = writeIntegrationFile(tool, toolConfig, projectPath);
|
|
@@ -1678,16 +1852,11 @@ export function regenerateIntegrations(projectPath, manifest) {
|
|
|
1678
1852
|
results.updated.push(result.path);
|
|
1679
1853
|
generatedFiles.add(targetFile);
|
|
1680
1854
|
|
|
1681
|
-
//
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
manifest.fileHashes = {};
|
|
1687
|
-
}
|
|
1688
|
-
const normalizedPath = result.path.replace(/\\/g, '/');
|
|
1689
|
-
manifest.fileHashes[normalizedPath] = { ...hashInfo, installedAt: now };
|
|
1690
|
-
}
|
|
1855
|
+
// XSPEC-418 R6: no whole-file `fileHashes` entry for an integration
|
|
1856
|
+
// file — only the UDS block is tracked, below. A whole-file entry here
|
|
1857
|
+
// is exactly what made `uds check` report "CLAUDE.md (modified)" for
|
|
1858
|
+
// content outside the block that UDS's own marker-based update leaves
|
|
1859
|
+
// alone on purpose.
|
|
1691
1860
|
|
|
1692
1861
|
// Track integration block hash for UDS content integrity
|
|
1693
1862
|
if (result.blockHashInfo) {
|
|
@@ -1726,6 +1895,14 @@ export function regenerateIntegrations(projectPath, manifest) {
|
|
|
1726
1895
|
}
|
|
1727
1896
|
}
|
|
1728
1897
|
|
|
1898
|
+
// XSPEC-418 R6: drop any fileHashes entry left over from an older CLI (or
|
|
1899
|
+
// from refreshTrackedFileHash above, which predates this rule and still
|
|
1900
|
+
// refreshes AGENTS.md's whole-file hash if one is already tracked). Called
|
|
1901
|
+
// here rather than relying only on refreshIntegrationBlockHashes downstream
|
|
1902
|
+
// because not every caller of this function calls that afterward (`uds
|
|
1903
|
+
// config`'s AI-tools flow does not).
|
|
1904
|
+
pruneIntegrationFileHashes(manifest);
|
|
1905
|
+
|
|
1729
1906
|
return {
|
|
1730
1907
|
success: results.errors.length === 0,
|
|
1731
1908
|
updated: results.updated,
|
|
@@ -1793,9 +1970,11 @@ async function updateIntegrationsOnly(projectPath, manifest, options = {}) {
|
|
|
1793
1970
|
console.log(chalk.bold('=== Integration Plan (dry run — nothing is written) ==='));
|
|
1794
1971
|
const wouldChange = [];
|
|
1795
1972
|
const unchanged = [];
|
|
1973
|
+
const ambiguous = [];
|
|
1796
1974
|
const seen = new Set();
|
|
1797
1975
|
for (const tool of aiTools) {
|
|
1798
|
-
|
|
1976
|
+
// XSPEC-418 R3: the plan must show the actual target, not the default.
|
|
1977
|
+
const targetFile = resolveIntegrationTargetFile(tool, manifest);
|
|
1799
1978
|
if (seen.has(targetFile)) continue;
|
|
1800
1979
|
seen.add(targetFile);
|
|
1801
1980
|
const savedMode = manifest.contentMode || 'auto';
|
|
@@ -1822,7 +2001,18 @@ async function updateIntegrationsOnly(projectPath, manifest, options = {}) {
|
|
|
1822
2001
|
const full = join(projectPath, targetFile);
|
|
1823
2002
|
const current = existsSync(full) ? readFileSync(full, 'utf8') : '';
|
|
1824
2003
|
const format = targetFile.endsWith('.md') ? 'markdown' : 'plaintext';
|
|
1825
|
-
|
|
2004
|
+
// XSPEC adopter-report Q5: an ambiguous marker pair can't be diffed
|
|
2005
|
+
// against — report it explicitly instead of crashing the whole plan.
|
|
2006
|
+
let cur;
|
|
2007
|
+
try {
|
|
2008
|
+
cur = extractMarkedContent(current, format).content || '';
|
|
2009
|
+
} catch (error) {
|
|
2010
|
+
if (error instanceof AmbiguousMarkerError) {
|
|
2011
|
+
ambiguous.push(`${targetFile}: ${error.message}`);
|
|
2012
|
+
continue;
|
|
2013
|
+
}
|
|
2014
|
+
throw error;
|
|
2015
|
+
}
|
|
1826
2016
|
// 比對正規化過的內容:只關心「受管區塊會不會變」,不關心尾端空白。
|
|
1827
2017
|
if (cur.trim() === String(next).trim()) {
|
|
1828
2018
|
unchanged.push(targetFile);
|
|
@@ -1831,6 +2021,10 @@ async function updateIntegrationsOnly(projectPath, manifest, options = {}) {
|
|
|
1831
2021
|
wouldChange.push(`${targetFile} (${delta >= 0 ? '+' : ''}${delta} bytes in the UDS block)`);
|
|
1832
2022
|
}
|
|
1833
2023
|
}
|
|
2024
|
+
if (ambiguous.length > 0) {
|
|
2025
|
+
console.log(chalk.red(` ✗ Ambiguous UDS markers (${ambiguous.length}) — will not be touched by --apply:`));
|
|
2026
|
+
for (const a of ambiguous) console.log(chalk.gray(` ${a}`));
|
|
2027
|
+
}
|
|
1834
2028
|
if (wouldChange.length > 0) {
|
|
1835
2029
|
console.log(chalk.yellow(` ~ Would update (${wouldChange.length}):`));
|
|
1836
2030
|
for (const f of wouldChange) console.log(chalk.gray(` ${f}`));
|
|
@@ -1854,7 +2048,7 @@ async function updateIntegrationsOnly(projectPath, manifest, options = {}) {
|
|
|
1854
2048
|
spinner.succeed(msg.regeneratedIntegrations.replace('{count}', results.updated.length));
|
|
1855
2049
|
|
|
1856
2050
|
// Update manifest
|
|
1857
|
-
manifest
|
|
2051
|
+
bumpManifestVersion(manifest);
|
|
1858
2052
|
refreshIntegrationBlockHashes(manifest, projectPath);
|
|
1859
2053
|
writeManifest(manifest, projectPath);
|
|
1860
2054
|
|
|
@@ -1897,7 +2091,10 @@ export function backfillIntegrationConfigs(manifest) {
|
|
|
1897
2091
|
const tools = manifest.aiTools?.length ? manifest.aiTools : [];
|
|
1898
2092
|
const byFile = new Map();
|
|
1899
2093
|
for (const tool of tools) {
|
|
1900
|
-
|
|
2094
|
+
// XSPEC-418 R3: keyed by the actual target file, or integrationConfigs
|
|
2095
|
+
// would key claude-code's entry as 'CLAUDE.md' while every other record
|
|
2096
|
+
// (integrations, integrationBlockHashes) already uses CLAUDE.local.md.
|
|
2097
|
+
const file = resolveIntegrationTargetFile(tool, manifest);
|
|
1901
2098
|
if (file && !byFile.has(file)) byFile.set(file, tool);
|
|
1902
2099
|
}
|
|
1903
2100
|
// Integrations can be recorded as file names (`CLAUDE.md`) or tool keys.
|
|
@@ -1943,6 +2140,14 @@ function resolveToolKeyFromEntry(entry) {
|
|
|
1943
2140
|
* tracking it would convert every legitimate edit into a reported fault. This
|
|
1944
2141
|
* only ever refreshes a record that already exists.
|
|
1945
2142
|
*
|
|
2143
|
+
* XSPEC-418 R6 narrowed that further: AGENTS.md is also tracked by its UDS
|
|
2144
|
+
* block (`integrationBlockHashes`) once one exists, and every call site of
|
|
2145
|
+
* this function now also calls `pruneIntegrationFileHashes` afterward — so a
|
|
2146
|
+
* `fileHashes` entry this refreshes is removed again in the same run. Kept
|
|
2147
|
+
* (rather than deleted outright) because it still matters for the moment
|
|
2148
|
+
* between this call and the prune, and for any caller added later that
|
|
2149
|
+
* forgets to prune; the prune is what actually enforces R6.
|
|
2150
|
+
*
|
|
1946
2151
|
* @param {Object} manifest - Project manifest (mutated)
|
|
1947
2152
|
* @param {string} projectPath - Project root
|
|
1948
2153
|
* @param {string} relativePath - Path as UDS reports it
|
|
@@ -2152,15 +2357,8 @@ async function syncIntegrationReferences(projectPath, manifest, { plan = false }
|
|
|
2152
2357
|
generatedAt: now
|
|
2153
2358
|
};
|
|
2154
2359
|
|
|
2155
|
-
//
|
|
2156
|
-
|
|
2157
|
-
if (hashInfo) {
|
|
2158
|
-
if (!manifest.fileHashes) {
|
|
2159
|
-
manifest.fileHashes = {};
|
|
2160
|
-
}
|
|
2161
|
-
const normalizedIntPath = integrationPath.replace(/\\/g, '/');
|
|
2162
|
-
manifest.fileHashes[normalizedIntPath] = { ...hashInfo, installedAt: now };
|
|
2163
|
-
}
|
|
2360
|
+
// XSPEC-418 R6: no whole-file fileHashes entry — only the UDS block is
|
|
2361
|
+
// tracked, below.
|
|
2164
2362
|
|
|
2165
2363
|
// Track integration block hash for UDS content integrity
|
|
2166
2364
|
if (result.blockHashInfo) {
|
|
@@ -2180,7 +2378,7 @@ async function syncIntegrationReferences(projectPath, manifest, { plan = false }
|
|
|
2180
2378
|
|
|
2181
2379
|
// Update manifest version and save
|
|
2182
2380
|
if (updatedCount > 0) {
|
|
2183
|
-
manifest
|
|
2381
|
+
bumpManifestVersion(manifest);
|
|
2184
2382
|
refreshIntegrationBlockHashes(manifest, projectPath);
|
|
2185
2383
|
if (plan) {
|
|
2186
2384
|
console.log(chalk.gray(' (dry run — the manifest was not written)'));
|
|
@@ -2217,6 +2415,66 @@ async function syncIntegrationReferences(projectPath, manifest, { plan = false }
|
|
|
2217
2415
|
* "Write then restore" leaves a broken tree if it dies halfway, which is worse
|
|
2218
2416
|
* than having no dry run at all — the same reasoning as the integrations plan.
|
|
2219
2417
|
*/
|
|
2418
|
+
/**
|
|
2419
|
+
* A general `uds update --plan` (no --skills/--commands scope) never called
|
|
2420
|
+
* planSkills/planCommands — those are the only two places version staleness
|
|
2421
|
+
* is computed, and they only run under `--plan --skills`/`--plan --commands`.
|
|
2422
|
+
* An adopter running plain `--plan` saw a clean reconciliation plan and
|
|
2423
|
+
* nothing else, with Skills or Commands a version behind and no hint that a
|
|
2424
|
+
* scoped plan would have said so. This prints a short, best-effort note —
|
|
2425
|
+
* not a full plan — so the general path is never silent about it.
|
|
2426
|
+
*
|
|
2427
|
+
* Skills staleness is read the same way planSkills does: per-installation,
|
|
2428
|
+
* from what is actually on disk (`getInstalledSkillsInfoForAgent`), because
|
|
2429
|
+
* different agents/levels can be out of sync independently. Commands have no
|
|
2430
|
+
* per-installation version on disk (only a file count), so Commands
|
|
2431
|
+
* staleness is read from the one version the manifest itself records
|
|
2432
|
+
* (`manifest.commands.version`, written by every path that installs
|
|
2433
|
+
* commands) against the latest version UDS ships. // implements XSPEC adopter-report Q3
|
|
2434
|
+
*
|
|
2435
|
+
* @param {string} projectPath
|
|
2436
|
+
* @param {Object} manifest
|
|
2437
|
+
*/
|
|
2438
|
+
function reportStaleSkillsCommandsHint(projectPath, manifest) {
|
|
2439
|
+
const repoInfo = getRepositoryInfo();
|
|
2440
|
+
const latestVersion = repoInfo.skills.version;
|
|
2441
|
+
const stale = [];
|
|
2442
|
+
|
|
2443
|
+
const skillsInstallations = (manifest.skills?.installations || []).filter((i) => i.level !== 'marketplace');
|
|
2444
|
+
for (const inst of skillsInstallations) {
|
|
2445
|
+
const info = getInstalledSkillsInfoForAgent(inst.agent, inst.level, projectPath);
|
|
2446
|
+
const current = info?.version;
|
|
2447
|
+
if (current && current !== latestVersion) {
|
|
2448
|
+
stale.push({
|
|
2449
|
+
label: `${getAgentDisplayName(inst.agent)} (${inst.level}): Skills v${current} → v${latestVersion}`,
|
|
2450
|
+
flag: '--skills'
|
|
2451
|
+
});
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
if (manifest.commands?.installed && (manifest.commands?.installations || []).length > 0) {
|
|
2456
|
+
const current = manifest.commands.version;
|
|
2457
|
+
if (current && current !== latestVersion) {
|
|
2458
|
+
stale.push({
|
|
2459
|
+
label: `Commands: v${current} → v${latestVersion}`,
|
|
2460
|
+
flag: '--commands'
|
|
2461
|
+
});
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
if (stale.length === 0) return;
|
|
2466
|
+
|
|
2467
|
+
console.log(chalk.yellow(' Skills/Commands installed but out of date:'));
|
|
2468
|
+
for (const s of stale) {
|
|
2469
|
+
console.log(chalk.gray(` ~ ${s.label}`));
|
|
2470
|
+
}
|
|
2471
|
+
const flags = [...new Set(stale.map((s) => s.flag))];
|
|
2472
|
+
for (const flag of flags) {
|
|
2473
|
+
console.log(chalk.gray(` Run \`uds update --apply --yes ${flag}\` to update.`));
|
|
2474
|
+
}
|
|
2475
|
+
console.log();
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2220
2478
|
async function planSkills(projectPath, manifest, options) {
|
|
2221
2479
|
const repoInfo = getRepositoryInfo();
|
|
2222
2480
|
const latestVersion = repoInfo.skills.version;
|
|
@@ -3001,7 +3259,7 @@ async function handleRollback(projectPath) {
|
|
|
3001
3259
|
/**
|
|
3002
3260
|
* Handle --plan: show what the reconciler would do without executing.
|
|
3003
3261
|
*/
|
|
3004
|
-
async function handlePlan(projectPath, options) {
|
|
3262
|
+
async function handlePlan(projectPath, options, manifest) {
|
|
3005
3263
|
const spinner = createSpinner('Calculating reconciliation plan...').start();
|
|
3006
3264
|
|
|
3007
3265
|
const result = await reconcilerPlan(projectPath, { force: false });
|
|
@@ -3018,6 +3276,12 @@ async function handlePlan(projectPath, options) {
|
|
|
3018
3276
|
console.log(formatPlan(result.plan));
|
|
3019
3277
|
console.log();
|
|
3020
3278
|
|
|
3279
|
+
// XSPEC adopter-report Q3: the reconciliation plan above never covers
|
|
3280
|
+
// Skills/Commands version staleness — only --plan --skills/--commands did.
|
|
3281
|
+
if (manifest) {
|
|
3282
|
+
reportStaleSkillsCommandsHint(projectPath, manifest);
|
|
3283
|
+
}
|
|
3284
|
+
|
|
3021
3285
|
if (result.plan.actions.length > 0) {
|
|
3022
3286
|
// NOT `uds update`. That runs the legacy path, which never executes this
|
|
3023
3287
|
// plan — it refreshes existing standards and reports success for having done
|
package/src/core/manifest.js
CHANGED
|
@@ -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
|