universal-dev-standards 6.7.4 → 6.8.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 (45) hide show
  1. package/bin/uds.js +7 -0
  2. package/bundled/ai/standards/ai-instruction-standards.ai.yaml +6 -6
  3. package/bundled/core/ai-instruction-standards.md +9 -7
  4. package/bundled/locales/zh-CN/CHANGELOG.md +32 -3
  5. package/bundled/locales/zh-CN/README.md +6 -6
  6. package/bundled/locales/zh-CN/SECURITY.md +1 -1
  7. package/bundled/locales/zh-CN/core/ai-instruction-standards.md +10 -8
  8. package/bundled/locales/zh-CN/docs/CHEATSHEET.md +20 -6
  9. package/bundled/locales/zh-CN/docs/FEATURE-REFERENCE.md +149 -11
  10. package/bundled/locales/zh-CN/skills/agents/README.md +1 -1
  11. package/bundled/locales/zh-CN/skills/reverse-engineer/tdd-analysis.md +13 -23
  12. package/bundled/locales/zh-CN/skills/spec-derivation/SKILL.md +5 -1
  13. package/bundled/locales/zh-CN/skills/workflows/README.md +2 -11
  14. package/bundled/locales/zh-TW/CHANGELOG.md +32 -3
  15. package/bundled/locales/zh-TW/README.md +6 -6
  16. package/bundled/locales/zh-TW/SECURITY.md +1 -1
  17. package/bundled/locales/zh-TW/core/ai-instruction-standards.md +10 -8
  18. package/bundled/locales/zh-TW/docs/CHEATSHEET.md +20 -6
  19. package/bundled/locales/zh-TW/docs/FEATURE-REFERENCE.md +149 -11
  20. package/bundled/locales/zh-TW/skills/agents/README.md +1 -1
  21. package/bundled/locales/zh-TW/skills/reverse-engineer/tdd-analysis.md +13 -23
  22. package/bundled/locales/zh-TW/skills/spec-derivation/SKILL.md +5 -1
  23. package/bundled/locales/zh-TW/skills/workflows/README.md +2 -11
  24. package/bundled/skills/agents/README.md +1 -1
  25. package/bundled/skills/audit-assistant/SKILL.md +24 -1
  26. package/bundled/skills/changelog-guide/SKILL.md +24 -1
  27. package/bundled/skills/commit-standards/SKILL.md +24 -1
  28. package/bundled/skills/docs-generator/SKILL.md +24 -1
  29. package/bundled/skills/project-discovery/SKILL.md +24 -1
  30. package/bundled/skills/release-standards/SKILL.md +24 -1
  31. package/bundled/skills/reverse-engineer/SKILL.md +24 -1
  32. package/bundled/skills/reverse-engineer/tdd-analysis.md +16 -23
  33. package/bundled/skills/spec-derivation/SKILL.md +24 -1
  34. package/bundled/skills/workflows/README.md +2 -11
  35. package/package.json +4 -2
  36. package/src/commands/lint.js +96 -0
  37. package/src/commands/quickstart.js +16 -13
  38. package/src/i18n/messages.js +3 -3
  39. package/src/reconciler/actual-state-scanner.js +14 -3
  40. package/src/utils/hasher.js +63 -8
  41. package/src/utils/integration-generator.js +21 -3
  42. package/src/utils/skills-installer.js +17 -3
  43. package/src/utils/spec-linter.js +35 -76
  44. package/standards-registry.json +7 -7
  45. package/src/commands/sync.js +0 -133
@@ -8,7 +8,30 @@ description: |
8
8
  Keywords: docgen, usage docs, cheatsheet, feature reference, generated documentation, 使用文件, 速查表, 文件產生.
9
9
  allowed-tools: Read, Write, Grep, Glob, Bash(*)
10
10
  argument-hint: "[config file | 設定檔]"
11
- disable-model-invocation: true
11
+ status: stable
12
+ # 2026-08-18: `disable-model-invocation: true` removed, and a status recorded.
13
+ #
14
+ # The flag was applied by d415937e alongside the description rewrite and, like
15
+ # the two lifted on 2026-08-17, followed no stateable rule. These eight were
16
+ # left alone that day for a reason that was correct at the time: the rule
17
+ # settled on was "a reference is model-invocable", and none of them carried a
18
+ # `status` at all, so lifting them would have replaced one unruled state with
19
+ # one unruled action.
20
+ #
21
+ # Measured 2026-08-18, which is what closed it: all eight carry a full
22
+ # `Use when:` trigger and a `Not for:` exclusion, all eight describe an action
23
+ # rather than reference material, and all eight already have a slash command —
24
+ # which is exactly the shape of `code-review-assistant`, whose paired `/code-review`
25
+ # was ruled not to justify the flag. `journey-test-assistant` is the standing
26
+ # precedent: same "Generate X" shape, `status: stable`, never disabled.
27
+ #
28
+ # `stable` rather than a new value: `skills/` uses reference, stable and
29
+ # experimental, and inventing a fourth would be the same unruled-action mistake
30
+ # in different clothing.
31
+ #
32
+ # The cost of being wrong is asymmetric and observable in only one direction.
33
+ # Over-triggering shows up and is undone by deleting a line; a skill that is
34
+ # structurally unable to fire produces no signal at all. (XSPEC-378 R5)
12
35
  ---
13
36
 
14
37
  # Documentation Generator | 文件產生器
@@ -8,7 +8,30 @@ description: |
8
8
  Keywords: discovery, project assessment, legacy onboarding, risk register, technical debt, 現況評估, 專案盤點, 風險登記簿.
9
9
  allowed-tools: Read, Grep, Glob, Bash(npm test:*), Bash(npm audit:*), Bash(npm outdated:*)
10
10
  argument-hint: "[feature area | 功能範圍]"
11
- disable-model-invocation: true
11
+ status: stable
12
+ # 2026-08-18: `disable-model-invocation: true` removed, and a status recorded.
13
+ #
14
+ # The flag was applied by d415937e alongside the description rewrite and, like
15
+ # the two lifted on 2026-08-17, followed no stateable rule. These eight were
16
+ # left alone that day for a reason that was correct at the time: the rule
17
+ # settled on was "a reference is model-invocable", and none of them carried a
18
+ # `status` at all, so lifting them would have replaced one unruled state with
19
+ # one unruled action.
20
+ #
21
+ # Measured 2026-08-18, which is what closed it: all eight carry a full
22
+ # `Use when:` trigger and a `Not for:` exclusion, all eight describe an action
23
+ # rather than reference material, and all eight already have a slash command —
24
+ # which is exactly the shape of `code-review-assistant`, whose paired `/code-review`
25
+ # was ruled not to justify the flag. `journey-test-assistant` is the standing
26
+ # precedent: same "Generate X" shape, `status: stable`, never disabled.
27
+ #
28
+ # `stable` rather than a new value: `skills/` uses reference, stable and
29
+ # experimental, and inventing a fourth would be the same unruled-action mistake
30
+ # in different clothing.
31
+ #
32
+ # The cost of being wrong is asymmetric and observable in only one direction.
33
+ # Over-triggering shows up and is undone by deleting a line; a skill that is
34
+ # structurally unable to fire produces no signal at all. (XSPEC-378 R5)
12
35
  ---
13
36
 
14
37
  # Project Discovery | 專案現況評估
@@ -9,7 +9,30 @@ description: |
9
9
  allowed-tools: Read, Write, Grep, Bash(git:*), Bash(npm version:*)
10
10
  argument-hint: "[version]"
11
11
  prerequisites: ["release-check"]
12
- disable-model-invocation: true
12
+ status: stable
13
+ # 2026-08-18: `disable-model-invocation: true` removed, and a status recorded.
14
+ #
15
+ # The flag was applied by d415937e alongside the description rewrite and, like
16
+ # the two lifted on 2026-08-17, followed no stateable rule. These eight were
17
+ # left alone that day for a reason that was correct at the time: the rule
18
+ # settled on was "a reference is model-invocable", and none of them carried a
19
+ # `status` at all, so lifting them would have replaced one unruled state with
20
+ # one unruled action.
21
+ #
22
+ # Measured 2026-08-18, which is what closed it: all eight carry a full
23
+ # `Use when:` trigger and a `Not for:` exclusion, all eight describe an action
24
+ # rather than reference material, and all eight already have a slash command —
25
+ # which is exactly the shape of `code-review-assistant`, whose paired `/code-review`
26
+ # was ruled not to justify the flag. `journey-test-assistant` is the standing
27
+ # precedent: same "Generate X" shape, `status: stable`, never disabled.
28
+ #
29
+ # `stable` rather than a new value: `skills/` uses reference, stable and
30
+ # experimental, and inventing a fourth would be the same unruled-action mistake
31
+ # in different clothing.
32
+ #
33
+ # The cost of being wrong is asymmetric and observable in only one direction.
34
+ # Over-triggering shows up and is undone by deleting a line; a skill that is
35
+ # structurally unable to fire produces no signal at all. (XSPEC-378 R5)
13
36
  ---
14
37
 
15
38
  # Release Assistant | 發布助手
@@ -8,7 +8,30 @@ description: |
8
8
  Keywords: reverse engineering, system archeology, legacy code, spec extraction, data model, runtime, 反向工程, 系統考古, 規格提取.
9
9
  allowed-tools: Read, Grep, Glob, Bash(pg_dump:*), Bash(mysql:*), Bash(sqlite3:*), Bash(npm run:*), Bash(cat:*), Bash(docker:*)
10
10
  argument-hint: "[spec|data|runtime|bdd|tdd] <input>"
11
- disable-model-invocation: true
11
+ status: stable
12
+ # 2026-08-18: `disable-model-invocation: true` removed, and a status recorded.
13
+ #
14
+ # The flag was applied by d415937e alongside the description rewrite and, like
15
+ # the two lifted on 2026-08-17, followed no stateable rule. These eight were
16
+ # left alone that day for a reason that was correct at the time: the rule
17
+ # settled on was "a reference is model-invocable", and none of them carried a
18
+ # `status` at all, so lifting them would have replaced one unruled state with
19
+ # one unruled action.
20
+ #
21
+ # Measured 2026-08-18, which is what closed it: all eight carry a full
22
+ # `Use when:` trigger and a `Not for:` exclusion, all eight describe an action
23
+ # rather than reference material, and all eight already have a slash command —
24
+ # which is exactly the shape of `code-review-assistant`, whose paired `/code-review`
25
+ # was ruled not to justify the flag. `journey-test-assistant` is the standing
26
+ # precedent: same "Generate X" shape, `status: stable`, never disabled.
27
+ #
28
+ # `stable` rather than a new value: `skills/` uses reference, stable and
29
+ # experimental, and inventing a fourth would be the same unruled-action mistake
30
+ # in different clothing.
31
+ #
32
+ # The cost of being wrong is asymmetric and observable in only one direction.
33
+ # Over-triggering shows up and is undone by deleting a line; a skill that is
34
+ # structurally unable to fire produces no signal at all. (XSPEC-378 R5)
12
35
  ---
13
36
 
14
37
  # Reverse Engineering Assistant | 反向工程助手
@@ -597,9 +597,22 @@ Mark both scenarios as [Inferred] with shared test reference
597
597
 
598
598
  ## CI/CD Integration
599
599
 
600
- ### GitHub Actions Example
600
+ ### GitHub Actions — not currently scriptable
601
+
602
+ There is no `uds reverse-tdd` CLI command. Coverage-gap analysis is delivered
603
+ as the **`reverse-tdd`** AI agent (`uds agent install reverse-tdd`) — an
604
+ interactive, LLM-driven analysis run from your AI tool, not a deterministic
605
+ script that emits a `coverage.json` file. A prior version of this doc showed
606
+ a GitHub Actions step invoking a CLI command that never existed (XSPEC-383
607
+ R4, 2026-08-19).
608
+
609
+ The sketch below is **illustrative only** — it shows the shape a scriptable
610
+ CI integration would need if one is built, not something you can paste into
611
+ a workflow today:
601
612
 
602
613
  ```yaml
614
+ # ASPIRATIONAL — no scriptable equivalent exists yet.
615
+ # Today: run the reverse-tdd agent interactively from your AI tool instead.
603
616
  name: BDD Coverage Check
604
617
 
605
618
  on: [pull_request]
@@ -610,29 +623,9 @@ jobs:
610
623
  steps:
611
624
  - uses: actions/checkout@v4
612
625
 
613
- - name: Run BDD Coverage Analysis
614
- run: |
615
- uds reverse-tdd features/ --output coverage.json
616
-
617
- - name: Check Coverage Threshold
626
+ - name: Run BDD Coverage Analysis (not yet implemented)
618
627
  run: |
619
- COVERAGE=$(jq '.summary.effectiveCoverage' coverage.json)
620
- if (( $(echo "$COVERAGE < 0.80" | bc -l) )); then
621
- echo "Coverage $COVERAGE below 80% threshold"
622
- exit 1
623
- fi
624
-
625
- - name: Comment on PR
626
- uses: actions/github-script@v6
627
- with:
628
- script: |
629
- const coverage = require('./coverage.json');
630
- github.rest.issues.createComment({
631
- issue_number: context.issue.number,
632
- owner: context.repo.owner,
633
- repo: context.repo.repo,
634
- body: `## BDD Coverage Report\n\nCoverage: ${(coverage.summary.effectiveCoverage * 100).toFixed(1)}%`
635
- });
628
+ echo "No CLI equivalent yet — see reverse-tdd agent for interactive use"
636
629
  ```
637
630
 
638
631
  ---
@@ -9,7 +9,30 @@ description: |
9
9
  allowed-tools: Read, Write, Grep, Glob
10
10
  argument-hint: "[all|bdd|tdd|it|e2e|atdd] <spec-file>"
11
11
  prerequisites: ["spec-approved"]
12
- disable-model-invocation: true
12
+ status: stable
13
+ # 2026-08-18: `disable-model-invocation: true` removed, and a status recorded.
14
+ #
15
+ # The flag was applied by d415937e alongside the description rewrite and, like
16
+ # the two lifted on 2026-08-17, followed no stateable rule. These eight were
17
+ # left alone that day for a reason that was correct at the time: the rule
18
+ # settled on was "a reference is model-invocable", and none of them carried a
19
+ # `status` at all, so lifting them would have replaced one unruled state with
20
+ # one unruled action.
21
+ #
22
+ # Measured 2026-08-18, which is what closed it: all eight carry a full
23
+ # `Use when:` trigger and a `Not for:` exclusion, all eight describe an action
24
+ # rather than reference material, and all eight already have a slash command —
25
+ # which is exactly the shape of `code-review-assistant`, whose paired `/code-review`
26
+ # was ruled not to justify the flag. `journey-test-assistant` is the standing
27
+ # precedent: same "Generate X" shape, `status: stable`, never disabled.
28
+ #
29
+ # `stable` rather than a new value: `skills/` uses reference, stable and
30
+ # experimental, and inventing a fourth would be the same unruled-action mistake
31
+ # in different clothing.
32
+ #
33
+ # The cost of being wrong is asymmetric and observable in only one direction.
34
+ # Over-triggering shows up and is undone by deleting a line; a skill that is
35
+ # structurally unable to fire produces no signal at all. (XSPEC-378 R5)
13
36
  ---
14
37
 
15
38
  # Forward Derivation | 正向推演
@@ -157,18 +157,9 @@ steps:
157
157
 
158
158
  ## Usage
159
159
 
160
- ### CLI Installation
160
+ ### Getting Workflows
161
161
 
162
- ```bash
163
- # List available workflows
164
- uds workflow list
165
-
166
- # Install specific workflow
167
- uds workflow install integrated-flow
168
-
169
- # Install all workflows
170
- uds workflow install --all
171
- ```
162
+ `uds workflow` was removed in v6.0.0 (process orchestration moved to the adoption layer — DEC-049; see [MIGRATION-v6.md §3](../../docs/MIGRATION-v6.md#3-removed-4-deprecated-cli-commands)). There is no CLI install step: workflow definitions live directly under `skills/workflows/*.workflow.yaml` in this repo (see the table above), so reference them by path or copy them into your project's `.claude/workflows/`.
172
163
 
173
164
  ### Execution
174
165
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-dev-standards",
3
- "version": "6.7.4",
3
+ "version": "6.8.0",
4
4
  "description": "CLI tool for adopting Universal Development Standards",
5
5
  "keywords": [
6
6
  "documentation",
@@ -56,7 +56,9 @@
56
56
  "prepare": "node ../scripts/setup-husky.mjs",
57
57
  "test:upgrade": "node scripts/test-upgrade-path.mjs",
58
58
  "check:bundle-parity": "node scripts/check-bundle-parity.mjs",
59
- "prepack": "node scripts/prepack.mjs"
59
+ "prepack": "node scripts/prepack.mjs",
60
+ "check:reachability": "node scripts/check-module-reachability.mjs",
61
+ "check:command-existence": "node scripts/check-command-existence.mjs"
60
62
  },
61
63
  "dependencies": {
62
64
  "@inquirer/prompts": "^8.4.2",
@@ -0,0 +1,96 @@
1
+ /**
2
+ * `uds lint` — dependency validity + size checks against installed specs
3
+ * (specs/*.md).
4
+ *
5
+ * XSPEC-383 R5 (Option E). `cli/src/utils/spec-linter.js` and this command's
6
+ * JSON shape existed since 2026-04-07, but no `uds lint` command was ever
7
+ * registered — VibeOps's `lint-executor.ts` has been calling
8
+ * `npx uds lint --json` since the same day and getting `command not found`
9
+ * every time, four and a half months, without anyone noticing (see
10
+ * `cli/scripts/check-module-reachability.mjs` for the full incident).
11
+ *
12
+ * This registers the command with exactly the two checks that survived
13
+ * `lintAll()`'s AC-coverage removal (see spec-linter.js for why AC coverage
14
+ * was dropped rather than patched). The `--json` shape is deliberately NOT a
15
+ * new design — it matches the shape VibeOps's `lint-executor.ts` has already
16
+ * been parsing (`result.summary.fail`, `result.results[].specId/.status/.message`)
17
+ * since it was written, so wiring this up does not also require a change on
18
+ * the VibeOps side.
19
+ *
20
+ * @module commands/lint
21
+ */
22
+
23
+ import chalk from 'chalk';
24
+ import { lintAll } from '../utils/spec-linter.js';
25
+
26
+ /**
27
+ * Render a one-line human message for a single spec's lint result.
28
+ * Exported for tests; also used to build the `message` field of `--json`
29
+ * output.
30
+ */
31
+ export function buildMessage(result) {
32
+ const parts = [];
33
+ if (result.deps.broken.length > 0) {
34
+ const targets = result.deps.broken.map((b) => b.target).join(', ');
35
+ parts.push(
36
+ `${result.deps.broken.length} broken dependenc${result.deps.broken.length === 1 ? 'y' : 'ies'}: ${targets}`
37
+ );
38
+ }
39
+ parts.push(`${result.size.effectiveLines} effective lines (${result.size.status})`);
40
+ return parts.join('; ');
41
+ }
42
+
43
+ export async function lintCommand(options = {}) {
44
+ const projectPath = process.cwd();
45
+ const result = lintAll(projectPath);
46
+
47
+ if (options.json) {
48
+ const payload = {
49
+ summary: result.summary,
50
+ results: result.results.map((r) => ({
51
+ specId: r.spec,
52
+ status: r.status,
53
+ message: buildMessage(r),
54
+ })),
55
+ };
56
+ console.log(JSON.stringify(payload, null, 2));
57
+ if (result.summary.fail > 0) process.exitCode = 1;
58
+ return;
59
+ }
60
+
61
+ console.log();
62
+ console.log(chalk.bold('Spec Lint'));
63
+ console.log(chalk.gray('─'.repeat(50)));
64
+
65
+ if (!result.specsDirExists) {
66
+ // 查無 spec 目錄 must say so explicitly — an empty { pass: 0, warn: 0,
67
+ // fail: 0 } summary is indistinguishable from "checked, all clean" unless
68
+ // the command says out loud that nothing was scanned.
69
+ console.log(chalk.yellow(` 查無 spec 目錄(./${result.specsDir}/ 不存在)`));
70
+ console.log(chalk.gray(' 沒有東西被掃描——這不代表沒有問題,代表沒有檢查。'));
71
+ console.log();
72
+ return;
73
+ }
74
+
75
+ console.log(chalk.gray(` 掃描 ${result.results.length} 份 spec(./${result.specsDir}/)`));
76
+ console.log();
77
+
78
+ for (const r of result.results) {
79
+ const msg = buildMessage(r);
80
+ if (r.status === 'fail') {
81
+ console.log(chalk.red(` ✗ ${r.spec}: ${msg}`));
82
+ } else if (r.status === 'warn') {
83
+ console.log(chalk.yellow(` ⚠ ${r.spec}: ${msg}`));
84
+ } else {
85
+ console.log(chalk.green(` ✓ ${r.spec}: ${msg}`));
86
+ }
87
+ }
88
+
89
+ console.log();
90
+ console.log(
91
+ chalk.gray(` ${result.summary.pass} pass, ${result.summary.warn} warn, ${result.summary.fail} fail`)
92
+ );
93
+ console.log();
94
+
95
+ if (result.summary.fail > 0) process.exitCode = 1;
96
+ }
@@ -3,6 +3,19 @@
3
3
  *
4
4
  * Reduces cognitive load by guiding users to common workflow paths.
5
5
  *
6
+ * ⚠️ Every `cmd` that starts with `uds ` MUST be a command this CLI actually
7
+ * registers, with flags it actually accepts. This is the one place whose entire
8
+ * job is "find the right commands quickly", so a wrong entry here does the exact
9
+ * opposite of what the command exists for.
10
+ *
11
+ * 2026-08-19: four entries pointed at things that do not exist — `uds lint`
12
+ * (twice), `uds sync` (twice, one of them the sole content of a whole
13
+ * workflow), `uds check --spec-size`, and `uds spec create --boost`. Verify with
14
+ * `uds <cmd> --definitely-not-a-real-flag` and look for "unknown command";
15
+ * `uds <cmd> --help` is NOT a valid check — commander prints the general help
16
+ * for an unknown command instead of erroring, so --help reports every name as
17
+ * valid.
18
+ *
6
19
  * @module commands/quickstart
7
20
  */
8
21
 
@@ -24,11 +37,10 @@ export const WORKFLOWS = [
24
37
  name: 'Full SDD Spec Flow (Boost)',
25
38
  description: 'Complete spec-driven development for complex features',
26
39
  steps: [
27
- { cmd: 'uds spec create "your feature" --boost', desc: 'Create full SDD spec with design sections' },
28
- { cmd: 'uds lint', desc: 'Validate spec quality and cross-references' },
40
+ { cmd: '# Use the /sdd skill', desc: 'Full spec lifecycle with review (see `uds spec --help`)' },
41
+ { cmd: 'uds spec create "your feature" --scope fullstack', desc: 'Create the spec, scoped' },
29
42
  { cmd: 'uds spec confirm SPEC-XXX', desc: 'Confirm after review' },
30
43
  { cmd: '# Implement with /derive → /tdd', desc: 'Use forward derivation and TDD' },
31
- { cmd: 'uds sync', desc: 'Export context for session resume' },
32
44
  ],
33
45
  },
34
46
  {
@@ -46,19 +58,10 @@ export const WORKFLOWS = [
46
58
  description: 'Audit standards compliance and spec quality',
47
59
  steps: [
48
60
  { cmd: 'uds check', desc: 'Check standards file integrity' },
49
- { cmd: 'uds check --spec-size', desc: 'Check spec sizes against limits' },
50
- { cmd: 'uds lint', desc: 'Lint specs for AC coverage and dependency validity' },
61
+ { cmd: 'uds check --i18n', desc: 'Run i18n lint rules across canonical + locale variants' },
51
62
  { cmd: 'uds audit', desc: 'Deep health diagnosis' },
52
63
  ],
53
64
  },
54
- {
55
- name: 'Resume Previous Work',
56
- description: 'Restore context from a previous session',
57
- steps: [
58
- { cmd: 'uds sync', desc: 'Generate context.md from git diff + workflow state' },
59
- { cmd: 'cat .workflow-state/context.md', desc: 'Read context in new session' },
60
- ],
61
- },
62
65
  ];
63
66
 
64
67
  /**
@@ -757,7 +757,7 @@ export const messages = {
757
757
  actionsAvailable: 'Actions available:',
758
758
  restoreOption: '• Run `uds check --restore` to restore all modified/missing files',
759
759
  diffOption: '• Run `uds check --diff` to view changes',
760
- interactiveOption: '• Run `uds check --interactive` for file-by-file decisions',
760
+ interactiveOption: '• Run `uds check` for file-by-file decisions (interactive by default)',
761
761
  // Interactive mode
762
762
  interactiveMode: 'Interactive Mode:',
763
763
  filesNeedAttention: '{count} file(s) need attention.',
@@ -1991,7 +1991,7 @@ export const messages = {
1991
1991
  actionsAvailable: '可用操作:',
1992
1992
  restoreOption: '• 執行 `uds check --restore` 還原所有已修改/遺失的檔案',
1993
1993
  diffOption: '• 執行 `uds check --diff` 檢視變更',
1994
- interactiveOption: '• 執行 `uds check --interactive` 逐一處理檔案',
1994
+ interactiveOption: '• 執行 `uds check` 逐一處理檔案(預設為互動模式)',
1995
1995
  // Interactive mode
1996
1996
  interactiveMode: '互動模式:',
1997
1997
  filesNeedAttention: '{count} 個檔案需要注意。',
@@ -3240,7 +3240,7 @@ export const messages = {
3240
3240
  actionsAvailable: '可用操作:',
3241
3241
  restoreOption: '• 执行 `uds check --restore` 恢复所有已修改/缺失文件',
3242
3242
  diffOption: '• 执行 `uds check --diff` 查看更改',
3243
- interactiveOption: '• 执行 `uds check --interactive` 逐文件决策',
3243
+ interactiveOption: '• 执行 `uds check` 逐文件决策(默认为交互模式)',
3244
3244
  // Interactive mode
3245
3245
  interactiveMode: '交互模式:',
3246
3246
  filesNeedAttention: '{count} 个文件需要关注。',
@@ -11,7 +11,7 @@ import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
11
11
  import { createHash } from 'crypto';
12
12
  import { join, relative } from 'path';
13
13
  import { readManifest } from '../core/manifest.js';
14
- import { computeFileHash, computeIntegrationBlockHash } from '../utils/hasher.js';
14
+ import { computeFileHash, computeIntegrationBlockHash, normalizeLineEndings } from '../utils/hasher.js';
15
15
  import { SUPPORTED_AI_TOOLS, UDS_MARKERS } from '../core/constants.js';
16
16
  import { getSkillsDirForAgent, getCommandsDirForAgent, getCommandFileExtension } from '../config/ai-agent-paths.js';
17
17
  import { getSkillsSourceEntryNames, getAvailableCommandNames } from '../utils/skills-installer.js';
@@ -322,6 +322,13 @@ function shippedCommandNames() {
322
322
  * materialising a second array first. The format is the contract; if it changes
323
323
  * in one place and not the other every skill reports as changed, which the
324
324
  * paired tests in `skill-content-hash.test.js` exist to catch.
325
+ *
326
+ * Content is line-ending normalized before hashing (GitHub issue #155): these
327
+ * are files installed into the adopter's own project, which `git checkout`
328
+ * may have rewritten to CRLF under `core.autocrlf=true` on Windows even
329
+ * though the desired side (`computeSkillContentHash`, reading UDS's own
330
+ * package source) never sees a `\r`. Without matching normalization here,
331
+ * every skill would report as changed on every Windows `uds update`.
325
332
  */
326
333
  function hashInstalledSkillDir(dirPath) {
327
334
  let entries;
@@ -346,7 +353,7 @@ function hashInstalledSkillDir(dirPath) {
346
353
  }
347
354
  h.update(name);
348
355
  h.update('\0');
349
- h.update(content);
356
+ h.update(normalizeLineEndings(content));
350
357
  h.update('\0');
351
358
  }
352
359
  return `sha256:${h.digest('hex')}`;
@@ -437,12 +444,16 @@ function scanCommands(state, projectPath, manifest) {
437
444
  // Content hash of the installed file, for UDS-managed commands only —
438
445
  // same reasoning as skills: an adopter's own command has no desired
439
446
  // counterpart to compare against. (XSPEC-382 R7)
447
+ //
448
+ // Line-ending normalized before hashing (GitHub issue #155), matching
449
+ // `computeCommandContentHash` on the desired side — the same CRLF
450
+ // checkout risk `hashInstalledSkillDir` above documents applies here.
440
451
  const cmdUdsManaged = shippedCommandNames().has(cmdName);
441
452
  let cmdHash = null;
442
453
  if (cmdUdsManaged) {
443
454
  try {
444
455
  cmdHash = `sha256:${createHash('sha256')
445
- .update(readFileSync(join(cmdsDir, entry.name), 'utf-8'))
456
+ .update(normalizeLineEndings(readFileSync(join(cmdsDir, entry.name), 'utf-8')))
446
457
  .digest('hex')}`;
447
458
  } catch {
448
459
  // Unreadable → no hash. A partial answer would claim a match that
@@ -1,22 +1,75 @@
1
1
  import { createHash } from 'crypto';
2
- import { readFileSync, statSync, existsSync, readdirSync } from 'fs';
2
+ import { readFileSync, existsSync, readdirSync } from 'fs';
3
3
  import { join, relative } from 'path';
4
4
  import { UDS_MARKERS } from '../core/constants.js';
5
5
  import { resolveIntegrationFile } from '../core/constants.js';
6
6
 
7
+ // GitHub issue #155. `git config core.autocrlf true` (the common
8
+ // Windows default) rewrites LF to CRLF on checkout. The manifest's stored
9
+ // hashes are computed from the LF bytes git carries in the blob (that is what
10
+ // every non-Windows install reads), so a Windows working tree — content
11
+ // byte-for-byte what `git status` calls clean — hashed to something else
12
+ // entirely. Every `.standards/*` file, every skill/command file, and every
13
+ // CLAUDE.md/AGENTS.md UDS block came back "modified" although nothing had
14
+ // changed. Normalizing line endings before hashing (both when the manifest is
15
+ // written and when it is compared) makes the hash line-ending agnostic, so
16
+ // LF and CRLF checkouts of the same content agree.
17
+ //
18
+ // This intentionally makes "someone converted this file's line endings, text
19
+ // otherwise identical" invisible to `uds check`/`uds update`. That is the
20
+ // point, not a gap: line-ending convention is a checkout artifact, not
21
+ // content a standards library should track as a modification — it is the
22
+ // same normalization `git diff`/`git status` already apply before deciding a
23
+ // tracked file is dirty.
24
+
25
+ /**
26
+ * Heuristic binary-content detector, matching the approach git itself uses:
27
+ * a NUL byte anywhere in the first 8000 bytes marks the buffer as binary.
28
+ * Binary content must never be line-ending-normalized — flipping `\r\n` bytes
29
+ * inside an image or font would silently corrupt the hash rather than make it
30
+ * platform-agnostic, and could hide (or manufacture) a real difference.
31
+ * Every file `uds` currently manages under `.standards/`, skills, and
32
+ * commands is text (`.md`/`.yaml`/`.json`), but this function is the general
33
+ * per-file entry point (also used for arbitrary skill directory contents),
34
+ * so the check stays in place for whatever gets added later.
35
+ * @param {Buffer} buffer
36
+ * @returns {boolean} True if the buffer looks binary
37
+ */
38
+ export function isBinaryContent(buffer) {
39
+ const sampleSize = Math.min(buffer.length, 8000);
40
+ for (let i = 0; i < sampleSize; i++) {
41
+ if (buffer[i] === 0) return true;
42
+ }
43
+ return false;
44
+ }
45
+
46
+ /**
47
+ * Normalize CRLF and lone-CR line endings to LF.
48
+ * @param {string} text
49
+ * @returns {string} Normalized text
50
+ */
51
+ export function normalizeLineEndings(text) {
52
+ return text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
53
+ }
54
+
7
55
  /**
8
- * Compute SHA-256 hash for a file
56
+ * Compute SHA-256 hash for a file, normalizing line endings first (unless the
57
+ * file is binary — see `isBinaryContent`). `size` is the byte length of the
58
+ * normalized content, not the raw on-disk size, so the two stay consistent
59
+ * with each other for the quick-reject check in `compareFileHash`.
9
60
  * @param {string} filePath - Absolute file path
10
61
  * @returns {Object|null} { hash, size } or null if file doesn't exist
11
62
  */
12
63
  export function computeFileHash(filePath) {
13
64
  try {
14
- const content = readFileSync(filePath);
65
+ const raw = readFileSync(filePath);
66
+ const content = isBinaryContent(raw)
67
+ ? raw
68
+ : Buffer.from(normalizeLineEndings(raw.toString('utf-8')), 'utf-8');
15
69
  const hash = createHash('sha256').update(content).digest('hex');
16
- const stats = statSync(filePath);
17
70
  return {
18
71
  hash: `sha256:${hash}`,
19
- size: stats.size
72
+ size: content.length
20
73
  };
21
74
  } catch {
22
75
  return null;
@@ -276,7 +329,10 @@ function extractBlockContent(content, format) {
276
329
  */
277
330
  export function computeIntegrationBlockHash(filePath) {
278
331
  try {
279
- const content = readFileSync(filePath, 'utf-8');
332
+ // CLAUDE.md / AGENTS.md are always text — no binary check needed here,
333
+ // unlike `computeFileHash`.
334
+ const rawContent = readFileSync(filePath, 'utf-8');
335
+ const content = normalizeLineEndings(rawContent);
280
336
  const format = detectFormat(filePath);
281
337
  const { blockContent } = extractBlockContent(content, format);
282
338
 
@@ -287,13 +343,12 @@ export function computeIntegrationBlockHash(filePath) {
287
343
 
288
344
  const blockHash = createHash('sha256').update(blockContent).digest('hex');
289
345
  const fullHash = createHash('sha256').update(content).digest('hex');
290
- const stats = statSync(filePath);
291
346
 
292
347
  return {
293
348
  blockHash: `sha256:${blockHash}`,
294
349
  blockSize: Buffer.byteLength(blockContent, 'utf-8'),
295
350
  fullHash: `sha256:${fullHash}`,
296
- fullSize: stats.size
351
+ fullSize: Buffer.byteLength(content, 'utf-8')
297
352
  };
298
353
  } catch {
299
354
  return null;
@@ -3181,8 +3181,8 @@ export function parseStandardsIndexCount(content) {
3181
3181
  export function wrapWithMarkers(content, format) {
3182
3182
  const markers = UDS_MARKERS[format] || UDS_MARKERS.markdown;
3183
3183
  const warning = format === 'plaintext'
3184
- ? '# WARNING: This block is managed by UDS (universal-dev-standards). DO NOT manually edit. Use \'npx uds install\' or \'npx uds update\' to modify.'
3185
- : '<!-- WARNING: This block is managed by UDS (universal-dev-standards). DO NOT manually edit. Use \'npx uds install\' or \'npx uds update\' to modify. -->';
3184
+ ? '# WARNING: This block is managed by UDS (universal-dev-standards). DO NOT manually edit. Use \'npx uds init\' or \'npx uds update\' to modify.'
3185
+ : '<!-- WARNING: This block is managed by UDS (universal-dev-standards). DO NOT manually edit. Use \'npx uds init\' or \'npx uds update\' to modify. -->';
3186
3186
  // 冪等:warning 位於 markers **內部**,而 extractMarkedContent 取出的內容也含它,
3187
3187
  // 於是重新包裝會疊出第二份(dev-platform CLAUDE.md 實測 178/179 兩行完全相同)。
3188
3188
  // 這裡先剝掉內容開頭既有的 warning,不論上游哪條路徑造成都能修掉。
@@ -3362,7 +3362,25 @@ export function generateAgentsMdSummary(config = {}) {
3362
3362
  lines.push('# AGENTS.md');
3363
3363
  lines.push('');
3364
3364
  lines.push('> Auto-generated by [Universal Dev Standards (UDS)](https://github.com/AsiaOstrich/universal-dev-standards).');
3365
- lines.push('> Full standards available in the `.standards/` directory.');
3365
+ lines.push('>');
3366
+ // Say it as an instruction, and say what this file is NOT.
3367
+ //
3368
+ // The previous line — "Full standards available in the `.standards/` directory" — was a
3369
+ // description, and a description asks for nothing. Measured 2026-07-23: Codex read this
3370
+ // file, listed the 65 standards it indexes, and opened none of them, so the rules had the
3371
+ // same effect as not installing UDS at all. Measured again 2026-08-18 on a fresh
3372
+ // `uds init -y`: the generated file is 5,667 bytes containing 69 filename references and
3373
+ // **zero rule statements**. That is not a bug in the generator — 143 `.ai.yaml` files come
3374
+ // to roughly 248k tokens, so inlining them is not possible — which is exactly why the file
3375
+ // has to be explicit that it is an index and that the rules are elsewhere.
3376
+ //
3377
+ // This does not prove the rules get read; only XSPEC-357's P7 probe can measure that, and
3378
+ // it is not built yet. It removes the one thing that was certainly wrong: a file that read
3379
+ // as though it carried the standards when it carried their filenames. (XSPEC-357 R7)
3380
+ lines.push('> **This file is an index, not the standards.** The rules are NOT reproduced here.');
3381
+ lines.push('> Before acting on anything below, open the relevant file under `.standards/`');
3382
+ lines.push('> and follow its contents. Working from this summary alone means working');
3383
+ lines.push('> without the standards.');
3366
3384
  lines.push('');
3367
3385
 
3368
3386
  // Build & Test Commands (auto-detect project type)