universal-dev-standards 6.8.0 → 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 (194) hide show
  1. package/bin/uds.js +12 -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/code-review.ai.yaml +13 -3
  5. package/bundled/ai/standards/commit-message.ai.yaml +8 -4
  6. package/bundled/ai/standards/deferred-item-exit.ai.yaml +225 -0
  7. package/bundled/ai/standards/feature-discovery-standards.ai.yaml +14 -2
  8. package/bundled/ai/standards/governance-layer.ai.yaml +128 -2
  9. package/bundled/ai/standards/logging.ai.yaml +2 -2
  10. package/bundled/ai/standards/retrospective-standards.ai.yaml +14 -2
  11. package/bundled/ai/standards/reverse-engineering-standards.ai.yaml +73 -2
  12. package/bundled/ai/standards/security-standards.ai.yaml +2 -2
  13. package/bundled/ai/standards/spec-driven-development.ai.yaml +14 -2
  14. package/bundled/ai/standards/tech-debt-standards.ai.yaml +87 -3
  15. package/bundled/ai/standards/turn-completion-integrity.ai.yaml +131 -0
  16. package/bundled/core/acceptance-criteria-traceability.md +5 -2
  17. package/bundled/core/adr-standards.md +26 -2
  18. package/bundled/core/code-review-checklist.md +5 -2
  19. package/bundled/core/context-aware-loading.md +1 -1
  20. package/bundled/core/deferred-item-exit.md +254 -0
  21. package/bundled/core/feature-discovery-standards.md +5 -1
  22. package/bundled/core/governance-layer.md +114 -2
  23. package/bundled/core/retrospective-standards.md +4 -2
  24. package/bundled/core/reverse-engineering-standards.md +81 -2
  25. package/bundled/core/spec-driven-development.md +8 -2
  26. package/bundled/core/tech-debt-standards.md +67 -8
  27. package/bundled/core/turn-completion-integrity.md +196 -0
  28. package/bundled/hooks/check-dangerous-cmd.mjs +60 -0
  29. package/bundled/hooks/check-logging-standard.mjs +59 -0
  30. package/bundled/hooks/check-turn-completion.mjs +233 -0
  31. package/bundled/hooks/inject-standards.mjs +183 -0
  32. package/bundled/hooks/telemetry-wrapper.mjs +77 -0
  33. package/bundled/hooks/turn-completion/detect.mjs +99 -0
  34. package/bundled/hooks/turn-completion/locales/en.mjs +159 -0
  35. package/bundled/hooks/turn-completion/locales/zh-TW.mjs +166 -0
  36. package/bundled/hooks/validate-commit-msg.mjs +104 -0
  37. package/bundled/locales/zh-CN/CHANGELOG.md +47 -3
  38. package/bundled/locales/zh-CN/CLAUDE.md +1 -1
  39. package/bundled/locales/zh-CN/README.md +2 -2
  40. package/bundled/locales/zh-CN/SECURITY.md +1 -1
  41. package/bundled/locales/zh-CN/core/adr-standards.md +1 -1
  42. package/bundled/locales/zh-CN/core/governance-layer.md +118 -6
  43. package/bundled/locales/zh-CN/core/retrospective-standards.md +1 -1
  44. package/bundled/locales/zh-CN/core/tech-debt-standards.md +71 -4
  45. package/bundled/locales/zh-CN/core/turn-completion-integrity.md +190 -0
  46. package/bundled/locales/zh-CN/docs/CHEATSHEET.md +8 -1
  47. package/bundled/locales/zh-CN/docs/CLI-INIT-OPTIONS.md +29 -68
  48. package/bundled/locales/zh-CN/docs/FEATURE-REFERENCE.md +25 -15
  49. package/bundled/locales/zh-CN/docs/USAGE-MODES-COMPARISON.md +1 -2
  50. package/bundled/locales/zh-CN/integrations/google-antigravity/{INSTRUCTIONS.md → AGENTS.md} +1 -1
  51. package/bundled/locales/zh-CN/integrations/google-antigravity/README.md +3 -3
  52. package/bundled/locales/zh-CN/skills/atdd-assistant/SKILL.md +2 -0
  53. package/bundled/locales/zh-CN/skills/bdd-assistant/SKILL.md +2 -0
  54. package/bundled/locales/zh-CN/skills/brainstorm-assistant/SKILL.md +22 -12
  55. package/bundled/locales/zh-CN/skills/brainstorm-assistant/guide.md +12 -9
  56. package/bundled/locales/zh-CN/skills/code-review-assistant/SKILL.md +1 -0
  57. package/bundled/locales/zh-CN/skills/commands/brainstorm.md +17 -13
  58. package/bundled/locales/zh-CN/skills/commands/config.md +0 -1
  59. package/bundled/locales/zh-CN/skills/commands/init.md +1 -2
  60. package/bundled/locales/zh-CN/skills/commit-standards/SKILL.md +2 -0
  61. package/bundled/locales/zh-CN/skills/contract-test-assistant/SKILL.md +1 -0
  62. package/bundled/locales/zh-CN/skills/dev-methodology/SKILL.md +2 -0
  63. package/bundled/locales/zh-CN/skills/observability-assistant/SKILL.md +1 -0
  64. package/bundled/locales/zh-CN/skills/project-structure-guide/SKILL.md +1 -0
  65. package/bundled/locales/zh-CN/skills/release-standards/SKILL.md +3 -0
  66. package/bundled/locales/zh-CN/skills/requirement-assistant/SKILL.md +2 -0
  67. package/bundled/locales/zh-CN/skills/reverse-engineer/SKILL.md +3 -0
  68. package/bundled/locales/zh-CN/skills/runbook-assistant/SKILL.md +1 -0
  69. package/bundled/locales/zh-CN/skills/slo-assistant/SKILL.md +1 -0
  70. package/bundled/locales/zh-CN/skills/tdd-assistant/SKILL.md +2 -0
  71. package/bundled/locales/zh-TW/CHANGELOG.md +47 -3
  72. package/bundled/locales/zh-TW/CLAUDE.md +1 -1
  73. package/bundled/locales/zh-TW/README.md +2 -2
  74. package/bundled/locales/zh-TW/SECURITY.md +1 -1
  75. package/bundled/locales/zh-TW/core/acceptance-criteria-traceability.md +2 -0
  76. package/bundled/locales/zh-TW/core/adr-standards.md +26 -5
  77. package/bundled/locales/zh-TW/core/code-review-checklist.md +2 -0
  78. package/bundled/locales/zh-TW/core/container-image-standards.md +2 -2
  79. package/bundled/locales/zh-TW/core/contract-testing-standards.md +2 -2
  80. package/bundled/locales/zh-TW/core/cross-flow-regression.md +8 -7
  81. package/bundled/locales/zh-TW/core/data-contract.md +2 -2
  82. package/bundled/locales/zh-TW/core/data-migration-testing.md +2 -2
  83. package/bundled/locales/zh-TW/core/data-pipeline.md +2 -2
  84. package/bundled/locales/zh-TW/core/deferred-item-exit.md +251 -0
  85. package/bundled/locales/zh-TW/core/documentation-writing-standards.md +228 -3
  86. package/bundled/locales/zh-TW/core/full-coverage-testing.md +15 -2
  87. package/bundled/locales/zh-TW/core/governance-layer.md +118 -5
  88. package/bundled/locales/zh-TW/core/iac-design-principles.md +2 -2
  89. package/bundled/locales/zh-TW/core/incident-response.md +2 -2
  90. package/bundled/locales/zh-TW/core/model-provenance.md +4 -2
  91. package/bundled/locales/zh-TW/core/pii-classification.md +42 -6
  92. package/bundled/locales/zh-TW/core/prd-standards.md +4 -2
  93. package/bundled/locales/zh-TW/core/product-metrics-standards.md +4 -2
  94. package/bundled/locales/zh-TW/core/release-readiness-gate.md +2 -2
  95. package/bundled/locales/zh-TW/core/resource-cost-boundary.md +2 -2
  96. package/bundled/locales/zh-TW/core/retrospective-standards.md +5 -3
  97. package/bundled/locales/zh-TW/core/reverse-engineering-standards.md +83 -5
  98. package/bundled/locales/zh-TW/core/runbook.md +2 -2
  99. package/bundled/locales/zh-TW/core/schema-evolution.md +2 -2
  100. package/bundled/locales/zh-TW/core/secret-management-standards.md +2 -2
  101. package/bundled/locales/zh-TW/core/slo-sli.md +2 -2
  102. package/bundled/locales/zh-TW/core/spec-driven-development.md +2 -0
  103. package/bundled/locales/zh-TW/core/tech-debt-standards.md +71 -4
  104. package/bundled/locales/zh-TW/core/turn-completion-integrity.md +190 -0
  105. package/bundled/locales/zh-TW/core/user-journey-testing.md +2 -2
  106. package/bundled/locales/zh-TW/core/user-story-mapping.md +2 -2
  107. package/bundled/locales/zh-TW/core/verification-oracle.md +2 -2
  108. package/bundled/locales/zh-TW/docs/CHEATSHEET.md +8 -1
  109. package/bundled/locales/zh-TW/docs/CLI-INIT-OPTIONS.md +29 -68
  110. package/bundled/locales/zh-TW/docs/FEATURE-REFERENCE.md +25 -15
  111. package/bundled/locales/zh-TW/docs/USAGE-MODES-COMPARISON.md +1 -2
  112. package/bundled/locales/zh-TW/integrations/google-antigravity/{INSTRUCTIONS.md → AGENTS.md} +1 -1
  113. package/bundled/locales/zh-TW/integrations/google-antigravity/README.md +3 -3
  114. package/bundled/locales/zh-TW/skills/adr-assistant/SKILL.md +1 -1
  115. package/bundled/locales/zh-TW/skills/atdd-assistant/SKILL.md +2 -0
  116. package/bundled/locales/zh-TW/skills/bdd-assistant/SKILL.md +2 -0
  117. package/bundled/locales/zh-TW/skills/brainstorm-assistant/SKILL.md +22 -12
  118. package/bundled/locales/zh-TW/skills/brainstorm-assistant/guide.md +12 -9
  119. package/bundled/locales/zh-TW/skills/code-review-assistant/SKILL.md +1 -0
  120. package/bundled/locales/zh-TW/skills/commands/brainstorm.md +17 -13
  121. package/bundled/locales/zh-TW/skills/commands/config.md +0 -1
  122. package/bundled/locales/zh-TW/skills/commands/init.md +1 -2
  123. package/bundled/locales/zh-TW/skills/commit-standards/SKILL.md +2 -0
  124. package/bundled/locales/zh-TW/skills/contract-test-assistant/SKILL.md +2 -1
  125. package/bundled/locales/zh-TW/skills/dev-methodology/SKILL.md +2 -0
  126. package/bundled/locales/zh-TW/skills/dev-workflow-guide/SKILL.md +1 -1
  127. package/bundled/locales/zh-TW/skills/knowledge-graph/guide.md +2 -2
  128. package/bundled/locales/zh-TW/skills/migration-assistant/SKILL.md +1 -1
  129. package/bundled/locales/zh-TW/skills/observability-assistant/SKILL.md +1 -0
  130. package/bundled/locales/zh-TW/skills/project-discovery/SKILL.md +1 -0
  131. package/bundled/locales/zh-TW/skills/project-structure-guide/SKILL.md +1 -0
  132. package/bundled/locales/zh-TW/skills/release-standards/SKILL.md +3 -0
  133. package/bundled/locales/zh-TW/skills/requirement-assistant/SKILL.md +2 -0
  134. package/bundled/locales/zh-TW/skills/reverse-engineer/SKILL.md +3 -0
  135. package/bundled/locales/zh-TW/skills/runbook-assistant/SKILL.md +1 -0
  136. package/bundled/locales/zh-TW/skills/slo-assistant/SKILL.md +1 -0
  137. package/bundled/locales/zh-TW/skills/tdd-assistant/SKILL.md +2 -0
  138. package/bundled/skills/atdd-assistant/SKILL.md +2 -0
  139. package/bundled/skills/bdd-assistant/SKILL.md +2 -0
  140. package/bundled/skills/brainstorm-assistant/SKILL.md +31 -13
  141. package/bundled/skills/brainstorm-assistant/guide.md +9 -6
  142. package/bundled/skills/code-review-assistant/SKILL.md +1 -0
  143. package/bundled/skills/commands/brainstorm.md +12 -9
  144. package/bundled/skills/commands/config.md +0 -1
  145. package/bundled/skills/commands/init.md +2 -3
  146. package/bundled/skills/commit-standards/SKILL.md +2 -0
  147. package/bundled/skills/contract-test-assistant/SKILL.md +1 -0
  148. package/bundled/skills/dev-methodology/SKILL.md +4 -0
  149. package/bundled/skills/observability-assistant/SKILL.md +1 -0
  150. package/bundled/skills/project-discovery/SKILL.md +1 -0
  151. package/bundled/skills/project-structure-guide/SKILL.md +1 -0
  152. package/bundled/skills/release-standards/SKILL.md +3 -0
  153. package/bundled/skills/requirement-assistant/SKILL.md +2 -0
  154. package/bundled/skills/reverse-engineer/SKILL.md +3 -0
  155. package/bundled/skills/runbook-assistant/SKILL.md +1 -0
  156. package/bundled/skills/slo-assistant/SKILL.md +1 -0
  157. package/bundled/skills/tdd-assistant/SKILL.md +2 -0
  158. package/bundled/templates/.ai-context.yaml.template +194 -0
  159. package/bundled/templates/CLAUDE.md.template +145 -0
  160. package/bundled/templates/DESIGN.md +237 -0
  161. package/bundled/templates/SKILL-BRIEF-TEMPLATE.md +57 -0
  162. package/bundled/templates/SKILL-CANDIDATES.md +39 -0
  163. package/bundled/templates/gates/check-error-exit.mjs +309 -0
  164. package/bundled/templates/mcp-config.json +10 -0
  165. package/bundled/templates/methodology-template.yaml +209 -0
  166. package/bundled/templates/migration-template.md +408 -0
  167. package/bundled/templates/requirement-checklist.md +410 -0
  168. package/bundled/templates/requirement-document-template.md +591 -0
  169. package/bundled/templates/requirement-template.md +881 -0
  170. package/bundled/templates/reverse-spec-template.md +409 -0
  171. package/bundled/templates/test-case-template.md +74 -0
  172. package/bundled/templates/test-plan-template.md +74 -0
  173. package/package.json +7 -5
  174. package/src/commands/audit.js +82 -0
  175. package/src/commands/check.js +66 -10
  176. package/src/commands/init.js +161 -16
  177. package/src/commands/update.js +286 -14
  178. package/src/compilers/claude-code-compiler.js +4 -1
  179. package/src/config/ai-agent-paths.js +62 -17
  180. package/src/core/constants.js +42 -11
  181. package/src/core/manifest.js +201 -3
  182. package/src/core/paths.js +2 -2
  183. package/src/i18n/messages.js +6 -29
  184. package/src/installers/hooks-installer.js +167 -75
  185. package/src/installers/integration-installer.js +9 -5
  186. package/src/prompts/init.js +14 -14
  187. package/src/utils/detector.js +21 -1
  188. package/src/utils/effect-boundary.js +1093 -0
  189. package/src/utils/hasher.js +166 -1
  190. package/src/utils/hook-stats.js +1 -1
  191. package/src/utils/integration-generator.js +79 -1
  192. package/src/utils/reference-sync.js +4 -1
  193. package/src/utils/yaml-generator.js +51 -9
  194. package/standards-registry.json +31 -8
@@ -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
+ ];