universal-dev-standards 6.11.0 → 6.13.0-beta.1

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 (33) hide show
  1. package/bundled/ai/standards/developer-memory.ai.yaml +24 -2
  2. package/bundled/ai/standards/open-work-tracking.ai.yaml +216 -0
  3. package/bundled/ai/standards/turn-completion-integrity.ai.yaml +34 -2
  4. package/bundled/core/developer-memory.md +58 -2
  5. package/bundled/core/open-work-tracking.md +333 -0
  6. package/bundled/core/turn-completion-integrity.md +36 -2
  7. package/bundled/hooks/check-turn-completion-codex.mjs +116 -0
  8. package/bundled/hooks/check-turn-completion-gemini.mjs +81 -0
  9. package/bundled/hooks/check-turn-completion.mjs +24 -149
  10. package/bundled/hooks/turn-completion/engine.mjs +206 -0
  11. package/bundled/hooks/turn-completion/locales/en.mjs +29 -1
  12. package/bundled/hooks/turn-completion/locales/zh-TW.mjs +29 -1
  13. package/bundled/locales/zh-CN/CHANGELOG.md +23 -3
  14. package/bundled/locales/zh-CN/CLAUDE.md +1 -1
  15. package/bundled/locales/zh-CN/README.md +2 -2
  16. package/bundled/locales/zh-CN/SECURITY.md +2 -1
  17. package/bundled/locales/zh-CN/core/turn-completion-integrity.md +36 -6
  18. package/bundled/locales/zh-CN/docs/CHEATSHEET.md +2 -1
  19. package/bundled/locales/zh-CN/docs/CLI-INIT-OPTIONS.md +24 -5
  20. package/bundled/locales/zh-CN/docs/FEATURE-REFERENCE.md +4 -3
  21. package/bundled/locales/zh-TW/CHANGELOG.md +23 -3
  22. package/bundled/locales/zh-TW/CLAUDE.md +1 -1
  23. package/bundled/locales/zh-TW/README.md +2 -2
  24. package/bundled/locales/zh-TW/SECURITY.md +2 -1
  25. package/bundled/locales/zh-TW/core/open-work-tracking.md +255 -0
  26. package/bundled/locales/zh-TW/core/turn-completion-integrity.md +36 -6
  27. package/bundled/locales/zh-TW/docs/CHEATSHEET.md +2 -1
  28. package/bundled/locales/zh-TW/docs/CLI-INIT-OPTIONS.md +24 -5
  29. package/bundled/locales/zh-TW/docs/FEATURE-REFERENCE.md +4 -3
  30. package/package.json +1 -1
  31. package/src/commands/init.js +26 -0
  32. package/src/installers/hooks-installer.js +104 -0
  33. package/standards-registry.json +19 -7
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * UDS Hook: Turn Completion Integrity
3
+ * UDS Hook: Turn Completion Integrity — Claude Code adapter
4
4
  *
5
5
  * Runs at turn end. Blocks when the agent's final message states a first-person
6
6
  * commitment to a next action that the turn then ended without taking.
@@ -14,6 +14,13 @@
14
14
  * that can trap a session is worse than none, because the only recovery a human
15
15
  * has is to disable it, and they will disable it permanently.
16
16
  *
17
+ * This file is one of several per-harness adapters (see also
18
+ * check-turn-completion-codex.mjs, check-turn-completion-gemini.mjs). Judgement
19
+ * — packs, cooldown, the rolling window, the self-echo marker — lives in
20
+ * turn-completion/engine.mjs and is shared by all of them; this file's only job
21
+ * is reading Claude Code's stdin/transcript shape and writing Claude Code's
22
+ * output shape.
23
+ *
17
24
  * Usage: node check-turn-completion.mjs (reads stdin)
18
25
  * node check-turn-completion.mjs --self-test
19
26
  * node check-turn-completion.mjs --languages
@@ -21,60 +28,13 @@
21
28
  * @see docs/specs/SPEC-HOOKS-001-core-standard-hooks.md
22
29
  * @see core/turn-completion-integrity.md
23
30
  */
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
- }
31
+ import { readFileSync, existsSync } from 'node:fs';
32
+ import {
33
+ VERSION, SHIPPED_LOCALES, SELF_ECHO, decide, loadPacks, isSelfEcho,
34
+ runSelfTest, printLanguages,
35
+ } from './turn-completion/engine.mjs';
67
36
 
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)';
37
+ export { VERSION, SHIPPED_LOCALES, SELF_ECHO, loadPacks };
78
38
 
79
39
  /** Plain text of a transcript message, or '' if it carries none. */
80
40
  function textOf(msg) {
@@ -104,33 +64,11 @@ export function lastMessages(transcriptPath) {
104
64
  const t = textOf(msg);
105
65
  if (!t) continue;
106
66
  if (msg.role === 'assistant') assistant = t;
107
- else if (msg.role === 'user' && !t.includes(SELF_ECHO)) user = t;
67
+ else if (msg.role === 'user' && !isSelfEcho(t)) user = t;
108
68
  }
109
69
  return { assistant, user };
110
70
  }
111
71
 
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
72
  async function main() {
135
73
  let raw = '';
136
74
  try {
@@ -147,87 +85,24 @@ async function main() {
147
85
  const tp = data.transcript_path;
148
86
  if (!tp || !existsSync(tp)) return; // cannot tell is not the same as should block
149
87
 
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
88
  let msgs;
161
89
  try { msgs = lastMessages(tp); } catch { return; }
162
- if (!msgs.assistant.trim()) return;
163
90
 
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`);
91
+ const verdict = await decide({
92
+ sessionId: data.session_id,
93
+ assistantText: msgs.assistant,
94
+ userText: msgs.user,
95
+ });
96
+ if (!verdict.fire) return;
218
97
 
219
- console.log(`[turn-completion] self-test ${ok ? 'passed' : 'FAILED'}`);
220
- process.exit(ok ? 0 : 1);
98
+ process.stdout.write(JSON.stringify({ decision: 'block', reason: verdict.reason }));
221
99
  }
222
100
 
223
101
  const arg = process.argv[2];
224
102
  if (arg === '--self-test') {
225
- await selfTest();
103
+ process.exit((await runSelfTest('turn-completion')) ? 0 : 1);
226
104
  } 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.');
105
+ await printLanguages();
231
106
  } else {
232
107
  try { await main(); } catch { /* R5 */ }
233
108
  }
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Shared decision engine for the turn-completion-integrity check.
3
+ *
4
+ * Every harness adapter (Claude Code, Codex, Gemini CLI, ...) reduces its own
5
+ * stdin contract down to { sessionId, assistantText, userText } and hands it
6
+ * to decide() here. This file owns judgement — pack loading, cooldown, the
7
+ * rolling-window cap, the self-echo marker — so that three (or more) copies
8
+ * of that logic cannot drift out of sync with each other. An adapter's only
9
+ * remaining job is reading its tool's input and writing its tool's output
10
+ * shape for a block; neither of those touches this file.
11
+ *
12
+ * @see core/turn-completion-integrity.md
13
+ */
14
+ import { readFileSync, mkdirSync, writeFileSync, existsSync } from 'node:fs';
15
+ import { homedir } from 'node:os';
16
+ import { join, dirname } from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+ import { detectCommitment, userAskedToStop } from './detect.mjs';
19
+
20
+ export const VERSION = '1.2.0';
21
+
22
+ const COOLDOWN_SEC = 120;
23
+ // A cap counted per session with no reset is an off switch on a delay: it
24
+ // disarms silently in exactly the long session where the rule matters most.
25
+ // A rolling window bounds runaway loops just as well and recovers by itself.
26
+ const MAX_BLOCKS_PER_WINDOW = 6;
27
+ const WINDOW_SEC = 3600;
28
+
29
+ // Overridable so tests (and anything else that must not touch the
30
+ // developer's real state) can point this at a throwaway directory instead of
31
+ // mocking fs. Read once per call, not once at import time, so a test can set
32
+ // it per-case without re-importing the module.
33
+ function stateDir() {
34
+ return process.env.UDS_TURN_COMPLETION_STATE_DIR || join(homedir(), '.uds', 'turn-completion');
35
+ }
36
+
37
+ const HERE = dirname(fileURLToPath(import.meta.url));
38
+
39
+ /** Locales this hook ships. The self-test fails if any of them will not load. */
40
+ export const SHIPPED_LOCALES = ['en', 'zh-TW'];
41
+
42
+ /**
43
+ * Load every shipped locale pack.
44
+ *
45
+ * At runtime a pack that fails to load is skipped (R5: a broken pack must not
46
+ * stop the turn from ending). 🔴 But that swallow is also how a shipping
47
+ * mistake hides: with zero packs loaded the hook runs, exits 0, and can never
48
+ * fire — the same shape as good behaviour. So `failed` is returned rather than
49
+ * discarded, and the self-test treats a non-empty `failed` as a failure.
50
+ */
51
+ export async function loadPacks() {
52
+ const packs = [];
53
+ const failed = [];
54
+ for (const id of SHIPPED_LOCALES) {
55
+ try {
56
+ packs.push(await import(join(HERE, 'locales', `${id}.mjs`)));
57
+ } catch (e) {
58
+ failed.push({ id, why: String((e && e.message) || e) });
59
+ }
60
+ }
61
+ return { packs, failed };
62
+ }
63
+
64
+ /**
65
+ * 🔴 This hook's own block message can re-enter a transcript as a human turn
66
+ * (measured on Claude Code — the block reason is written back as the next
67
+ * "user" message). On the next run it would therefore be read as "the human's
68
+ * last message", hiding the real one ("pause, I'm going home") and silently
69
+ * voiding the R9 exemption. Every adapter that reads a transcript for the
70
+ * human's last message MUST strip a message containing this marker before
71
+ * treating it as the human's words; stripSelfEcho() below does that.
72
+ */
73
+ export const SELF_ECHO = 'UDS standard turn-completion-integrity (R1)';
74
+
75
+ /** True if this text is (or contains) the hook's own prior block message. */
76
+ export function isSelfEcho(text) {
77
+ return typeof text === 'string' && text.includes(SELF_ECHO);
78
+ }
79
+
80
+ function readState(path) {
81
+ try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return {}; }
82
+ }
83
+
84
+ export function reason(packId, sentence) {
85
+ return [
86
+ 'Your last message stated a next action, and then the turn ended without taking it.',
87
+ '',
88
+ `Detected by the ${packId} pack, in: "${sentence}"`,
89
+ '',
90
+ 'UDS standard turn-completion-integrity (R1): a stated next action is not optional.',
91
+ '',
92
+ 'Do one of these now:',
93
+ ' (a) take the action you just said you would take;',
94
+ ' (b) if it is actually blocked, say what blocks it and what you need, then do',
95
+ ' the next item that is not blocked;',
96
+ ' (c) if everything is done or blocked, list every remaining item WITH the',
97
+ ' person or input it waits on. That itemized list is the only shape of',
98
+ ' ending that R2 accepts — "the main parts are done" is not one.',
99
+ ].join('\n');
100
+ }
101
+
102
+ /**
103
+ * Decide whether this turn should block, and own the per-session cooldown and
104
+ * rolling-window state shared by every adapter.
105
+ *
106
+ * @param {object} input
107
+ * @param {string} input.sessionId
108
+ * @param {string} input.assistantText - the agent's final message for this turn
109
+ * @param {string} input.userText - the human's most recent message. The
110
+ * caller is responsible for excluding this hook's own SELF_ECHO from it
111
+ * where that tool's transcript can carry it (see isSelfEcho/stripSelfEcho).
112
+ * @returns {Promise<{ fire: boolean, reason?: string, packId?: string, sentence?: string }>}
113
+ */
114
+ export async function decide({ sessionId, assistantText, userText }) {
115
+ if (!assistantText || !assistantText.trim()) return { fire: false };
116
+
117
+ const statePath = join(stateDir(), `${String(sessionId || 'unknown')}.json`);
118
+ const st = readState(statePath);
119
+ const now = Date.now() / 1000;
120
+
121
+ const stamps = (Array.isArray(st.stamps) ? st.stamps : [])
122
+ .filter((t) => typeof t === 'number' && now - t < WINDOW_SEC);
123
+ if (stamps.length >= MAX_BLOCKS_PER_WINDOW) return { fire: false };
124
+ if (now - (st.last || 0) < COOLDOWN_SEC) return { fire: false };
125
+
126
+ const { packs } = await loadPacks();
127
+ if (packs.length === 0) return { fire: false };
128
+
129
+ // The human asked for the turn to end. That is a legitimate ending, and the
130
+ // agent's own words cannot distinguish it from an abandoned commitment.
131
+ if (userAskedToStop(userText, packs)) return { fire: false };
132
+
133
+ const hit = detectCommitment(assistantText, packs);
134
+ if (!hit.fired) return { fire: false };
135
+
136
+ stamps.push(now);
137
+ try {
138
+ mkdirSync(stateDir(), { recursive: true });
139
+ writeFileSync(statePath, JSON.stringify({ stamps, last: now }));
140
+ } catch {
141
+ /* state is an optimisation; failing to write it must not change the verdict */
142
+ }
143
+
144
+ return { fire: true, reason: reason(hit.packId, hit.sentence), packId: hit.packId, sentence: hit.sentence };
145
+ }
146
+
147
+ /**
148
+ * Run every locale pack's corpus and print the result. Shared by every
149
+ * adapter's `--self-test` so the corpus can only be correct or wrong once,
150
+ * not once per tool.
151
+ *
152
+ * @param {string} label - printed in the header, e.g. "turn-completion-codex"
153
+ * @returns {Promise<boolean>} true if every corpus case and the self-echo
154
+ * marker check passed
155
+ */
156
+ export async function runSelfTest(label = 'turn-completion') {
157
+ const { packs, failed } = await loadPacks();
158
+ let ok = failed.length === 0 && packs.length === SHIPPED_LOCALES.length;
159
+ console.log(`[${label}] v${VERSION} — packs: ${packs.map((p) => p.id).join(', ')}`
160
+ + ` (${packs.length}/${SHIPPED_LOCALES.length} shipped locales)`);
161
+ for (const f of failed) console.log(` x locale ${f.id} failed to load — ${f.why}`);
162
+
163
+ for (const pack of packs) {
164
+ for (const [want, caseLabel, text] of pack.corpus) {
165
+ // Run against ALL packs, which is the shipped configuration. A pack that
166
+ // is correct alone and wrong beside another is not correct.
167
+ const got = detectCommitment(text, packs).fired;
168
+ const good = got === want;
169
+ ok &&= good;
170
+ console.log(` ${good ? 'OK ' : 'x '} [${pack.id}] ${want ? 'must block' : 'must pass'} — ${caseLabel}`
171
+ + (good ? '' : ` (got: ${got ? 'block' : 'pass'})`));
172
+ }
173
+ for (const [want, caseLabel, text] of pack.stopCorpus || []) {
174
+ const got = userAskedToStop(text, packs);
175
+ const good = got === want;
176
+ ok &&= good;
177
+ console.log(` ${good ? 'OK ' : 'x '} [${pack.id}] user ${want ? 'IS' : 'is NOT'} asking to stop — ${caseLabel}`
178
+ + (good ? '' : ` (got: ${got ? 'exempt' : 'not exempt'})`));
179
+ }
180
+ }
181
+ // The block message must contain the marker, or a transcript-reading adapter
182
+ // cannot tell its own output from the human's next instruction.
183
+ const selfRecognised = reason('en', 'x').includes(SELF_ECHO);
184
+ ok &&= selfRecognised;
185
+ console.log(` ${selfRecognised ? 'OK ' : 'x '} block message carries the self-echo marker`);
186
+
187
+ console.log(`[${label}] self-test ${ok ? 'passed' : 'FAILED'}`);
188
+ return ok;
189
+ }
190
+
191
+ /**
192
+ * Print the languages this hook ships, and the R8 disclosure. Shared by every
193
+ * adapter's `--languages` so the installer's probeLanguageLimits() gets the
194
+ * same answer regardless of which tool's script it runs.
195
+ */
196
+ export async function printLanguages() {
197
+ const { packs } = await loadPacks();
198
+ console.log(packs.map((p) => `${p.id} (${p.label})`).join('\n'));
199
+ console.log('\nThis check reads prose. If you work in a language not listed above,'
200
+ + '\nit is installed and running but cannot fire. See turn-completion-integrity R8.');
201
+ }
202
+
203
+ /** True if the given path exists — re-exported so adapters don't add their own fs import just for this. */
204
+ export function pathExists(p) {
205
+ return !!p && existsSync(p);
206
+ }
@@ -46,6 +46,16 @@ const REPORTING =
46
46
  const ASKING =
47
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
48
 
49
+ // "Once you pick, I'll ..." — the precondition is the human's decision, not a
50
+ // request for information, so ASKING above never caught it (measured: the
51
+ // zh-TW mirror of this shape fired as an unkept commitment). Grammar-based, not
52
+ // a verb list, to match this pack's own design note above: "once/after/as soon
53
+ // as you", up to 20 non-terminating characters, a comma, then "I".
54
+ // Known limit, left uncovered on purpose rather than widened past this shape:
55
+ // a version with no comma ("After you merged it I will …") still fires. See
56
+ // the corpus entry below.
57
+ const CONDITIONAL_ON_YOU = /\b(once|after|as soon as) you\b[^,.\n]{0,20},\s*I\b/i;
58
+
49
59
  /**
50
60
  * Expand contractions so the patterns below never have to fight an apostrophe.
51
61
  * Both the ASCII and typographic apostrophes appear in real transcripts.
@@ -96,7 +106,8 @@ export function isCommitment(sentence) {
96
106
  }
97
107
 
98
108
  export function isAsking(text) {
99
- return ASKING.test(normalize(text));
109
+ const n = normalize(text);
110
+ return ASKING.test(n) || CONDITIONAL_ON_YOU.test(n);
100
111
  }
101
112
 
102
113
  /**
@@ -142,6 +153,23 @@ export const corpus = [
142
153
  [false, 'legitimate stop: the next move is the human\'s', 'I will wait for your key before deploying.'],
143
154
  [false, 'conditional: asking in the same paragraph',
144
155
  'Tell me which file you meant and I will check it.'],
156
+ // 🔴 A third shape of conditional: the precondition is the human's decision,
157
+ // not a request for information or a report-back. Mirrors a false block
158
+ // measured in the zh-TW pack for the same grammar ("你選定後,我會…").
159
+ [false, 'conditional: once you pick, I will',
160
+ "Once you pick a model, I'll wire it into the config."],
161
+ [false, 'conditional: after you confirm, I will',
162
+ 'After you confirm the plan, I will kick off the deploy.'],
163
+ [false, 'conditional: once you decide, I will',
164
+ 'Once you decide, I will draft the ADR and file the tickets.'],
165
+ [true, 'subject is not you, still commits',
166
+ "After I fix this, I'll push it up."],
167
+ // Known limit, not fixed here: no comma between the precondition and "I"
168
+ // still fires. Widening past a comma risks swallowing real unkept
169
+ // commitments ("After you leave I will still ship it" has no comma either),
170
+ // so this stays a documented gap rather than a wider pattern.
171
+ [true, 'known limit: same shape, no comma — still fires',
172
+ 'After you merged it I will follow up.'],
145
173
  ];
146
174
 
147
175
  /**
@@ -44,7 +44,15 @@ const ASKING = new RegExp(
44
44
  // 「跟我說一聲,我會到 PC15 上確認」 — the precondition sat in a heading one paragraph up, so
45
45
  // the paragraph opens with the request. A report-back phrase, a comma, then 我 at once is
46
46
  // "you tell me, then I act". A report-back phrase alone does not qualify.
47
- '|(跟我說|告訴我|回報我|讓我知道)(一聲)?[,,]\\s*我)'
47
+ '|(跟我說|告訴我|回報我|讓我知道)(一聲)?[,,]\\s*我' +
48
+ // 「你選定後,我會…」— the precondition is the human's decision, not a request
49
+ // for information or a report-back, so neither branch above caught it. Measured:
50
+ // fired as an unkept commitment. Narrow on purpose: literal 你, then 1-6 chars
51
+ // that are not another 我/你 or punctuation (a short decision verb — 選定/選好/
52
+ // 決定/確認/回覆/點頭 — not a whole clause), then 後, then a comma and 我. 你 with
53
+ // no 後 right after (「你選定的那份我會接著處理」) and 後 with no 你 right before
54
+ // (「改好後我接著合併」, 「他確認後,我會…」) must both still fall through and block.
55
+ '|你[^,,。\\n我你]{1,6}(之)?後[,,]\\s*我)'
48
56
  );
49
57
 
50
58
  // First person + future marker + action verb, within one sentence.
@@ -150,6 +158,26 @@ export const corpus = [
150
158
  '我先去確認有沒有新的發現。'],
151
159
  [true, '沒有要求回報的承諾(仍必須擋)',
152
160
  '設定檔已經改好了。我接著把驗證結果寫進規格,然後回報。'],
161
+ // 🔴 條件式承諾的第三種形狀:前提是使用者的決定,不是資訊或回報。
162
+ // 09-25 實測:「你選定後,我會…」被判成未兌現承諾(fired=true)。
163
+ [false, '條件式承諾:你選定後,我會…',
164
+ '你選定後,我會把這一輪的發想寫成正式決策紀錄,再分別派工給 UDS 和 EGR。'],
165
+ [false, '條件式承諾:你決定後',
166
+ '你決定後,我會接著把設定寫回 repo。'],
167
+ [false, '條件式承諾:你確認後',
168
+ '你確認後,我會把清單送出。'],
169
+ [false, '條件式承諾:你回覆後',
170
+ '你回覆後,我會接著處理。'],
171
+ [false, '條件式承諾:你選好後',
172
+ '你選好後,我會接著跑一次測試。'],
173
+ [false, '條件式承諾:你點頭後',
174
+ '你點頭後,我會接著把這份規格送出。'],
175
+ [true, '主詞不是你,仍必須擋——「後」在,但前面不是你',
176
+ '改好後我接著合併。'],
177
+ [true, '主詞不是你,仍必須擋——第三人稱的「後」',
178
+ '他確認後,我會把清單整理好。'],
179
+ [true, '你在句中,但後面沒有「後」,仍必須擋',
180
+ '你選定的那份我會接著處理。'],
153
181
  ];
154
182
 
155
183
  /**
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  source: ../../CHANGELOG.md
3
- source_version: 6.11.0
4
- translation_version: 6.11.0
5
- last_synced: 2026-09-18
3
+ source_version: 6.13.0-beta.1
4
+ translation_version: 6.13.0-beta.1
5
+ last_synced: 2026-09-25
6
6
  status: current
7
7
  ---
8
8
 
@@ -17,6 +17,26 @@ status: current
17
17
 
18
18
  ## [Unreleased]
19
19
 
20
+ ## [6.13.0-beta.1] - 2026-09-26
21
+
22
+ > **测试版** — 以 `npm install -g universal-dev-standards@beta` 安装。要测什么、已知限制、如何退回正式版:见 [docs/PRE-RELEASE.md](../../docs/PRE-RELEASE.md)。已知限制:`uds uninstall` 尚不会移除 Codex/Gemini CLI 设置里的关卡。
23
+
24
+ ### 新增
25
+
26
+ - **`turn-completion-integrity` 1.4.0 把 Stop 关卡推广到 Codex 与 Gemini CLI,与既有的 Claude Code 适配层并存。** 判断逻辑(语料包、冷却、滚动窗口、自我回音标记)抽到共用的 `turn-completion/engine.mjs`,三个工具脚本各自只转译自己工具的契约,不再各带一份判断逻辑。Codex(`.codex/hooks.json`,Stop 事件)直接从 stdin 读取 `last_assistant_message`,在 exit 0 时于 stdout 输出 `{"decision":"block","reason":...}` 来拦截——官方文档写明这个事件纯文本或空输出无效,这点与 Claude Code「沉默即放行」不同;用户的最后一条消息用 `transcript_path` 尽力读取,因为其确切格式未对照真实安装验证过,所以 R9(豁免用户主动喊停的回合)在 Codex 上是文档记载的已知落差,不是静默失效。Gemini CLI(`.gemini/settings.json`,`AfterAgent` 事件)的 stdin 直接给出 `prompt` 与 `prompt_response`,完全不需要解析对话记录,拦截方式是 `{"decision":"deny","reason":...}`——官方文档标记为优先于 exit code 2 的做法。`uds init --with-hooks` 现在也会调用 `installCodexHooks`/`installGeminiHooks`,门槛是采用者有没有选那个工具,没用到 Codex 或 Gemini CLI 的项目不会被写入任何东西。Cursor 已评估,明确标记为不支持(Cursor 的 stop hook 能不能真的拦下一个回合仍未确定)。见[支持的执行环境](../../core/turn-completion-integrity.md#supported-harnesses)与 [CLI-INIT-OPTIONS.md](../../docs/CLI-INIT-OPTIONS.md#claude-code-以外的强制执行-hooks)。
27
+
28
+ - **`developer-memory` 1.2.0:新增 `code-reference` 过期检查——记忆引用的文件路径或符号一旦搬走或不存在,浮现前就会被标记,不再被悄悄沿用。** 沿用 `knowledge-graph-memory` 1.0.0 已定义的双模式(§2),不另外发明第三种:降级模式(没有图引擎——AI 自己用 Glob/Grep/Read 确认引用还在,与既有的记忆验证原则同一套机制)与引擎模式(有图引擎时,例如 EngramGraph 的 `egr refs check`,报告每个引用的状态:`present`/`moved`(附新位置)/`missing`/`unresolvable`)。`unresolvable` 一律不得当成 `present` 或 `missing`——它代表检查器无法判断,不是引用没事或已消失。时机挂在既有的 `proactive-surfacing` 规则(§4.1),检查在记忆浮现**之前**进行,不是之后。第一批只涵盖文件路径与符号名称(函数/类);`file:line` 明确排除在外——行号会随任何不相关的编辑漂移,属于不同种类的过期(见 DEC-115 OQ-1,2027-01-31 前重新评估)。`core/developer-memory.md` §11 加入一段非规范性的 Claude Code `SessionStart` hook 范例;其他工具则改走各自 repo 的说明文件(CLAUDE.md/AGENTS.md/.cursorrules 等)。(DEC-115-L1)
29
+
30
+ ### 修复
31
+
32
+ - **`turn-completion-integrity` 的 zh-TW 与 en 检测器,会在前提是用户自己决定的条件式承诺上误拦。** 「你选定后,我会把这一轮的发想写成正式决策记录…」被判成未兑现的承诺——既有的豁免只涵盖「要求信息」与「要求回报」,没有涵盖「前提是用户的决定」这种条件句。zh-TW 新增一个窄模式:你 + 短决定动词(选定/选好/决定/确认/回覆/点头)+ 后 + 逗号 + 我;en 新增以语法为准(不是动词清单,延续本包既有设计)的 `(once|after|as soon as) you ..., I` 模式。两份语料都补了成对的反例(主语不是你/you,或只有一半的形状),证明收窄没有连带漏拦真的未兑现承诺;en 版本也记下一个刻意留下未解的已知限制(前提与 "I" 之间没有逗号时仍会误拦——放宽会漏拦真的未兑现承诺)。
33
+
34
+ ## [6.12.0] - 2026-09-25
35
+
36
+ ### 新增
37
+
38
+ - **新标准 `open-work-tracking`——`deferred-item-exit` 的下游一半。** `deferred-item-exit` 要求被推迟的条目离开原文件、走向可追溯的出口,但刻意不规定出口的承载处;条目进入承载处之后,没有任何规则防止承载处本身腐坏。本标准以 16 条要求(OWT-001~016)补上:低摩擦的记录点(必填字段至多两个)、每个等待中的条目旁写明解除条件、可推导的字段由生成而非手写、以内容证明"最新"而非可随手改的时间戳、覆盖率数字要写出它看不到什么,以及每轮结束时报告未完成工作但**从不阻挡**的检查点。最后一点刻意与挂在同一事件、会阻挡的 `turn-completion-integrity` 相反;标准内附对照表,避免采用者把两者接成同一件事。其中两个数字门槛标明为初始判断、非测量结果。
39
+
20
40
  ## [6.11.0] - 2026-09-18
21
41
 
22
42
  ### 修复
@@ -14,7 +14,7 @@ status: current
14
14
 
15
15
  Universal Development Standards 是一个语言无关、框架无关的文件化标准框架。它提供:
16
16
 
17
- - **核心规范** (`core/`):152 个基础开发标准
17
+ - **核心规范** (`core/`):153 个基础开发标准
18
18
  - **AI 技能** (`skills/`):用于 AI 辅助开发的 Claude Code 技能
19
19
  - **CLI 工具** (`cli/`):用于采用标准的 Node.js CLI
20
20
  - **整合** (`integrations/`):各种 AI 工具的配置
@@ -15,7 +15,7 @@ status: current
15
15
 
16
16
  > **语言**: [English](../../README.md) | [繁體中文](../zh-TW/README.md) | 简体中文
17
17
 
18
- **版本**: 6.11.0 | **发布日期**: 2026-09-16 | **授权**: [双重授权](../../LICENSE) (CC BY 4.0 + MIT)
18
+ **版本**: 6.13.0-beta.1 (Pre-release) | **发布日期**: 2026-09-26 | **授权**: [双重授权](../../LICENSE) (CC BY 4.0 + MIT)
19
19
 
20
20
  语言无关、框架无关的软件项目文档标准。通过 AI 原生工作流,确保不同技术栈之间的一致性、质量和可维护性。
21
21
 
@@ -76,7 +76,7 @@ npx universal-dev-standards init
76
76
  <!-- UDS_STATS_TABLE_START -->
77
77
  | 类别 | 数量 | 说明 |
78
78
  |----------|-------|-------------|
79
- | **核心标准** | 152 | 通用开发准则 |
79
+ | **核心标准** | 153 | 通用开发准则 |
80
80
  | **AI Skills** | 55 | 互动式技能 |
81
81
  | **斜线命令** | 51 | 快速操作 |
82
82
  | **CLI 命令** | 23 | 项目设置与维护 |
@@ -13,7 +13,8 @@ status: current
13
13
  <!-- UDS_SUPPORTED_VERSIONS_START -->
14
14
  | 版本 | 支持状态 |
15
15
  |------|--------|
16
- | 6.11.0 | ✅ 最新正式版 |
16
+ | 6.13.0-beta.1 | ✅ 预发布版本 |
17
+ | 6.12.0 | ✅ 最新正式版 |
17
18
  | < 6.0.0 | ❌ 已终止支持 |
18
19
  <!-- UDS_SUPPORTED_VERSIONS_END -->
19
20