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.
- package/bin/uds.js +19 -2
- package/bundled/ai/standards/acceptance-criteria-traceability.ai.yaml +14 -2
- package/bundled/ai/standards/adr-standards.ai.yaml +14 -2
- package/bundled/ai/standards/ai-instruction-standards.ai.yaml +6 -6
- package/bundled/ai/standards/code-review.ai.yaml +13 -3
- package/bundled/ai/standards/commit-message.ai.yaml +8 -4
- package/bundled/ai/standards/deferred-item-exit.ai.yaml +225 -0
- package/bundled/ai/standards/feature-discovery-standards.ai.yaml +14 -2
- package/bundled/ai/standards/governance-layer.ai.yaml +128 -2
- package/bundled/ai/standards/logging.ai.yaml +2 -2
- package/bundled/ai/standards/retrospective-standards.ai.yaml +14 -2
- package/bundled/ai/standards/reverse-engineering-standards.ai.yaml +73 -2
- package/bundled/ai/standards/security-standards.ai.yaml +2 -2
- package/bundled/ai/standards/spec-driven-development.ai.yaml +14 -2
- package/bundled/ai/standards/tech-debt-standards.ai.yaml +87 -3
- package/bundled/ai/standards/turn-completion-integrity.ai.yaml +131 -0
- package/bundled/core/acceptance-criteria-traceability.md +5 -2
- package/bundled/core/adr-standards.md +26 -2
- package/bundled/core/ai-instruction-standards.md +9 -7
- package/bundled/core/code-review-checklist.md +5 -2
- package/bundled/core/context-aware-loading.md +1 -1
- package/bundled/core/deferred-item-exit.md +254 -0
- package/bundled/core/feature-discovery-standards.md +5 -1
- package/bundled/core/governance-layer.md +114 -2
- package/bundled/core/retrospective-standards.md +4 -2
- package/bundled/core/reverse-engineering-standards.md +81 -2
- package/bundled/core/spec-driven-development.md +8 -2
- package/bundled/core/tech-debt-standards.md +67 -8
- package/bundled/core/turn-completion-integrity.md +196 -0
- package/bundled/hooks/check-dangerous-cmd.mjs +60 -0
- package/bundled/hooks/check-logging-standard.mjs +59 -0
- package/bundled/hooks/check-turn-completion.mjs +233 -0
- package/bundled/hooks/inject-standards.mjs +183 -0
- package/bundled/hooks/telemetry-wrapper.mjs +77 -0
- package/bundled/hooks/turn-completion/detect.mjs +99 -0
- package/bundled/hooks/turn-completion/locales/en.mjs +159 -0
- package/bundled/hooks/turn-completion/locales/zh-TW.mjs +166 -0
- package/bundled/hooks/validate-commit-msg.mjs +104 -0
- package/bundled/locales/zh-CN/CHANGELOG.md +65 -3
- package/bundled/locales/zh-CN/CLAUDE.md +1 -1
- package/bundled/locales/zh-CN/README.md +7 -7
- package/bundled/locales/zh-CN/SECURITY.md +1 -1
- package/bundled/locales/zh-CN/core/adr-standards.md +1 -1
- package/bundled/locales/zh-CN/core/ai-instruction-standards.md +10 -8
- package/bundled/locales/zh-CN/core/governance-layer.md +118 -6
- package/bundled/locales/zh-CN/core/retrospective-standards.md +1 -1
- package/bundled/locales/zh-CN/core/tech-debt-standards.md +71 -4
- package/bundled/locales/zh-CN/core/turn-completion-integrity.md +190 -0
- package/bundled/locales/zh-CN/docs/CHEATSHEET.md +27 -6
- package/bundled/locales/zh-CN/docs/CLI-INIT-OPTIONS.md +29 -68
- package/bundled/locales/zh-CN/docs/FEATURE-REFERENCE.md +172 -24
- package/bundled/locales/zh-CN/docs/USAGE-MODES-COMPARISON.md +1 -2
- package/bundled/locales/zh-CN/integrations/google-antigravity/{INSTRUCTIONS.md → AGENTS.md} +1 -1
- package/bundled/locales/zh-CN/integrations/google-antigravity/README.md +3 -3
- package/bundled/locales/zh-CN/skills/agents/README.md +1 -1
- package/bundled/locales/zh-CN/skills/atdd-assistant/SKILL.md +2 -0
- package/bundled/locales/zh-CN/skills/bdd-assistant/SKILL.md +2 -0
- package/bundled/locales/zh-CN/skills/brainstorm-assistant/SKILL.md +22 -12
- package/bundled/locales/zh-CN/skills/brainstorm-assistant/guide.md +12 -9
- package/bundled/locales/zh-CN/skills/code-review-assistant/SKILL.md +1 -0
- package/bundled/locales/zh-CN/skills/commands/brainstorm.md +17 -13
- package/bundled/locales/zh-CN/skills/commands/config.md +0 -1
- package/bundled/locales/zh-CN/skills/commands/init.md +1 -2
- package/bundled/locales/zh-CN/skills/commit-standards/SKILL.md +2 -0
- package/bundled/locales/zh-CN/skills/contract-test-assistant/SKILL.md +1 -0
- package/bundled/locales/zh-CN/skills/dev-methodology/SKILL.md +2 -0
- package/bundled/locales/zh-CN/skills/observability-assistant/SKILL.md +1 -0
- package/bundled/locales/zh-CN/skills/project-structure-guide/SKILL.md +1 -0
- package/bundled/locales/zh-CN/skills/release-standards/SKILL.md +3 -0
- package/bundled/locales/zh-CN/skills/requirement-assistant/SKILL.md +2 -0
- package/bundled/locales/zh-CN/skills/reverse-engineer/SKILL.md +3 -0
- package/bundled/locales/zh-CN/skills/reverse-engineer/tdd-analysis.md +13 -23
- package/bundled/locales/zh-CN/skills/runbook-assistant/SKILL.md +1 -0
- package/bundled/locales/zh-CN/skills/slo-assistant/SKILL.md +1 -0
- package/bundled/locales/zh-CN/skills/tdd-assistant/SKILL.md +2 -0
- package/bundled/locales/zh-CN/skills/workflows/README.md +2 -11
- package/bundled/locales/zh-TW/CHANGELOG.md +65 -3
- package/bundled/locales/zh-TW/CLAUDE.md +1 -1
- package/bundled/locales/zh-TW/README.md +7 -7
- package/bundled/locales/zh-TW/SECURITY.md +1 -1
- package/bundled/locales/zh-TW/core/acceptance-criteria-traceability.md +2 -0
- package/bundled/locales/zh-TW/core/adr-standards.md +26 -5
- package/bundled/locales/zh-TW/core/ai-instruction-standards.md +10 -8
- package/bundled/locales/zh-TW/core/code-review-checklist.md +2 -0
- package/bundled/locales/zh-TW/core/container-image-standards.md +2 -2
- package/bundled/locales/zh-TW/core/contract-testing-standards.md +2 -2
- package/bundled/locales/zh-TW/core/cross-flow-regression.md +8 -7
- package/bundled/locales/zh-TW/core/data-contract.md +2 -2
- package/bundled/locales/zh-TW/core/data-migration-testing.md +2 -2
- package/bundled/locales/zh-TW/core/data-pipeline.md +2 -2
- package/bundled/locales/zh-TW/core/deferred-item-exit.md +251 -0
- package/bundled/locales/zh-TW/core/documentation-writing-standards.md +228 -3
- package/bundled/locales/zh-TW/core/full-coverage-testing.md +15 -2
- package/bundled/locales/zh-TW/core/governance-layer.md +118 -5
- package/bundled/locales/zh-TW/core/iac-design-principles.md +2 -2
- package/bundled/locales/zh-TW/core/incident-response.md +2 -2
- package/bundled/locales/zh-TW/core/model-provenance.md +4 -2
- package/bundled/locales/zh-TW/core/pii-classification.md +42 -6
- package/bundled/locales/zh-TW/core/prd-standards.md +4 -2
- package/bundled/locales/zh-TW/core/product-metrics-standards.md +4 -2
- package/bundled/locales/zh-TW/core/release-readiness-gate.md +2 -2
- package/bundled/locales/zh-TW/core/resource-cost-boundary.md +2 -2
- package/bundled/locales/zh-TW/core/retrospective-standards.md +5 -3
- package/bundled/locales/zh-TW/core/reverse-engineering-standards.md +83 -5
- package/bundled/locales/zh-TW/core/runbook.md +2 -2
- package/bundled/locales/zh-TW/core/schema-evolution.md +2 -2
- package/bundled/locales/zh-TW/core/secret-management-standards.md +2 -2
- package/bundled/locales/zh-TW/core/slo-sli.md +2 -2
- package/bundled/locales/zh-TW/core/spec-driven-development.md +2 -0
- package/bundled/locales/zh-TW/core/tech-debt-standards.md +71 -4
- package/bundled/locales/zh-TW/core/turn-completion-integrity.md +190 -0
- package/bundled/locales/zh-TW/core/user-journey-testing.md +2 -2
- package/bundled/locales/zh-TW/core/user-story-mapping.md +2 -2
- package/bundled/locales/zh-TW/core/verification-oracle.md +2 -2
- package/bundled/locales/zh-TW/docs/CHEATSHEET.md +27 -6
- package/bundled/locales/zh-TW/docs/CLI-INIT-OPTIONS.md +29 -68
- package/bundled/locales/zh-TW/docs/FEATURE-REFERENCE.md +172 -24
- package/bundled/locales/zh-TW/docs/USAGE-MODES-COMPARISON.md +1 -2
- package/bundled/locales/zh-TW/integrations/google-antigravity/{INSTRUCTIONS.md → AGENTS.md} +1 -1
- package/bundled/locales/zh-TW/integrations/google-antigravity/README.md +3 -3
- package/bundled/locales/zh-TW/skills/adr-assistant/SKILL.md +1 -1
- package/bundled/locales/zh-TW/skills/agents/README.md +1 -1
- package/bundled/locales/zh-TW/skills/atdd-assistant/SKILL.md +2 -0
- package/bundled/locales/zh-TW/skills/bdd-assistant/SKILL.md +2 -0
- package/bundled/locales/zh-TW/skills/brainstorm-assistant/SKILL.md +22 -12
- package/bundled/locales/zh-TW/skills/brainstorm-assistant/guide.md +12 -9
- package/bundled/locales/zh-TW/skills/code-review-assistant/SKILL.md +1 -0
- package/bundled/locales/zh-TW/skills/commands/brainstorm.md +17 -13
- package/bundled/locales/zh-TW/skills/commands/config.md +0 -1
- package/bundled/locales/zh-TW/skills/commands/init.md +1 -2
- package/bundled/locales/zh-TW/skills/commit-standards/SKILL.md +2 -0
- package/bundled/locales/zh-TW/skills/contract-test-assistant/SKILL.md +2 -1
- package/bundled/locales/zh-TW/skills/dev-methodology/SKILL.md +2 -0
- package/bundled/locales/zh-TW/skills/dev-workflow-guide/SKILL.md +1 -1
- package/bundled/locales/zh-TW/skills/knowledge-graph/guide.md +2 -2
- package/bundled/locales/zh-TW/skills/migration-assistant/SKILL.md +1 -1
- package/bundled/locales/zh-TW/skills/observability-assistant/SKILL.md +1 -0
- package/bundled/locales/zh-TW/skills/project-discovery/SKILL.md +1 -0
- package/bundled/locales/zh-TW/skills/project-structure-guide/SKILL.md +1 -0
- package/bundled/locales/zh-TW/skills/release-standards/SKILL.md +3 -0
- package/bundled/locales/zh-TW/skills/requirement-assistant/SKILL.md +2 -0
- package/bundled/locales/zh-TW/skills/reverse-engineer/SKILL.md +3 -0
- package/bundled/locales/zh-TW/skills/reverse-engineer/tdd-analysis.md +13 -23
- package/bundled/locales/zh-TW/skills/runbook-assistant/SKILL.md +1 -0
- package/bundled/locales/zh-TW/skills/slo-assistant/SKILL.md +1 -0
- package/bundled/locales/zh-TW/skills/tdd-assistant/SKILL.md +2 -0
- package/bundled/locales/zh-TW/skills/workflows/README.md +2 -11
- package/bundled/skills/agents/README.md +1 -1
- package/bundled/skills/atdd-assistant/SKILL.md +2 -0
- package/bundled/skills/bdd-assistant/SKILL.md +2 -0
- package/bundled/skills/brainstorm-assistant/SKILL.md +31 -13
- package/bundled/skills/brainstorm-assistant/guide.md +9 -6
- package/bundled/skills/code-review-assistant/SKILL.md +1 -0
- package/bundled/skills/commands/brainstorm.md +12 -9
- package/bundled/skills/commands/config.md +0 -1
- package/bundled/skills/commands/init.md +2 -3
- package/bundled/skills/commit-standards/SKILL.md +2 -0
- package/bundled/skills/contract-test-assistant/SKILL.md +1 -0
- package/bundled/skills/dev-methodology/SKILL.md +4 -0
- package/bundled/skills/observability-assistant/SKILL.md +1 -0
- package/bundled/skills/project-discovery/SKILL.md +1 -0
- package/bundled/skills/project-structure-guide/SKILL.md +1 -0
- package/bundled/skills/release-standards/SKILL.md +3 -0
- package/bundled/skills/requirement-assistant/SKILL.md +2 -0
- package/bundled/skills/reverse-engineer/SKILL.md +3 -0
- package/bundled/skills/reverse-engineer/tdd-analysis.md +16 -23
- package/bundled/skills/runbook-assistant/SKILL.md +1 -0
- package/bundled/skills/slo-assistant/SKILL.md +1 -0
- package/bundled/skills/tdd-assistant/SKILL.md +2 -0
- package/bundled/skills/workflows/README.md +2 -11
- package/bundled/templates/.ai-context.yaml.template +194 -0
- package/bundled/templates/CLAUDE.md.template +145 -0
- package/bundled/templates/DESIGN.md +237 -0
- package/bundled/templates/SKILL-BRIEF-TEMPLATE.md +57 -0
- package/bundled/templates/SKILL-CANDIDATES.md +39 -0
- package/bundled/templates/gates/check-error-exit.mjs +309 -0
- package/bundled/templates/mcp-config.json +10 -0
- package/bundled/templates/methodology-template.yaml +209 -0
- package/bundled/templates/migration-template.md +408 -0
- package/bundled/templates/requirement-checklist.md +410 -0
- package/bundled/templates/requirement-document-template.md +591 -0
- package/bundled/templates/requirement-template.md +881 -0
- package/bundled/templates/reverse-spec-template.md +409 -0
- package/bundled/templates/test-case-template.md +74 -0
- package/bundled/templates/test-plan-template.md +74 -0
- package/package.json +9 -5
- package/src/commands/audit.js +82 -0
- package/src/commands/check.js +66 -10
- package/src/commands/init.js +161 -16
- package/src/commands/lint.js +96 -0
- package/src/commands/quickstart.js +16 -13
- package/src/commands/update.js +286 -14
- package/src/compilers/claude-code-compiler.js +4 -1
- package/src/config/ai-agent-paths.js +62 -17
- package/src/core/constants.js +42 -11
- package/src/core/manifest.js +201 -3
- package/src/core/paths.js +2 -2
- package/src/i18n/messages.js +9 -32
- package/src/installers/hooks-installer.js +167 -75
- package/src/installers/integration-installer.js +9 -5
- package/src/prompts/init.js +14 -14
- package/src/reconciler/actual-state-scanner.js +14 -3
- package/src/utils/detector.js +21 -1
- package/src/utils/effect-boundary.js +1093 -0
- package/src/utils/hasher.js +229 -9
- package/src/utils/hook-stats.js +1 -1
- package/src/utils/integration-generator.js +100 -4
- package/src/utils/reference-sync.js +4 -1
- package/src/utils/skills-installer.js +17 -3
- package/src/utils/spec-linter.js +35 -76
- package/src/utils/yaml-generator.js +51 -9
- package/standards-registry.json +31 -8
- 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
|
|
5
|
-
*
|
|
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,
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
*
|
|
50
|
-
*
|
|
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
|
|
58
|
-
const
|
|
59
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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 {
|
|
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/
|
|
36
|
-
target: '
|
|
35
|
+
source: 'integrations/google-antigravity/AGENTS.md',
|
|
36
|
+
target: '.agents/AGENTS.md'
|
|
37
37
|
},
|
|
38
38
|
codex: {
|
|
39
|
-
|
|
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'
|
|
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'
|
|
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
|
package/src/prompts/init.js
CHANGED
|
@@ -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('(
|
|
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
|
-
*
|
|
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: '
|
|
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
|
-
|
|
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
|
package/src/utils/detector.js
CHANGED
|
@@ -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
|
-
|
|
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'))
|