universal-dev-standards 6.13.0 → 6.14.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 (30) hide show
  1. package/bundled/ai/standards/open-work-tracking.ai.yaml +69 -4
  2. package/bundled/ai/standards/turn-completion-integrity.ai.yaml +14 -7
  3. package/bundled/core/open-work-tracking.md +111 -8
  4. package/bundled/core/turn-completion-integrity.md +58 -11
  5. package/bundled/hooks/check-turn-completion-agy.mjs +147 -0
  6. package/bundled/locales/zh-CN/CHANGELOG.md +29 -3
  7. package/bundled/locales/zh-CN/README.md +1 -1
  8. package/bundled/locales/zh-CN/SECURITY.md +2 -1
  9. package/bundled/locales/zh-CN/core/turn-completion-integrity.md +46 -13
  10. package/bundled/locales/zh-CN/docs/CHEATSHEET.md +4 -1
  11. package/bundled/locales/zh-CN/docs/CLI-INIT-OPTIONS.md +9 -8
  12. package/bundled/locales/zh-CN/docs/FEATURE-REFERENCE.md +9 -6
  13. package/bundled/locales/zh-TW/CHANGELOG.md +29 -3
  14. package/bundled/locales/zh-TW/README.md +1 -1
  15. package/bundled/locales/zh-TW/SECURITY.md +2 -1
  16. package/bundled/locales/zh-TW/core/open-work-tracking.md +88 -9
  17. package/bundled/locales/zh-TW/core/turn-completion-integrity.md +46 -13
  18. package/bundled/locales/zh-TW/docs/CHEATSHEET.md +4 -1
  19. package/bundled/locales/zh-TW/docs/CLI-INIT-OPTIONS.md +9 -8
  20. package/bundled/locales/zh-TW/docs/FEATURE-REFERENCE.md +9 -6
  21. package/package.json +1 -1
  22. package/src/commands/init.js +15 -2
  23. package/src/commands/uninstall.js +1 -1
  24. package/src/commands/update.js +23 -12
  25. package/src/installers/hooks-installer.js +89 -2
  26. package/src/reconciler/desired-state-calculator.js +84 -1
  27. package/src/reconciler/plan-executor.js +34 -1
  28. package/src/uninstallers/hook-uninstaller.js +107 -4
  29. package/src/utils/integration-generator.js +97 -19
  30. package/standards-registry.json +8 -8
@@ -12,6 +12,7 @@ import { getAllStandards, getStandardSource, findOption, getOptionSource } from
12
12
  import {
13
13
  resolveToolKey,
14
14
  SUPPORTED_AI_TOOLS,
15
+ LEGACY_TOOL_MAPPINGS,
15
16
  MANIFEST_OPTION_BINDINGS,
16
17
  OPTIONS_INSTALL_DIR
17
18
  } from '../core/constants.js';
@@ -19,7 +20,9 @@ import {
19
20
  resolveIntegrationTargetFile,
20
21
  buildToolIntegrationConfig,
21
22
  generateIntegrationContent,
22
- extractMarkedContent
23
+ extractMarkedContent,
24
+ generateAgentsMdSummary,
25
+ buildAgentsMdSummaryConfig
23
26
  } from '../utils/integration-generator.js';
24
27
  import { PathResolver } from '../core/paths.js';
25
28
  import { computeFileHash } from '../utils/hasher.js';
@@ -299,6 +302,61 @@ function computeExpectedIntegrationBlockHash(manifest, toolName, format) {
299
302
  }
300
303
  }
301
304
 
305
+ /**
306
+ * The same block-hash computation as computeExpectedIntegrationBlockHash,
307
+ * for the OTHER AGENTS.md producer (the universal summary — see
308
+ * isAgentsMdGenuinelyToolOwned's docblock for why there are two).
309
+ *
310
+ * @param {Object} manifest
311
+ * @returns {string|null}
312
+ */
313
+ function computeExpectedAgentsMdSummaryHash(manifest) {
314
+ try {
315
+ const generated = generateAgentsMdSummary(buildAgentsMdSummaryConfig(manifest));
316
+ const { content: blockContent } = extractMarkedContent(generated, 'markdown');
317
+ if (!blockContent) return null;
318
+ return `sha256:${createHash('sha256').update(blockContent).digest('hex')}`;
319
+ } catch {
320
+ return null;
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Whether `toolName` is a tool the project actually selected, as opposed to
326
+ * one `resolveToolKey` happened to resolve an entry to because a filename
327
+ * collided.
328
+ *
329
+ * AGENTS.md is the one case in `SUPPORTED_AI_TOOLS` where this distinction
330
+ * matters: it is opencode's per-tool file, but it is ALSO the generic
331
+ * "universal AGENTS.md summary" target that `generateAgentsMd` writes for
332
+ * ANY project regardless of which AI tools are selected (`uds init` always
333
+ * pushes 'AGENTS.md' into `manifest.integrations` once that summary is
334
+ * written — see init.js's `generateUniversalAgentsMd` call site — purely as
335
+ * a "this file is UDS-managed" marker, not as a record of a tool choice).
336
+ * `resolveToolKey('AGENTS.md')` resolves to 'opencode' regardless, because
337
+ * that is simply the first (only) tool whose default file matches.
338
+ *
339
+ * Every other file in `SUPPORTED_AI_TOOLS` maps 1:1 to exactly one tool, so
340
+ * this check is only ever consulted for opencode/AGENTS.md; it is written
341
+ * generically in case that ever changes.
342
+ *
343
+ * Confirmed as a real bug, not a hypothetical: `uds init` (aiTools:
344
+ * ['claude-code'] only) followed immediately by `uds update --apply` — zero
345
+ * manifest edits, zero version change, nothing to reconcile — silently
346
+ * replaced AGENTS.md's content, because `calculateIntegrations` treated the
347
+ * bare tracking marker as evidence opencode was selected. Confirmed in
348
+ * production on two real adopters (asiaostrich-telemetry-server,
349
+ * EngramGraph) and dev-platform's own `.standards/` install, all three with
350
+ * `aiTools` containing no codex/opencode entry.
351
+ *
352
+ * @param {string} toolName - Tool key to check (e.g. 'opencode')
353
+ * @param {Object} manifest - Project manifest
354
+ * @returns {boolean}
355
+ */
356
+ function isToolGenuinelySelected(toolName, manifest) {
357
+ return (manifest.aiTools || []).some((t) => (LEGACY_TOOL_MAPPINGS[t] || t) === toolName);
358
+ }
359
+
302
360
  /**
303
361
  * Calculate expected integration files.
304
362
  * For integrations we track the UDS marker block, not the entire file.
@@ -325,6 +383,31 @@ function calculateIntegrations(state, manifest) {
325
383
  // detection) would treat that file as unmanaged and CLAUDE.md as desired.
326
384
  const relativePath = resolveIntegrationTargetFile(toolName, manifest) || toolConfig.file;
327
385
 
386
+ // See isToolGenuinelySelected's docblock. `relativePath === 'AGENTS.md'`
387
+ // rather than `toolName === 'opencode'`: correct either way today (only
388
+ // opencode's file is 'AGENTS.md'), but this is the actual thing that
389
+ // matters — which FILE is ambiguous — should another tool ever share it.
390
+ if (relativePath === 'AGENTS.md' && !isToolGenuinelySelected(toolName, manifest)) {
391
+ if (!manifest.generateAgentsMd) continue; // Not wanted at all; nothing to track.
392
+ state.integrations.set(relativePath, {
393
+ relativePath,
394
+ hash: computeExpectedAgentsMdSummaryHash(manifest),
395
+ size: null,
396
+ category: 'integration',
397
+ sourcePath: null,
398
+ metadata: {
399
+ toolName: null,
400
+ // Read by plan-executor.js's executeMigrateBlock to pick the
401
+ // matching writer (writeAgentsMdSummary, not writeIntegrationFile).
402
+ generator: 'summary',
403
+ format: 'markdown',
404
+ toolCategory: null,
405
+ supports: []
406
+ }
407
+ });
408
+ continue;
409
+ }
410
+
328
411
  state.integrations.set(relativePath, {
329
412
  relativePath,
330
413
  // XSPEC adopter-report Q6: this used to be a hardcoded `null` ("hashes
@@ -14,7 +14,12 @@
14
14
  import { existsSync, unlinkSync, mkdirSync, rmSync, copyFileSync, statSync } from 'fs';
15
15
  import { join, dirname } from 'path';
16
16
  import { copyStandard } from '../utils/copier.js';
17
- import { writeIntegrationFile, buildToolIntegrationConfig } from '../utils/integration-generator.js';
17
+ import {
18
+ writeIntegrationFile,
19
+ buildToolIntegrationConfig,
20
+ writeAgentsMdSummary,
21
+ buildAgentsMdSummaryConfig
22
+ } from '../utils/integration-generator.js';
18
23
  import {
19
24
  installSkillsToMultipleAgents,
20
25
  installCommandsToMultipleAgents
@@ -342,6 +347,34 @@ function executeDelete(projectPath, action, manifest) {
342
347
  * Uses writeIntegrationFile which handles marker-based section replacement.
343
348
  */
344
349
  function executeMigrateBlock(projectPath, action, manifest) {
350
+ // AGENTS.md tracked as the universal summary rather than a genuine
351
+ // per-tool integration — see desired-state-calculator.js's
352
+ // isToolGenuinelySelected docblock. Written through writeAgentsMdSummary,
353
+ // not writeIntegrationFile, so `--apply` produces the SAME content a plain
354
+ // `uds update` would for this case, instead of the opencode-shaped block a
355
+ // project with no codex/opencode selected never asked for.
356
+ // `diffIntegrations` (diff-engine.js) nests the desired-state metadata
357
+ // under `details.metadata`, not at the top level of `details` — only
358
+ // `toolName`/`format`/the hash fields are copied out. Checking
359
+ // `action.details?.generator` (rather than `action.details?.metadata?.generator`)
360
+ // is silently always false, and this branch would never fire; the
361
+ // fallback below then reads `action.details?.toolName` as `null` (correctly
362
+ // propagated from calculateIntegrations) and fails with "No tool name in
363
+ // action details" — reproduced live via `uds update --apply` E2E
364
+ // (tests/e2e/update-version-advances.test.js) before this fix.
365
+ if (action.details?.metadata?.generator === 'summary') {
366
+ const config = buildAgentsMdSummaryConfig(manifest);
367
+ const result = writeAgentsMdSummary(config, projectPath);
368
+ if (result.success) {
369
+ if (result.blockHashInfo) {
370
+ manifest.integrationBlockHashes[result.path] = result.blockHashInfo;
371
+ }
372
+ if (manifest.fileHashes) delete manifest.fileHashes[result.path];
373
+ return { action, success: true };
374
+ }
375
+ return { action, success: false, error: result.error };
376
+ }
377
+
345
378
  const toolName = action.details?.toolName;
346
379
  if (!toolName) {
347
380
  return { action, success: false, error: 'No tool name in action details' };
@@ -2,7 +2,7 @@ import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
2
2
  import { join, basename } from 'path';
3
3
  import {
4
4
  collectHookConfigs, standardsSourceDir, hooksSourceDir,
5
- CODEX_HOOK_SCRIPT, GEMINI_HOOK_SCRIPT,
5
+ CODEX_HOOK_SCRIPT, GEMINI_HOOK_SCRIPT, AGY_HOOK_SCRIPT,
6
6
  } from '../installers/hooks-installer.js';
7
7
 
8
8
  /**
@@ -17,7 +17,7 @@ import {
17
17
  * test placing a user hook at `scripts/hooks/my-own-hook.mjs` lost it).
18
18
  * The safe signature is path *and* a script basename UDS is actually known to
19
19
  * ship right now — the same set collectHookConfigs() derives for install,
20
- * plus the two fixed Codex/Gemini script names.
20
+ * plus the three fixed Codex/Gemini/agy script names.
21
21
  *
22
22
  * 🔴 Until this file added the three functions below, uninstall only ever
23
23
  * touched .husky/pre-commit and .git/hooks/pre-commit — the settings.json /
@@ -37,7 +37,7 @@ function commandOf(entry) {
37
37
  /** Script basenames UDS currently ships and would install a hook entry for. */
38
38
  function knownUdsHookScripts() {
39
39
  const { scripts } = collectHookConfigs(standardsSourceDir(), hooksSourceDir());
40
- return new Set([...scripts.map((s) => basename(s)), CODEX_HOOK_SCRIPT, GEMINI_HOOK_SCRIPT]);
40
+ return new Set([...scripts.map((s) => basename(s)), CODEX_HOOK_SCRIPT, GEMINI_HOOK_SCRIPT, AGY_HOOK_SCRIPT]);
41
41
  }
42
42
 
43
43
  function isUdsHookEntry(entry, knownScripts) {
@@ -159,10 +159,112 @@ export function uninstallGeminiHooks(projectPath, options = {}) {
159
159
  });
160
160
  }
161
161
 
162
+ /**
163
+ * A bare `{ type, command }` handler (agy's shape — no `hooks[]` wrapper) that
164
+ * runs one of the scripts UDS ships.
165
+ */
166
+ function isUdsAgyHandler(handler, knownScripts) {
167
+ const cmd = handler && typeof handler.command === 'string' ? handler.command : undefined;
168
+ // agy's hook cwd is `.agents/`, so the installed command climbs out first
169
+ // (`node ../scripts/hooks/...`); the earlier `node scripts/hooks/...` form
170
+ // is still recognised so an install made before that fix can be removed.
171
+ return typeof cmd === 'string' && /^node (?:\.\.\/)?scripts\/hooks\//.test(cmd) && knownScripts.has(basename(cmd));
172
+ }
173
+
174
+ /**
175
+ * Remove UDS's Stop handler from .agents/hooks.json (installAgyHooks()'s output).
176
+ *
177
+ * agy's file is `{ "<hook name>": { "<Event>": handler[] } }`, not the
178
+ * `{ hooks: { <Event>: entry[] } }` the other three share, so the generic
179
+ * stripper cannot be used. Only handlers that run a UDS-shipped script are
180
+ * removed, and from ANY hook name (an adopter may have renamed our key); a
181
+ * user's own handler, even inside the same name or the same event array, stays.
182
+ * A name left with no event after removal is dropped, and a file left empty is
183
+ * deleted.
184
+ */
185
+ export function uninstallAgyHooks(projectPath, options = {}) {
186
+ const configPath = join(projectPath, '.agents', 'hooks.json');
187
+ const label = '.agents/hooks.json';
188
+ const dryRun = options.dryRun || false;
189
+ const result = { removed: [], skipped: [], errors: [] };
190
+ if (!existsSync(configPath)) return result;
191
+
192
+ let config;
193
+ try {
194
+ config = JSON.parse(readFileSync(configPath, 'utf-8'));
195
+ } catch (error) {
196
+ result.errors.push(`${label} — could not parse (${error.message}); left untouched`);
197
+ return result;
198
+ }
199
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
200
+ result.skipped.push(`${label} (no UDS hook entries found)`);
201
+ return result;
202
+ }
203
+
204
+ const knownScripts = knownUdsHookScripts();
205
+ let removedCount = 0;
206
+ const next = {};
207
+ for (const [name, def] of Object.entries(config)) {
208
+ if (!def || typeof def !== 'object' || Array.isArray(def)) {
209
+ next[name] = def;
210
+ continue;
211
+ }
212
+ const nextDef = {};
213
+ let touched = false;
214
+ let eventsLeft = 0;
215
+ for (const [key, value] of Object.entries(def)) {
216
+ if (!Array.isArray(value)) {
217
+ nextDef[key] = value; // e.g. `enabled`
218
+ continue;
219
+ }
220
+ const kept = value.filter((h) => {
221
+ if (isUdsAgyHandler(h, knownScripts)) {
222
+ removedCount += 1;
223
+ touched = true;
224
+ return false;
225
+ }
226
+ return true;
227
+ });
228
+ if (kept.length > 0) {
229
+ nextDef[key] = kept;
230
+ eventsLeft += 1;
231
+ }
232
+ }
233
+ // A name we emptied is dropped (nothing but `enabled` would remain);
234
+ // a name we did not touch is kept exactly as found.
235
+ if (touched && eventsLeft === 0) continue;
236
+ next[name] = touched ? nextDef : def;
237
+ }
238
+
239
+ if (removedCount === 0) {
240
+ result.skipped.push(`${label} (no UDS hook entries found)`);
241
+ return result;
242
+ }
243
+
244
+ const entryLabel = `${label} (${removedCount} UDS hook ${removedCount === 1 ? 'entry' : 'entries'})`;
245
+ if (dryRun) {
246
+ result.removed.push(entryLabel);
247
+ return result;
248
+ }
249
+ try {
250
+ if (Object.keys(next).length === 0) {
251
+ unlinkSync(configPath);
252
+ result.removed.push(`${entryLabel}, file removed — created by UDS, now empty`);
253
+ } else {
254
+ writeFileSync(configPath, JSON.stringify(next, null, 2) + '\n');
255
+ result.removed.push(entryLabel);
256
+ }
257
+ } catch (error) {
258
+ result.errors.push(`${label} — ${error.message}`);
259
+ }
260
+ return result;
261
+ }
262
+
162
263
  /**
163
264
  * Remove UDS-related lines from .husky/pre-commit, the native
164
265
  * .git/hooks/pre-commit fallback, and the enforcement-hook entries UDS wrote
165
- * into .claude/settings.json, .codex/hooks.json and .gemini/settings.json.
266
+ * into .claude/settings.json, .codex/hooks.json, .gemini/settings.json and
267
+ * .agents/hooks.json.
166
268
  * @param {string} projectPath - Project root path
167
269
  * @param {Object} options - { dryRun: boolean }
168
270
  * @returns {Object} { removed: string[], skipped: string[], errors: string[] }
@@ -231,6 +333,7 @@ export function uninstallHook(projectPath, options = {}) {
231
333
  uninstallClaudeCodeHooks(projectPath, { dryRun }),
232
334
  uninstallCodexHooks(projectPath, { dryRun }),
233
335
  uninstallGeminiHooks(projectPath, { dryRun }),
336
+ uninstallAgyHooks(projectPath, { dryRun }),
234
337
  ]) {
235
338
  result.removed.push(...sub.removed);
236
339
  result.skipped.push(...sub.skipped);
@@ -3462,21 +3462,41 @@ function withSelectedOptions(manifest) {
3462
3462
  return [...standards, ...extra];
3463
3463
  }
3464
3464
 
3465
- export function buildToolIntegrationConfig(manifest, tool) {
3465
+ /**
3466
+ * Resolve the content (display) language an integration block should be
3467
+ * written in, from a project's manifest.
3468
+ *
3469
+ * Content language comes from `display_language` — the setting whose entire
3470
+ * job is "what language do you want to read" — and only falls back to
3471
+ * `output_language`, which is the commit-message language, when the project
3472
+ * has no display setting to go on. They are different questions: `uds init`
3473
+ * has always used the first, and this was the single place that used the
3474
+ * second correctly (XSPEC-343 R2 / a Windows-adopter fix on 2026-09-16).
3475
+ *
3476
+ * `update.js`'s two other, older per-tool config builders (the plain
3477
+ * `uds update` main flow and its `--plan` dry-run) each derived `language`
3478
+ * independently and never picked up that fix — they read only
3479
+ * `output_language`/`commit_language`, so a project installed with
3480
+ * `display_language: zh-tw` and `output_language: bilingual` got the correct
3481
+ * Chinese heading from `init`/the reconciler and an English one the next
3482
+ * plain `uds update`, silently overwriting the block that was already there.
3483
+ * Both call sites now use this function instead of re-deriving the value.
3484
+ *
3485
+ * @param {Object} manifest - Project manifest
3486
+ * @returns {'en'|'zh-tw'|'bilingual'} Resolved content language
3487
+ */
3488
+ export function resolveIntegrationLanguage(manifest) {
3466
3489
  const selected = manifest.options?.output_language || manifest.options?.commit_language || 'english';
3467
-
3468
- // Content language comes from `display_language` — the setting whose entire
3469
- // job is "what language do you want to read" — and only falls back to
3470
- // `output_language`, which is the commit-message language, when the project
3471
- // has no display setting to go on. They are different questions: `uds init`
3472
- // has always used the first, every regeneration path used the second, and a
3473
- // project installed with `--locale zh-tw` therefore got Chinese instructions
3474
- // from `init` and English ones from the next `uds update`, silently.
3475
3490
  const display = manifest.options?.display_language;
3476
3491
  const fromOutput = selected === 'bilingual'
3477
3492
  ? 'bilingual'
3478
3493
  : selected === 'traditional-chinese' ? 'zh-tw' : 'en';
3479
- const language = ['en', 'zh-tw', 'bilingual'].includes(display) ? display : fromOutput;
3494
+ return ['en', 'zh-tw', 'bilingual'].includes(display) ? display : fromOutput;
3495
+ }
3496
+
3497
+ export function buildToolIntegrationConfig(manifest, tool) {
3498
+ const language = resolveIntegrationLanguage(manifest);
3499
+ const selected = manifest.options?.output_language || manifest.options?.commit_language || 'english';
3480
3500
  const resolved = resolveContentModeForTool(tool, manifest.contentMode || 'auto');
3481
3501
 
3482
3502
  return {
@@ -3502,6 +3522,33 @@ export function buildToolIntegrationConfig(manifest, tool) {
3502
3522
  };
3503
3523
  }
3504
3524
 
3525
+ /**
3526
+ * Build the generation config for the universal AGENTS.md summary
3527
+ * (`generateAgentsMdSummary`/`writeAgentsMdSummary`) from the manifest.
3528
+ *
3529
+ * This is the OTHER AGENTS.md producer — used when no genuinely-selected AI
3530
+ * tool (codex/opencode) claims AGENTS.md as its own file, i.e. the file is
3531
+ * present in `manifest.integrations` only because `uds init` tracks it there
3532
+ * once `generateAgentsMd` writes one, not because a tool was chosen. Mirrors
3533
+ * `commands/update.js`'s own construction of this same config exactly, so
3534
+ * the reconciler (`--apply`) and a plain `uds update` compute and write
3535
+ * IDENTICAL content for this case — see resolveIntegrationLanguage's and
3536
+ * this whole area's history for what happens when "the same config" is
3537
+ * derived independently in two places instead of shared.
3538
+ *
3539
+ * @param {Object} manifest - Project manifest
3540
+ * @returns {Object} Config for generateAgentsMdSummary/writeAgentsMdSummary
3541
+ */
3542
+ export function buildAgentsMdSummaryConfig(manifest) {
3543
+ return {
3544
+ installedStandards: manifest.standards || [],
3545
+ standardsFormat: manifest.format || 'ai',
3546
+ language: manifest.options?.display_language || 'en',
3547
+ outputLanguage: manifest.options?.output_language || manifest.options?.commit_language || 'english',
3548
+ standardOptions: manifest.options || {}
3549
+ };
3550
+ }
3551
+
3505
3552
  /**
3506
3553
  * Heading of the index-mode standards block. Not localised — both the English and
3507
3554
  * the Chinese body sit under this exact string, so it is a stable marker.
@@ -3835,13 +3882,45 @@ export function generateAgentsMdSummary(config = {}) {
3835
3882
  lines.push('');
3836
3883
 
3837
3884
  // Installed Standards Index
3838
- lines.push('<!-- UDS:STANDARDS:START -->');
3839
- lines.push('## Installed Standards');
3840
- lines.push('');
3885
+ //
3886
+ // Built as a separate block body and wrapped with wrapWithMarkers (the
3887
+ // same helper generateIntegrationContent's own block uses), rather than
3888
+ // hand-rolling the `<!-- UDS:STANDARDS:START/END -->` lines directly.
3889
+ // Hand-rolled, this block never carried the
3890
+ // `<!-- WARNING: This block is managed by UDS ... -->` line that
3891
+ // wrapWithMarkers adds — so a hash computed straight from this function's
3892
+ // output (desired-state-calculator.js's computeExpectedAgentsMdSummaryHash)
3893
+ // never matched a real on-disk file's hash (computeIntegrationBlockHash,
3894
+ // hasher.js — which always includes that line, since a written file only
3895
+ // ever gets it added by the merge path's own wrapWithMarkers call), and
3896
+ // `uds update --apply` reported "content differs" and re-wrote AGENTS.md
3897
+ // on EVERY run forever, settled state or not — a non-convergent
3898
+ // reconciliation loop, reproduced with a second `--apply` immediately
3899
+ // after a first that had already "fixed" everything.
3900
+ const blockLines = [];
3901
+ // XSPEC-357 R7's disclosure was added to this function's HEADER above
3902
+ // (2026-08-18) but never to the marker block itself. `writeAgentsMdSummary`'s
3903
+ // marker-based update for an EXISTING file only ever replaces the content
3904
+ // BETWEEN the markers — the header (including that reminder) is copied
3905
+ // verbatim from whatever the file already had. Every adopter whose
3906
+ // AGENTS.md predates 2026-08-18 therefore never received the reminder via
3907
+ // the header (nothing ever regenerates it) and never received it in the
3908
+ // block either, since it was never written there. Measured 2026-09-28
3909
+ // against two real adopters (asiaostrich-telemetry-server, EngramGraph):
3910
+ // reminder present at neither the file level nor the block level, 0 of 2.
3911
+ // Placed here, inside the block, it reaches every project on every
3912
+ // `uds update`/`--apply`, new or existing, regardless of the header's age.
3913
+ // Uses the SAME wording as the per-tool generator's index/minimal
3914
+ // templates (generateIndexDisclosure) rather than inventing a third
3915
+ // phrasing for a third producer of the same idea.
3916
+ blockLines.push(generateIndexDisclosure('markdown', language));
3917
+ blockLines.push('');
3918
+ blockLines.push('## Installed Standards');
3919
+ blockLines.push('');
3841
3920
 
3842
3921
  if (installedStandards.length > 0) {
3843
- lines.push('All standards are in `.standards/`. Installed standards:');
3844
- lines.push('');
3922
+ blockLines.push('All standards are in `.standards/`. Installed standards:');
3923
+ blockLines.push('');
3845
3924
 
3846
3925
  // Resolved before filtering, not after. The filter asks for an `.ai.yaml`
3847
3926
  // suffix and a manifest's core entries are registry IDs, which carry no
@@ -3867,14 +3946,13 @@ export function generateAgentsMdSummary(config = {}) {
3867
3946
  if (!filename?.endsWith(suffix)) continue;
3868
3947
  const isOption = entry.includes('/options/') || entry.includes('\\options\\');
3869
3948
  const dir = isOption ? '.standards/options' : '.standards';
3870
- lines.push(`- \`${dir}/${filename}\` — ${filename.replace(suffix, '')}`);
3949
+ blockLines.push(`- \`${dir}/${filename}\` — ${filename.replace(suffix, '')}`);
3871
3950
  }
3872
3951
  } else {
3873
- lines.push('No standards installed yet. Run `npx uds init` to install.');
3952
+ blockLines.push('No standards installed yet. Run `npx uds init` to install.');
3874
3953
  }
3875
3954
 
3876
- lines.push('');
3877
- lines.push('<!-- UDS:STANDARDS:END -->');
3955
+ lines.push(wrapWithMarkers(blockLines.join('\n'), 'markdown'));
3878
3956
  lines.push('');
3879
3957
 
3880
3958
  // Boundaries
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "version": "6.13.0",
3
+ "version": "6.14.0-beta.1",
4
4
  "lastUpdated": "2026-05-13",
5
5
  "description": "Standards registry for universal-dev-standards with integrated skills and AI-optimized formats",
6
6
  "formats": {
@@ -58,14 +58,14 @@
58
58
  "standards": {
59
59
  "name": "universal-dev-standards",
60
60
  "url": "https://github.com/AsiaOstrich/universal-dev-standards",
61
- "version": "6.13.0"
61
+ "version": "6.14.0-beta.1"
62
62
  },
63
63
  "skills": {
64
64
  "name": "universal-dev-standards",
65
65
  "url": "https://github.com/AsiaOstrich/universal-dev-standards",
66
66
  "localPath": "skills",
67
67
  "rawUrl": "https://raw.githubusercontent.com/AsiaOstrich/universal-dev-standards/main/skills",
68
- "version": "6.13.0",
68
+ "version": "6.14.0-beta.1",
69
69
  "note": "Skills are now included in the main repository under skills/"
70
70
  }
71
71
  },
@@ -1799,7 +1799,7 @@
1799
1799
  },
1800
1800
  "category": "reference",
1801
1801
  "skillName": null,
1802
- "description": "Downstream of deferred-item-exit: once an item has an exit, its carrier must stay trustworthy. Capture costs no more than two fields; a waiting item records its release condition; derivable fields are generated, not hand-written; a coverage figure states what it could not see; a checkpoint at turn end reports open work but never blocks"
1802
+ "description": "Downstream of deferred-item-exit: once an item has an exit, its carrier must stay trustworthy. Capture costs no more than two fields; a waiting item records its release condition; derivable fields are generated, not hand-written; a coverage figure states what it could not see; a checkpoint at turn end reports open work but never blocks; goal and progress live in different carriers, an edit to the goal leaves a record, and a next action names a file, test, command or requirement"
1803
1803
  },
1804
1804
  {
1805
1805
  "id": "verification-evidence",
@@ -2295,7 +2295,7 @@
2295
2295
  "id": "license-compliance",
2296
2296
  "name": "License Compliance Standards",
2297
2297
  "nameZh": "授權合規標準",
2298
- "version": "6.13.0",
2298
+ "version": "6.14.0-beta.1",
2299
2299
  "source": {
2300
2300
  "human": "core/license-compliance.md",
2301
2301
  "ai": "ai/standards/license-compliance.ai.yaml"
@@ -2307,7 +2307,7 @@
2307
2307
  "id": "verification-oracle",
2308
2308
  "name": "Verification Oracle Standards",
2309
2309
  "nameZh": "驗證 Oracle 標準",
2310
- "version": "6.13.0",
2310
+ "version": "6.14.0-beta.1",
2311
2311
  "source": {
2312
2312
  "human": "core/verification-oracle.md",
2313
2313
  "ai": "ai/standards/verification-oracle.ai.yaml"
@@ -2319,7 +2319,7 @@
2319
2319
  "id": "model-provenance",
2320
2320
  "name": "Model Provenance Policy Standards",
2321
2321
  "nameZh": "模型來源政策標準",
2322
- "version": "6.13.0",
2322
+ "version": "6.14.0-beta.1",
2323
2323
  "source": {
2324
2324
  "human": "core/model-provenance.md",
2325
2325
  "ai": "ai/standards/model-provenance.ai.yaml"
@@ -2331,7 +2331,7 @@
2331
2331
  "id": "resource-cost-boundary",
2332
2332
  "name": "Resource / Cost Boundary Declaration Standards",
2333
2333
  "nameZh": "資源/成本邊界宣告標準",
2334
- "version": "6.13.0",
2334
+ "version": "6.14.0-beta.1",
2335
2335
  "source": {
2336
2336
  "human": "core/resource-cost-boundary.md",
2337
2337
  "ai": "ai/standards/resource-cost-boundary.ai.yaml"