universal-dev-standards 6.14.0-beta.2 → 6.14.0-beta.3

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.
@@ -875,7 +875,7 @@ export const messages = {
875
875
  // Pre-commit hook wiring (XSPEC: uds init writes a hook but never
876
876
  // confirmed git would run it — this reports the gap, read-only)
877
877
  hookNotWiredTitle: '⚠ [pre-commit] {file} was installed, but git will not run it.',
878
- hookNotWiredOverride: ' git core.hooksPath is set to "{path}" — UDS will not override it. Confirm the hook there also runs `npx uds check`, or switch it yourself: git config --local core.hooksPath .husky',
878
+ hookNotWiredOverride: ' git core.hooksPath is set to "{path}" — UDS will not override it. Confirm the hook there also runs `universal-dev-standards check` (the installed CLI — not the short name "uds", which on the npm registry is an unrelated package), or switch it yourself: git config --local core.hooksPath .husky',
879
879
  hookNotWiredUnwired: ' git core.hooksPath is not set, and {file} is not on git\'s default hook path.',
880
880
  hookNotWiredFix: ' Fix (per-clone — not committed, teammates must repeat it): git config --local core.hooksPath .husky',
881
881
  hookNotWiredFixNative: ' Fix: point core.hooksPath back to the default (git config --local --unset core.hooksPath), or add `uds check` under "{path}" instead.',
@@ -892,6 +892,8 @@ export const messages = {
892
892
  // file or directory". Measured 2026-09-27 in CI (windows-latest).
893
893
  hookMissingShebangTitle: '⚠ [pre-commit] {file} has no shebang line — git cannot run it on Windows.',
894
894
  hookMissingShebangFix: ' Fix: add a `#!/bin/sh` shebang as the very first line of {file} (a husky-managed hook gets this automatically the next time `uds init` touches it), or re-run `uds init`.',
895
+ hookBareUdsTitle: '⚠ [pre-commit] {file} asks npm to run the bare name "uds", which on the npm registry is an unrelated package (not this project).',
896
+ hookBareUdsFix: ' Fix: run `uds update` — it replaces the line UDS itself wrote with one that only runs the installed UDS CLI. A line you wrote or edited yourself is left alone; change it to `universal-dev-standards check`.',
895
897
  // Summary mode (--summary)
896
898
  summary_mode: {
897
899
  title: 'UDS Status Summary',
@@ -1038,6 +1040,11 @@ export const messages = {
1038
1040
  currentVersion: 'Current version',
1039
1041
  latestVersion: 'Latest version',
1040
1042
  upToDate: '✓ Standards are up to date.',
1043
+ // Pre-commit hook migration (bare-name `uds` on the npm registry is not this project)
1044
+ hookMigrated: '✓ .husky/pre-commit: replaced the line an older UDS wrote (it asked npm to resolve the bare name "uds", which is not this project) with one that runs only the installed UDS CLI.',
1045
+ hookWouldMigrate: '+ .husky/pre-commit: would replace the line an older UDS wrote (it asks npm to resolve the bare name "uds", which is not this project) with one that runs only the installed UDS CLI.',
1046
+ hookLegacyKept: '⚠ .husky/pre-commit line {line} ("{text}") asks npm to resolve the bare name "uds", which on the npm registry is an unrelated package. UDS did not write this exact line, so it was left alone. Change it to "universal-dev-standards check" — it needs the UDS CLI installed in this project or on PATH.',
1047
+ hookMigrateFailed: '⚠ Could not rewrite .husky/pre-commit: {error}. The line that asks npm for the bare name "uds" is still there — change it to "universal-dev-standards check".',
1041
1048
  newerVersion: '(You have a newer version than the registry: {version})',
1042
1049
  updateAvailable: 'Update available: {current} → {latest}',
1043
1050
  // Files
@@ -1264,7 +1271,11 @@ export const messages = {
1264
1271
  uninstallPartial: '⚠ Uninstall completed with errors.',
1265
1272
  removed: 'Removed',
1266
1273
  skippedLabel: 'Skipped',
1267
- errorsLabel: 'Errors'
1274
+ errorsLabel: 'Errors',
1275
+ cannotPrompt: 'Cannot ask for confirmation: nothing is attached to answer the prompt (non-interactive shell, CI, or a pipe).',
1276
+ cannotPromptHint: 'Nothing has been changed. Re-run with --yes to confirm up front, or with --dry-run to preview the changes.',
1277
+ promptClosed: 'The prompt was closed before an answer was given. Nothing has been changed.',
1278
+ promptClosedMidRun: 'A prompt was closed part-way through. Steps already completed stay done; run `uds uninstall` again to finish.'
1268
1279
  }
1269
1280
  }
1270
1281
  },
@@ -2135,7 +2146,7 @@ export const messages = {
2135
2146
  missingSkillsHint: '提示:執行 `uds update` 安裝缺少的 Skills/斜線命令',
2136
2147
  // 提交前檢查是否真的會被 git 執行(uds init 寫了檢查檔,卻沒確認 git 會跑它——這裡只回報,不寫入)
2137
2148
  hookNotWiredTitle: '⚠ [pre-commit] {file} 已安裝,但 git 實際不會執行它。',
2138
- hookNotWiredOverride: ' git core.hooksPath 已設定為「{path}」——UDS 不會覆蓋它。請確認該路徑下的檔案也會執行 `npx uds check`,或自行改用:git config --local core.hooksPath .husky',
2149
+ hookNotWiredOverride: ' git core.hooksPath 已設定為「{path}」——UDS 不會覆蓋它。請確認該路徑下的檔案也會執行 `universal-dev-standards check`(要用已安裝的 CLI——不要用短名稱 uds,它在 npm registry 上是不相干的套件),或自行改用:git config --local core.hooksPath .husky',
2139
2150
  hookNotWiredUnwired: ' git core.hooksPath 未設定,而 {file} 也不在 git 預設會讀取的路徑上。',
2140
2151
  hookNotWiredFix: ' 修復方式(僅對此 clone 生效,不會進版控,其他人 clone 後要自己再跑一次):git config --local core.hooksPath .husky',
2141
2152
  hookNotWiredFixNative: ' 修復方式:把 core.hooksPath 改回預設(git config --local --unset core.hooksPath),或改在「{path}」底下也加上 `uds check`。',
@@ -2151,6 +2162,8 @@ export const messages = {
2151
2162
  // 2026-09-27 於 CI(windows-latest)實測。
2152
2163
  hookMissingShebangTitle: '⚠ [pre-commit] {file} 沒有 shebang 行——在 Windows 上 git 無法執行它。',
2153
2164
  hookMissingShebangFix: ' 修復方式:在 {file} 的第一行加上 shebang `#!/bin/sh`(由 husky 管理的 hook,下次 `uds init` 動到它時會自動處理),或重新執行 `uds init`。',
2165
+ hookBareUdsTitle: '⚠ [pre-commit] {file} 會請 npm 執行裸名稱 uds,而它在 npm registry 上是不相干的套件(不是本專案)。',
2166
+ hookBareUdsFix: ' 修復方式:執行 `uds update`——它會把 UDS 自己寫入的那一行換成只執行已安裝 UDS CLI 的寫法。你自己寫或改過的那一行不會被動,請自行改成 `universal-dev-standards check`。',
2154
2167
  // Summary mode (--summary)
2155
2168
  summary_mode: {
2156
2169
  title: 'UDS 狀態摘要',
@@ -2294,6 +2307,11 @@ export const messages = {
2294
2307
  currentVersion: '目前版本',
2295
2308
  latestVersion: '最新版本',
2296
2309
  upToDate: '✓ 標準已是最新版本。',
2310
+ // pre-commit hook 遷移(npm registry 上的裸名稱 uds 不是本專案)
2311
+ hookMigrated: '✓ .husky/pre-commit:已把舊版 UDS 寫入的那一行(它會請 npm 解析裸名稱 uds,而那不是本專案)換成只執行已安裝 UDS CLI 的寫法。',
2312
+ hookWouldMigrate: '+ .husky/pre-commit:將把舊版 UDS 寫入的那一行(它會請 npm 解析裸名稱 uds,而那不是本專案)換成只執行已安裝 UDS CLI 的寫法。',
2313
+ hookLegacyKept: '⚠ .husky/pre-commit 第 {line} 行(「{text}」)會請 npm 解析裸名稱 uds,而它在 npm registry 上是不相干的套件。這一行不是 UDS 寫入的原樣,所以沒有動它。請自行改成 universal-dev-standards check——需要在本專案或 PATH 上已安裝 UDS CLI。',
2314
+ hookMigrateFailed: '⚠ 無法改寫 .husky/pre-commit:{error}。請 npm 解析裸名稱 uds 的那一行仍在,請自行改成 universal-dev-standards check。',
2297
2315
  newerVersion: '(你的版本比登錄庫更新:{version})',
2298
2316
  updateAvailable: '有可用更新:{current} → {latest}',
2299
2317
  // Files
@@ -2520,7 +2538,11 @@ export const messages = {
2520
2538
  uninstallPartial: '⚠ 解除安裝完成但有錯誤。',
2521
2539
  removed: '已移除',
2522
2540
  skippedLabel: '已跳過',
2523
- errorsLabel: '錯誤'
2541
+ errorsLabel: '錯誤',
2542
+ cannotPrompt: '無法詢問確認:目前沒有可回答提示的終端機(非互動 shell、CI 或管線)。',
2543
+ cannotPromptHint: '尚未變更任何檔案。請加上 --yes 事先確認,或加上 --dry-run 預覽變更。',
2544
+ promptClosed: '提示在回答前被關閉。尚未變更任何檔案。',
2545
+ promptClosedMidRun: '執行途中提示被關閉。已完成的步驟維持完成;請再執行一次 `uds uninstall` 完成其餘部分。'
2524
2546
  }
2525
2547
  }
2526
2548
  },
@@ -3407,7 +3429,7 @@ export const messages = {
3407
3429
  missingSkillsHint: '提示:执行 `uds update` 安装缺少的 Skills/斜线命令',
3408
3430
  // 提交前检查是否真的会被 git 执行(uds init 写了检查文件,却没确认 git 会跑它——这里只回报,不写入)
3409
3431
  hookNotWiredTitle: '⚠ [pre-commit] {file} 已安装,但 git 实际不会执行它。',
3410
- hookNotWiredOverride: ' git core.hooksPath 已设定为“{path}”——UDS 不会覆盖它。请确认该路径下的文件也会执行 `npx uds check`,或自行改用:git config --local core.hooksPath .husky',
3432
+ hookNotWiredOverride: ' git core.hooksPath 已设定为“{path}”——UDS 不会覆盖它。请确认该路径下的文件也会执行 `universal-dev-standards check`(要用已安装的 CLI——不要用短名称 uds,它在 npm registry 上是不相干的包),或自行改用:git config --local core.hooksPath .husky',
3411
3433
  hookNotWiredUnwired: ' git core.hooksPath 未设定,而 {file} 也不在 git 默认会读取的路径上。',
3412
3434
  hookNotWiredFix: ' 修复方式(仅对此 clone 生效,不会进版控,其他人 clone 后要自己再跑一次):git config --local core.hooksPath .husky',
3413
3435
  hookNotWiredFixNative: ' 修复方式:把 core.hooksPath 改回默认(git config --local --unset core.hooksPath),或改在“{path}”下也加上 `uds check`。',
@@ -3419,6 +3441,8 @@ export const messages = {
3419
3441
  // 2026-09-27 于 CI(windows-latest)实测。
3420
3442
  hookMissingShebangTitle: '⚠ [pre-commit] {file} 没有 shebang 行——在 Windows 上 git 无法执行它。',
3421
3443
  hookMissingShebangFix: ' 修复方式:在 {file} 的第一行加上 shebang `#!/bin/sh`(由 husky 管理的 hook,下次 `uds init` 动到它时会自动处理),或重新运行 `uds init`。',
3444
+ hookBareUdsTitle: '⚠ [pre-commit] {file} 会请 npm 执行裸名称 uds,而它在 npm registry 上是不相干的包(不是本项目)。',
3445
+ hookBareUdsFix: ' 修复方式:运行 `uds update`——它会把 UDS 自己写入的那一行换成只执行已安装 UDS CLI 的写法。你自己写或改过的那一行不会被动,请自行改成 `universal-dev-standards check`。',
3422
3446
  // Summary mode (--summary)
3423
3447
  summary_mode: {
3424
3448
  title: 'UDS 状态摘要',
@@ -3474,6 +3498,11 @@ export const messages = {
3474
3498
  currentVersion: '当前版本',
3475
3499
  latestVersion: '最新版本',
3476
3500
  upToDate: '✓ 标准是最新的。',
3501
+ // pre-commit hook 迁移(npm registry 上的裸名称 uds 不是本项目)
3502
+ hookMigrated: '✓ .husky/pre-commit:已把旧版 UDS 写入的那一行(它会请 npm 解析裸名称 uds,而那不是本项目)换成只执行已安装 UDS CLI 的写法。',
3503
+ hookWouldMigrate: '+ .husky/pre-commit:将把旧版 UDS 写入的那一行(它会请 npm 解析裸名称 uds,而那不是本项目)换成只执行已安装 UDS CLI 的写法。',
3504
+ hookLegacyKept: '⚠ .husky/pre-commit 第 {line} 行(“{text}”)会请 npm 解析裸名称 uds,而它在 npm registry 上是不相干的包。这一行不是 UDS 写入的原样,所以没有动它。请自行改成 universal-dev-standards check——需要在本项目或 PATH 上已安装 UDS CLI。',
3505
+ hookMigrateFailed: '⚠ 无法改写 .husky/pre-commit:{error}。请 npm 解析裸名称 uds 的那一行仍在,请自行改成 universal-dev-standards check。',
3477
3506
  newerVersion: '(您有比注册表更新的版本:{version})',
3478
3507
  updateAvailable: '有可用更新:{current} → {latest}',
3479
3508
  // Files
@@ -3700,7 +3729,11 @@ export const messages = {
3700
3729
  uninstallPartial: '⚠ 卸载完成但有错误。',
3701
3730
  removed: '已移除',
3702
3731
  skippedLabel: '已跳过',
3703
- errorsLabel: '错误'
3732
+ errorsLabel: '错误',
3733
+ cannotPrompt: '无法询问确认:当前没有可回答提示的终端(非交互 shell、CI 或管道)。',
3734
+ cannotPromptHint: '尚未更改任何文件。请加上 --yes 提前确认,或加上 --dry-run 预览变更。',
3735
+ promptClosed: '提示在回答前被关闭。尚未更改任何文件。',
3736
+ promptClosedMidRun: '执行途中提示被关闭。已完成的步骤保持完成;请再运行一次 `uds uninstall` 完成其余部分。'
3704
3737
  }
3705
3738
  }
3706
3739
  }
@@ -31,12 +31,13 @@
31
31
  * @see core/turn-completion-integrity.md
32
32
  */
33
33
 
34
- import { existsSync, readFileSync, writeFileSync, mkdirSync, cpSync, readdirSync } from 'fs';
35
- import { join, dirname, basename } from 'path';
34
+ import { existsSync, readFileSync, writeFileSync, copyFileSync, readdirSync } from 'fs';
35
+ import { join, dirname, basename, relative } from 'path';
36
36
  import { fileURLToPath } from 'url';
37
37
  import { execFileSync } from 'child_process';
38
38
  import { load as parseYaml } from 'js-yaml';
39
39
  import { detectAITools } from '../utils/detector.js';
40
+ import { newRecorder, mkdirTracked, recordFile, RECORD_KINDS } from '../core/install-records.js';
40
41
 
41
42
  const __dirname = dirname(fileURLToPath(import.meta.url));
42
43
 
@@ -172,7 +173,41 @@ function probeLanguageLimits(hooksDir, scripts) {
172
173
  return out;
173
174
  }
174
175
 
175
- export function installHooks(projectPath, { overwriteScripts = true } = {}) {
176
+ /**
177
+ * Copy the shipped hook scripts (recursively — a hook may ship a directory beside
178
+ * it: locale packs, fixtures) into `<project>/scripts/hooks`, and RECORD every
179
+ * file this call actually wrote and every directory it had to create.
180
+ *
181
+ * `cpSync` did the copy before and returns nothing, which is why `uds uninstall`
182
+ * had no way to know which of the files under `scripts/hooks/` were UDS's — a
183
+ * directory an adopter's own hook scripts may share. A file that already exists
184
+ * and is byte-identical is not recorded (this call did not write it, so it
185
+ * proves nothing about who did); a file that exists, differs, and is not
186
+ * overwritten is left alone and not recorded.
187
+ *
188
+ * @param {string} srcDir Shipped hooks directory
189
+ * @param {string} destDir Destination directory (absolute)
190
+ * @param {{ overwrite: boolean, recorder: object, projectPath: string }} ctx
191
+ */
192
+ function copyTreeRecorded(srcDir, destDir, ctx) {
193
+ mkdirTracked(ctx.recorder, ctx.projectPath, destDir);
194
+ for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
195
+ const src = join(srcDir, entry.name);
196
+ const dest = join(destDir, entry.name);
197
+ if (entry.isDirectory()) {
198
+ copyTreeRecorded(src, dest, ctx);
199
+ continue;
200
+ }
201
+ if (existsSync(dest)) {
202
+ if (!ctx.overwrite) continue;
203
+ if (readFileSync(src).equals(readFileSync(dest))) continue;
204
+ }
205
+ copyFileSync(src, dest);
206
+ recordFile(ctx.recorder, ctx.projectPath, relative(ctx.projectPath, dest), RECORD_KINDS.HOOK_SCRIPT);
207
+ }
208
+ }
209
+
210
+ export function installHooks(projectPath, { overwriteScripts = true, recorder = newRecorder() } = {}) {
176
211
  const claudeDir = join(projectPath, '.claude');
177
212
  const settingsPath = join(claudeDir, 'settings.json');
178
213
  const hooksDir = join(projectPath, 'scripts', 'hooks');
@@ -187,11 +222,8 @@ export function installHooks(projectPath, { overwriteScripts = true } = {}) {
187
222
  return { installed: false, scriptsCount: 0, settingsPath, events, skipped };
188
223
  }
189
224
 
190
- if (!existsSync(claudeDir)) mkdirSync(claudeDir, { recursive: true });
191
- if (!existsSync(hooksDir)) mkdirSync(hooksDir, { recursive: true });
192
-
193
- // Recursive: a hook may ship a directory beside it (locale packs, fixtures).
194
- cpSync(hookDir, hooksDir, { recursive: true, force: overwriteScripts });
225
+ mkdirTracked(recorder, projectPath, claudeDir);
226
+ copyTreeRecorded(hookDir, hooksDir, { overwrite: overwriteScripts, recorder, projectPath });
195
227
 
196
228
  let settings = {};
197
229
  if (existsSync(settingsPath)) {
@@ -217,6 +249,7 @@ export function installHooks(projectPath, { overwriteScripts = true } = {}) {
217
249
  events,
218
250
  skipped,
219
251
  languageLimits: probeLanguageLimits(hooksDir, scripts),
252
+ artifacts: recorder,
220
253
  };
221
254
  }
222
255
 
@@ -264,9 +297,8 @@ export const AGY_HOOK_COMMAND_LEGACY = `node scripts/hooks/${AGY_HOOK_SCRIPT}`;
264
297
  * `overwrite: false` keeps a script that already exists (an adopter may have
265
298
  * edited it); it is what `uds update --with-hooks` uses unless `--force` is given.
266
299
  */
267
- function copyHookScripts(hookDir, hooksDir, { overwrite = true } = {}) {
268
- if (!existsSync(hooksDir)) mkdirSync(hooksDir, { recursive: true });
269
- cpSync(hookDir, hooksDir, { recursive: true, force: overwrite });
300
+ function copyHookScripts(hookDir, hooksDir, { overwrite = true, recorder, projectPath } = {}) {
301
+ copyTreeRecorded(hookDir, hooksDir, { overwrite, recorder, projectPath });
270
302
  }
271
303
 
272
304
  /**
@@ -283,7 +315,7 @@ function copyHookScripts(hookDir, hooksDir, { overwrite = true } = {}) {
283
315
  * @param {string} projectPath
284
316
  * @returns {{ installed: boolean, settingsPath: string, event?: string, reason?: string }}
285
317
  */
286
- export function installCodexHooks(projectPath, { overwriteScripts = true } = {}) {
318
+ export function installCodexHooks(projectPath, { overwriteScripts = true, recorder = newRecorder() } = {}) {
287
319
  const hooksJsonPath = join(projectPath, '.codex', 'hooks.json');
288
320
  const hookDir = hooksSourceDir();
289
321
 
@@ -291,9 +323,8 @@ export function installCodexHooks(projectPath, { overwriteScripts = true } = {})
291
323
  return { installed: false, settingsPath: hooksJsonPath, reason: `hook script not found: ${CODEX_HOOK_SCRIPT}` };
292
324
  }
293
325
 
294
- const codexDir = join(projectPath, '.codex');
295
- if (!existsSync(codexDir)) mkdirSync(codexDir, { recursive: true });
296
- copyHookScripts(hookDir, join(projectPath, 'scripts', 'hooks'), { overwrite: overwriteScripts });
326
+ mkdirTracked(recorder, projectPath, join(projectPath, '.codex'));
327
+ copyHookScripts(hookDir, join(projectPath, 'scripts', 'hooks'), { overwrite: overwriteScripts, recorder, projectPath });
297
328
 
298
329
  let config = {};
299
330
  if (existsSync(hooksJsonPath)) {
@@ -310,7 +341,7 @@ export function installCodexHooks(projectPath, { overwriteScripts = true } = {})
310
341
  ]);
311
342
 
312
343
  writeFileSync(hooksJsonPath, JSON.stringify(config, null, 2) + '\n');
313
- return { installed: true, settingsPath: hooksJsonPath, event: 'Stop' };
344
+ return { installed: true, settingsPath: hooksJsonPath, event: 'Stop', artifacts: recorder };
314
345
  }
315
346
 
316
347
  /**
@@ -323,7 +354,7 @@ export function installCodexHooks(projectPath, { overwriteScripts = true } = {})
323
354
  * @param {string} projectPath
324
355
  * @returns {{ installed: boolean, settingsPath: string, event?: string, reason?: string }}
325
356
  */
326
- export function installGeminiHooks(projectPath, { overwriteScripts = true } = {}) {
357
+ export function installGeminiHooks(projectPath, { overwriteScripts = true, recorder = newRecorder() } = {}) {
327
358
  const settingsPath = join(projectPath, '.gemini', 'settings.json');
328
359
  const hookDir = hooksSourceDir();
329
360
 
@@ -331,9 +362,8 @@ export function installGeminiHooks(projectPath, { overwriteScripts = true } = {}
331
362
  return { installed: false, settingsPath, reason: `hook script not found: ${GEMINI_HOOK_SCRIPT}` };
332
363
  }
333
364
 
334
- const geminiDir = join(projectPath, '.gemini');
335
- if (!existsSync(geminiDir)) mkdirSync(geminiDir, { recursive: true });
336
- copyHookScripts(hookDir, join(projectPath, 'scripts', 'hooks'), { overwrite: overwriteScripts });
365
+ mkdirTracked(recorder, projectPath, join(projectPath, '.gemini'));
366
+ copyHookScripts(hookDir, join(projectPath, 'scripts', 'hooks'), { overwrite: overwriteScripts, recorder, projectPath });
337
367
 
338
368
  let settings = {};
339
369
  if (existsSync(settingsPath)) {
@@ -349,7 +379,7 @@ export function installGeminiHooks(projectPath, { overwriteScripts = true } = {}
349
379
  ]);
350
380
 
351
381
  writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
352
- return { installed: true, settingsPath, event: 'AfterAgent' };
382
+ return { installed: true, settingsPath, event: 'AfterAgent', artifacts: recorder };
353
383
  }
354
384
 
355
385
  /**
@@ -377,7 +407,7 @@ export function installGeminiHooks(projectPath, { overwriteScripts = true } = {}
377
407
  * @param {string} projectPath
378
408
  * @returns {{ installed: boolean, settingsPath: string, event?: string, reason?: string }}
379
409
  */
380
- export function installAgyHooks(projectPath, { overwriteScripts = true } = {}) {
410
+ export function installAgyHooks(projectPath, { overwriteScripts = true, recorder = newRecorder() } = {}) {
381
411
  const hooksJsonPath = join(projectPath, '.agents', 'hooks.json');
382
412
  const hookDir = hooksSourceDir();
383
413
 
@@ -397,9 +427,8 @@ export function installAgyHooks(projectPath, { overwriteScripts = true } = {}) {
397
427
  }
398
428
  }
399
429
 
400
- const agentsDir = join(projectPath, '.agents');
401
- if (!existsSync(agentsDir)) mkdirSync(agentsDir, { recursive: true });
402
- copyHookScripts(hookDir, join(projectPath, 'scripts', 'hooks'), { overwrite: overwriteScripts });
430
+ mkdirTracked(recorder, projectPath, join(projectPath, '.agents'));
431
+ copyHookScripts(hookDir, join(projectPath, 'scripts', 'hooks'), { overwrite: overwriteScripts, recorder, projectPath });
403
432
 
404
433
  const command = AGY_HOOK_COMMAND;
405
434
  const entry = config[AGY_HOOK_NAME];
@@ -416,7 +445,7 @@ export function installAgyHooks(projectPath, { overwriteScripts = true } = {}) {
416
445
  config[AGY_HOOK_NAME] = { ...mine, Stop: stop };
417
446
 
418
447
  writeFileSync(hooksJsonPath, JSON.stringify(config, null, 2) + '\n');
419
- return { installed: true, settingsPath: hooksJsonPath, event: 'Stop' };
448
+ return { installed: true, settingsPath: hooksJsonPath, event: 'Stop', artifacts: recorder };
420
449
  }
421
450
 
422
451
  // ─────────────────────────────────────────────────────────────────────────────
@@ -574,9 +603,11 @@ const INSTALLERS = {
574
603
  * @param {string} projectPath
575
604
  * @param {string[]} tools
576
605
  * @param {{ plan?: boolean, overwriteScripts?: boolean }} [opts]
577
- * @returns {{ results: Array<{tool:string, outcome:'installed'|'unchanged'|'would-install'|'blocked'|'failed', path:string, why?:string, repaired?:boolean}>, scripts: {added:number, identical:number, kept:string[]} }}
606
+ * @returns {{ results: Array<{tool:string, outcome:'installed'|'unchanged'|'would-install'|'blocked'|'failed', path:string, why?:string, repaired?:boolean}>, scripts: {added:number, identical:number, kept:string[]}, artifacts: object }}
607
+ * `artifacts` is the recorder of what this call wrote (see core/install-records.js);
608
+ * the caller persists it into the manifest so `uds uninstall` can remove exactly that.
578
609
  */
579
- export function installMissingHooks(projectPath, tools, { plan = false, overwriteScripts = false } = {}) {
610
+ export function installMissingHooks(projectPath, tools, { plan = false, overwriteScripts = false, recorder = newRecorder() } = {}) {
580
611
  // Measured BEFORE anything is copied: afterwards every script would read "identical".
581
612
  const scripts = scriptDiff(projectPath);
582
613
  const results = [];
@@ -585,9 +616,9 @@ export function installMissingHooks(projectPath, tools, { plan = false, overwrit
585
616
  if (st.status === 'present') { results.push({ tool, outcome: 'unchanged', path: st.path }); continue; }
586
617
  if (st.status === 'blocked') { results.push({ tool, outcome: 'blocked', path: st.path, why: st.why }); continue; }
587
618
  if (plan) { results.push({ tool, outcome: 'would-install', path: st.path, repaired: st.status === 'stale' }); continue; }
588
- const r = INSTALLERS[tool](projectPath, { overwriteScripts });
619
+ const r = INSTALLERS[tool](projectPath, { overwriteScripts, recorder });
589
620
  if (r.installed) results.push({ tool, outcome: 'installed', path: r.settingsPath ?? st.path, repaired: st.status === 'stale' });
590
621
  else results.push({ tool, outcome: 'failed', path: r.settingsPath ?? st.path, why: r.reason ?? 'no standard produced a usable hook' });
591
622
  }
592
- return { results, scripts };
623
+ return { results, scripts, artifacts: recorder };
593
624
  }
@@ -141,6 +141,10 @@ export async function installIntegrations(config, projectPath) {
141
141
  if (result.success) {
142
142
  results.integrations.push(result.path);
143
143
  generatedFiles.add(targetFile);
144
+ // Files this run created from nothing (not merged into an existing file);
145
+ // init records them so uninstall can prove they are wholly UDS's. Added only
146
+ // when there is one, so an empty run keeps its original result shape.
147
+ if (result.created) (results.createdFiles ||= []).push(result.path);
144
148
 
145
149
  // Capture integration block hash for tracking UDS content
146
150
  if (result.blockHashInfo) {
@@ -310,7 +314,7 @@ export async function generateUniversalAgentsMd(config, integrationResults, proj
310
314
 
311
315
  if (result.success) {
312
316
  spinner.succeed(msg.generatedAgentsMd || 'Generated AGENTS.md (universal summary)');
313
- return { path: result.path, error: null, blockHashInfo: result.blockHashInfo };
317
+ return { path: result.path, error: null, blockHashInfo: result.blockHashInfo, created: result.created };
314
318
  } else {
315
319
  spinner.warn(msg.couldNotGenerateAgentsMd || 'Could not generate AGENTS.md');
316
320
  return { path: null, error: result.error };