universal-dev-standards 6.7.5 → 6.9.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 (213) hide show
  1. package/bin/uds.js +19 -2
  2. package/bundled/ai/standards/acceptance-criteria-traceability.ai.yaml +14 -2
  3. package/bundled/ai/standards/adr-standards.ai.yaml +14 -2
  4. package/bundled/ai/standards/ai-instruction-standards.ai.yaml +6 -6
  5. package/bundled/ai/standards/code-review.ai.yaml +13 -3
  6. package/bundled/ai/standards/commit-message.ai.yaml +8 -4
  7. package/bundled/ai/standards/deferred-item-exit.ai.yaml +225 -0
  8. package/bundled/ai/standards/feature-discovery-standards.ai.yaml +14 -2
  9. package/bundled/ai/standards/governance-layer.ai.yaml +128 -2
  10. package/bundled/ai/standards/logging.ai.yaml +2 -2
  11. package/bundled/ai/standards/retrospective-standards.ai.yaml +14 -2
  12. package/bundled/ai/standards/reverse-engineering-standards.ai.yaml +73 -2
  13. package/bundled/ai/standards/security-standards.ai.yaml +2 -2
  14. package/bundled/ai/standards/spec-driven-development.ai.yaml +14 -2
  15. package/bundled/ai/standards/tech-debt-standards.ai.yaml +87 -3
  16. package/bundled/ai/standards/turn-completion-integrity.ai.yaml +131 -0
  17. package/bundled/core/acceptance-criteria-traceability.md +5 -2
  18. package/bundled/core/adr-standards.md +26 -2
  19. package/bundled/core/ai-instruction-standards.md +9 -7
  20. package/bundled/core/code-review-checklist.md +5 -2
  21. package/bundled/core/context-aware-loading.md +1 -1
  22. package/bundled/core/deferred-item-exit.md +254 -0
  23. package/bundled/core/feature-discovery-standards.md +5 -1
  24. package/bundled/core/governance-layer.md +114 -2
  25. package/bundled/core/retrospective-standards.md +4 -2
  26. package/bundled/core/reverse-engineering-standards.md +81 -2
  27. package/bundled/core/spec-driven-development.md +8 -2
  28. package/bundled/core/tech-debt-standards.md +67 -8
  29. package/bundled/core/turn-completion-integrity.md +196 -0
  30. package/bundled/hooks/check-dangerous-cmd.mjs +60 -0
  31. package/bundled/hooks/check-logging-standard.mjs +59 -0
  32. package/bundled/hooks/check-turn-completion.mjs +233 -0
  33. package/bundled/hooks/inject-standards.mjs +183 -0
  34. package/bundled/hooks/telemetry-wrapper.mjs +77 -0
  35. package/bundled/hooks/turn-completion/detect.mjs +99 -0
  36. package/bundled/hooks/turn-completion/locales/en.mjs +159 -0
  37. package/bundled/hooks/turn-completion/locales/zh-TW.mjs +166 -0
  38. package/bundled/hooks/validate-commit-msg.mjs +104 -0
  39. package/bundled/locales/zh-CN/CHANGELOG.md +65 -3
  40. package/bundled/locales/zh-CN/CLAUDE.md +1 -1
  41. package/bundled/locales/zh-CN/README.md +7 -7
  42. package/bundled/locales/zh-CN/SECURITY.md +1 -1
  43. package/bundled/locales/zh-CN/core/adr-standards.md +1 -1
  44. package/bundled/locales/zh-CN/core/ai-instruction-standards.md +10 -8
  45. package/bundled/locales/zh-CN/core/governance-layer.md +118 -6
  46. package/bundled/locales/zh-CN/core/retrospective-standards.md +1 -1
  47. package/bundled/locales/zh-CN/core/tech-debt-standards.md +71 -4
  48. package/bundled/locales/zh-CN/core/turn-completion-integrity.md +190 -0
  49. package/bundled/locales/zh-CN/docs/CHEATSHEET.md +27 -6
  50. package/bundled/locales/zh-CN/docs/CLI-INIT-OPTIONS.md +29 -68
  51. package/bundled/locales/zh-CN/docs/FEATURE-REFERENCE.md +172 -24
  52. package/bundled/locales/zh-CN/docs/USAGE-MODES-COMPARISON.md +1 -2
  53. package/bundled/locales/zh-CN/integrations/google-antigravity/{INSTRUCTIONS.md → AGENTS.md} +1 -1
  54. package/bundled/locales/zh-CN/integrations/google-antigravity/README.md +3 -3
  55. package/bundled/locales/zh-CN/skills/agents/README.md +1 -1
  56. package/bundled/locales/zh-CN/skills/atdd-assistant/SKILL.md +2 -0
  57. package/bundled/locales/zh-CN/skills/bdd-assistant/SKILL.md +2 -0
  58. package/bundled/locales/zh-CN/skills/brainstorm-assistant/SKILL.md +22 -12
  59. package/bundled/locales/zh-CN/skills/brainstorm-assistant/guide.md +12 -9
  60. package/bundled/locales/zh-CN/skills/code-review-assistant/SKILL.md +1 -0
  61. package/bundled/locales/zh-CN/skills/commands/brainstorm.md +17 -13
  62. package/bundled/locales/zh-CN/skills/commands/config.md +0 -1
  63. package/bundled/locales/zh-CN/skills/commands/init.md +1 -2
  64. package/bundled/locales/zh-CN/skills/commit-standards/SKILL.md +2 -0
  65. package/bundled/locales/zh-CN/skills/contract-test-assistant/SKILL.md +1 -0
  66. package/bundled/locales/zh-CN/skills/dev-methodology/SKILL.md +2 -0
  67. package/bundled/locales/zh-CN/skills/observability-assistant/SKILL.md +1 -0
  68. package/bundled/locales/zh-CN/skills/project-structure-guide/SKILL.md +1 -0
  69. package/bundled/locales/zh-CN/skills/release-standards/SKILL.md +3 -0
  70. package/bundled/locales/zh-CN/skills/requirement-assistant/SKILL.md +2 -0
  71. package/bundled/locales/zh-CN/skills/reverse-engineer/SKILL.md +3 -0
  72. package/bundled/locales/zh-CN/skills/reverse-engineer/tdd-analysis.md +13 -23
  73. package/bundled/locales/zh-CN/skills/runbook-assistant/SKILL.md +1 -0
  74. package/bundled/locales/zh-CN/skills/slo-assistant/SKILL.md +1 -0
  75. package/bundled/locales/zh-CN/skills/tdd-assistant/SKILL.md +2 -0
  76. package/bundled/locales/zh-CN/skills/workflows/README.md +2 -11
  77. package/bundled/locales/zh-TW/CHANGELOG.md +65 -3
  78. package/bundled/locales/zh-TW/CLAUDE.md +1 -1
  79. package/bundled/locales/zh-TW/README.md +7 -7
  80. package/bundled/locales/zh-TW/SECURITY.md +1 -1
  81. package/bundled/locales/zh-TW/core/acceptance-criteria-traceability.md +2 -0
  82. package/bundled/locales/zh-TW/core/adr-standards.md +26 -5
  83. package/bundled/locales/zh-TW/core/ai-instruction-standards.md +10 -8
  84. package/bundled/locales/zh-TW/core/code-review-checklist.md +2 -0
  85. package/bundled/locales/zh-TW/core/container-image-standards.md +2 -2
  86. package/bundled/locales/zh-TW/core/contract-testing-standards.md +2 -2
  87. package/bundled/locales/zh-TW/core/cross-flow-regression.md +8 -7
  88. package/bundled/locales/zh-TW/core/data-contract.md +2 -2
  89. package/bundled/locales/zh-TW/core/data-migration-testing.md +2 -2
  90. package/bundled/locales/zh-TW/core/data-pipeline.md +2 -2
  91. package/bundled/locales/zh-TW/core/deferred-item-exit.md +251 -0
  92. package/bundled/locales/zh-TW/core/documentation-writing-standards.md +228 -3
  93. package/bundled/locales/zh-TW/core/full-coverage-testing.md +15 -2
  94. package/bundled/locales/zh-TW/core/governance-layer.md +118 -5
  95. package/bundled/locales/zh-TW/core/iac-design-principles.md +2 -2
  96. package/bundled/locales/zh-TW/core/incident-response.md +2 -2
  97. package/bundled/locales/zh-TW/core/model-provenance.md +4 -2
  98. package/bundled/locales/zh-TW/core/pii-classification.md +42 -6
  99. package/bundled/locales/zh-TW/core/prd-standards.md +4 -2
  100. package/bundled/locales/zh-TW/core/product-metrics-standards.md +4 -2
  101. package/bundled/locales/zh-TW/core/release-readiness-gate.md +2 -2
  102. package/bundled/locales/zh-TW/core/resource-cost-boundary.md +2 -2
  103. package/bundled/locales/zh-TW/core/retrospective-standards.md +5 -3
  104. package/bundled/locales/zh-TW/core/reverse-engineering-standards.md +83 -5
  105. package/bundled/locales/zh-TW/core/runbook.md +2 -2
  106. package/bundled/locales/zh-TW/core/schema-evolution.md +2 -2
  107. package/bundled/locales/zh-TW/core/secret-management-standards.md +2 -2
  108. package/bundled/locales/zh-TW/core/slo-sli.md +2 -2
  109. package/bundled/locales/zh-TW/core/spec-driven-development.md +2 -0
  110. package/bundled/locales/zh-TW/core/tech-debt-standards.md +71 -4
  111. package/bundled/locales/zh-TW/core/turn-completion-integrity.md +190 -0
  112. package/bundled/locales/zh-TW/core/user-journey-testing.md +2 -2
  113. package/bundled/locales/zh-TW/core/user-story-mapping.md +2 -2
  114. package/bundled/locales/zh-TW/core/verification-oracle.md +2 -2
  115. package/bundled/locales/zh-TW/docs/CHEATSHEET.md +27 -6
  116. package/bundled/locales/zh-TW/docs/CLI-INIT-OPTIONS.md +29 -68
  117. package/bundled/locales/zh-TW/docs/FEATURE-REFERENCE.md +172 -24
  118. package/bundled/locales/zh-TW/docs/USAGE-MODES-COMPARISON.md +1 -2
  119. package/bundled/locales/zh-TW/integrations/google-antigravity/{INSTRUCTIONS.md → AGENTS.md} +1 -1
  120. package/bundled/locales/zh-TW/integrations/google-antigravity/README.md +3 -3
  121. package/bundled/locales/zh-TW/skills/adr-assistant/SKILL.md +1 -1
  122. package/bundled/locales/zh-TW/skills/agents/README.md +1 -1
  123. package/bundled/locales/zh-TW/skills/atdd-assistant/SKILL.md +2 -0
  124. package/bundled/locales/zh-TW/skills/bdd-assistant/SKILL.md +2 -0
  125. package/bundled/locales/zh-TW/skills/brainstorm-assistant/SKILL.md +22 -12
  126. package/bundled/locales/zh-TW/skills/brainstorm-assistant/guide.md +12 -9
  127. package/bundled/locales/zh-TW/skills/code-review-assistant/SKILL.md +1 -0
  128. package/bundled/locales/zh-TW/skills/commands/brainstorm.md +17 -13
  129. package/bundled/locales/zh-TW/skills/commands/config.md +0 -1
  130. package/bundled/locales/zh-TW/skills/commands/init.md +1 -2
  131. package/bundled/locales/zh-TW/skills/commit-standards/SKILL.md +2 -0
  132. package/bundled/locales/zh-TW/skills/contract-test-assistant/SKILL.md +2 -1
  133. package/bundled/locales/zh-TW/skills/dev-methodology/SKILL.md +2 -0
  134. package/bundled/locales/zh-TW/skills/dev-workflow-guide/SKILL.md +1 -1
  135. package/bundled/locales/zh-TW/skills/knowledge-graph/guide.md +2 -2
  136. package/bundled/locales/zh-TW/skills/migration-assistant/SKILL.md +1 -1
  137. package/bundled/locales/zh-TW/skills/observability-assistant/SKILL.md +1 -0
  138. package/bundled/locales/zh-TW/skills/project-discovery/SKILL.md +1 -0
  139. package/bundled/locales/zh-TW/skills/project-structure-guide/SKILL.md +1 -0
  140. package/bundled/locales/zh-TW/skills/release-standards/SKILL.md +3 -0
  141. package/bundled/locales/zh-TW/skills/requirement-assistant/SKILL.md +2 -0
  142. package/bundled/locales/zh-TW/skills/reverse-engineer/SKILL.md +3 -0
  143. package/bundled/locales/zh-TW/skills/reverse-engineer/tdd-analysis.md +13 -23
  144. package/bundled/locales/zh-TW/skills/runbook-assistant/SKILL.md +1 -0
  145. package/bundled/locales/zh-TW/skills/slo-assistant/SKILL.md +1 -0
  146. package/bundled/locales/zh-TW/skills/tdd-assistant/SKILL.md +2 -0
  147. package/bundled/locales/zh-TW/skills/workflows/README.md +2 -11
  148. package/bundled/skills/agents/README.md +1 -1
  149. package/bundled/skills/atdd-assistant/SKILL.md +2 -0
  150. package/bundled/skills/bdd-assistant/SKILL.md +2 -0
  151. package/bundled/skills/brainstorm-assistant/SKILL.md +31 -13
  152. package/bundled/skills/brainstorm-assistant/guide.md +9 -6
  153. package/bundled/skills/code-review-assistant/SKILL.md +1 -0
  154. package/bundled/skills/commands/brainstorm.md +12 -9
  155. package/bundled/skills/commands/config.md +0 -1
  156. package/bundled/skills/commands/init.md +2 -3
  157. package/bundled/skills/commit-standards/SKILL.md +2 -0
  158. package/bundled/skills/contract-test-assistant/SKILL.md +1 -0
  159. package/bundled/skills/dev-methodology/SKILL.md +4 -0
  160. package/bundled/skills/observability-assistant/SKILL.md +1 -0
  161. package/bundled/skills/project-discovery/SKILL.md +1 -0
  162. package/bundled/skills/project-structure-guide/SKILL.md +1 -0
  163. package/bundled/skills/release-standards/SKILL.md +3 -0
  164. package/bundled/skills/requirement-assistant/SKILL.md +2 -0
  165. package/bundled/skills/reverse-engineer/SKILL.md +3 -0
  166. package/bundled/skills/reverse-engineer/tdd-analysis.md +16 -23
  167. package/bundled/skills/runbook-assistant/SKILL.md +1 -0
  168. package/bundled/skills/slo-assistant/SKILL.md +1 -0
  169. package/bundled/skills/tdd-assistant/SKILL.md +2 -0
  170. package/bundled/skills/workflows/README.md +2 -11
  171. package/bundled/templates/.ai-context.yaml.template +194 -0
  172. package/bundled/templates/CLAUDE.md.template +145 -0
  173. package/bundled/templates/DESIGN.md +237 -0
  174. package/bundled/templates/SKILL-BRIEF-TEMPLATE.md +57 -0
  175. package/bundled/templates/SKILL-CANDIDATES.md +39 -0
  176. package/bundled/templates/gates/check-error-exit.mjs +309 -0
  177. package/bundled/templates/mcp-config.json +10 -0
  178. package/bundled/templates/methodology-template.yaml +209 -0
  179. package/bundled/templates/migration-template.md +408 -0
  180. package/bundled/templates/requirement-checklist.md +410 -0
  181. package/bundled/templates/requirement-document-template.md +591 -0
  182. package/bundled/templates/requirement-template.md +881 -0
  183. package/bundled/templates/reverse-spec-template.md +409 -0
  184. package/bundled/templates/test-case-template.md +74 -0
  185. package/bundled/templates/test-plan-template.md +74 -0
  186. package/package.json +9 -5
  187. package/src/commands/audit.js +82 -0
  188. package/src/commands/check.js +66 -10
  189. package/src/commands/init.js +161 -16
  190. package/src/commands/lint.js +96 -0
  191. package/src/commands/quickstart.js +16 -13
  192. package/src/commands/update.js +286 -14
  193. package/src/compilers/claude-code-compiler.js +4 -1
  194. package/src/config/ai-agent-paths.js +62 -17
  195. package/src/core/constants.js +42 -11
  196. package/src/core/manifest.js +201 -3
  197. package/src/core/paths.js +2 -2
  198. package/src/i18n/messages.js +9 -32
  199. package/src/installers/hooks-installer.js +167 -75
  200. package/src/installers/integration-installer.js +9 -5
  201. package/src/prompts/init.js +14 -14
  202. package/src/reconciler/actual-state-scanner.js +14 -3
  203. package/src/utils/detector.js +21 -1
  204. package/src/utils/effect-boundary.js +1093 -0
  205. package/src/utils/hasher.js +229 -9
  206. package/src/utils/hook-stats.js +1 -1
  207. package/src/utils/integration-generator.js +100 -4
  208. package/src/utils/reference-sync.js +4 -1
  209. package/src/utils/skills-installer.js +17 -3
  210. package/src/utils/spec-linter.js +35 -76
  211. package/src/utils/yaml-generator.js +51 -9
  212. package/standards-registry.json +31 -8
  213. package/src/commands/sync.js +0 -133
@@ -1,105 +1,197 @@
1
1
  /**
2
2
  * UDS Hook Installer
3
3
  *
4
- * Copies hook scripts and generates/merges .claude/settings.json
5
- * with hook configurations.
4
+ * Copies hook scripts and merges .claude/settings.json with hook configurations
5
+ * derived from the `enforcement:` blocks of the shipped standards.
6
6
  *
7
7
  * Key constraints:
8
8
  * - MUST merge, never overwrite existing settings.json
9
9
  * - MUST be idempotent (no duplicate hooks on re-install)
10
+ * - MUST emit the hook handler shape Claude Code actually executes
11
+ *
12
+ * 🔴 Three defects fixed here on 2026-09-08, each of which failed silently and
13
+ * all three of which were present at once:
14
+ *
15
+ * 1. Hook entries were written as bare strings (`hooks: ['node x.js']`).
16
+ * Measured with a two-arm run — a bare-string entry and an object entry
17
+ * side by side in the same event of the same session: the object ran, the
18
+ * bare string did not. Claude Code dispatches on `type`, and an entry
19
+ * without one is skipped. settings.json stays valid JSON, the install
20
+ * reports success, and nothing ever runs.
21
+ * 2. The hook scripts were resolved from `<repo>/scripts/hooks`, which is
22
+ * outside the published package (`npm pack` listed 1 file matching
23
+ * "hooks": this installer). Adopters installing from npm copied zero
24
+ * scripts and still got `installed: true`.
25
+ * 3. The event/script list was hardcoded here, so a standard declaring an
26
+ * `enforcement:` block got no hook unless someone also remembered to edit
27
+ * this file. It is now derived by walking the standards.
10
28
  *
11
29
  * @module installers/hooks-installer
12
30
  * @see docs/specs/SPEC-HOOKS-001-core-standard-hooks.md (REQ-4)
31
+ * @see core/turn-completion-integrity.md
13
32
  */
14
33
 
15
- import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from 'fs';
16
- import { join, dirname } from 'path';
34
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, cpSync, readdirSync } from 'fs';
35
+ import { join, dirname, basename } from 'path';
17
36
  import { fileURLToPath } from 'url';
37
+ import { execFileSync } from 'child_process';
38
+ import { load as parseYaml } from 'js-yaml';
18
39
 
19
40
  const __dirname = dirname(fileURLToPath(import.meta.url));
20
41
 
21
- const HOOK_CONFIGS = {
22
- PreToolUse: [
23
- {
24
- matcher: 'Bash',
25
- hooks: ['node scripts/hooks/check-dangerous-cmd.js'],
26
- },
27
- ],
28
- PostToolUse: [
29
- {
30
- matcher: 'Bash',
31
- hooks: ['node scripts/hooks/check-logging-standard.js'],
32
- },
33
- ],
34
- UserPromptSubmit: [
35
- {
36
- matcher: '',
37
- hooks: ['node scripts/hooks/validate-commit-msg.js'],
38
- },
39
- ],
40
- };
41
-
42
- const HOOK_SCRIPTS = [
43
- 'validate-commit-msg.js',
44
- 'check-dangerous-cmd.js',
45
- 'check-logging-standard.js',
46
- ];
42
+ /** Prefer the bundled copy (present in the npm package), fall back to the repo. */
43
+ function resolveDir(bundledRel, repoRel) {
44
+ const bundled = join(__dirname, '..', '..', 'bundled', ...bundledRel);
45
+ if (existsSync(bundled)) return bundled;
46
+ const repo = join(__dirname, '..', '..', '..', ...repoRel);
47
+ return existsSync(repo) ? repo : null;
48
+ }
49
+
50
+ export const standardsSourceDir = () => resolveDir(['ai', 'standards'], ['ai', 'standards']);
51
+ export const hooksSourceDir = () => resolveDir(['hooks'], ['scripts', 'hooks']);
52
+
53
+ /**
54
+ * Claude Code's matcher is a string. Some standards declare it as an object
55
+ * (`matcher: { tool: Bash }`), which would be written into settings.json
56
+ * verbatim and never match anything.
57
+ */
58
+ function normalizeMatcher(matcher) {
59
+ if (typeof matcher === 'string') return matcher;
60
+ if (matcher && typeof matcher === 'object' && typeof matcher.tool === 'string') return matcher.tool;
61
+ return '';
62
+ }
63
+
64
+ /**
65
+ * Walk the shipped standards and build the hook configuration from every
66
+ * complete `enforcement:` block.
67
+ *
68
+ * @param {string|null} stdDir - directory of *.ai.yaml standards
69
+ * @param {string|null} hookDir - directory the hook scripts live in
70
+ * @returns {{ configs: Object, scripts: string[], skipped: Array<{id: string, why: string}> }}
71
+ */
72
+ export function collectHookConfigs(stdDir, hookDir) {
73
+ const configs = {};
74
+ const scripts = [];
75
+ const skipped = [];
76
+ if (!stdDir) return { configs, scripts, skipped: [{ id: '*', why: 'standards directory not found' }] };
77
+
78
+ for (const file of readdirSync(stdDir).filter((f) => f.endsWith('.ai.yaml')).sort()) {
79
+ let std;
80
+ try {
81
+ std = parseYaml(readFileSync(join(stdDir, file), 'utf-8'));
82
+ } catch {
83
+ skipped.push({ id: file, why: 'unparseable YAML' });
84
+ continue;
85
+ }
86
+ const e = std && std.enforcement;
87
+ if (!e) continue;
88
+
89
+ const id = (std && std.id) || file;
90
+ if (typeof e.trigger !== 'string' || !e.trigger || typeof e.hook_script !== 'string' || !e.hook_script) {
91
+ // A block missing either field used to compile to hooks["undefined"]:
92
+ // [{ hooks: ["node undefined"] }] with no error at any layer.
93
+ skipped.push({ id, why: 'enforcement block missing trigger or hook_script' });
94
+ continue;
95
+ }
96
+
97
+ const rel = e.hook_script.replace(/^scripts\/hooks\//, '');
98
+ if (hookDir && !existsSync(join(hookDir, rel))) {
99
+ skipped.push({ id, why: `hook script not found: ${e.hook_script}` });
100
+ continue;
101
+ }
102
+
103
+ (configs[e.trigger] ||= []).push({
104
+ matcher: normalizeMatcher(e.matcher),
105
+ hooks: [{ type: 'command', command: `node scripts/hooks/${rel}` }],
106
+ });
107
+ scripts.push(rel);
108
+ }
109
+ return { configs, scripts, skipped };
110
+ }
111
+
112
+ /** The command string an entry runs, whatever shape it is stored in. */
113
+ function commandOf(entry) {
114
+ const h = entry && entry.hooks && entry.hooks[0];
115
+ if (typeof h === 'string') return h;
116
+ return h && typeof h.command === 'string' ? h.command : undefined;
117
+ }
47
118
 
48
119
  /**
49
- * Deep merge hook arrays, deduplicating by hook script path.
50
- * @param {Array} existing - Existing hook entries
51
- * @param {Array} incoming - New hook entries to merge
52
- * @returns {Array} Merged array without duplicates
120
+ * Merge hook arrays, deduplicating by (command, matcher).
121
+ * Existing entries are never rewritten — an adopter may have edited them.
53
122
  */
54
- function mergeHookArray(existing, incoming) {
123
+ export function mergeHookArray(existing, incoming) {
55
124
  const result = [...existing];
56
125
  for (const entry of incoming) {
57
- const hookScript = entry.hooks?.[0];
58
- const isDuplicate = result.some(
59
- (e) => e.hooks?.[0] === hookScript && e.matcher === entry.matcher
60
- );
61
- if (!isDuplicate) {
62
- result.push(entry);
63
- }
126
+ const cmd = commandOf(entry);
127
+ const dup = result.some((e) => commandOf(e) === cmd && e.matcher === entry.matcher);
128
+ if (!dup) result.push(entry);
64
129
  }
65
130
  return result;
66
131
  }
67
132
 
68
133
  /**
69
134
  * Install UDS hooks into a target project.
70
- * - Copies hook scripts to scripts/hooks/
71
- * - Creates or merges .claude/settings.json with hook configs
72
135
  *
73
136
  * @param {string} projectPath - Target project root path
74
- * @returns {{ installed: boolean, scriptsCount: number, settingsPath: string }}
137
+ * @returns {{ installed: boolean, scriptsCount: number, settingsPath: string, events: string[], skipped: Array }}
138
+ */
139
+ /**
140
+ * Which installed hooks only work in some languages, asked of the hooks themselves.
141
+ *
142
+ * turn-completion-integrity R8 says an unsupported language must be declared,
143
+ * and the hook already answers `--languages`. Restating that list here would
144
+ * make a second hand-maintained copy of it, and the copy would drift — quietly,
145
+ * because both halves keep looking like valid data.
146
+ *
147
+ * 🔴 Why it matters at all: a prose-reading hook in a language it does not ship
148
+ * is installed, running, and structurally unable to fire. Its output is
149
+ * byte-identical to a turn with nothing wrong. Without this line the adopter
150
+ * has no way to tell those apart, ever.
151
+ *
152
+ * A hook with no such flag simply produces nothing here; failure is not an
153
+ * error, because "cannot introspect" must not block an install.
75
154
  */
155
+ function probeLanguageLimits(hooksDir, scripts) {
156
+ const out = [];
157
+ for (const rel of new Set(scripts)) {
158
+ const abs = join(hooksDir, basename(rel));
159
+ if (!existsSync(abs)) continue;
160
+ try {
161
+ const text = execFileSync(process.execPath, [abs, '--languages'], {
162
+ encoding: 'utf-8',
163
+ timeout: 5000,
164
+ stdio: ['ignore', 'pipe', 'ignore'],
165
+ }).trim();
166
+ if (text) out.push({ script: basename(rel), languages: text });
167
+ } catch {
168
+ // no --languages, or it refused: nothing to declare.
169
+ }
170
+ }
171
+ return out;
172
+ }
173
+
76
174
  export function installHooks(projectPath) {
77
175
  const claudeDir = join(projectPath, '.claude');
78
176
  const settingsPath = join(claudeDir, 'settings.json');
177
+ const hooksDir = join(projectPath, 'scripts', 'hooks');
79
178
 
80
- // Ensure .claude/ directory exists
81
- if (!existsSync(claudeDir)) {
82
- mkdirSync(claudeDir, { recursive: true });
83
- }
179
+ const stdDir = standardsSourceDir();
180
+ const hookDir = hooksSourceDir();
181
+ const { configs, scripts, skipped } = collectHookConfigs(stdDir, hookDir);
182
+ const events = Object.keys(configs);
84
183
 
85
- // Copy hook scripts
86
- const hooksDir = join(projectPath, 'scripts', 'hooks');
87
- if (!existsSync(hooksDir)) {
88
- mkdirSync(hooksDir, { recursive: true });
184
+ if (events.length === 0) {
185
+ // Reporting success here is what let three separate breakages ship.
186
+ return { installed: false, scriptsCount: 0, settingsPath, events, skipped };
89
187
  }
90
188
 
91
- const sourceHooksDir = join(__dirname, '..', '..', '..', 'scripts', 'hooks');
92
- let scriptsCount = 0;
93
- for (const script of HOOK_SCRIPTS) {
94
- const src = join(sourceHooksDir, script);
95
- const dest = join(hooksDir, script);
96
- if (existsSync(src)) {
97
- copyFileSync(src, dest);
98
- scriptsCount++;
99
- }
100
- }
189
+ if (!existsSync(claudeDir)) mkdirSync(claudeDir, { recursive: true });
190
+ if (!existsSync(hooksDir)) mkdirSync(hooksDir, { recursive: true });
191
+
192
+ // Recursive: a hook may ship a directory beside it (locale packs, fixtures).
193
+ cpSync(hookDir, hooksDir, { recursive: true });
101
194
 
102
- // Read or create settings.json
103
195
  let settings = {};
104
196
  if (existsSync(settingsPath)) {
105
197
  try {
@@ -108,21 +200,21 @@ export function installHooks(projectPath) {
108
200
  settings = {};
109
201
  }
110
202
  }
203
+ if (!settings.hooks) settings.hooks = {};
111
204
 
112
- // Merge hooks
113
- if (!settings.hooks) {
114
- settings.hooks = {};
115
- }
116
-
117
- for (const [event, entries] of Object.entries(HOOK_CONFIGS)) {
118
- if (!settings.hooks[event]) {
119
- settings.hooks[event] = [];
120
- }
205
+ for (const [event, entries] of Object.entries(configs)) {
206
+ if (!settings.hooks[event]) settings.hooks[event] = [];
121
207
  settings.hooks[event] = mergeHookArray(settings.hooks[event], entries);
122
208
  }
123
209
 
124
- // Write settings.json
125
210
  writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
126
211
 
127
- return { installed: true, scriptsCount, settingsPath };
212
+ return {
213
+ installed: true,
214
+ scriptsCount: new Set(scripts).size,
215
+ settingsPath,
216
+ events,
217
+ skipped,
218
+ languageLimits: probeLanguageLimits(hooksDir, scripts),
219
+ };
128
220
  }
@@ -32,11 +32,15 @@ export const INTEGRATION_MAPPINGS = {
32
32
  target: '.github/copilot-instructions.md'
33
33
  },
34
34
  antigravity: {
35
- source: 'integrations/google-antigravity/INSTRUCTIONS.md',
36
- target: 'INSTRUCTIONS.md'
35
+ source: 'integrations/google-antigravity/AGENTS.md',
36
+ target: '.agents/AGENTS.md'
37
37
  },
38
38
  codex: {
39
- source: 'integrations/openai-codex/AGENTS.md',
39
+ // 🔴 Was 'integrations/openai-codex/AGENTS.md'; that directory does not exist.
40
+ // This map is only reached when generation fails, so a broken entry here is a
41
+ // fallback that fails exactly when it is needed. Found by walking the map and
42
+ // stat-ing every source — nothing was checking that these paths resolve.
43
+ source: 'integrations/codex/AGENTS.md',
40
44
  target: 'AGENTS.md'
41
45
  },
42
46
  'gemini-cli': {
@@ -56,7 +60,7 @@ export const INTEGRATION_MAPPINGS = {
56
60
  * @param {Array<string>} config.integrations - List of AI tool names (e.g., ['cursor', 'windsurf'])
57
61
  * @param {Object} config.integrationConfigs - Tool-specific configurations
58
62
  * @param {Array<string>} config.installedStandards - List of installed standard file names
59
- * @param {string} config.contentMode - Content mode ('minimal', 'index', 'full')
63
+ * @param {string} config.contentMode - Content mode ('minimal', 'index'; 'full' retired — XSPEC-357 R7)
60
64
  * @param {number} config.level - Adoption level (1-3)
61
65
  * @param {string} config.commonLanguage - Language setting ('en', 'zh-tw', 'zh-cn', 'bilingual')
62
66
  * @param {string} config.outputLanguage - Output language preference
@@ -191,7 +195,7 @@ export async function installIntegrations(config, projectPath) {
191
195
  * @param {Object} config - Claude Code configuration
192
196
  * @param {Array<string>} config.aiTools - List of selected AI tools
193
197
  * @param {Array<string>} config.installedStandards - List of installed standard file names
194
- * @param {string} config.contentMode - Content mode ('minimal', 'index', 'full')
198
+ * @param {string} config.contentMode - Content mode ('minimal', 'index'; 'full' retired — XSPEC-357 R7)
195
199
  * @param {number} config.level - Adoption level (1-3)
196
200
  * @param {string} config.commonLanguage - Language setting ('en', 'zh-tw', 'zh-cn', 'bilingual')
197
201
  * @param {string} config.outputLanguage - Output language preference
@@ -109,7 +109,7 @@ export async function promptAITools(detected = {}) {
109
109
  checked: detected.copilot || false
110
110
  },
111
111
  {
112
- name: `Google Antigravity ${chalk.gray('(INSTRUCTIONS.md)')}`,
112
+ name: `Google Antigravity ${chalk.gray('(.agents/AGENTS.md)')}`,
113
113
  value: 'antigravity',
114
114
  checked: detected.antigravity || false
115
115
  },
@@ -1053,10 +1053,14 @@ export async function promptConfirm(message) {
1053
1053
  * Best with Skills (which provide real-time guidance).
1054
1054
  * - index: Summary + MUST/SHOULD task mapping. AI knows when to read which file.
1055
1055
  * Best balance of context usage and compliance.
1056
- * - full: All rules embedded. AI has everything in context immediately.
1057
- * Highest compliance but uses more context space.
1058
1056
  *
1059
- * @returns {Promise<string>} 'full', 'index', or 'minimal'
1057
+ * A third choice, `full` ("all rules embedded, highest compliance, uses more
1058
+ * context"), was removed in XSPEC-357 R7. None of that was true: it shared the
1059
+ * `index` branch in the generator and produced identical bytes. Offering it
1060
+ * asked adopters to trade context budget for compliance in a menu where both
1061
+ * options were the same file. (XSPEC-357 R7)
1062
+ *
1063
+ * @returns {Promise<string>} 'index' or 'minimal'
1060
1064
  */
1061
1065
  export async function promptContentMode() {
1062
1066
  const msg = t().contentMode;
@@ -1075,10 +1079,6 @@ export async function promptContentMode() {
1075
1079
  name: `${chalk.green(msg.labels.index)} ${chalk.gray(`(${t().recommended})`)} - ${msg.choices.index}`,
1076
1080
  value: 'index'
1077
1081
  },
1078
- {
1079
- name: `${chalk.blue(msg.labels.full)} - ${msg.choices.full}`,
1080
- value: 'full'
1081
- },
1082
1082
  {
1083
1083
  name: `${chalk.gray(msg.labels.minimal)} - ${msg.choices.minimal}`,
1084
1084
  value: 'minimal'
@@ -1159,7 +1159,7 @@ const AI_TOOL_DEFINITIONS = {
1159
1159
  windsurf: { name: 'Windsurf', file: '.windsurfrules' },
1160
1160
  cline: { name: 'Cline', file: '.clinerules' },
1161
1161
  copilot: { name: 'GitHub Copilot', file: '.github/copilot-instructions.md' },
1162
- antigravity: { name: 'Google Antigravity', file: 'INSTRUCTIONS.md' },
1162
+ antigravity: { name: 'Google Antigravity', file: '.agents/AGENTS.md' },
1163
1163
  codex: { name: 'OpenAI Codex', file: 'AGENTS.md' },
1164
1164
  'gemini-cli': { name: 'Gemini CLI', file: 'GEMINI.md' },
1165
1165
  opencode: { name: 'OpenCode', file: 'AGENTS.md' }
@@ -1286,16 +1286,16 @@ export async function promptContentModeChange(currentMode) {
1286
1286
  name: `${chalk.green(msg.labels.index)} ${chalk.gray(`(${t().recommended})`)} - ${msg.choices.index}`,
1287
1287
  value: 'index'
1288
1288
  },
1289
- {
1290
- name: `${chalk.blue(msg.labels.full)} - ${msg.choices.full}`,
1291
- value: 'full'
1292
- },
1293
1289
  {
1294
1290
  name: `${chalk.gray(msg.labels.minimal)} - ${msg.choices.minimal}`,
1295
1291
  value: 'minimal'
1296
1292
  }
1297
1293
  ],
1298
- default: currentMode === 'full' ? 1 : currentMode === 'minimal' ? 2 : 0
1294
+ // A stored `full` lands on `index`, which is the mode it was already
1295
+ // getting. Indices shifted when the middle choice went, so this is
1296
+ // recomputed rather than adjusted — an off-by-one here silently
1297
+ // preselects the wrong mode.
1298
+ default: currentMode === 'minimal' ? 1 : 0
1299
1299
  });
1300
1300
 
1301
1301
  if (mode !== currentMode) {
@@ -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
@@ -126,7 +126,27 @@ export function detectAITools(projectPath) {
126
126
  copilot: existsSync(join(projectPath, '.github', 'copilot-instructions.md')),
127
127
  claudeCode: existsSync(join(projectPath, '.claude')) ||
128
128
  existsSync(join(projectPath, 'CLAUDE.md')),
129
- antigravity: existsSync(join(projectPath, 'INSTRUCTIONS.md')),
129
+ // Antigravity never read INSTRUCTIONS.md.
130
+ // Measured 2026-09-08 with two positive controls in the same run: tokens planted in `AGENTS.md` and `.agents/AGENTS.md` both came back with correct attribution; the one in INSTRUCTIONS.md did not.
131
+ // `.agents/AGENTS.md` is used rather than the repo root so it does not collide with Codex/OpenCode, which both target root AGENTS.md.
132
+ antigravity: existsSync(join(projectPath, '.agents', 'AGENTS.md')),
133
+ // 🔴 Roo Code had a full entry in the path table (`.roo/skills/`, tier "complete"
134
+ // in REGISTRY.json) and NO line here, so `uds init` could never install for it —
135
+ // however correct those paths were. Found by `check:install-paths`, which walks
136
+ // the path table instead of naming tools; nothing else was looking.
137
+ //
138
+ // Markers are the ones Roo Code's own docs name, not a guess from the tool name:
139
+ // `.roo/rules/` — "Workspace-wide rules", the current directory-based method
140
+ // `.roorules` — the legacy single-file fallback, still read
141
+ // (roocodeinc.github.io/Roo-Code/features/custom-instructions, read 2026-09-08)
142
+ // `.roo` is matched rather than `.roo/rules` because Roo also uses `.roo/skills/`
143
+ // and `.roo/rules-{mode}/`; the directory is what marks the tool.
144
+ //
145
+ // ⚠️ Deliberately NOT `.clinerules`. REGISTRY.json lists it under Roo Code as
146
+ // legacy backward-compat, and it is also Cline's own marker — detecting on it
147
+ // would make the two tools indistinguishable.
148
+ 'roo-code': existsSync(join(projectPath, '.roo')) ||
149
+ existsSync(join(projectPath, '.roorules')),
130
150
  codex: hasAgentsMd,
131
151
  opencode: hasAgentsMd,
132
152
  geminiCli: existsSync(join(projectPath, 'GEMINI.md'))