universal-dev-standards 6.8.0 → 6.10.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 (225) 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/agent-communication-protocol.md +8 -0
  19. package/bundled/core/branch-completion.md +8 -0
  20. package/bundled/core/change-batching-standards.md +8 -0
  21. package/bundled/core/code-review-checklist.md +5 -2
  22. package/bundled/core/context-aware-loading.md +1 -1
  23. package/bundled/core/deferred-item-exit.md +254 -0
  24. package/bundled/core/execution-history.md +8 -0
  25. package/bundled/core/feature-discovery-standards.md +5 -1
  26. package/bundled/core/governance-layer.md +114 -2
  27. package/bundled/core/pipeline-integration-standards.md +8 -0
  28. package/bundled/core/retrospective-standards.md +4 -2
  29. package/bundled/core/reverse-engineering-standards.md +81 -2
  30. package/bundled/core/spec-driven-development.md +8 -2
  31. package/bundled/core/tech-debt-standards.md +67 -8
  32. package/bundled/core/turn-completion-integrity.md +196 -0
  33. package/bundled/core/workflow-enforcement.md +8 -0
  34. package/bundled/core/workflow-state-protocol.md +8 -0
  35. package/bundled/hooks/check-dangerous-cmd.mjs +60 -0
  36. package/bundled/hooks/check-logging-standard.mjs +59 -0
  37. package/bundled/hooks/check-turn-completion.mjs +233 -0
  38. package/bundled/hooks/inject-standards.mjs +183 -0
  39. package/bundled/hooks/telemetry-wrapper.mjs +77 -0
  40. package/bundled/hooks/turn-completion/detect.mjs +99 -0
  41. package/bundled/hooks/turn-completion/locales/en.mjs +159 -0
  42. package/bundled/hooks/turn-completion/locales/zh-TW.mjs +166 -0
  43. package/bundled/hooks/validate-commit-msg.mjs +104 -0
  44. package/bundled/locales/zh-CN/CHANGELOG.md +73 -3
  45. package/bundled/locales/zh-CN/CLAUDE.md +1 -1
  46. package/bundled/locales/zh-CN/README.md +2 -2
  47. package/bundled/locales/zh-CN/SECURITY.md +1 -1
  48. package/bundled/locales/zh-CN/core/adr-standards.md +1 -1
  49. package/bundled/locales/zh-CN/core/agent-communication-protocol.md +7 -0
  50. package/bundled/locales/zh-CN/core/branch-completion.md +7 -0
  51. package/bundled/locales/zh-CN/core/change-batching-standards.md +7 -0
  52. package/bundled/locales/zh-CN/core/execution-history.md +7 -0
  53. package/bundled/locales/zh-CN/core/governance-layer.md +118 -6
  54. package/bundled/locales/zh-CN/core/pipeline-integration-standards.md +7 -0
  55. package/bundled/locales/zh-CN/core/retrospective-standards.md +1 -1
  56. package/bundled/locales/zh-CN/core/tech-debt-standards.md +71 -4
  57. package/bundled/locales/zh-CN/core/turn-completion-integrity.md +190 -0
  58. package/bundled/locales/zh-CN/core/workflow-enforcement.md +7 -0
  59. package/bundled/locales/zh-CN/core/workflow-state-protocol.md +7 -0
  60. package/bundled/locales/zh-CN/docs/CHEATSHEET.md +8 -1
  61. package/bundled/locales/zh-CN/docs/CLI-INIT-OPTIONS.md +29 -68
  62. package/bundled/locales/zh-CN/docs/FEATURE-REFERENCE.md +25 -15
  63. package/bundled/locales/zh-CN/docs/MIGRATION-v6.md +8 -4
  64. package/bundled/locales/zh-CN/docs/USAGE-MODES-COMPARISON.md +1 -2
  65. package/bundled/locales/zh-CN/integrations/google-antigravity/{INSTRUCTIONS.md → AGENTS.md} +1 -1
  66. package/bundled/locales/zh-CN/integrations/google-antigravity/README.md +3 -3
  67. package/bundled/locales/zh-CN/skills/atdd-assistant/SKILL.md +2 -0
  68. package/bundled/locales/zh-CN/skills/bdd-assistant/SKILL.md +2 -0
  69. package/bundled/locales/zh-CN/skills/brainstorm-assistant/SKILL.md +22 -12
  70. package/bundled/locales/zh-CN/skills/brainstorm-assistant/guide.md +12 -9
  71. package/bundled/locales/zh-CN/skills/code-review-assistant/SKILL.md +1 -0
  72. package/bundled/locales/zh-CN/skills/commands/brainstorm.md +17 -13
  73. package/bundled/locales/zh-CN/skills/commands/config.md +0 -1
  74. package/bundled/locales/zh-CN/skills/commands/init.md +1 -2
  75. package/bundled/locales/zh-CN/skills/commit-standards/SKILL.md +2 -0
  76. package/bundled/locales/zh-CN/skills/contract-test-assistant/SKILL.md +1 -0
  77. package/bundled/locales/zh-CN/skills/dev-methodology/SKILL.md +2 -0
  78. package/bundled/locales/zh-CN/skills/observability-assistant/SKILL.md +1 -0
  79. package/bundled/locales/zh-CN/skills/project-structure-guide/SKILL.md +1 -0
  80. package/bundled/locales/zh-CN/skills/release-standards/SKILL.md +3 -0
  81. package/bundled/locales/zh-CN/skills/requirement-assistant/SKILL.md +2 -0
  82. package/bundled/locales/zh-CN/skills/reverse-engineer/SKILL.md +3 -0
  83. package/bundled/locales/zh-CN/skills/runbook-assistant/SKILL.md +1 -0
  84. package/bundled/locales/zh-CN/skills/slo-assistant/SKILL.md +1 -0
  85. package/bundled/locales/zh-CN/skills/tdd-assistant/SKILL.md +2 -0
  86. package/bundled/locales/zh-TW/CHANGELOG.md +74 -3
  87. package/bundled/locales/zh-TW/CLAUDE.md +1 -1
  88. package/bundled/locales/zh-TW/README.md +2 -2
  89. package/bundled/locales/zh-TW/SECURITY.md +1 -1
  90. package/bundled/locales/zh-TW/core/acceptance-criteria-traceability.md +2 -0
  91. package/bundled/locales/zh-TW/core/adr-standards.md +26 -5
  92. package/bundled/locales/zh-TW/core/agent-communication-protocol.md +7 -0
  93. package/bundled/locales/zh-TW/core/branch-completion.md +7 -0
  94. package/bundled/locales/zh-TW/core/change-batching-standards.md +7 -0
  95. package/bundled/locales/zh-TW/core/code-review-checklist.md +2 -0
  96. package/bundled/locales/zh-TW/core/container-image-standards.md +2 -2
  97. package/bundled/locales/zh-TW/core/contract-testing-standards.md +2 -2
  98. package/bundled/locales/zh-TW/core/cross-flow-regression.md +8 -7
  99. package/bundled/locales/zh-TW/core/data-contract.md +2 -2
  100. package/bundled/locales/zh-TW/core/data-migration-testing.md +2 -2
  101. package/bundled/locales/zh-TW/core/data-pipeline.md +2 -2
  102. package/bundled/locales/zh-TW/core/deferred-item-exit.md +251 -0
  103. package/bundled/locales/zh-TW/core/documentation-writing-standards.md +228 -3
  104. package/bundled/locales/zh-TW/core/execution-history.md +7 -0
  105. package/bundled/locales/zh-TW/core/full-coverage-testing.md +15 -2
  106. package/bundled/locales/zh-TW/core/governance-layer.md +118 -5
  107. package/bundled/locales/zh-TW/core/iac-design-principles.md +2 -2
  108. package/bundled/locales/zh-TW/core/incident-response.md +2 -2
  109. package/bundled/locales/zh-TW/core/model-provenance.md +4 -2
  110. package/bundled/locales/zh-TW/core/pii-classification.md +42 -6
  111. package/bundled/locales/zh-TW/core/pipeline-integration-standards.md +7 -0
  112. package/bundled/locales/zh-TW/core/prd-standards.md +4 -2
  113. package/bundled/locales/zh-TW/core/product-metrics-standards.md +4 -2
  114. package/bundled/locales/zh-TW/core/release-readiness-gate.md +2 -2
  115. package/bundled/locales/zh-TW/core/resource-cost-boundary.md +2 -2
  116. package/bundled/locales/zh-TW/core/retrospective-standards.md +5 -3
  117. package/bundled/locales/zh-TW/core/reverse-engineering-standards.md +83 -5
  118. package/bundled/locales/zh-TW/core/runbook.md +2 -2
  119. package/bundled/locales/zh-TW/core/schema-evolution.md +2 -2
  120. package/bundled/locales/zh-TW/core/secret-management-standards.md +2 -2
  121. package/bundled/locales/zh-TW/core/slo-sli.md +2 -2
  122. package/bundled/locales/zh-TW/core/spec-driven-development.md +2 -0
  123. package/bundled/locales/zh-TW/core/tech-debt-standards.md +71 -4
  124. package/bundled/locales/zh-TW/core/turn-completion-integrity.md +190 -0
  125. package/bundled/locales/zh-TW/core/user-journey-testing.md +2 -2
  126. package/bundled/locales/zh-TW/core/user-story-mapping.md +2 -2
  127. package/bundled/locales/zh-TW/core/verification-oracle.md +2 -2
  128. package/bundled/locales/zh-TW/core/workflow-enforcement.md +7 -0
  129. package/bundled/locales/zh-TW/core/workflow-state-protocol.md +7 -0
  130. package/bundled/locales/zh-TW/docs/CHEATSHEET.md +8 -1
  131. package/bundled/locales/zh-TW/docs/CLI-INIT-OPTIONS.md +29 -68
  132. package/bundled/locales/zh-TW/docs/FEATURE-REFERENCE.md +25 -15
  133. package/bundled/locales/zh-TW/docs/MIGRATION-v6.md +8 -4
  134. package/bundled/locales/zh-TW/docs/USAGE-MODES-COMPARISON.md +1 -2
  135. package/bundled/locales/zh-TW/integrations/google-antigravity/{INSTRUCTIONS.md → AGENTS.md} +1 -1
  136. package/bundled/locales/zh-TW/integrations/google-antigravity/README.md +3 -3
  137. package/bundled/locales/zh-TW/skills/adr-assistant/SKILL.md +1 -1
  138. package/bundled/locales/zh-TW/skills/atdd-assistant/SKILL.md +2 -0
  139. package/bundled/locales/zh-TW/skills/bdd-assistant/SKILL.md +2 -0
  140. package/bundled/locales/zh-TW/skills/brainstorm-assistant/SKILL.md +22 -12
  141. package/bundled/locales/zh-TW/skills/brainstorm-assistant/guide.md +12 -9
  142. package/bundled/locales/zh-TW/skills/code-review-assistant/SKILL.md +1 -0
  143. package/bundled/locales/zh-TW/skills/commands/brainstorm.md +17 -13
  144. package/bundled/locales/zh-TW/skills/commands/config.md +0 -1
  145. package/bundled/locales/zh-TW/skills/commands/init.md +1 -2
  146. package/bundled/locales/zh-TW/skills/commit-standards/SKILL.md +2 -0
  147. package/bundled/locales/zh-TW/skills/contract-test-assistant/SKILL.md +2 -1
  148. package/bundled/locales/zh-TW/skills/dev-methodology/SKILL.md +2 -0
  149. package/bundled/locales/zh-TW/skills/dev-workflow-guide/SKILL.md +1 -1
  150. package/bundled/locales/zh-TW/skills/knowledge-graph/guide.md +2 -2
  151. package/bundled/locales/zh-TW/skills/migration-assistant/SKILL.md +1 -1
  152. package/bundled/locales/zh-TW/skills/observability-assistant/SKILL.md +1 -0
  153. package/bundled/locales/zh-TW/skills/project-discovery/SKILL.md +1 -0
  154. package/bundled/locales/zh-TW/skills/project-structure-guide/SKILL.md +1 -0
  155. package/bundled/locales/zh-TW/skills/release-standards/SKILL.md +3 -0
  156. package/bundled/locales/zh-TW/skills/requirement-assistant/SKILL.md +2 -0
  157. package/bundled/locales/zh-TW/skills/reverse-engineer/SKILL.md +3 -0
  158. package/bundled/locales/zh-TW/skills/runbook-assistant/SKILL.md +1 -0
  159. package/bundled/locales/zh-TW/skills/slo-assistant/SKILL.md +1 -0
  160. package/bundled/locales/zh-TW/skills/tdd-assistant/SKILL.md +2 -0
  161. package/bundled/skills/atdd-assistant/SKILL.md +2 -0
  162. package/bundled/skills/bdd-assistant/SKILL.md +2 -0
  163. package/bundled/skills/brainstorm-assistant/SKILL.md +31 -13
  164. package/bundled/skills/brainstorm-assistant/guide.md +9 -6
  165. package/bundled/skills/code-review-assistant/SKILL.md +1 -0
  166. package/bundled/skills/commands/brainstorm.md +12 -9
  167. package/bundled/skills/commands/config.md +0 -1
  168. package/bundled/skills/commands/init.md +2 -3
  169. package/bundled/skills/commit-standards/SKILL.md +2 -0
  170. package/bundled/skills/contract-test-assistant/SKILL.md +1 -0
  171. package/bundled/skills/dev-methodology/SKILL.md +4 -0
  172. package/bundled/skills/observability-assistant/SKILL.md +1 -0
  173. package/bundled/skills/project-discovery/SKILL.md +1 -0
  174. package/bundled/skills/project-structure-guide/SKILL.md +1 -0
  175. package/bundled/skills/release-standards/SKILL.md +3 -0
  176. package/bundled/skills/requirement-assistant/SKILL.md +2 -0
  177. package/bundled/skills/reverse-engineer/SKILL.md +3 -0
  178. package/bundled/skills/runbook-assistant/SKILL.md +1 -0
  179. package/bundled/skills/slo-assistant/SKILL.md +1 -0
  180. package/bundled/skills/tdd-assistant/SKILL.md +2 -0
  181. package/bundled/templates/.ai-context.yaml.template +194 -0
  182. package/bundled/templates/CLAUDE.md.template +145 -0
  183. package/bundled/templates/DESIGN.md +237 -0
  184. package/bundled/templates/SKILL-BRIEF-TEMPLATE.md +57 -0
  185. package/bundled/templates/SKILL-CANDIDATES.md +39 -0
  186. package/bundled/templates/gates/check-error-exit.mjs +309 -0
  187. package/bundled/templates/mcp-config.json +10 -0
  188. package/bundled/templates/methodology-template.yaml +209 -0
  189. package/bundled/templates/migration-template.md +408 -0
  190. package/bundled/templates/requirement-checklist.md +410 -0
  191. package/bundled/templates/requirement-document-template.md +591 -0
  192. package/bundled/templates/requirement-template.md +881 -0
  193. package/bundled/templates/reverse-spec-template.md +409 -0
  194. package/bundled/templates/test-case-template.md +74 -0
  195. package/bundled/templates/test-plan-template.md +74 -0
  196. package/package.json +12 -9
  197. package/src/commands/audit.js +82 -0
  198. package/src/commands/check.js +227 -34
  199. package/src/commands/config.js +19 -19
  200. package/src/commands/init.js +174 -20
  201. package/src/commands/spec.js +2 -2
  202. package/src/commands/update.js +537 -43
  203. package/src/compilers/claude-code-compiler.js +4 -1
  204. package/src/config/ai-agent-paths.js +62 -17
  205. package/src/core/constants.js +42 -11
  206. package/src/core/manifest.js +201 -3
  207. package/src/core/paths.js +2 -2
  208. package/src/i18n/messages.js +42 -29
  209. package/src/installers/hooks-installer.js +167 -75
  210. package/src/installers/integration-installer.js +13 -9
  211. package/src/installers/skills-installer.js +4 -4
  212. package/src/installers/standards-installer.js +3 -3
  213. package/src/prompts/init.js +47 -18
  214. package/src/reconciler/diff-engine.js +57 -2
  215. package/src/reconciler/plan-executor.js +31 -11
  216. package/src/utils/detector.js +21 -1
  217. package/src/utils/effect-boundary.js +1093 -0
  218. package/src/utils/hasher.js +166 -1
  219. package/src/utils/hook-stats.js +1 -1
  220. package/src/utils/integration-generator.js +288 -82
  221. package/src/utils/reference-sync.js +107 -8
  222. package/src/utils/registry.js +57 -0
  223. package/src/utils/spinner.js +31 -0
  224. package/src/utils/yaml-generator.js +51 -9
  225. package/standards-registry.json +32 -9
@@ -0,0 +1,233 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * UDS Hook: Turn Completion Integrity
4
+ *
5
+ * Runs at turn end. Blocks when the agent's final message states a first-person
6
+ * commitment to a next action that the turn then ended without taking.
7
+ *
8
+ * Contract (Claude Code Stop hook):
9
+ * stdin — JSON with session_id, transcript_path, stop_hook_active
10
+ * block — print {"decision":"block","reason":"..."} on stdout, exit 0
11
+ * allow — print nothing, exit 0
12
+ *
13
+ * Every failure path allows. See core/turn-completion-integrity.md R5: a hook
14
+ * that can trap a session is worse than none, because the only recovery a human
15
+ * has is to disable it, and they will disable it permanently.
16
+ *
17
+ * Usage: node check-turn-completion.mjs (reads stdin)
18
+ * node check-turn-completion.mjs --self-test
19
+ * node check-turn-completion.mjs --languages
20
+ *
21
+ * @see docs/specs/SPEC-HOOKS-001-core-standard-hooks.md
22
+ * @see core/turn-completion-integrity.md
23
+ */
24
+ import { readFileSync, mkdirSync, writeFileSync, existsSync } from 'node:fs';
25
+ import { homedir } from 'node:os';
26
+ import { join, dirname } from 'node:path';
27
+ import { fileURLToPath } from 'node:url';
28
+ import { detectCommitment, userAskedToStop } from './turn-completion/detect.mjs';
29
+
30
+ export const VERSION = '1.1.0';
31
+
32
+ const COOLDOWN_SEC = 120;
33
+ // A cap counted per session with no reset is an off switch on a delay: it
34
+ // disarms silently in exactly the long session where the rule matters most.
35
+ // A rolling window bounds runaway loops just as well and recovers by itself.
36
+ const MAX_BLOCKS_PER_WINDOW = 6;
37
+ const WINDOW_SEC = 3600;
38
+
39
+ const STATE_DIR = join(homedir(), '.uds', 'turn-completion');
40
+ const HERE = dirname(fileURLToPath(import.meta.url));
41
+
42
+ /** Locales this hook ships. The self-test fails if any of them will not load. */
43
+ export const SHIPPED_LOCALES = ['en', 'zh-TW'];
44
+
45
+ /**
46
+ * Load every shipped locale pack.
47
+ *
48
+ * At runtime a pack that fails to load is skipped (R5: a broken pack must not
49
+ * stop the turn from ending). 🔴 But that swallow is also how a shipping
50
+ * mistake hides: with zero packs loaded the hook runs, exits 0, and can never
51
+ * fire — the same shape as good behaviour. So `failed` is returned rather than
52
+ * discarded, and the self-test treats a non-empty `failed` as a failure.
53
+ * Caught while renaming the packs to .mjs: the loader still asked for .js.
54
+ */
55
+ export async function loadPacks() {
56
+ const packs = [];
57
+ const failed = [];
58
+ for (const id of SHIPPED_LOCALES) {
59
+ try {
60
+ packs.push(await import(join(HERE, 'turn-completion', 'locales', `${id}.mjs`)));
61
+ } catch (e) {
62
+ failed.push({ id, why: String((e && e.message) || e) });
63
+ }
64
+ }
65
+ return { packs, failed };
66
+ }
67
+
68
+ /**
69
+ * 🔴 This hook's own block message enters the transcript as a user turn. On the
70
+ * next run it would therefore be read as "the human's last message", hiding the
71
+ * real one ("pause, I'm going home") and silently voiding the R9 exemption.
72
+ *
73
+ * Same family as the detector matching the prose that documents it — except
74
+ * here it reads its own output. The reason string carries this line so it can
75
+ * recognise itself.
76
+ */
77
+ export const SELF_ECHO = 'UDS standard turn-completion-integrity (R1)';
78
+
79
+ /** Plain text of a transcript message, or '' if it carries none. */
80
+ function textOf(msg) {
81
+ const c = msg && msg.content;
82
+ if (typeof c === 'string') return c;
83
+ if (!Array.isArray(c)) return '';
84
+ // Tool results also arrive with role "user"; only text blocks are prose.
85
+ const parts = c.filter((p) => p && p.type === 'text').map((p) => p.text || '');
86
+ return parts.some((p) => p.trim()) ? parts.join('\n') : '';
87
+ }
88
+
89
+ /**
90
+ * Last assistant message and last human message in a Claude Code JSONL
91
+ * transcript. The human's message is needed because a turn that ends by their
92
+ * instruction looks, from the agent's words alone, exactly like one that ends
93
+ * on an abandoned commitment.
94
+ */
95
+ export function lastMessages(transcriptPath) {
96
+ let assistant = '';
97
+ let user = '';
98
+ for (const line of readFileSync(transcriptPath, 'utf8').split('\n')) {
99
+ if (!line.trim()) continue;
100
+ let ev;
101
+ try { ev = JSON.parse(line); } catch { continue; }
102
+ const msg = ev && ev.message;
103
+ if (!msg) continue;
104
+ const t = textOf(msg);
105
+ if (!t) continue;
106
+ if (msg.role === 'assistant') assistant = t;
107
+ else if (msg.role === 'user' && !t.includes(SELF_ECHO)) user = t;
108
+ }
109
+ return { assistant, user };
110
+ }
111
+
112
+ function readState(path) {
113
+ try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return {}; }
114
+ }
115
+
116
+ function reason(packId, sentence) {
117
+ return [
118
+ 'Your last message stated a next action, and then the turn ended without taking it.',
119
+ '',
120
+ `Detected by the ${packId} pack, in: "${sentence}"`,
121
+ '',
122
+ 'UDS standard turn-completion-integrity (R1): a stated next action is not optional.',
123
+ '',
124
+ 'Do one of these now:',
125
+ ' (a) take the action you just said you would take;',
126
+ ' (b) if it is actually blocked, say what blocks it and what you need, then do',
127
+ ' the next item that is not blocked;',
128
+ ' (c) if everything is done or blocked, list every remaining item WITH the',
129
+ ' person or input it waits on. That itemized list is the only shape of',
130
+ ' ending that R2 accepts — "the main parts are done" is not one.',
131
+ ].join('\n');
132
+ }
133
+
134
+ async function main() {
135
+ let raw = '';
136
+ try {
137
+ raw = readFileSync(0, 'utf8');
138
+ } catch {
139
+ return;
140
+ }
141
+
142
+ let data;
143
+ try { data = JSON.parse(raw); } catch { return; }
144
+ if (!data || typeof data !== 'object') return;
145
+ if (data.stop_hook_active === true) return;
146
+
147
+ const tp = data.transcript_path;
148
+ if (!tp || !existsSync(tp)) return; // cannot tell is not the same as should block
149
+
150
+ const sid = String(data.session_id || 'unknown');
151
+ const statePath = join(STATE_DIR, `${sid}.json`);
152
+ const st = readState(statePath);
153
+ const now = Date.now() / 1000;
154
+
155
+ const stamps = (Array.isArray(st.stamps) ? st.stamps : [])
156
+ .filter((t) => typeof t === 'number' && now - t < WINDOW_SEC);
157
+ if (stamps.length >= MAX_BLOCKS_PER_WINDOW) return;
158
+ if (now - (st.last || 0) < COOLDOWN_SEC) return;
159
+
160
+ let msgs;
161
+ try { msgs = lastMessages(tp); } catch { return; }
162
+ if (!msgs.assistant.trim()) return;
163
+
164
+ const { packs } = await loadPacks();
165
+ if (packs.length === 0) return;
166
+
167
+ // The human asked for the turn to end. That is a legitimate ending, and the
168
+ // agent's own words cannot distinguish it from an abandoned commitment.
169
+ if (userAskedToStop(msgs.user, packs)) return;
170
+
171
+ const hit = detectCommitment(msgs.assistant, packs);
172
+ if (!hit.fired) return;
173
+
174
+ stamps.push(now);
175
+ try {
176
+ mkdirSync(STATE_DIR, { recursive: true });
177
+ writeFileSync(statePath, JSON.stringify({ stamps, last: now }));
178
+ } catch {
179
+ /* state is an optimisation; failing to write it must not change the verdict */
180
+ }
181
+
182
+ process.stdout.write(JSON.stringify({
183
+ decision: 'block',
184
+ reason: reason(hit.packId, hit.sentence),
185
+ }));
186
+ }
187
+
188
+ async function selfTest() {
189
+ const { packs, failed } = await loadPacks();
190
+ let ok = failed.length === 0 && packs.length === SHIPPED_LOCALES.length;
191
+ console.log(`[turn-completion] v${VERSION} — packs: ${packs.map((p) => p.id).join(', ')}`
192
+ + ` (${packs.length}/${SHIPPED_LOCALES.length} shipped locales)`);
193
+ for (const f of failed) console.log(` x locale ${f.id} failed to load — ${f.why}`);
194
+
195
+ for (const pack of packs) {
196
+ for (const [want, label, text] of pack.corpus) {
197
+ // Run against ALL packs, which is the shipped configuration. A pack that
198
+ // is correct alone and wrong beside another is not correct.
199
+ const got = detectCommitment(text, packs).fired;
200
+ const good = got === want;
201
+ ok &&= good;
202
+ console.log(` ${good ? 'OK ' : 'x '} [${pack.id}] ${want ? 'must block' : 'must pass'} — ${label}`
203
+ + (good ? '' : ` (got: ${got ? 'block' : 'pass'})`));
204
+ }
205
+ for (const [want, label, text] of pack.stopCorpus || []) {
206
+ const got = userAskedToStop(text, packs);
207
+ const good = got === want;
208
+ ok &&= good;
209
+ console.log(` ${good ? 'OK ' : 'x '} [${pack.id}] user ${want ? 'IS' : 'is NOT'} asking to stop — ${label}`
210
+ + (good ? '' : ` (got: ${got ? 'exempt' : 'not exempt'})`));
211
+ }
212
+ }
213
+ // The block message must contain the marker, or the hook cannot tell its own
214
+ // output from the human's next instruction.
215
+ const selfRecognised = reason('en', 'x').includes(SELF_ECHO);
216
+ ok &&= selfRecognised;
217
+ console.log(` ${selfRecognised ? 'OK ' : 'x '} block message carries the self-echo marker`);
218
+
219
+ console.log(`[turn-completion] self-test ${ok ? 'passed' : 'FAILED'}`);
220
+ process.exit(ok ? 0 : 1);
221
+ }
222
+
223
+ const arg = process.argv[2];
224
+ if (arg === '--self-test') {
225
+ await selfTest();
226
+ } else if (arg === '--languages') {
227
+ const { packs } = await loadPacks();
228
+ console.log(packs.map((p) => `${p.id} (${p.label})`).join('\n'));
229
+ console.log('\nThis check reads prose. If you work in a language not listed above,'
230
+ + '\nit is installed and running but cannot fire. See turn-completion-integrity R8.');
231
+ } else {
232
+ try { await main(); } catch { /* R5 */ }
233
+ }
@@ -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
+ }