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
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* UDS Hook-Based Standard Injection
|
|
4
|
+
* Claude Code UserPromptSubmit hook that reads the user's prompt,
|
|
5
|
+
* matches it against manifest.json domain triggers, and outputs
|
|
6
|
+
* matching standard file paths as additional context.
|
|
7
|
+
*
|
|
8
|
+
* Usage: Configured as a Claude Code hook in .claude/settings.json
|
|
9
|
+
*
|
|
10
|
+
* Performance target: < 500ms
|
|
11
|
+
* Error handling: failures are non-blocking (exit 0 with empty output)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFileSync, existsSync, appendFileSync, mkdirSync } from 'fs';
|
|
15
|
+
import { join, dirname } from 'path';
|
|
16
|
+
import { fileURLToPath } from 'url';
|
|
17
|
+
|
|
18
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
|
|
20
|
+
function findManifest(cwd) {
|
|
21
|
+
const paths = [
|
|
22
|
+
join(cwd, '.standards', 'manifest.json'),
|
|
23
|
+
join(cwd, 'manifest.json'),
|
|
24
|
+
];
|
|
25
|
+
for (const p of paths) {
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(readFileSync(p, 'utf-8'));
|
|
28
|
+
} catch {
|
|
29
|
+
// continue
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function matchDomains(prompt, manifest) {
|
|
36
|
+
const matched = new Set();
|
|
37
|
+
const promptLower = prompt.toLowerCase();
|
|
38
|
+
|
|
39
|
+
const allDomains = { ...(manifest.domains || {}) };
|
|
40
|
+
|
|
41
|
+
// Add custom domains from extensions
|
|
42
|
+
for (const ext of manifest.extensions || []) {
|
|
43
|
+
if (ext.type === 'custom-domain' && ext.domain && ext.triggers) {
|
|
44
|
+
allDomains[ext.domain] = {
|
|
45
|
+
_triggers: ext.triggers,
|
|
46
|
+
_standards: ext.standards || [],
|
|
47
|
+
_isCustom: true,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const [domainName, domainValue] of Object.entries(allDomains)) {
|
|
53
|
+
if (domainName === 'always-on') continue;
|
|
54
|
+
|
|
55
|
+
// Get triggers: from ai.yaml structure or custom domain
|
|
56
|
+
let triggers = [];
|
|
57
|
+
if (domainValue._isCustom) {
|
|
58
|
+
triggers = domainValue._triggers;
|
|
59
|
+
} else {
|
|
60
|
+
// Built-in domains don't store triggers in manifest.json,
|
|
61
|
+
// use the domain name and known command patterns
|
|
62
|
+
const domainTriggerMap = {
|
|
63
|
+
testing: ['test', 'coverage', '/tdd', '/bdd', '/atdd', '/coverage', '.test.', '.spec.'],
|
|
64
|
+
specification: ['/sdd', '/spec', '/derive', '/reverse', '/requirement', 'docs/specs/'],
|
|
65
|
+
quality: ['/code-review', '/refactor', '/check', 'code review', 'pull request', 'security', 'performance'],
|
|
66
|
+
documentation: ['/docs', '/changelog', '/generate-docs', 'readme', 'changelog', 'documentation'],
|
|
67
|
+
workflow: ['/release', '/commit', 'branch', 'merge', 'release', 'deploy', 'version'],
|
|
68
|
+
architecture: ['architecture', 'project setup', 'error handling', 'logging', 'project structure'],
|
|
69
|
+
enforcement: ['hook', 'enforce', 'validate', 'dangerous', 'security check', 'logging check'],
|
|
70
|
+
};
|
|
71
|
+
triggers = domainTriggerMap[domainName] || [domainName];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const trigger of triggers) {
|
|
75
|
+
const triggerLower = trigger.toLowerCase();
|
|
76
|
+
if (triggerLower.startsWith('/')) {
|
|
77
|
+
// Slash command match
|
|
78
|
+
if (promptLower.includes(triggerLower)) {
|
|
79
|
+
if (domainValue._isCustom) {
|
|
80
|
+
domainValue._standards.forEach(s => matched.add(s));
|
|
81
|
+
} else if (Array.isArray(domainValue)) {
|
|
82
|
+
domainValue.forEach(s => matched.add(s));
|
|
83
|
+
}
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
} else if (triggerLower.includes('*')) {
|
|
87
|
+
// File pattern — check if prompt mentions matching files
|
|
88
|
+
const pattern = triggerLower.replace(/\*/g, '');
|
|
89
|
+
if (promptLower.includes(pattern)) {
|
|
90
|
+
if (domainValue._isCustom) {
|
|
91
|
+
domainValue._standards.forEach(s => matched.add(s));
|
|
92
|
+
} else if (Array.isArray(domainValue)) {
|
|
93
|
+
domainValue.forEach(s => matched.add(s));
|
|
94
|
+
}
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
// Keyword match
|
|
99
|
+
if (promptLower.includes(triggerLower)) {
|
|
100
|
+
if (domainValue._isCustom) {
|
|
101
|
+
domainValue._standards.forEach(s => matched.add(s));
|
|
102
|
+
} else if (Array.isArray(domainValue)) {
|
|
103
|
+
domainValue.forEach(s => matched.add(s));
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return [...matched];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function main() {
|
|
115
|
+
try {
|
|
116
|
+
// Read stdin
|
|
117
|
+
const chunks = [];
|
|
118
|
+
for await (const chunk of process.stdin) {
|
|
119
|
+
chunks.push(chunk);
|
|
120
|
+
}
|
|
121
|
+
const input = JSON.parse(Buffer.concat(chunks).toString());
|
|
122
|
+
|
|
123
|
+
const prompt = input.prompt || '';
|
|
124
|
+
const cwd = input.cwd || process.cwd();
|
|
125
|
+
|
|
126
|
+
if (!prompt) {
|
|
127
|
+
process.exit(0);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const manifest = findManifest(cwd);
|
|
131
|
+
if (!manifest) {
|
|
132
|
+
process.exit(0);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const matchedStandards = matchDomains(prompt, manifest);
|
|
136
|
+
|
|
137
|
+
// Record hook stats (silent, non-blocking)
|
|
138
|
+
try {
|
|
139
|
+
const configPath = join(cwd, '.uds', 'config.json');
|
|
140
|
+
let statsEnabled = false; // Default OFF — opt-in via .uds/config.json
|
|
141
|
+
try {
|
|
142
|
+
if (existsSync(configPath)) {
|
|
143
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
144
|
+
if (config.hookStats === true) statsEnabled = true;
|
|
145
|
+
}
|
|
146
|
+
} catch { /* default disabled */ }
|
|
147
|
+
|
|
148
|
+
if (statsEnabled) {
|
|
149
|
+
const statsDir = join(cwd, '.uds');
|
|
150
|
+
if (!existsSync(statsDir)) mkdirSync(statsDir, { recursive: true });
|
|
151
|
+
const statsPath = join(statsDir, 'hook-stats.jsonl');
|
|
152
|
+
const record = JSON.stringify({
|
|
153
|
+
timestamp: new Date().toISOString(),
|
|
154
|
+
matched_standards: matchedStandards.map(s => s.replace(/.*\//, '').replace('.ai.yaml', '')),
|
|
155
|
+
matched_count: matchedStandards.length,
|
|
156
|
+
total_available: Object.keys(manifest.domains || {}).length,
|
|
157
|
+
prompt_length: prompt.length
|
|
158
|
+
});
|
|
159
|
+
appendFileSync(statsPath, record + '\n');
|
|
160
|
+
}
|
|
161
|
+
} catch { /* silent failure — never block hook */ }
|
|
162
|
+
|
|
163
|
+
if (matchedStandards.length === 0) {
|
|
164
|
+
process.exit(0);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Output as additional context
|
|
168
|
+
const context = [
|
|
169
|
+
'[UDS Context-Aware Loading] Matched standards for this prompt:',
|
|
170
|
+
...matchedStandards.map(s => ` - ${s}`),
|
|
171
|
+
'',
|
|
172
|
+
'Load these standards if not already in context.',
|
|
173
|
+
].join('\n');
|
|
174
|
+
|
|
175
|
+
process.stdout.write(context);
|
|
176
|
+
process.exit(0);
|
|
177
|
+
} catch {
|
|
178
|
+
// Non-blocking: exit 0 on any error
|
|
179
|
+
process.exit(0);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
main();
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* UDS Hook Telemetry Wrapper
|
|
4
|
+
*
|
|
5
|
+
* Records hook execution telemetry to .standards/telemetry.jsonl.
|
|
6
|
+
* Format: {timestamp, standard_id, hook_type, result, duration_ms}
|
|
7
|
+
*
|
|
8
|
+
* If telemetryUpload=true and telemetryApiKey≠"" in .uds/config.json,
|
|
9
|
+
* also uploads the result to the remote telemetry server (opt-in).
|
|
10
|
+
*
|
|
11
|
+
* @see docs/specs/SPEC-TELEMETRY-001-hook-telemetry.md (REQ-1)
|
|
12
|
+
* @see docs/specs/SPEC-TELEMETRY-002-hook-upload.md
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync, readFileSync, appendFileSync, mkdirSync, statSync, writeFileSync } from 'fs';
|
|
16
|
+
import { join, dirname } from 'path';
|
|
17
|
+
|
|
18
|
+
export const TELEMETRY_FILE = '.standards/telemetry.jsonl';
|
|
19
|
+
const MAX_TELEMETRY_SIZE = 2 * 1024 * 1024; // 2MB
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Record a hook execution telemetry entry.
|
|
23
|
+
* @param {string} projectPath - Project root path
|
|
24
|
+
* @param {{ standard_id: string, hook_type: string, exitCode: number, duration_ms: number }} entry
|
|
25
|
+
*/
|
|
26
|
+
export function recordTelemetry(projectPath, entry) {
|
|
27
|
+
try {
|
|
28
|
+
const telPath = join(projectPath, TELEMETRY_FILE);
|
|
29
|
+
const dir = dirname(telPath);
|
|
30
|
+
if (!existsSync(dir)) {
|
|
31
|
+
mkdirSync(dir, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const record = {
|
|
35
|
+
timestamp: new Date().toISOString(),
|
|
36
|
+
standard_id: entry.standard_id,
|
|
37
|
+
hook_type: entry.hook_type,
|
|
38
|
+
result: entry.exitCode === 0 ? 'pass' : 'fail',
|
|
39
|
+
duration_ms: entry.duration_ms,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Rotation: truncate if exceeding 2MB
|
|
43
|
+
if (existsSync(telPath)) {
|
|
44
|
+
try {
|
|
45
|
+
const size = statSync(telPath).size;
|
|
46
|
+
if (size > MAX_TELEMETRY_SIZE) {
|
|
47
|
+
const content = readFileSync(telPath, 'utf-8');
|
|
48
|
+
const lines = content.trim().split('\n');
|
|
49
|
+
const keepLines = lines.slice(Math.floor(lines.length / 2));
|
|
50
|
+
writeFileSync(telPath, keepLines.join('\n') + '\n');
|
|
51
|
+
}
|
|
52
|
+
} catch { /* ignore rotation errors */ }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
appendFileSync(telPath, JSON.stringify(record) + '\n');
|
|
56
|
+
} catch {
|
|
57
|
+
// Silently ignore write failures
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Upload hook result to remote telemetry server (opt-in via .uds/config.json).
|
|
63
|
+
* Silently fails — never blocks hook execution.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} projectPath - Project root path
|
|
66
|
+
* @param {{ standard_id: string, hook_type: string, exitCode: number, duration_ms: number }} entry
|
|
67
|
+
* @returns {Promise<void>}
|
|
68
|
+
*/
|
|
69
|
+
export async function uploadTelemetry(projectPath, entry) {
|
|
70
|
+
try {
|
|
71
|
+
// Dynamic import to avoid blocking if CLI module not available in hook context
|
|
72
|
+
const { uploadHookTelemetry } = await import('../../cli/src/utils/telemetry-uploader.js');
|
|
73
|
+
await uploadHookTelemetry(projectPath, entry);
|
|
74
|
+
} catch {
|
|
75
|
+
// Silently ignore upload failures (AC-4: never block hook)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared text preparation and pack runner for the turn-completion check.
|
|
3
|
+
*
|
|
4
|
+
* The language-specific part lives in ./locales/<id>.js. This file holds only
|
|
5
|
+
* what is true of every language, so that adding a language cannot accidentally
|
|
6
|
+
* change the behaviour of the ones already shipped.
|
|
7
|
+
*
|
|
8
|
+
* @see core/turn-completion-integrity.md (R7)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Remove the parts of a message that quote a pattern rather than enact it.
|
|
13
|
+
*
|
|
14
|
+
* Every exclusion here was added because its absence produced a false block:
|
|
15
|
+
* a detector reliably matches the text that documents it. Fenced code, inline
|
|
16
|
+
* backticks and markdown table rows are all places where an example of a
|
|
17
|
+
* commitment gets written down without anyone committing to anything.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} text
|
|
20
|
+
* @returns {string}
|
|
21
|
+
*/
|
|
22
|
+
export function stripNonProse(text) {
|
|
23
|
+
return text
|
|
24
|
+
.replace(/```[\s\S]*?```/g, ' ')
|
|
25
|
+
.split('\n')
|
|
26
|
+
.filter((line) => !line.trimStart().startsWith('|'))
|
|
27
|
+
.filter((line) => !line.trimStart().startsWith('>'))
|
|
28
|
+
.join('\n')
|
|
29
|
+
.replace(/`[^`\n]*`/g, ' ');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Split into paragraphs on blank lines. */
|
|
33
|
+
export function splitParagraphs(text) {
|
|
34
|
+
return text.split(/\n\s*\n/).filter((p) => p.trim());
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Split into sentences.
|
|
39
|
+
*
|
|
40
|
+
* Judgement is made per sentence, never per paragraph: a negated clause
|
|
41
|
+
* followed by a real commitment produces exactly one paragraph-level match,
|
|
42
|
+
* and the negation swallows the commitment.
|
|
43
|
+
*/
|
|
44
|
+
export function splitSentences(text) {
|
|
45
|
+
return text.split(/[.!?。!?\n]+/).filter((s) => s.trim());
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Did the human ask for the turn to end?
|
|
50
|
+
*
|
|
51
|
+
* 🔴 The check reads only the agent's final message, so a turn that ends
|
|
52
|
+
* because the human said "pause, I'm going home" is indistinguishable from one
|
|
53
|
+
* that ends on an abandoned commitment — the agent's words are the same in both.
|
|
54
|
+
* Measured 2026-09-08: the first real firing after shipping was exactly this.
|
|
55
|
+
*
|
|
56
|
+
* A user-directed stop is the one legitimate ending the message-only design
|
|
57
|
+
* cannot represent, so the check has to look at the other side of the exchange.
|
|
58
|
+
*
|
|
59
|
+
* @param {string} text - the human's most recent message
|
|
60
|
+
* @param {Array<{isStopRequest: (t: string) => boolean}>} packs
|
|
61
|
+
* @returns {boolean}
|
|
62
|
+
*/
|
|
63
|
+
export function userAskedToStop(text, packs) {
|
|
64
|
+
if (!text || !text.trim()) return false;
|
|
65
|
+
return packs.some((p) => typeof p.isStopRequest === 'function' && p.isStopRequest(text));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Run every locale pack over one message.
|
|
70
|
+
*
|
|
71
|
+
* All packs run, and any one of them firing is enough. A bilingual transcript
|
|
72
|
+
* is the normal case, not the exception, and asking the adopter to configure
|
|
73
|
+
* which language they write in is a knob that will be set wrong.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} text - the assistant's final message
|
|
76
|
+
* @param {Array<{id: string, isCommitment: (s: string) => boolean, isAsking: (t: string) => boolean}>} packs
|
|
77
|
+
* @returns {{ fired: boolean, packId: string|null, sentence: string|null }}
|
|
78
|
+
*/
|
|
79
|
+
export function detectCommitment(text, packs) {
|
|
80
|
+
const body = stripNonProse(text);
|
|
81
|
+
|
|
82
|
+
for (const paragraph of splitParagraphs(body)) {
|
|
83
|
+
// A commitment that shares a paragraph with a request for information is
|
|
84
|
+
// conditional ("give me X and I will do Y"), not an unkept promise.
|
|
85
|
+
// Stopping there is correct behaviour, and blocking it teaches the adopter
|
|
86
|
+
// to uninstall the hook.
|
|
87
|
+
const asking = packs.some((p) => p.isAsking(paragraph));
|
|
88
|
+
if (asking) continue;
|
|
89
|
+
|
|
90
|
+
for (const sentence of splitSentences(paragraph)) {
|
|
91
|
+
for (const pack of packs) {
|
|
92
|
+
if (pack.isCommitment(sentence)) {
|
|
93
|
+
return { fired: true, packId: pack.id, sentence: sentence.trim().slice(0, 160) };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { fired: false, packId: null, sentence: null };
|
|
99
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* English locale pack for the turn-completion check.
|
|
3
|
+
*
|
|
4
|
+
* Design note — why there is no verb whitelist.
|
|
5
|
+
* The Traditional Chinese detector this was derived from was defeated twice by
|
|
6
|
+
* its own lesson: version one enumerated phrases and missed a phrase; version
|
|
7
|
+
* two enumerated verbs and missed a verb. Swapping a phrase list for a verb
|
|
8
|
+
* list is still enumeration, one layer down.
|
|
9
|
+
*
|
|
10
|
+
* English gets the inverted shape: take whatever word follows the future
|
|
11
|
+
* marker and treat it as an action UNLESS it is in a small, closed list of
|
|
12
|
+
* verbs that describe rather than do. The blacklist can be enumerated honestly
|
|
13
|
+
* because "verbs that are not work" is a bounded set; "verbs that are work" is
|
|
14
|
+
* not.
|
|
15
|
+
*
|
|
16
|
+
* @see core/turn-completion-integrity.md
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** Verbs that report, decide-not-to, or legitimately wait. Following one is not a commitment to work. */
|
|
20
|
+
const NON_ACTION = new Set([
|
|
21
|
+
// describing rather than doing
|
|
22
|
+
'say', 'says', 'said', 'note', 'noted', 'mention', 'explain', 'describe',
|
|
23
|
+
'report', 'summarize', 'summarise', 'list', 'show', 'tell', 'clarify',
|
|
24
|
+
'quote', 'point', 'highlight', 'flag', 'call', 'answer', 'respond', 'reply',
|
|
25
|
+
// stating a belief, not an action
|
|
26
|
+
'know', 'think', 'assume', 'believe', 'remember', 'recommend', 'suggest', 'propose',
|
|
27
|
+
// a legitimate stop: the next move is not the agent's
|
|
28
|
+
'wait', 'hold', 'pause', 'defer', 'stop', 'skip', 'leave', 'need', 'want', 'require',
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
/** Reported speech: a reporting verb governing a clause. "when I say I will do X" is narration. */
|
|
32
|
+
const REPORTED_SPEECH =
|
|
33
|
+
/\b(say|says|said|saying|note|noted|noting|mention|mentioned|tell|told|report|reported|write|wrote|claim|claimed|promise|promised)\b\s+(that\s+)?(I|you|we|it|he|she|they)\b/i;
|
|
34
|
+
|
|
35
|
+
const NEGATION = /\b(not|never|no longer|neither|nor)\b/i;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A reporting verb sitting between "I" and the future marker means the sentence
|
|
39
|
+
* describes a commitment rather than making one: "when I say I will do X".
|
|
40
|
+
* Dropping this check let exactly that sentence block on the first corpus run.
|
|
41
|
+
*/
|
|
42
|
+
const REPORTING =
|
|
43
|
+
/\b(say|says|said|saying|note|noted|mention|mentioned|tell|told|report|reported|write|wrote|claim|claimed|promise|promised|explain|explained)\b/i;
|
|
44
|
+
|
|
45
|
+
/** Requests for information from the human. A commitment beside one is conditional. */
|
|
46
|
+
const ASKING =
|
|
47
|
+
/(\bshould I\b|\bdo you want\b|\bwould you like\b|\bwhich (one|file|option)\b|\blet me know\b|\btell me\b|\bgive me\b|\bpaste\b|\bwaiting on you\b|\byour call\b|\bup to you\b|\?)/i;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Expand contractions so the patterns below never have to fight an apostrophe.
|
|
51
|
+
* Both the ASCII and typographic apostrophes appear in real transcripts.
|
|
52
|
+
*/
|
|
53
|
+
export function normalize(text) {
|
|
54
|
+
return text
|
|
55
|
+
.replace(/[‘’]/g, "'")
|
|
56
|
+
.replace(/\bI'll\b/gi, 'I will')
|
|
57
|
+
.replace(/\bI'm\b/gi, 'I am')
|
|
58
|
+
.replace(/\bI've\b/gi, 'I have')
|
|
59
|
+
.replace(/\bwon't\b/gi, 'will not')
|
|
60
|
+
.replace(/\bcan't\b/gi, 'can not')
|
|
61
|
+
.replace(/n't\b/gi, ' not');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const FUTURE = '(?:will|am going to|going to|am about to|about to)';
|
|
65
|
+
// Adverbs and filler between the future marker and the verb it governs.
|
|
66
|
+
const FILLER = "(?:not|never|also|then|now|next|first|just|be|still|already|soon|finally|quickly|\\w+ly)";
|
|
67
|
+
const COMMIT = new RegExp(
|
|
68
|
+
`\\bI\\b([^.!?\\n]{0,10}?)\\b${FUTURE}\\b((?:\\s+${FILLER}\\b|\\s*,)*)\\s+([a-z][a-z-]*)`,
|
|
69
|
+
'gi'
|
|
70
|
+
);
|
|
71
|
+
const LET_ME = /\blet me\b\s+((?:(?:just|now|first|quickly|\w+ly)\s+)*)([a-z][a-z-]*)/gi;
|
|
72
|
+
|
|
73
|
+
/** Does this sentence contain an unconditional first-person commitment to a next action? */
|
|
74
|
+
export function isCommitment(sentence) {
|
|
75
|
+
const s = normalize(sentence);
|
|
76
|
+
|
|
77
|
+
for (const re of [COMMIT, LET_ME]) {
|
|
78
|
+
re.lastIndex = 0;
|
|
79
|
+
let m;
|
|
80
|
+
while ((m = re.exec(s)) !== null) {
|
|
81
|
+
const isLetMe = re === LET_ME;
|
|
82
|
+
const gap = isLetMe ? '' : m[1];
|
|
83
|
+
const filler = isLetMe ? m[1] : m[2];
|
|
84
|
+
const verb = (isLetMe ? m[2] : m[3]).toLowerCase();
|
|
85
|
+
|
|
86
|
+
if (NON_ACTION.has(verb)) continue;
|
|
87
|
+
if (NEGATION.test(gap) || NEGATION.test(filler)) continue;
|
|
88
|
+
if (REPORTING.test(gap)) continue;
|
|
89
|
+
// A reporting verb governing a clause anywhere earlier in the sentence
|
|
90
|
+
// means this is narration about a commitment, not a commitment.
|
|
91
|
+
if (REPORTED_SPEECH.test(s.slice(0, m.index + 2))) continue;
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function isAsking(text) {
|
|
99
|
+
return ASKING.test(normalize(text));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The human asking for the turn to end. Deliberately narrow: a false positive
|
|
104
|
+
* here disables the check for the rest of the session, which is worse than a
|
|
105
|
+
* missed block. It must read as an instruction to stop, not a mention of
|
|
106
|
+
* stopping.
|
|
107
|
+
*/
|
|
108
|
+
const STOP_REQUEST =
|
|
109
|
+
/(\blet's (stop|pause|pick this up later)\b|\b(pause|stop) (here|for now|there)\b|\bhold (on|off)\b|\bthat's (enough|it) for (now|today)\b|\b(done|enough) for (now|today)\b|\bwrap (it |this )?up\b|\bcontinue (this )?later\b|\bpick (this|it) up (tomorrow|later)\b|\btake a break\b|\bI'?m (heading|going) (home|out)\b|\bgood ?night\b)/i;
|
|
110
|
+
|
|
111
|
+
export function isStopRequest(text) {
|
|
112
|
+
return STOP_REQUEST.test(normalize(text));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export const id = 'en';
|
|
116
|
+
export const label = 'English';
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The corpus. This is the pack's contract: it runs in CI, and a pack whose
|
|
120
|
+
* corpus does not pass is not shipped. Cases marked `true` must block; cases
|
|
121
|
+
* marked `false` must let the turn end.
|
|
122
|
+
*/
|
|
123
|
+
export const corpus = [
|
|
124
|
+
[true, 'plain commitment', 'I will update the remaining two files.'],
|
|
125
|
+
[true, 'contraction + filler', "I'll go ahead and rewrite the detector."],
|
|
126
|
+
[true, 'going to', "Next I'm going to run the test suite."],
|
|
127
|
+
[true, 'let me', 'Let me fix the exclusion and re-run the corpus.'],
|
|
128
|
+
[true, 'about to', "I'm about to move on to the next item in the same class."],
|
|
129
|
+
[true, 'negation then a real commitment in the next sentence',
|
|
130
|
+
'I will not touch that config. I will wire the gate into CI next.'],
|
|
131
|
+
[false, 'past tense report', 'I said I would fix it, and the commit is pushed.'],
|
|
132
|
+
[false, 'negation', "I won't be changing that config."],
|
|
133
|
+
[false, 'asking', 'Should I update the remaining files, or leave them?'],
|
|
134
|
+
[false, 'narrating the pattern', 'The hook fires when I say I will do something and then stop.'],
|
|
135
|
+
// Only REPORTED_SPEECH catches this one: the gap between "I" and "will" is
|
|
136
|
+
// empty, so the gap-level reporting check cannot see the "said that".
|
|
137
|
+
// Added after mutation testing showed REPORTED_SPEECH could be deleted with
|
|
138
|
+
// the corpus staying green — an unproven guard is indistinguishable from a
|
|
139
|
+
// dead one.
|
|
140
|
+
[false, 'third-person reported speech', 'You said that I will handle the merge.'],
|
|
141
|
+
[false, 'reporting verb as the action', "I'll explain why the exclusion exists."],
|
|
142
|
+
[false, 'legitimate stop: the next move is the human\'s', 'I will wait for your key before deploying.'],
|
|
143
|
+
[false, 'conditional: asking in the same paragraph',
|
|
144
|
+
'Tell me which file you meant and I will check it.'],
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Messages from the human, and whether each one ends the turn by request.
|
|
149
|
+
* Marked `true` must exempt; marked `false` must NOT — a message that merely
|
|
150
|
+
* mentions stopping is not an instruction to stop.
|
|
151
|
+
*/
|
|
152
|
+
export const stopCorpus = [
|
|
153
|
+
[true, 'plain pause', "Let's pause here, I'm heading home."],
|
|
154
|
+
[true, 'enough for today', "That's enough for today, we can continue later."],
|
|
155
|
+
[true, 'hold on', 'Hold on, I need to step out.'],
|
|
156
|
+
[false, 'mentions stopping but is not one', 'Explain why the hook stops the turn.'],
|
|
157
|
+
[false, 'asks for work', 'Stop using the hardcoded list and walk the registry instead.'],
|
|
158
|
+
[false, 'ordinary instruction', 'Fix the detector and push it.'],
|
|
159
|
+
];
|