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.
- package/bundled/ai/standards/open-work-tracking.ai.yaml +69 -4
- package/bundled/ai/standards/turn-completion-integrity.ai.yaml +14 -7
- package/bundled/core/open-work-tracking.md +111 -8
- package/bundled/core/turn-completion-integrity.md +58 -11
- package/bundled/hooks/check-turn-completion-agy.mjs +147 -0
- package/bundled/locales/zh-CN/CHANGELOG.md +29 -3
- package/bundled/locales/zh-CN/README.md +1 -1
- package/bundled/locales/zh-CN/SECURITY.md +2 -1
- package/bundled/locales/zh-CN/core/turn-completion-integrity.md +46 -13
- package/bundled/locales/zh-CN/docs/CHEATSHEET.md +4 -1
- package/bundled/locales/zh-CN/docs/CLI-INIT-OPTIONS.md +9 -8
- package/bundled/locales/zh-CN/docs/FEATURE-REFERENCE.md +9 -6
- package/bundled/locales/zh-TW/CHANGELOG.md +29 -3
- package/bundled/locales/zh-TW/README.md +1 -1
- package/bundled/locales/zh-TW/SECURITY.md +2 -1
- package/bundled/locales/zh-TW/core/open-work-tracking.md +88 -9
- package/bundled/locales/zh-TW/core/turn-completion-integrity.md +46 -13
- package/bundled/locales/zh-TW/docs/CHEATSHEET.md +4 -1
- package/bundled/locales/zh-TW/docs/CLI-INIT-OPTIONS.md +9 -8
- package/bundled/locales/zh-TW/docs/FEATURE-REFERENCE.md +9 -6
- package/package.json +1 -1
- package/src/commands/init.js +15 -2
- package/src/commands/uninstall.js +1 -1
- package/src/commands/update.js +23 -12
- package/src/installers/hooks-installer.js +89 -2
- package/src/reconciler/desired-state-calculator.js +84 -1
- package/src/reconciler/plan-executor.js +34 -1
- package/src/uninstallers/hook-uninstaller.js +107 -4
- package/src/utils/integration-generator.js +97 -19
- 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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
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
|
-
|
|
3844
|
-
|
|
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
|
-
|
|
3949
|
+
blockLines.push(`- \`${dir}/${filename}\` — ${filename.replace(suffix, '')}`);
|
|
3871
3950
|
}
|
|
3872
3951
|
} else {
|
|
3873
|
-
|
|
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
|
package/standards-registry.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"version": "6.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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"
|