oh-my-customcode 1.1.105 → 1.1.107

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 (34) hide show
  1. package/README.md +8 -9
  2. package/dist/cli/index.js +323 -273
  3. package/dist/index.js +223 -140
  4. package/package.json +4 -3
  5. package/templates/.claude/hooks/hooks.json +0 -10
  6. package/templates/.claude/hooks/scripts/claude-md-reinject.sh +7 -1
  7. package/templates/.claude/hooks/scripts/cost-cap-advisor.sh +107 -52
  8. package/templates/.claude/hooks/scripts/session-env-check.sh +0 -8
  9. package/templates/.claude/hooks/scripts/session-reflection.sh +60 -15
  10. package/templates/.claude/hooks/scripts/stuck-detector.sh +110 -14
  11. package/templates/.claude/rules/MAY-optimization.md +1 -1
  12. package/templates/.claude/rules/MUST-agent-design.md +1 -1
  13. package/templates/.claude/rules/SHOULD-memory-integration.md +1 -1
  14. package/templates/.claude/rules/SHOULD-verification-ladder.md +9 -0
  15. package/templates/.claude/settings.json +0 -10
  16. package/templates/.claude/skills/claude-native/SKILL.md +36 -2
  17. package/templates/.claude/skills/intent-detection/patterns/agent-triggers.yaml +0 -8
  18. package/templates/.claude/skills/omcustom-feedback/SKILL.md +51 -22
  19. package/templates/.claude/skills/pipeline/workflows/auto-dev.yaml +251 -29
  20. package/templates/.claude/skills/post-release-followup/SKILL.md +108 -14
  21. package/templates/.claude/skills/scout/SKILL.md +41 -6
  22. package/templates/.github/workflows/wiki-sync.yml +26 -93
  23. package/templates/CLAUDE.md +1 -1
  24. package/templates/CLAUDE.md.en +1 -1
  25. package/templates/CLAUDE.md.ko +1 -1
  26. package/templates/README.md +4 -4
  27. package/templates/guides/claude-code/15-version-compatibility.md +6 -0
  28. package/templates/guides/external-tools/ecc-absorption-decisions.md +2 -0
  29. package/templates/guides/multi-provider-exec/README.md +7 -18
  30. package/templates/manifest.json +3 -3
  31. package/templates/workflows/auto-dev.yaml +251 -29
  32. package/templates/.claude/hooks/scripts/rtk-intercept.sh +0 -77
  33. package/templates/.claude/skills/rtk-exec/SKILL.md +0 -199
  34. package/templates/.claude/skills/rtk-exec/scripts/rtk-wrapper.cjs +0 -377
package/dist/index.js CHANGED
@@ -416,7 +416,7 @@ var MESSAGES = {
416
416
  "update.file_applied": "Applied update to {{path}}",
417
417
  "update.lockfile_regenerated": "Lockfile regenerated ({{files}} files tracked)",
418
418
  "update.lockfile_failed": "Failed to regenerate lockfile: {{error}}",
419
- "update.protected_file_updated": "⟳ Protected file {{file}} in {{component}} updated: {{hint}}",
419
+ "update.protected_file_updated": "⟳ Protected file {{file}} in {{component}} selected for update: The protected-file policy allows this template update.",
420
420
  "update.namespace_synced": "Namespace synced: {{file}} ({{component}})",
421
421
  "update.hook_commands_migrated": "Migrated {{count}} hook/statusLine command(s) to CLAUDE_PROJECT_DIR-anchored paths",
422
422
  "update.hook_commands_migration_failed": "Failed to migrate hook commands in {{path}}",
@@ -430,18 +430,10 @@ var MESSAGES = {
430
430
  "codex.installing_npm": "Installing Codex CLI via npm...",
431
431
  "codex.install_failed": "Failed to install Codex CLI: {{error}}",
432
432
  "codex.unsupported_os": "Codex CLI auto-install is not supported on {{os}}",
433
- "rtk.already_installed": "RTK is already installed",
434
- "rtk.installing_brew": "Installing RTK via Homebrew...",
435
- "rtk.installing_curl": "Installing RTK via install script...",
436
- "rtk.install_failed": "Failed to install RTK: {{error}}",
437
- "rtk.unsupported_os": "RTK auto-install is not supported on {{os}}",
438
433
  "mcp.ontology_rag_configured": "ontology-rag MCP server configured successfully",
439
434
  "install.codex_installing": "Installing Codex CLI...",
440
435
  "install.codex_success": "Codex CLI installed successfully",
441
436
  "install.codex_already": "Codex CLI is already installed",
442
- "install.rtk_installing": "Installing RTK...",
443
- "install.rtk_success": "RTK installed successfully",
444
- "install.rtk_already": "RTK is already installed",
445
437
  "install.preserved": "Preserved {{files}} file(s) and {{dirs}} directory(ies) from the existing installation",
446
438
  "install.restored": "Restored {{files}} file(s) and {{dirs}} directory(ies) after installation",
447
439
  "install.backup_failed": "Failed to back up {{path}}: {{error}}",
@@ -453,7 +445,6 @@ var MESSAGES = {
453
445
  "preserve.restore_failed": "Failed to restore file {{file}}: {{error}}",
454
446
  "preserve_files.invalid_path": "Ignored invalid preserve_files entry from {{source}}: {{path}} ({{reason}})",
455
447
  "update.codex_missing": "Codex CLI is not installed",
456
- "update.rtk_missing": "RTK is not installed",
457
448
  "update.self_update_skipped": "Skipped update: source project cannot update itself",
458
449
  "update.deprecated_file_invalid_path": "Skipped deprecated file with invalid path: {{path}} ({{reason}})",
459
450
  "update.deprecated_file_removed": "Removed deprecated file: {{path}} ({{reason}})",
@@ -461,8 +452,8 @@ var MESSAGES = {
461
452
  "update.entry_doc_force_updated": "Entry document force-updated: {{path}}",
462
453
  "update.entry_merge_warning": "Entry document merge warning: {{warning}}",
463
454
  "update.entry_template_not_found": "Entry document template not found: {{template}}",
464
- "update.protected_file_force_overwrite": "Protected file {{file}} in {{component}} overwritten: {{hint}}",
465
- "update.protected_file_skipped": "Protected file {{file}} in {{component}} skipped: {{hint}}",
455
+ "update.protected_file_force_overwrite": "Protected file {{file}} in {{component}} selected for overwrite: The --force-overwrite-all option bypasses the protected-file preservation policy.",
456
+ "update.protected_file_skipped": "Protected file {{file}} in {{component}} skipped: The protected-file preservation policy skips this update. Use --force-overwrite-all to override.",
466
457
  "update.settings_local_backfill_failed": "Failed to backfill statusLine refreshInterval in {{path}}",
467
458
  "general.done": "Done!",
468
459
  "general.failed": "Failed",
@@ -538,7 +529,7 @@ var MESSAGES = {
538
529
  "update.file_applied": "{{path}} 업데이트 적용",
539
530
  "update.lockfile_regenerated": "잠금 파일 재생성 완료 ({{files}}개 파일 추적)",
540
531
  "update.lockfile_failed": "잠금 파일 재생성 실패: {{error}}",
541
- "update.protected_file_updated": "⟳ 보호 파일 {{file}} ({{component}}) 업데이트됨: {{hint}}",
532
+ "update.protected_file_updated": "⟳ 보호 파일 {{file}} ({{component}}) 업데이트 대상: 보호 파일 정책에 따라 템플릿 업데이트가 허용됩니다.",
542
533
  "update.namespace_synced": "네임스페이스 동기화: {{file}} ({{component}})",
543
534
  "update.hook_commands_migrated": "CLAUDE_PROJECT_DIR 기준 경로로 훅/상태줄 명령 {{count}}개를 마이그레이션했습니다",
544
535
  "update.hook_commands_migration_failed": "{{path}}의 훅 명령 마이그레이션에 실패했습니다",
@@ -552,18 +543,10 @@ var MESSAGES = {
552
543
  "codex.installing_npm": "npm으로 Codex CLI를 설치합니다...",
553
544
  "codex.install_failed": "Codex CLI 설치에 실패했습니다: {{error}}",
554
545
  "codex.unsupported_os": "{{os}}에서는 Codex CLI 자동 설치를 지원하지 않습니다",
555
- "rtk.already_installed": "RTK가 이미 설치되어 있습니다",
556
- "rtk.installing_brew": "Homebrew로 RTK를 설치합니다...",
557
- "rtk.installing_curl": "설치 스크립트로 RTK를 설치합니다...",
558
- "rtk.install_failed": "RTK 설치에 실패했습니다: {{error}}",
559
- "rtk.unsupported_os": "{{os}}에서는 RTK 자동 설치를 지원하지 않습니다",
560
546
  "mcp.ontology_rag_configured": "ontology-rag MCP 서버 구성을 완료했습니다",
561
547
  "install.codex_installing": "Codex CLI를 설치합니다...",
562
548
  "install.codex_success": "Codex CLI를 설치했습니다",
563
549
  "install.codex_already": "Codex CLI가 이미 설치되어 있습니다",
564
- "install.rtk_installing": "RTK를 설치합니다...",
565
- "install.rtk_success": "RTK를 설치했습니다",
566
- "install.rtk_already": "RTK가 이미 설치되어 있습니다",
567
550
  "install.preserved": "기존 설치에서 파일 {{files}}개와 디렉토리 {{dirs}}개를 보존했습니다",
568
551
  "install.restored": "설치 후 파일 {{files}}개와 디렉토리 {{dirs}}개를 복원했습니다",
569
552
  "install.backup_failed": "{{path}} 백업에 실패했습니다: {{error}}",
@@ -575,7 +558,6 @@ var MESSAGES = {
575
558
  "preserve.restore_failed": "파일 {{file}} 복원에 실패했습니다: {{error}}",
576
559
  "preserve_files.invalid_path": "{{source}}의 유효하지 않은 preserve_files 항목을 무시했습니다: {{path}} ({{reason}})",
577
560
  "update.codex_missing": "Codex CLI가 설치되어 있지 않습니다",
578
- "update.rtk_missing": "RTK가 설치되어 있지 않습니다",
579
561
  "update.self_update_skipped": "업데이트를 건너뜁니다: 소스 프로젝트는 스스로 업데이트할 수 없습니다",
580
562
  "update.deprecated_file_invalid_path": "경로가 유효하지 않아 지원 중단 파일을 건너뜁니다: {{path}} ({{reason}})",
581
563
  "update.deprecated_file_removed": "지원 중단 파일을 삭제했습니다: {{path}} ({{reason}})",
@@ -583,8 +565,8 @@ var MESSAGES = {
583
565
  "update.entry_doc_force_updated": "진입 문서를 강제로 업데이트했습니다: {{path}}",
584
566
  "update.entry_merge_warning": "진입 문서 병합 경고: {{warning}}",
585
567
  "update.entry_template_not_found": "진입 문서 템플릿을 찾을 수 없습니다: {{template}}",
586
- "update.protected_file_force_overwrite": "{{component}}의 보호 파일 {{file}}을(를) 덮어썼습니다: {{hint}}",
587
- "update.protected_file_skipped": "{{component}}의 보호 파일 {{file}}을(를) 건너뜁니다: {{hint}}",
568
+ "update.protected_file_force_overwrite": "{{component}}의 보호 파일 {{file}} 덮어쓰기 대상: --force-overwrite-all 옵션에 따라 보호 파일 보존 정책을 적용하지 않습니다.",
569
+ "update.protected_file_skipped": "{{component}}의 보호 파일 {{file}}을(를) 건너뜁니다: 보호 파일 보존 정책에 따라 업데이트를 건너뜁니다. 덮어쓰려면 --force-overwrite-all 옵션을 사용하십시오.",
588
570
  "update.settings_local_backfill_failed": "{{path}}의 statusLine refreshInterval 보충에 실패했습니다",
589
571
  "general.done": "완료!",
590
572
  "general.failed": "실패",
@@ -1173,6 +1155,7 @@ var LEGACY_OMCUSTOM_DESCRIPTIONS = [
1173
1155
  ];
1174
1156
  var STANDALONE_SCRIPT_COMMAND = /^(?:bash )?(?:"(?:\$\{CLAUDE_PROJECT_DIR:-\.\}\/|\.\/)?\.claude\/hooks\/((?:scripts\/)?[A-Za-z0-9._-]+\.sh)"|(?:\.\/)?\.claude\/hooks\/((?:scripts\/)?[A-Za-z0-9._-]+\.sh))$/;
1175
1157
  var SCRIPT_REFERENCE = /\.claude\/hooks\/((?:scripts\/)?[A-Za-z0-9._-]+\.sh)/g;
1158
+ var RETIRED_RTK_DESCRIPTION = "RTK auto-intercept — transparently rewrites CLI commands through RTK proxy when available (R013 advisory)";
1176
1159
  function matcherKey(group) {
1177
1160
  return typeof group.matcher === "string" ? group.matcher : "";
1178
1161
  }
@@ -1234,15 +1217,52 @@ function isOwnedCommand(hook, owned) {
1234
1217
  const script = match?.[1] ?? match?.[2];
1235
1218
  return script !== undefined && owned.scripts.has(script);
1236
1219
  }
1237
- function userResidual(group, owned, ownership) {
1220
+ function isRetiredRtkCommand(hook, event, matcher) {
1221
+ if (event !== "PreToolUse" || matcher !== "Bash" || !isRecord(hook) || hook.type !== "command" || typeof hook.command !== "string") {
1222
+ return false;
1223
+ }
1224
+ const match = STANDALONE_SCRIPT_COMMAND.exec(hook.command);
1225
+ return (match?.[1] ?? match?.[2]) === "scripts/rtk-intercept.sh";
1226
+ }
1227
+ function retireRtkGroup(group, event) {
1228
+ if (!isRecord(group) || !Array.isArray(group.hooks)) {
1229
+ return [group];
1230
+ }
1231
+ const remaining = group.hooks.filter((hook) => !isRetiredRtkCommand(hook, event, matcherKey(group)));
1232
+ if (remaining.length === group.hooks.length) {
1233
+ return [group];
1234
+ }
1235
+ return remaining.length === 0 ? [] : [{ ...group, hooks: remaining }];
1236
+ }
1237
+ function retireRtkHookBlocks(existing) {
1238
+ if (!isRecord(existing)) {
1239
+ return existing;
1240
+ }
1241
+ const updates = {};
1242
+ for (const [event, value] of Object.entries(existing)) {
1243
+ if (!Array.isArray(value)) {
1244
+ continue;
1245
+ }
1246
+ const residual = value.flatMap((group) => retireRtkGroup(group, event));
1247
+ if (residual.length !== value.length || residual.some((group, index) => group !== value[index])) {
1248
+ updates[event] = residual;
1249
+ }
1250
+ }
1251
+ return Object.keys(updates).length === 0 ? existing : { ...existing, ...updates };
1252
+ }
1253
+ function userResidual(group, event, owned, ownership) {
1238
1254
  if (!isRecord(group) || !Array.isArray(group.hooks)) {
1239
1255
  return [group];
1240
1256
  }
1257
+ const matcher = matcherKey(group);
1258
+ if (group.description === RETIRED_RTK_DESCRIPTION) {
1259
+ return retireRtkGroup(group, event);
1260
+ }
1241
1261
  if (typeof group.description === "string" && ownership.descriptions.has(group.description)) {
1242
1262
  return [];
1243
1263
  }
1244
- const ownedForMatcher = owned?.get(matcherKey(group));
1245
- const remaining = group.hooks.filter((hook) => !isOwnedCommand(hook, ownedForMatcher));
1264
+ const ownedForMatcher = owned?.get(matcher);
1265
+ const remaining = group.hooks.filter((hook) => !isRetiredRtkCommand(hook, event, matcher) && !isOwnedCommand(hook, ownedForMatcher));
1246
1266
  if (remaining.length === group.hooks.length) {
1247
1267
  return [group];
1248
1268
  }
@@ -1265,7 +1285,7 @@ function mergeHookBlocks(existing, generated) {
1265
1285
  continue;
1266
1286
  }
1267
1287
  const owned = ownership.events.get(event);
1268
- const residual = value.flatMap((group) => userResidual(group, owned, ownership));
1288
+ const residual = value.flatMap((group) => userResidual(group, event, owned, ownership));
1269
1289
  if (residual.length === 0) {
1270
1290
  continue;
1271
1291
  }
@@ -2221,8 +2241,23 @@ function sortFilesByPath(files) {
2221
2241
  const sorted = Object.keys(files).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
2222
2242
  return Object.fromEntries(sorted.map((key) => [key, files[key]]));
2223
2243
  }
2244
+ var RETIRED_RTK_PATHS = [
2245
+ ".claude/skills/rtk-exec/SKILL.md",
2246
+ ".claude/skills/rtk-exec/scripts/rtk-wrapper.cjs",
2247
+ ".claude/hooks/scripts/rtk-intercept.sh"
2248
+ ];
2249
+ function getRetiredPaths(options) {
2250
+ const retiredPaths = new Set([...RETIRED_RTK_PATHS, ...options.retiredPaths ?? []]);
2251
+ for (const path of retiredPaths) {
2252
+ if (typeof path !== "string" || /[\\:*?[\]{}\0]/.test(path) || path.split("/").some((segment) => segment === "" || segment === "." || segment === "..")) {
2253
+ throw new Error("retiredPaths must contain exact canonical relative file paths");
2254
+ }
2255
+ }
2256
+ return retiredPaths;
2257
+ }
2224
2258
  async function generateLockfile(targetDir, generatorVersion, templateVersion, options = {}) {
2225
2259
  const files = {};
2260
+ const retiredPaths = getRetiredPaths(options);
2226
2261
  const gitVisible = options.excludeGitIgnored ? await listGitVisiblePaths(targetDir) : null;
2227
2262
  const componentRoots = COMPONENT_PATHS.map(([prefix]) => join4(targetDir, prefix));
2228
2263
  for (const componentRoot of componentRoots) {
@@ -2231,7 +2266,7 @@ async function generateLockfile(targetDir, generatorVersion, templateVersion, op
2231
2266
  debug("lockfile.component_dir_missing", { path: componentRoot });
2232
2267
  continue;
2233
2268
  }
2234
- const allFiles = await collectFiles(componentRoot, targetDir, false);
2269
+ const allFiles = (await collectFiles(componentRoot, targetDir, false)).filter((absolutePath) => !retiredPaths.has(relative2(targetDir, absolutePath).replace(/\\/g, "/")));
2235
2270
  for (const absolutePath of allFiles) {
2236
2271
  const relativePath = relative2(targetDir, absolutePath).replace(/\\/g, "/");
2237
2272
  if (gitVisible !== null && !isGitVisible(gitVisible, relativePath)) {
@@ -2314,65 +2349,6 @@ async function generateAndWriteLockfileForDir(targetDir, options = {}) {
2314
2349
  }
2315
2350
  }
2316
2351
 
2317
- // src/core/rtk-installer.ts
2318
- import { execSync as execSync2 } from "node:child_process";
2319
- import { platform as platform2 } from "node:os";
2320
- var defaultDeps2 = {
2321
- exec: execSync2,
2322
- getPlatform: platform2
2323
- };
2324
- function isRtkInstalled(deps = defaultDeps2) {
2325
- try {
2326
- deps.exec("which rtk", { stdio: "pipe", timeout: 3000 });
2327
- return true;
2328
- } catch {
2329
- return false;
2330
- }
2331
- }
2332
- function installRtk(deps = defaultDeps2) {
2333
- if (process.env.CI || false || false) {
2334
- return false;
2335
- }
2336
- if (isRtkInstalled(deps)) {
2337
- info("rtk.already_installed");
2338
- return true;
2339
- }
2340
- const os = deps.getPlatform();
2341
- try {
2342
- if (os === "darwin") {
2343
- try {
2344
- info("rtk.installing_brew");
2345
- deps.exec("brew install rtk-ai/tap/rtk", {
2346
- stdio: "inherit",
2347
- timeout: 120000
2348
- });
2349
- return true;
2350
- } catch {
2351
- info("rtk.installing_curl");
2352
- deps.exec("curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh", {
2353
- stdio: "inherit",
2354
- timeout: 120000
2355
- });
2356
- return isRtkInstalled(deps);
2357
- }
2358
- } else if (os === "linux") {
2359
- info("rtk.installing_curl");
2360
- deps.exec("curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh", {
2361
- stdio: "inherit",
2362
- timeout: 120000
2363
- });
2364
- return isRtkInstalled(deps);
2365
- } else {
2366
- warn("rtk.unsupported_os", { os });
2367
- return false;
2368
- }
2369
- } catch (err) {
2370
- const message = err instanceof Error ? err.message : String(err);
2371
- warn("rtk.install_failed", { error: message });
2372
- return false;
2373
- }
2374
- }
2375
-
2376
2352
  // src/core/scope-filter.ts
2377
2353
  function getSkillScope(content) {
2378
2354
  const cleaned = content.replace(/^\uFEFF/, "");
@@ -2661,19 +2637,6 @@ async function updateInstallConfig(targetDir, options, installedComponents) {
2661
2637
  config.installedComponents = installedComponents;
2662
2638
  await saveConfig(targetDir, config);
2663
2639
  }
2664
- function installRtkIfNeeded(result) {
2665
- if (!isRtkInstalled()) {
2666
- info("install.rtk_installing");
2667
- const rtkInstalled = installRtk();
2668
- if (rtkInstalled) {
2669
- info("install.rtk_success");
2670
- } else {
2671
- result.warnings.push("RTK installation failed — install manually: brew install rtk-ai/tap/rtk");
2672
- }
2673
- } else {
2674
- info("install.rtk_already");
2675
- }
2676
- }
2677
2640
  function installCodexIfNeeded(result) {
2678
2641
  if (!isCodexInstalled()) {
2679
2642
  info("install.codex_installing");
@@ -2728,7 +2691,6 @@ async function install(options) {
2728
2691
  } else {
2729
2692
  info("install.lockfile_generated", { files: String(lockfileResult.fileCount) });
2730
2693
  }
2731
- installRtkIfNeeded(result);
2732
2694
  installCodexIfNeeded(result);
2733
2695
  result.success = true;
2734
2696
  success("install.success");
@@ -2936,14 +2898,15 @@ async function detectProvider(_options = {}) {
2936
2898
  };
2937
2899
  }
2938
2900
  // src/core/updater.ts
2939
- import { join as join8 } from "node:path";
2901
+ import { lstat, realpath as realpath2, unlink } from "node:fs/promises";
2902
+ import { isAbsolute as isAbsolute3, join as join8, relative as relative3, resolve as resolve3, sep as sep4 } from "node:path";
2940
2903
  // package.json
2941
2904
  var package_default = {
2942
2905
  name: "oh-my-customcode",
2943
2906
  workspaces: [
2944
2907
  "packages/*"
2945
2908
  ],
2946
- version: "1.1.105",
2909
+ version: "1.1.107",
2947
2910
  description: "Batteries-included agent harness for Claude Code",
2948
2911
  type: "module",
2949
2912
  bin: {
@@ -2977,7 +2940,7 @@ var package_default = {
2977
2940
  lint: "biome check src/ tests/ scripts/",
2978
2941
  "lint:fix": "biome check --write src/ tests/ scripts/",
2979
2942
  format: "biome format --write src/ tests/ scripts/",
2980
- typecheck: "tsc --noEmit",
2943
+ typecheck: "tsc --noEmit && tsc --noEmit -p tsconfig.wiki-links.json",
2981
2944
  "docs:dev": "vitepress dev docs",
2982
2945
  "docs:build": "vitepress build docs",
2983
2946
  prepare: "sh scripts/setup-hooks.sh || true",
@@ -2989,7 +2952,7 @@ var package_default = {
2989
2952
  "@inquirer/prompts": "^8.3.2",
2990
2953
  commander: "^15.0.0",
2991
2954
  i18next: "^26.0.2",
2992
- yaml: "^2.8.2"
2955
+ yaml: "2.8.3"
2993
2956
  },
2994
2957
  devDependencies: {
2995
2958
  "@anthropic-ai/sdk": "^0.127.0",
@@ -2998,6 +2961,7 @@ var package_default = {
2998
2961
  "@types/js-yaml": "^4.0.9",
2999
2962
  "@types/nodemailer": "^8.0.0",
3000
2963
  "js-yaml": "^5.0.0",
2964
+ "mdast-util-from-markdown": "2.1.0",
3001
2965
  nodemailer: "^10.0.0",
3002
2966
  typescript: "^7.0.2",
3003
2967
  vitepress: "^1.6.4"
@@ -5588,6 +5552,86 @@ ${MANAGED_END}`;
5588
5552
 
5589
5553
  // src/core/updater.ts
5590
5554
  var CUSTOMIZATION_MANIFEST_FILE = ".omcustom-customizations.json";
5555
+ var RETIRED_RTK_FILES = [
5556
+ { path: ".claude/skills/rtk-exec/SKILL.md", component: "skills" },
5557
+ { path: ".claude/skills/rtk-exec/scripts/rtk-wrapper.cjs", component: "skills" },
5558
+ { path: ".claude/hooks/scripts/rtk-intercept.sh", component: "hooks" }
5559
+ ];
5560
+ function getRtkRetirement(result) {
5561
+ result.rtkRetirement ??= {
5562
+ removed: [],
5563
+ wouldRemove: [],
5564
+ preserved: [],
5565
+ settingsChanged: [],
5566
+ wouldChangeSettings: [],
5567
+ settingsConflicts: []
5568
+ };
5569
+ return result.rtkRetirement;
5570
+ }
5571
+ async function validateRtkTarget(targetDir, path) {
5572
+ const validation = validatePreserveFilePath(path, targetDir);
5573
+ if (!validation.valid)
5574
+ throw new Error("path escapes project");
5575
+ const fullPath = resolve3(targetDir, path);
5576
+ const fileStat = await lstat(fullPath);
5577
+ if (!fileStat.isFile() || fileStat.isSymbolicLink())
5578
+ throw new Error("not a regular owned file");
5579
+ const root = await realpath2(targetDir);
5580
+ const resolvedFile = await realpath2(fullPath);
5581
+ const within = relative3(root, resolvedFile);
5582
+ if (within === "" || within === ".." || within.startsWith(`..${sep4}`) || isAbsolute3(within)) {
5583
+ throw new Error("resolved path escapes project");
5584
+ }
5585
+ }
5586
+ function isRtkPathPreserved(path, config, customizations) {
5587
+ const paths = [
5588
+ ...config.preserveFiles ?? [],
5589
+ ...customizations?.preserveFiles ?? [],
5590
+ ...customizations?.modifiedFiles ?? [],
5591
+ ...(config.customComponents ?? []).map((component) => component.path)
5592
+ ];
5593
+ return paths.some((preserved) => path === preserved || path.startsWith(`${preserved}/`));
5594
+ }
5595
+ async function retireRtkFile(path, options, lockfile, result, preserved) {
5596
+ const retirement = getRtkRetirement(result);
5597
+ try {
5598
+ await validateRtkTarget(options.targetDir, path);
5599
+ if (preserved)
5600
+ throw new Error("explicitly preserved");
5601
+ if (retirement.settingsConflicts.length > 0)
5602
+ throw new Error("settings retirement conflict");
5603
+ const entry = lockfile?.files[path];
5604
+ if (!entry || typeof entry.templateHash !== "string" || !/^[a-f0-9]{64}$/.test(entry.templateHash)) {
5605
+ throw new Error("missing or invalid old ownership");
5606
+ }
5607
+ const fullPath = join8(options.targetDir, path);
5608
+ if (await computeFileHash(fullPath) !== entry.templateHash)
5609
+ throw new Error("user-modified file");
5610
+ if (options.dryRun) {
5611
+ retirement.wouldRemove.push(path);
5612
+ return;
5613
+ }
5614
+ await validateRtkTarget(options.targetDir, path);
5615
+ await unlink(fullPath);
5616
+ retirement.removed.push(path);
5617
+ result.removedDeprecatedFiles.push(path);
5618
+ } catch (err) {
5619
+ if (err.code === "ENOENT")
5620
+ return;
5621
+ const reason = err instanceof Error ? err.message : "unreadable ownership or file";
5622
+ retirement.preserved.push({ path, reason });
5623
+ result.preservedFiles.push(path);
5624
+ result.warnings.push(`RTK retirement preserved ${path}: ${reason}`);
5625
+ }
5626
+ }
5627
+ async function retireRtkFiles(options, lockfile, result, config, customizations) {
5628
+ const fullUpdate = !options.components || options.components.length === 0;
5629
+ const files = RETIRED_RTK_FILES.filter((file) => fullUpdate || options.components?.includes(file.component));
5630
+ for (const file of files) {
5631
+ await retireRtkFile(file.path, options, lockfile, result, isRtkPathPreserved(file.path, config, customizations));
5632
+ }
5633
+ return RETIRED_RTK_FILES.map((file) => file.path);
5634
+ }
5591
5635
  function createUpdateResult() {
5592
5636
  return {
5593
5637
  success: false,
@@ -5775,32 +5819,52 @@ async function backfillStatusLineRefreshInterval(targetDir, options) {
5775
5819
  });
5776
5820
  }
5777
5821
  }
5778
- async function migrateHookCommandsInSettingsLocal(targetDir, options) {
5779
- if (options.dryRun) {
5780
- return;
5822
+ async function migrateHookCommandsInSettingsLocal(targetDir, options, result) {
5823
+ for (const name of ["settings.json", "settings.local.json"]) {
5824
+ await migrateRtkSettingsFile(targetDir, options, result, name);
5781
5825
  }
5826
+ }
5827
+ async function migrateRtkSettingsFile(targetDir, options, result, name) {
5782
5828
  const layout = getProviderLayout();
5783
- const settingsPath = join8(targetDir, layout.rootDir, "settings.local.json");
5784
- if (!await fileExists(settingsPath)) {
5785
- return;
5786
- }
5829
+ const path = `${layout.rootDir}/${name}`;
5830
+ const settingsPath = join8(targetDir, path);
5831
+ const retirement = getRtkRetirement(result);
5787
5832
  try {
5833
+ await validateRtkTarget(targetDir, path);
5788
5834
  const raw = await readTextFile(settingsPath);
5789
5835
  const bom = raw.startsWith("\uFEFF") ? "\uFEFF" : "";
5790
5836
  const parsed = JSON.parse(bom ? raw.slice(bom.length) : raw);
5791
5837
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
5838
+ throw new Error("unknown settings shape");
5839
+ }
5840
+ const current = parsed;
5841
+ const hooks = retireRtkHookBlocks(current.hooks);
5842
+ const retired = hooks !== current.hooks;
5843
+ const input = retired ? { ...current, hooks } : current;
5844
+ const { settings, rewritten } = name === "settings.local.json" ? migrateHookCommands(input) : { settings: input, rewritten: 0 };
5845
+ if (JSON.stringify(settings.hooks)?.includes("rtk-intercept.sh")) {
5846
+ retirement.settingsConflicts.push(path);
5847
+ result.warnings.push(`RTK retirement retained user or unknown wiring in ${path}`);
5848
+ }
5849
+ if (!retired && rewritten === 0) {
5792
5850
  return;
5793
5851
  }
5794
- const { settings, rewritten } = migrateHookCommands(parsed);
5795
- if (rewritten === 0) {
5852
+ if (options.dryRun) {
5853
+ retirement.wouldChangeSettings.push(path);
5796
5854
  return;
5797
5855
  }
5798
5856
  const trailingNewline = raw.endsWith(`
5799
5857
  `) ? `
5800
5858
  ` : "";
5859
+ await validateRtkTarget(targetDir, path);
5801
5860
  await writeTextFile(settingsPath, `${bom}${JSON.stringify(settings, null, 2)}${trailingNewline}`);
5861
+ retirement.settingsChanged.push(path);
5802
5862
  info("update.hook_commands_migrated", { count: String(rewritten) });
5803
- } catch {
5863
+ } catch (err) {
5864
+ if (err.code === "ENOENT")
5865
+ return;
5866
+ retirement.settingsConflicts.push(path);
5867
+ result.warnings.push(`RTK retirement could not safely migrate ${path}`);
5804
5868
  warn("update.hook_commands_migration_failed", {
5805
5869
  path: settingsPath
5806
5870
  });
@@ -5820,7 +5884,7 @@ async function runFullUpdatePostProcessing(options, result, config, customizatio
5820
5884
  }
5821
5885
  }
5822
5886
  if (isFullUpdate || options.components?.includes("hooks")) {
5823
- await migrateHookCommandsInSettingsLocal(options.targetDir, options);
5887
+ await migrateHookCommandsInSettingsLocal(options.targetDir, options, result);
5824
5888
  }
5825
5889
  if (!options.dryRun) {
5826
5890
  config.version = result.newVersion;
@@ -5837,6 +5901,29 @@ async function runFullUpdatePostProcessing(options, result, config, customizatio
5837
5901
  });
5838
5902
  }
5839
5903
  }
5904
+ async function getUpdateVersionDecision(installedVersion, cliVersion) {
5905
+ if (!(installedVersion !== "0.0.0" && compareSemver(installedVersion, cliVersion) > 0)) {
5906
+ return "standard";
5907
+ }
5908
+ if (installedVersion !== "2.0.0") {
5909
+ return "blocked";
5910
+ }
5911
+ const match = /^1\.1\.(0|[1-9]\d*)$/.exec(cliVersion);
5912
+ if (!match) {
5913
+ return "blocked";
5914
+ }
5915
+ const patch = Number(match[1]);
5916
+ if (!Number.isSafeInteger(patch) || patch < 107) {
5917
+ return "blocked";
5918
+ }
5919
+ const manifestPath = resolveTemplatePath(getProviderLayout().manifestFile);
5920
+ if (!await fileExists(manifestPath)) {
5921
+ return "blocked";
5922
+ }
5923
+ const manifest = await readJsonFile(manifestPath);
5924
+ const correctionAllowed = typeof manifest === "object" && manifest !== null && !Array.isArray(manifest) && "version" in manifest && manifest.version === cliVersion;
5925
+ return correctionAllowed ? "correction" : "blocked";
5926
+ }
5840
5927
  function compareSemver(a, b) {
5841
5928
  const pa = a.split(".").map(Number);
5842
5929
  const pb = b.split(".").map(Number);
@@ -5847,24 +5934,16 @@ function compareSemver(a, b) {
5847
5934
  }
5848
5935
  return 0;
5849
5936
  }
5850
- function checkAndInstallRtkAfterUpdate() {
5851
- if (!isRtkInstalled()) {
5852
- warn("update.rtk_missing");
5853
- console.log(i18n.t("cli.update.rtkMissing"));
5854
- const rtkInstalled = installRtk();
5855
- if (rtkInstalled) {
5856
- console.log(i18n.t("cli.update.rtkInstalled"));
5857
- }
5858
- }
5859
- }
5860
5937
  async function updateProjectRegistry(targetDir, newVersion) {
5861
5938
  try {
5862
5939
  await Promise.resolve().then(() => init_registry());
5863
5940
  await registerProject(targetDir, newVersion);
5864
5941
  } catch {}
5865
5942
  }
5866
- async function regenerateLockfile(targetDir, result) {
5867
- const lockfileResult = await generateAndWriteLockfileForDir(targetDir);
5943
+ async function regenerateLockfile(options, result, retiredPaths) {
5944
+ if (options.dryRun)
5945
+ return;
5946
+ const lockfileResult = await generateAndWriteLockfileForDir(options.targetDir, { retiredPaths });
5868
5947
  if (lockfileResult.warning) {
5869
5948
  result.warnings.push(lockfileResult.warning);
5870
5949
  warn("update.lockfile_failed", { error: lockfileResult.warning });
@@ -5905,7 +5984,8 @@ async function update(options) {
5905
5984
  const config = await loadConfig(options.targetDir);
5906
5985
  result.previousVersion = config.version;
5907
5986
  const cliVersion = package_default.version;
5908
- if (result.previousVersion !== "0.0.0" && compareSemver(result.previousVersion, cliVersion) > 0) {
5987
+ const versionDecision = await getUpdateVersionDecision(result.previousVersion, cliVersion);
5988
+ if (versionDecision === "blocked") {
5909
5989
  result.success = false;
5910
5990
  result.error = `Downgrade prevented: project has v${result.previousVersion} but CLI is v${cliVersion}. Update the CLI first: npm install -g oh-my-customcode@latest`;
5911
5991
  return result;
@@ -5914,6 +5994,11 @@ async function update(options) {
5914
5994
  return result;
5915
5995
  }
5916
5996
  const updateCheck = await checkForUpdates(options.targetDir);
5997
+ if (versionDecision === "correction" && updateCheck.latestVersion !== cliVersion) {
5998
+ result.success = false;
5999
+ result.error = `Version correction prevented: CLI is v${cliVersion} but templates are v${updateCheck.latestVersion}. Reinstall the CLI: npm install -g oh-my-customcode@latest`;
6000
+ return result;
6001
+ }
5917
6002
  result.newVersion = updateCheck.latestVersion;
5918
6003
  if (!updateCheck.hasUpdates && !options.force) {
5919
6004
  info("update.no_updates");
@@ -5921,7 +6006,7 @@ async function update(options) {
5921
6006
  result.skippedComponents = options.components || getAllUpdateComponents();
5922
6007
  return result;
5923
6008
  }
5924
- await handleBackupIfRequested(options.targetDir, !!options.backup, result);
6009
+ await handleBackupIfRequested(options.targetDir, !!options.backup && !options.dryRun, result);
5925
6010
  const manifestCustomizations = await resolveManifestCustomizations(options, options.targetDir);
5926
6011
  const configPreserveFiles = resolveConfigPreserveFiles(options, config);
5927
6012
  const customizations = resolveCustomizations(manifestCustomizations, configPreserveFiles, options.targetDir);
@@ -5929,9 +6014,10 @@ async function update(options) {
5929
6014
  const components = options.components || getAllUpdateComponents();
5930
6015
  await updateAllComponents(options.targetDir, components, updateCheck, customizations, options, result, config, lockfile);
5931
6016
  await runFullUpdatePostProcessing(options, result, config, customizations);
5932
- await regenerateLockfile(options.targetDir, result);
5933
- checkAndInstallRtkAfterUpdate();
5934
- checkAndInstallCodexAfterUpdate();
6017
+ const retiredPaths = await retireRtkFiles(options, lockfile, result, config, customizations);
6018
+ await regenerateLockfile(options, result, retiredPaths);
6019
+ if (!options.dryRun)
6020
+ checkAndInstallCodexAfterUpdate();
5935
6021
  if (result.success && !options.dryRun) {
5936
6022
  await updateProjectRegistry(options.targetDir, result.newVersion);
5937
6023
  }
@@ -6107,22 +6193,19 @@ async function updateComponent(targetDir, component, customizations, options, co
6107
6193
  if (options.forceOverwriteAll) {
6108
6194
  warn("update.protected_file_force_overwrite", {
6109
6195
  file: protectedPath,
6110
- component,
6111
- hint: "File contains AI behavioral constraints. Overwriting because --force-overwrite-all was set."
6196
+ component
6112
6197
  });
6113
6198
  } else {
6114
6199
  warn("update.protected_file_skipped", {
6115
6200
  file: protectedPath,
6116
- component,
6117
- hint: "File was modified by user and preserved. Use --force-overwrite-all to override."
6201
+ component
6118
6202
  });
6119
6203
  }
6120
6204
  }
6121
6205
  for (const updatedPath of protectedUpdatedPaths) {
6122
6206
  info("update.protected_file_updated", {
6123
6207
  file: updatedPath,
6124
- component,
6125
- hint: "Protected file updated (unmodified by user, matches lockfile hash)."
6208
+ component
6126
6209
  });
6127
6210
  }
6128
6211
  skipPaths.push(...protectedSkipPaths);
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "workspaces": [
4
4
  "packages/*"
5
5
  ],
6
- "version": "1.1.105",
6
+ "version": "1.1.107",
7
7
  "description": "Batteries-included agent harness for Claude Code",
8
8
  "type": "module",
9
9
  "bin": {
@@ -37,7 +37,7 @@
37
37
  "lint": "biome check src/ tests/ scripts/",
38
38
  "lint:fix": "biome check --write src/ tests/ scripts/",
39
39
  "format": "biome format --write src/ tests/ scripts/",
40
- "typecheck": "tsc --noEmit",
40
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.wiki-links.json",
41
41
  "docs:dev": "vitepress dev docs",
42
42
  "docs:build": "vitepress build docs",
43
43
  "prepare": "sh scripts/setup-hooks.sh || true",
@@ -49,7 +49,7 @@
49
49
  "@inquirer/prompts": "^8.3.2",
50
50
  "commander": "^15.0.0",
51
51
  "i18next": "^26.0.2",
52
- "yaml": "^2.8.2"
52
+ "yaml": "2.8.3"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@anthropic-ai/sdk": "^0.127.0",
@@ -58,6 +58,7 @@
58
58
  "@types/js-yaml": "^4.0.9",
59
59
  "@types/nodemailer": "^8.0.0",
60
60
  "js-yaml": "^5.0.0",
61
+ "mdast-util-from-markdown": "2.1.0",
61
62
  "nodemailer": "^10.0.0",
62
63
  "typescript": "^7.0.2",
63
64
  "vitepress": "^1.6.4"
@@ -82,16 +82,6 @@
82
82
  ],
83
83
  "description": "Schema-based tool input validation — Phase 1 advisory only"
84
84
  },
85
- {
86
- "matcher": "tool == \"Bash\"",
87
- "hooks": [
88
- {
89
- "type": "command",
90
- "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/rtk-intercept.sh\""
91
- }
92
- ],
93
- "description": "RTK auto-intercept — transparently rewrites CLI commands through RTK proxy when available (R013 advisory)"
94
- },
95
85
  {
96
86
  "matcher": "tool == \"Bash\" && tool_input.command matches \"git (reset|clean|checkout|restore|switch|rebase|merge)\"",
97
87
  "hooks": [