oh-my-customcode 1.1.106 → 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 (28) hide show
  1. package/README.md +8 -9
  2. package/dist/cli/index.js +227 -191
  3. package/dist/index.js +220 -138
  4. package/package.json +1 -1
  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/intent-detection/patterns/agent-triggers.yaml +0 -8
  17. package/templates/.claude/skills/pipeline/workflows/auto-dev.yaml +3 -1
  18. package/templates/CLAUDE.md +1 -1
  19. package/templates/CLAUDE.md.en +1 -1
  20. package/templates/CLAUDE.md.ko +1 -1
  21. package/templates/README.md +4 -4
  22. package/templates/guides/external-tools/ecc-absorption-decisions.md +2 -0
  23. package/templates/guides/multi-provider-exec/README.md +7 -18
  24. package/templates/manifest.json +3 -3
  25. package/templates/workflows/auto-dev.yaml +3 -1
  26. package/templates/.claude/hooks/scripts/rtk-intercept.sh +0 -77
  27. package/templates/.claude/skills/rtk-exec/SKILL.md +0 -199
  28. package/templates/.claude/skills/rtk-exec/scripts/rtk-wrapper.cjs +0 -377
package/dist/cli/index.js CHANGED
@@ -335,7 +335,7 @@ var init_package = __esm(() => {
335
335
  workspaces: [
336
336
  "packages/*"
337
337
  ],
338
- version: "1.1.106",
338
+ version: "1.1.107",
339
339
  description: "Batteries-included agent harness for Claude Code",
340
340
  type: "module",
341
341
  bin: {
@@ -14556,8 +14556,6 @@ var en_default = {
14556
14556
  selfUpdateReexec: "↻ Re-executing with updated CLI (v{{version}})...",
14557
14557
  reexecArgvMissing: "⚠ Cannot re-exec: process.argv[1] is empty — continuing without re-exec",
14558
14558
  allProjectSkippedSource: " ℹ Skipped: {{name}} (source project cannot self-update)",
14559
- rtkMissing: "[RTK] RTK is not installed. Attempting installation...",
14560
- rtkInstalled: "[RTK] ✓ RTK installed — 60-90% token savings activated",
14561
14559
  codexMissing: "[Codex] Codex CLI is not installed. Attempting installation...",
14562
14560
  codexInstalled: "[Codex] ✓ Codex CLI installed"
14563
14561
  },
@@ -14660,10 +14658,6 @@ var en_default = {
14660
14658
  pass: "Framework is up to date (v{{version}})",
14661
14659
  warn: "Framework is outdated: installed v{{installed}}, latest v{{latest}} ({{behind}} version(s) behind). Run 'omcustom update' to sync."
14662
14660
  },
14663
- rtk: {
14664
- pass: "RTK installed",
14665
- warn: "RTK not installed — token savings unavailable"
14666
- },
14667
14661
  codex: {
14668
14662
  pass: "Codex CLI installed",
14669
14663
  warn: "Codex CLI not installed — AI-assisted development unavailable"
@@ -14736,10 +14730,6 @@ var en_default = {
14736
14730
  }
14737
14731
  },
14738
14732
  install: {
14739
- rtk_installing: "Installing RTK...",
14740
- rtk_success: "RTK installed successfully",
14741
- rtk_already: "RTK already installed",
14742
- rtk_install_failed: "RTK installation failed",
14743
14733
  codex_installing: "Installing Codex CLI...",
14744
14734
  codex_success: "Codex CLI installed successfully",
14745
14735
  codex_already: "Codex CLI already installed",
@@ -14980,8 +14970,6 @@ var ko_default = {
14980
14970
  selfUpdateReexec: "↻ 새 CLI(v{{version}})로 재실행 중...",
14981
14971
  reexecArgvMissing: "⚠ 재실행 불가: process.argv[1]이 비어있습니다 — 재실행 없이 계속합니다",
14982
14972
  allProjectSkippedSource: " ℹ 스킵: {{name}} (소스 프로젝트는 자체 업데이트할 수 없음)",
14983
- rtkMissing: "[RTK] RTK가 설치되지 않았습니다. 설치를 시도합니다...",
14984
- rtkInstalled: "[RTK] ✓ RTK 설치 완료 — 토큰 60-90% 절감 활성화",
14985
14973
  codexMissing: "[Codex] Codex CLI가 설치되지 않았습니다. 설치를 시도합니다...",
14986
14974
  codexInstalled: "[Codex] ✓ Codex CLI 설치 완료"
14987
14975
  },
@@ -15084,10 +15072,6 @@ var ko_default = {
15084
15072
  pass: "프레임워크가 최신 상태입니다 (v{{version}})",
15085
15073
  warn: "프레임워크가 구버전입니다: 설치됨 v{{installed}}, 최신 v{{latest}} ({{behind}}개 버전 뒤처짐). 'omcustom update'를 실행하여 동기화하세요."
15086
15074
  },
15087
- rtk: {
15088
- pass: "RTK 설치됨",
15089
- warn: "RTK 미설치 — 토큰 절감 불가"
15090
- },
15091
15075
  codex: {
15092
15076
  pass: "Codex CLI 설치됨",
15093
15077
  warn: "Codex CLI 미설치 — AI 개발 지원 불가"
@@ -15160,10 +15144,6 @@ var ko_default = {
15160
15144
  }
15161
15145
  },
15162
15146
  install: {
15163
- rtk_installing: "RTK 설치 중...",
15164
- rtk_success: "RTK 설치 완료",
15165
- rtk_already: "RTK 이미 설치됨",
15166
- rtk_install_failed: "RTK 설치 실패",
15167
15147
  codex_installing: "Codex CLI 설치 중...",
15168
15148
  codex_success: "Codex CLI 설치 완료",
15169
15149
  codex_already: "Codex CLI 이미 설치됨",
@@ -15820,7 +15800,7 @@ var MESSAGES = {
15820
15800
  "update.file_applied": "Applied update to {{path}}",
15821
15801
  "update.lockfile_regenerated": "Lockfile regenerated ({{files}} files tracked)",
15822
15802
  "update.lockfile_failed": "Failed to regenerate lockfile: {{error}}",
15823
- "update.protected_file_updated": "⟳ Protected file {{file}} in {{component}} updated: {{hint}}",
15803
+ "update.protected_file_updated": "⟳ Protected file {{file}} in {{component}} selected for update: The protected-file policy allows this template update.",
15824
15804
  "update.namespace_synced": "Namespace synced: {{file}} ({{component}})",
15825
15805
  "update.hook_commands_migrated": "Migrated {{count}} hook/statusLine command(s) to CLAUDE_PROJECT_DIR-anchored paths",
15826
15806
  "update.hook_commands_migration_failed": "Failed to migrate hook commands in {{path}}",
@@ -15834,18 +15814,10 @@ var MESSAGES = {
15834
15814
  "codex.installing_npm": "Installing Codex CLI via npm...",
15835
15815
  "codex.install_failed": "Failed to install Codex CLI: {{error}}",
15836
15816
  "codex.unsupported_os": "Codex CLI auto-install is not supported on {{os}}",
15837
- "rtk.already_installed": "RTK is already installed",
15838
- "rtk.installing_brew": "Installing RTK via Homebrew...",
15839
- "rtk.installing_curl": "Installing RTK via install script...",
15840
- "rtk.install_failed": "Failed to install RTK: {{error}}",
15841
- "rtk.unsupported_os": "RTK auto-install is not supported on {{os}}",
15842
15817
  "mcp.ontology_rag_configured": "ontology-rag MCP server configured successfully",
15843
15818
  "install.codex_installing": "Installing Codex CLI...",
15844
15819
  "install.codex_success": "Codex CLI installed successfully",
15845
15820
  "install.codex_already": "Codex CLI is already installed",
15846
- "install.rtk_installing": "Installing RTK...",
15847
- "install.rtk_success": "RTK installed successfully",
15848
- "install.rtk_already": "RTK is already installed",
15849
15821
  "install.preserved": "Preserved {{files}} file(s) and {{dirs}} directory(ies) from the existing installation",
15850
15822
  "install.restored": "Restored {{files}} file(s) and {{dirs}} directory(ies) after installation",
15851
15823
  "install.backup_failed": "Failed to back up {{path}}: {{error}}",
@@ -15857,7 +15829,6 @@ var MESSAGES = {
15857
15829
  "preserve.restore_failed": "Failed to restore file {{file}}: {{error}}",
15858
15830
  "preserve_files.invalid_path": "Ignored invalid preserve_files entry from {{source}}: {{path}} ({{reason}})",
15859
15831
  "update.codex_missing": "Codex CLI is not installed",
15860
- "update.rtk_missing": "RTK is not installed",
15861
15832
  "update.self_update_skipped": "Skipped update: source project cannot update itself",
15862
15833
  "update.deprecated_file_invalid_path": "Skipped deprecated file with invalid path: {{path}} ({{reason}})",
15863
15834
  "update.deprecated_file_removed": "Removed deprecated file: {{path}} ({{reason}})",
@@ -15865,8 +15836,8 @@ var MESSAGES = {
15865
15836
  "update.entry_doc_force_updated": "Entry document force-updated: {{path}}",
15866
15837
  "update.entry_merge_warning": "Entry document merge warning: {{warning}}",
15867
15838
  "update.entry_template_not_found": "Entry document template not found: {{template}}",
15868
- "update.protected_file_force_overwrite": "Protected file {{file}} in {{component}} overwritten: {{hint}}",
15869
- "update.protected_file_skipped": "Protected file {{file}} in {{component}} skipped: {{hint}}",
15839
+ "update.protected_file_force_overwrite": "Protected file {{file}} in {{component}} selected for overwrite: The --force-overwrite-all option bypasses the protected-file preservation policy.",
15840
+ "update.protected_file_skipped": "Protected file {{file}} in {{component}} skipped: The protected-file preservation policy skips this update. Use --force-overwrite-all to override.",
15870
15841
  "update.settings_local_backfill_failed": "Failed to backfill statusLine refreshInterval in {{path}}",
15871
15842
  "general.done": "Done!",
15872
15843
  "general.failed": "Failed",
@@ -15942,7 +15913,7 @@ var MESSAGES = {
15942
15913
  "update.file_applied": "{{path}} 업데이트 적용",
15943
15914
  "update.lockfile_regenerated": "잠금 파일 재생성 완료 ({{files}}개 파일 추적)",
15944
15915
  "update.lockfile_failed": "잠금 파일 재생성 실패: {{error}}",
15945
- "update.protected_file_updated": "⟳ 보호 파일 {{file}} ({{component}}) 업데이트됨: {{hint}}",
15916
+ "update.protected_file_updated": "⟳ 보호 파일 {{file}} ({{component}}) 업데이트 대상: 보호 파일 정책에 따라 템플릿 업데이트가 허용됩니다.",
15946
15917
  "update.namespace_synced": "네임스페이스 동기화: {{file}} ({{component}})",
15947
15918
  "update.hook_commands_migrated": "CLAUDE_PROJECT_DIR 기준 경로로 훅/상태줄 명령 {{count}}개를 마이그레이션했습니다",
15948
15919
  "update.hook_commands_migration_failed": "{{path}}의 훅 명령 마이그레이션에 실패했습니다",
@@ -15956,18 +15927,10 @@ var MESSAGES = {
15956
15927
  "codex.installing_npm": "npm으로 Codex CLI를 설치합니다...",
15957
15928
  "codex.install_failed": "Codex CLI 설치에 실패했습니다: {{error}}",
15958
15929
  "codex.unsupported_os": "{{os}}에서는 Codex CLI 자동 설치를 지원하지 않습니다",
15959
- "rtk.already_installed": "RTK가 이미 설치되어 있습니다",
15960
- "rtk.installing_brew": "Homebrew로 RTK를 설치합니다...",
15961
- "rtk.installing_curl": "설치 스크립트로 RTK를 설치합니다...",
15962
- "rtk.install_failed": "RTK 설치에 실패했습니다: {{error}}",
15963
- "rtk.unsupported_os": "{{os}}에서는 RTK 자동 설치를 지원하지 않습니다",
15964
15930
  "mcp.ontology_rag_configured": "ontology-rag MCP 서버 구성을 완료했습니다",
15965
15931
  "install.codex_installing": "Codex CLI를 설치합니다...",
15966
15932
  "install.codex_success": "Codex CLI를 설치했습니다",
15967
15933
  "install.codex_already": "Codex CLI가 이미 설치되어 있습니다",
15968
- "install.rtk_installing": "RTK를 설치합니다...",
15969
- "install.rtk_success": "RTK를 설치했습니다",
15970
- "install.rtk_already": "RTK가 이미 설치되어 있습니다",
15971
15934
  "install.preserved": "기존 설치에서 파일 {{files}}개와 디렉토리 {{dirs}}개를 보존했습니다",
15972
15935
  "install.restored": "설치 후 파일 {{files}}개와 디렉토리 {{dirs}}개를 복원했습니다",
15973
15936
  "install.backup_failed": "{{path}} 백업에 실패했습니다: {{error}}",
@@ -15979,7 +15942,6 @@ var MESSAGES = {
15979
15942
  "preserve.restore_failed": "파일 {{file}} 복원에 실패했습니다: {{error}}",
15980
15943
  "preserve_files.invalid_path": "{{source}}의 유효하지 않은 preserve_files 항목을 무시했습니다: {{path}} ({{reason}})",
15981
15944
  "update.codex_missing": "Codex CLI가 설치되어 있지 않습니다",
15982
- "update.rtk_missing": "RTK가 설치되어 있지 않습니다",
15983
15945
  "update.self_update_skipped": "업데이트를 건너뜁니다: 소스 프로젝트는 스스로 업데이트할 수 없습니다",
15984
15946
  "update.deprecated_file_invalid_path": "경로가 유효하지 않아 지원 중단 파일을 건너뜁니다: {{path}} ({{reason}})",
15985
15947
  "update.deprecated_file_removed": "지원 중단 파일을 삭제했습니다: {{path}} ({{reason}})",
@@ -15987,8 +15949,8 @@ var MESSAGES = {
15987
15949
  "update.entry_doc_force_updated": "진입 문서를 강제로 업데이트했습니다: {{path}}",
15988
15950
  "update.entry_merge_warning": "진입 문서 병합 경고: {{warning}}",
15989
15951
  "update.entry_template_not_found": "진입 문서 템플릿을 찾을 수 없습니다: {{template}}",
15990
- "update.protected_file_force_overwrite": "{{component}}의 보호 파일 {{file}}을(를) 덮어썼습니다: {{hint}}",
15991
- "update.protected_file_skipped": "{{component}}의 보호 파일 {{file}}을(를) 건너뜁니다: {{hint}}",
15952
+ "update.protected_file_force_overwrite": "{{component}}의 보호 파일 {{file}} 덮어쓰기 대상: --force-overwrite-all 옵션에 따라 보호 파일 보존 정책을 적용하지 않습니다.",
15953
+ "update.protected_file_skipped": "{{component}}의 보호 파일 {{file}}을(를) 건너뜁니다: 보호 파일 보존 정책에 따라 업데이트를 건너뜁니다. 덮어쓰려면 --force-overwrite-all 옵션을 사용하십시오.",
15992
15954
  "update.settings_local_backfill_failed": "{{path}}의 statusLine refreshInterval 보충에 실패했습니다",
15993
15955
  "general.done": "완료!",
15994
15956
  "general.failed": "실패",
@@ -16570,8 +16532,23 @@ function sortFilesByPath(files) {
16570
16532
  const sorted = Object.keys(files).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
16571
16533
  return Object.fromEntries(sorted.map((key) => [key, files[key]]));
16572
16534
  }
16535
+ var RETIRED_RTK_PATHS = [
16536
+ ".claude/skills/rtk-exec/SKILL.md",
16537
+ ".claude/skills/rtk-exec/scripts/rtk-wrapper.cjs",
16538
+ ".claude/hooks/scripts/rtk-intercept.sh"
16539
+ ];
16540
+ function getRetiredPaths(options) {
16541
+ const retiredPaths = new Set([...RETIRED_RTK_PATHS, ...options.retiredPaths ?? []]);
16542
+ for (const path of retiredPaths) {
16543
+ if (typeof path !== "string" || /[\\:*?[\]{}\0]/.test(path) || path.split("/").some((segment) => segment === "" || segment === "." || segment === "..")) {
16544
+ throw new Error("retiredPaths must contain exact canonical relative file paths");
16545
+ }
16546
+ }
16547
+ return retiredPaths;
16548
+ }
16573
16549
  async function generateLockfile(targetDir, generatorVersion, templateVersion, options = {}) {
16574
16550
  const files = {};
16551
+ const retiredPaths = getRetiredPaths(options);
16575
16552
  const gitVisible = options.excludeGitIgnored ? await listGitVisiblePaths(targetDir) : null;
16576
16553
  const componentRoots = COMPONENT_PATHS.map(([prefix]) => join7(targetDir, prefix));
16577
16554
  for (const componentRoot of componentRoots) {
@@ -16580,7 +16557,7 @@ async function generateLockfile(targetDir, generatorVersion, templateVersion, op
16580
16557
  debug("lockfile.component_dir_missing", { path: componentRoot });
16581
16558
  continue;
16582
16559
  }
16583
- const allFiles = await collectFiles(componentRoot, targetDir, false);
16560
+ const allFiles = (await collectFiles(componentRoot, targetDir, false)).filter((absolutePath) => !retiredPaths.has(relative2(targetDir, absolutePath).replace(/\\/g, "/")));
16584
16561
  for (const absolutePath of allFiles) {
16585
16562
  const relativePath = relative2(targetDir, absolutePath).replace(/\\/g, "/");
16586
16563
  if (gitVisible !== null && !isGitVisible(gitVisible, relativePath)) {
@@ -16686,72 +16663,6 @@ function diffLockfiles(base, current) {
16686
16663
  return { added, removed, modified, unchanged };
16687
16664
  }
16688
16665
 
16689
- // src/core/rtk-installer.ts
16690
- import { execSync as execSync4 } from "node:child_process";
16691
- import { platform as platform2 } from "node:os";
16692
- var defaultDeps2 = {
16693
- exec: execSync4,
16694
- getPlatform: platform2
16695
- };
16696
- function isRtkInstalled(deps = defaultDeps2) {
16697
- try {
16698
- deps.exec("which rtk", { stdio: "pipe", timeout: 3000 });
16699
- return true;
16700
- } catch {
16701
- return false;
16702
- }
16703
- }
16704
- function getRtkVersion(deps = defaultDeps2) {
16705
- try {
16706
- return deps.exec("rtk --version", { encoding: "utf-8", stdio: "pipe", timeout: 3000 }).trim();
16707
- } catch {
16708
- return null;
16709
- }
16710
- }
16711
- function installRtk(deps = defaultDeps2) {
16712
- if (process.env.CI || false || false) {
16713
- return false;
16714
- }
16715
- if (isRtkInstalled(deps)) {
16716
- info("rtk.already_installed");
16717
- return true;
16718
- }
16719
- const os = deps.getPlatform();
16720
- try {
16721
- if (os === "darwin") {
16722
- try {
16723
- info("rtk.installing_brew");
16724
- deps.exec("brew install rtk-ai/tap/rtk", {
16725
- stdio: "inherit",
16726
- timeout: 120000
16727
- });
16728
- return true;
16729
- } catch {
16730
- info("rtk.installing_curl");
16731
- deps.exec("curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh", {
16732
- stdio: "inherit",
16733
- timeout: 120000
16734
- });
16735
- return isRtkInstalled(deps);
16736
- }
16737
- } else if (os === "linux") {
16738
- info("rtk.installing_curl");
16739
- deps.exec("curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh", {
16740
- stdio: "inherit",
16741
- timeout: 120000
16742
- });
16743
- return isRtkInstalled(deps);
16744
- } else {
16745
- warn("rtk.unsupported_os", { os });
16746
- return false;
16747
- }
16748
- } catch (err) {
16749
- const message = err instanceof Error ? err.message : String(err);
16750
- warn("rtk.install_failed", { error: message });
16751
- return false;
16752
- }
16753
- }
16754
-
16755
16666
  // src/cli/doctor.ts
16756
16667
  async function pathExists(targetPath) {
16757
16668
  try {
@@ -17051,23 +16962,6 @@ async function checkHooks(targetDir, rootDir = ".claude") {
17051
16962
  fixable: false
17052
16963
  };
17053
16964
  }
17054
- async function checkRtk() {
17055
- if (!isRtkInstalled()) {
17056
- return {
17057
- name: "RTK",
17058
- status: "warn",
17059
- message: "RTK not installed — token savings unavailable (brew install rtk-ai/tap/rtk)",
17060
- fixable: true
17061
- };
17062
- }
17063
- const version = getRtkVersion();
17064
- return {
17065
- name: "RTK",
17066
- status: "pass",
17067
- message: `RTK OK (${version ?? "unknown version"})`,
17068
- fixable: false
17069
- };
17070
- }
17071
16965
  async function checkCodex() {
17072
16966
  if (!isCodexInstalled()) {
17073
16967
  return {
@@ -17188,7 +17082,6 @@ async function fixSingleIssue(check, targetDir, rootDir = ".claude") {
17188
17082
  const fixedCount = await fixBrokenSymlinks(targetDir, fullPaths);
17189
17083
  return fixedCount > 0;
17190
17084
  },
17191
- RTK: async () => Promise.resolve(installRtk()),
17192
17085
  Codex: async () => Promise.resolve(installCodex())
17193
17086
  };
17194
17087
  const fixer = fixMap[check.name];
@@ -17338,7 +17231,6 @@ async function runAllChecks(targetDir, layout, packageVersion, includeUpdates) {
17338
17231
  checkHooks(targetDir, layout.rootDir),
17339
17232
  checkContexts(targetDir, layout.rootDir),
17340
17233
  checkCustomComponents(targetDir, layout.rootDir),
17341
- checkRtk(),
17342
17234
  checkCodex()
17343
17235
  ]);
17344
17236
  const frameworkCheck = await checkFrameworkDrift(targetDir, packageVersion);
@@ -17427,6 +17319,7 @@ var LEGACY_OMCUSTOM_DESCRIPTIONS = [
17427
17319
  ];
17428
17320
  var STANDALONE_SCRIPT_COMMAND = /^(?:bash )?(?:"(?:\$\{CLAUDE_PROJECT_DIR:-\.\}\/|\.\/)?\.claude\/hooks\/((?:scripts\/)?[A-Za-z0-9._-]+\.sh)"|(?:\.\/)?\.claude\/hooks\/((?:scripts\/)?[A-Za-z0-9._-]+\.sh))$/;
17429
17321
  var SCRIPT_REFERENCE = /\.claude\/hooks\/((?:scripts\/)?[A-Za-z0-9._-]+\.sh)/g;
17322
+ var RETIRED_RTK_DESCRIPTION = "RTK auto-intercept — transparently rewrites CLI commands through RTK proxy when available (R013 advisory)";
17430
17323
  function matcherKey(group) {
17431
17324
  return typeof group.matcher === "string" ? group.matcher : "";
17432
17325
  }
@@ -17488,15 +17381,52 @@ function isOwnedCommand(hook, owned) {
17488
17381
  const script = match?.[1] ?? match?.[2];
17489
17382
  return script !== undefined && owned.scripts.has(script);
17490
17383
  }
17491
- function userResidual(group, owned, ownership) {
17384
+ function isRetiredRtkCommand(hook, event, matcher) {
17385
+ if (event !== "PreToolUse" || matcher !== "Bash" || !isRecord(hook) || hook.type !== "command" || typeof hook.command !== "string") {
17386
+ return false;
17387
+ }
17388
+ const match = STANDALONE_SCRIPT_COMMAND.exec(hook.command);
17389
+ return (match?.[1] ?? match?.[2]) === "scripts/rtk-intercept.sh";
17390
+ }
17391
+ function retireRtkGroup(group, event) {
17392
+ if (!isRecord(group) || !Array.isArray(group.hooks)) {
17393
+ return [group];
17394
+ }
17395
+ const remaining = group.hooks.filter((hook) => !isRetiredRtkCommand(hook, event, matcherKey(group)));
17396
+ if (remaining.length === group.hooks.length) {
17397
+ return [group];
17398
+ }
17399
+ return remaining.length === 0 ? [] : [{ ...group, hooks: remaining }];
17400
+ }
17401
+ function retireRtkHookBlocks(existing) {
17402
+ if (!isRecord(existing)) {
17403
+ return existing;
17404
+ }
17405
+ const updates = {};
17406
+ for (const [event, value] of Object.entries(existing)) {
17407
+ if (!Array.isArray(value)) {
17408
+ continue;
17409
+ }
17410
+ const residual = value.flatMap((group) => retireRtkGroup(group, event));
17411
+ if (residual.length !== value.length || residual.some((group, index) => group !== value[index])) {
17412
+ updates[event] = residual;
17413
+ }
17414
+ }
17415
+ return Object.keys(updates).length === 0 ? existing : { ...existing, ...updates };
17416
+ }
17417
+ function userResidual(group, event, owned, ownership) {
17492
17418
  if (!isRecord(group) || !Array.isArray(group.hooks)) {
17493
17419
  return [group];
17494
17420
  }
17421
+ const matcher = matcherKey(group);
17422
+ if (group.description === RETIRED_RTK_DESCRIPTION) {
17423
+ return retireRtkGroup(group, event);
17424
+ }
17495
17425
  if (typeof group.description === "string" && ownership.descriptions.has(group.description)) {
17496
17426
  return [];
17497
17427
  }
17498
- const ownedForMatcher = owned?.get(matcherKey(group));
17499
- const remaining = group.hooks.filter((hook) => !isOwnedCommand(hook, ownedForMatcher));
17428
+ const ownedForMatcher = owned?.get(matcher);
17429
+ const remaining = group.hooks.filter((hook) => !isRetiredRtkCommand(hook, event, matcher) && !isOwnedCommand(hook, ownedForMatcher));
17500
17430
  if (remaining.length === group.hooks.length) {
17501
17431
  return [group];
17502
17432
  }
@@ -17519,7 +17449,7 @@ function mergeHookBlocks(existing, generated) {
17519
17449
  continue;
17520
17450
  }
17521
17451
  const owned = ownership.events.get(event);
17522
- const residual = value.flatMap((group) => userResidual(group, owned, ownership));
17452
+ const residual = value.flatMap((group) => userResidual(group, event, owned, ownership));
17523
17453
  if (residual.length === 0) {
17524
17454
  continue;
17525
17455
  }
@@ -18790,19 +18720,6 @@ async function updateInstallConfig(targetDir, options, installedComponents) {
18790
18720
  config.installedComponents = installedComponents;
18791
18721
  await saveConfig(targetDir, config);
18792
18722
  }
18793
- function installRtkIfNeeded(result) {
18794
- if (!isRtkInstalled()) {
18795
- info("install.rtk_installing");
18796
- const rtkInstalled = installRtk();
18797
- if (rtkInstalled) {
18798
- info("install.rtk_success");
18799
- } else {
18800
- result.warnings.push("RTK installation failed — install manually: brew install rtk-ai/tap/rtk");
18801
- }
18802
- } else {
18803
- info("install.rtk_already");
18804
- }
18805
- }
18806
18723
  function installCodexIfNeeded(result) {
18807
18724
  if (!isCodexInstalled()) {
18808
18725
  info("install.codex_installing");
@@ -18857,7 +18774,6 @@ async function install(options) {
18857
18774
  } else {
18858
18775
  info("install.lockfile_generated", { files: String(lockfileResult.fileCount) });
18859
18776
  }
18860
- installRtkIfNeeded(result);
18861
18777
  installCodexIfNeeded(result);
18862
18778
  result.success = true;
18863
18779
  success("install.success");
@@ -19089,7 +19005,7 @@ async function generateMCPConfig(targetDir) {
19089
19005
 
19090
19006
  // src/core/ontology-rag-setup.ts
19091
19007
  init_fs();
19092
- import { execSync as execSync5 } from "node:child_process";
19008
+ import { execSync as execSync4 } from "node:child_process";
19093
19009
  import { join as join11 } from "node:path";
19094
19010
  var MIN_PYTHON_MINOR = 10;
19095
19011
  var DETECTION_TIMEOUT_MS = 3000;
@@ -19102,7 +19018,7 @@ function parsePythonVersion(output) {
19102
19018
  }
19103
19019
  function checkPython3() {
19104
19020
  try {
19105
- const output = execSync5("python3 --version 2>&1", {
19021
+ const output = execSync4("python3 --version 2>&1", {
19106
19022
  stdio: "pipe",
19107
19023
  timeout: DETECTION_TIMEOUT_MS
19108
19024
  }).toString().trim();
@@ -19119,21 +19035,21 @@ function checkPython3() {
19119
19035
  }
19120
19036
  function checkUvAvailableForSetup() {
19121
19037
  try {
19122
- execSync5("uv --version", { stdio: "pipe", timeout: DETECTION_TIMEOUT_MS });
19038
+ execSync4("uv --version", { stdio: "pipe", timeout: DETECTION_TIMEOUT_MS });
19123
19039
  return true;
19124
19040
  } catch {
19125
19041
  return false;
19126
19042
  }
19127
19043
  }
19128
19044
  function createVenvWithUv(targetDir) {
19129
- execSync5("uv venv --python 3.12 .venv", {
19045
+ execSync4("uv venv --python 3.12 .venv", {
19130
19046
  cwd: targetDir,
19131
19047
  stdio: "pipe",
19132
19048
  timeout: INSTALL_TIMEOUT_MS
19133
19049
  });
19134
19050
  }
19135
19051
  function createVenvWithPython3(targetDir) {
19136
- execSync5("python3 -m venv .venv", {
19052
+ execSync4("python3 -m venv .venv", {
19137
19053
  cwd: targetDir,
19138
19054
  stdio: "pipe",
19139
19055
  timeout: INSTALL_TIMEOUT_MS
@@ -19143,13 +19059,13 @@ function installOntologyRagEditable(targetDir, useUv) {
19143
19059
  const packageRoot = getPackageRoot();
19144
19060
  const ontologyPkgPath = join11(packageRoot, "packages", "ontology-rag");
19145
19061
  if (useUv) {
19146
- execSync5(`uv pip install --python .venv/bin/python -e "${ontologyPkgPath}"`, {
19062
+ execSync4(`uv pip install --python .venv/bin/python -e "${ontologyPkgPath}"`, {
19147
19063
  cwd: targetDir,
19148
19064
  stdio: "pipe",
19149
19065
  timeout: INSTALL_TIMEOUT_MS
19150
19066
  });
19151
19067
  } else {
19152
- execSync5(`.venv/bin/pip install -e "${ontologyPkgPath}"`, {
19068
+ execSync4(`.venv/bin/pip install -e "${ontologyPkgPath}"`, {
19153
19069
  cwd: targetDir,
19154
19070
  stdio: "pipe",
19155
19071
  timeout: INSTALL_TIMEOUT_MS
@@ -21742,7 +21658,8 @@ import { constants as osConstants } from "node:os";
21742
21658
 
21743
21659
  // src/core/updater.ts
21744
21660
  init_package();
21745
- import { join as join19 } from "node:path";
21661
+ import { lstat, realpath as realpath2, unlink as unlink2 } from "node:fs/promises";
21662
+ import { isAbsolute as isAbsolute5, join as join19, relative as relative4, resolve as resolve5, sep as sep4 } from "node:path";
21746
21663
  init_fs();
21747
21664
 
21748
21665
  // src/core/entry-merger.ts
@@ -21869,6 +21786,86 @@ ${MANAGED_END}`;
21869
21786
 
21870
21787
  // src/core/updater.ts
21871
21788
  var CUSTOMIZATION_MANIFEST_FILE = ".omcustom-customizations.json";
21789
+ var RETIRED_RTK_FILES = [
21790
+ { path: ".claude/skills/rtk-exec/SKILL.md", component: "skills" },
21791
+ { path: ".claude/skills/rtk-exec/scripts/rtk-wrapper.cjs", component: "skills" },
21792
+ { path: ".claude/hooks/scripts/rtk-intercept.sh", component: "hooks" }
21793
+ ];
21794
+ function getRtkRetirement(result) {
21795
+ result.rtkRetirement ??= {
21796
+ removed: [],
21797
+ wouldRemove: [],
21798
+ preserved: [],
21799
+ settingsChanged: [],
21800
+ wouldChangeSettings: [],
21801
+ settingsConflicts: []
21802
+ };
21803
+ return result.rtkRetirement;
21804
+ }
21805
+ async function validateRtkTarget(targetDir, path) {
21806
+ const validation = validatePreserveFilePath(path, targetDir);
21807
+ if (!validation.valid)
21808
+ throw new Error("path escapes project");
21809
+ const fullPath = resolve5(targetDir, path);
21810
+ const fileStat = await lstat(fullPath);
21811
+ if (!fileStat.isFile() || fileStat.isSymbolicLink())
21812
+ throw new Error("not a regular owned file");
21813
+ const root = await realpath2(targetDir);
21814
+ const resolvedFile = await realpath2(fullPath);
21815
+ const within = relative4(root, resolvedFile);
21816
+ if (within === "" || within === ".." || within.startsWith(`..${sep4}`) || isAbsolute5(within)) {
21817
+ throw new Error("resolved path escapes project");
21818
+ }
21819
+ }
21820
+ function isRtkPathPreserved(path, config, customizations) {
21821
+ const paths = [
21822
+ ...config.preserveFiles ?? [],
21823
+ ...customizations?.preserveFiles ?? [],
21824
+ ...customizations?.modifiedFiles ?? [],
21825
+ ...(config.customComponents ?? []).map((component) => component.path)
21826
+ ];
21827
+ return paths.some((preserved) => path === preserved || path.startsWith(`${preserved}/`));
21828
+ }
21829
+ async function retireRtkFile(path, options, lockfile, result, preserved) {
21830
+ const retirement = getRtkRetirement(result);
21831
+ try {
21832
+ await validateRtkTarget(options.targetDir, path);
21833
+ if (preserved)
21834
+ throw new Error("explicitly preserved");
21835
+ if (retirement.settingsConflicts.length > 0)
21836
+ throw new Error("settings retirement conflict");
21837
+ const entry = lockfile?.files[path];
21838
+ if (!entry || typeof entry.templateHash !== "string" || !/^[a-f0-9]{64}$/.test(entry.templateHash)) {
21839
+ throw new Error("missing or invalid old ownership");
21840
+ }
21841
+ const fullPath = join19(options.targetDir, path);
21842
+ if (await computeFileHash(fullPath) !== entry.templateHash)
21843
+ throw new Error("user-modified file");
21844
+ if (options.dryRun) {
21845
+ retirement.wouldRemove.push(path);
21846
+ return;
21847
+ }
21848
+ await validateRtkTarget(options.targetDir, path);
21849
+ await unlink2(fullPath);
21850
+ retirement.removed.push(path);
21851
+ result.removedDeprecatedFiles.push(path);
21852
+ } catch (err) {
21853
+ if (err.code === "ENOENT")
21854
+ return;
21855
+ const reason = err instanceof Error ? err.message : "unreadable ownership or file";
21856
+ retirement.preserved.push({ path, reason });
21857
+ result.preservedFiles.push(path);
21858
+ result.warnings.push(`RTK retirement preserved ${path}: ${reason}`);
21859
+ }
21860
+ }
21861
+ async function retireRtkFiles(options, lockfile, result, config, customizations) {
21862
+ const fullUpdate = !options.components || options.components.length === 0;
21863
+ const files = RETIRED_RTK_FILES.filter((file) => fullUpdate || options.components?.includes(file.component));
21864
+ for (const file of files) {
21865
+ await retireRtkFile(file.path, options, lockfile, result, isRtkPathPreserved(file.path, config, customizations));
21866
+ }
21867
+ return RETIRED_RTK_FILES.map((file) => file.path);
21868
+ }
21872
21869
  function createUpdateResult() {
21873
21870
  return {
21874
21871
  success: false,
@@ -22056,32 +22053,52 @@ async function backfillStatusLineRefreshInterval(targetDir, options) {
22056
22053
  });
22057
22054
  }
22058
22055
  }
22059
- async function migrateHookCommandsInSettingsLocal(targetDir, options) {
22060
- if (options.dryRun) {
22061
- return;
22056
+ async function migrateHookCommandsInSettingsLocal(targetDir, options, result) {
22057
+ for (const name of ["settings.json", "settings.local.json"]) {
22058
+ await migrateRtkSettingsFile(targetDir, options, result, name);
22062
22059
  }
22060
+ }
22061
+ async function migrateRtkSettingsFile(targetDir, options, result, name) {
22063
22062
  const layout = getProviderLayout();
22064
- const settingsPath = join19(targetDir, layout.rootDir, "settings.local.json");
22065
- if (!await fileExists(settingsPath)) {
22066
- return;
22067
- }
22063
+ const path = `${layout.rootDir}/${name}`;
22064
+ const settingsPath = join19(targetDir, path);
22065
+ const retirement = getRtkRetirement(result);
22068
22066
  try {
22067
+ await validateRtkTarget(targetDir, path);
22069
22068
  const raw = await readTextFile(settingsPath);
22070
22069
  const bom = raw.startsWith("\uFEFF") ? "\uFEFF" : "";
22071
22070
  const parsed = JSON.parse(bom ? raw.slice(bom.length) : raw);
22072
22071
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
22072
+ throw new Error("unknown settings shape");
22073
+ }
22074
+ const current = parsed;
22075
+ const hooks = retireRtkHookBlocks(current.hooks);
22076
+ const retired = hooks !== current.hooks;
22077
+ const input = retired ? { ...current, hooks } : current;
22078
+ const { settings, rewritten } = name === "settings.local.json" ? migrateHookCommands(input) : { settings: input, rewritten: 0 };
22079
+ if (JSON.stringify(settings.hooks)?.includes("rtk-intercept.sh")) {
22080
+ retirement.settingsConflicts.push(path);
22081
+ result.warnings.push(`RTK retirement retained user or unknown wiring in ${path}`);
22082
+ }
22083
+ if (!retired && rewritten === 0) {
22073
22084
  return;
22074
22085
  }
22075
- const { settings, rewritten } = migrateHookCommands(parsed);
22076
- if (rewritten === 0) {
22086
+ if (options.dryRun) {
22087
+ retirement.wouldChangeSettings.push(path);
22077
22088
  return;
22078
22089
  }
22079
22090
  const trailingNewline = raw.endsWith(`
22080
22091
  `) ? `
22081
22092
  ` : "";
22093
+ await validateRtkTarget(targetDir, path);
22082
22094
  await writeTextFile(settingsPath, `${bom}${JSON.stringify(settings, null, 2)}${trailingNewline}`);
22095
+ retirement.settingsChanged.push(path);
22083
22096
  info("update.hook_commands_migrated", { count: String(rewritten) });
22084
- } catch {
22097
+ } catch (err) {
22098
+ if (err.code === "ENOENT")
22099
+ return;
22100
+ retirement.settingsConflicts.push(path);
22101
+ result.warnings.push(`RTK retirement could not safely migrate ${path}`);
22085
22102
  warn("update.hook_commands_migration_failed", {
22086
22103
  path: settingsPath
22087
22104
  });
@@ -22101,7 +22118,7 @@ async function runFullUpdatePostProcessing(options, result, config, customizatio
22101
22118
  }
22102
22119
  }
22103
22120
  if (isFullUpdate || options.components?.includes("hooks")) {
22104
- await migrateHookCommandsInSettingsLocal(options.targetDir, options);
22121
+ await migrateHookCommandsInSettingsLocal(options.targetDir, options, result);
22105
22122
  }
22106
22123
  if (!options.dryRun) {
22107
22124
  config.version = result.newVersion;
@@ -22118,6 +22135,29 @@ async function runFullUpdatePostProcessing(options, result, config, customizatio
22118
22135
  });
22119
22136
  }
22120
22137
  }
22138
+ async function getUpdateVersionDecision(installedVersion, cliVersion) {
22139
+ if (!(installedVersion !== "0.0.0" && compareSemver2(installedVersion, cliVersion) > 0)) {
22140
+ return "standard";
22141
+ }
22142
+ if (installedVersion !== "2.0.0") {
22143
+ return "blocked";
22144
+ }
22145
+ const match = /^1\.1\.(0|[1-9]\d*)$/.exec(cliVersion);
22146
+ if (!match) {
22147
+ return "blocked";
22148
+ }
22149
+ const patch = Number(match[1]);
22150
+ if (!Number.isSafeInteger(patch) || patch < 107) {
22151
+ return "blocked";
22152
+ }
22153
+ const manifestPath = resolveTemplatePath(getProviderLayout().manifestFile);
22154
+ if (!await fileExists(manifestPath)) {
22155
+ return "blocked";
22156
+ }
22157
+ const manifest = await readJsonFile(manifestPath);
22158
+ const correctionAllowed = typeof manifest === "object" && manifest !== null && !Array.isArray(manifest) && "version" in manifest && manifest.version === cliVersion;
22159
+ return correctionAllowed ? "correction" : "blocked";
22160
+ }
22121
22161
  function compareSemver2(a, b) {
22122
22162
  const pa = a.split(".").map(Number);
22123
22163
  const pb = b.split(".").map(Number);
@@ -22128,24 +22168,16 @@ function compareSemver2(a, b) {
22128
22168
  }
22129
22169
  return 0;
22130
22170
  }
22131
- function checkAndInstallRtkAfterUpdate() {
22132
- if (!isRtkInstalled()) {
22133
- warn("update.rtk_missing");
22134
- console.log(i18n.t("cli.update.rtkMissing"));
22135
- const rtkInstalled = installRtk();
22136
- if (rtkInstalled) {
22137
- console.log(i18n.t("cli.update.rtkInstalled"));
22138
- }
22139
- }
22140
- }
22141
22171
  async function updateProjectRegistry(targetDir, newVersion) {
22142
22172
  try {
22143
22173
  await Promise.resolve().then(() => init_registry());
22144
22174
  await registerProject(targetDir, newVersion);
22145
22175
  } catch {}
22146
22176
  }
22147
- async function regenerateLockfile(targetDir, result) {
22148
- const lockfileResult = await generateAndWriteLockfileForDir(targetDir);
22177
+ async function regenerateLockfile(options, result, retiredPaths) {
22178
+ if (options.dryRun)
22179
+ return;
22180
+ const lockfileResult = await generateAndWriteLockfileForDir(options.targetDir, { retiredPaths });
22149
22181
  if (lockfileResult.warning) {
22150
22182
  result.warnings.push(lockfileResult.warning);
22151
22183
  warn("update.lockfile_failed", { error: lockfileResult.warning });
@@ -22186,7 +22218,8 @@ async function update(options) {
22186
22218
  const config = await loadConfig(options.targetDir);
22187
22219
  result.previousVersion = config.version;
22188
22220
  const cliVersion = package_default.version;
22189
- if (result.previousVersion !== "0.0.0" && compareSemver2(result.previousVersion, cliVersion) > 0) {
22221
+ const versionDecision = await getUpdateVersionDecision(result.previousVersion, cliVersion);
22222
+ if (versionDecision === "blocked") {
22190
22223
  result.success = false;
22191
22224
  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`;
22192
22225
  return result;
@@ -22195,6 +22228,11 @@ async function update(options) {
22195
22228
  return result;
22196
22229
  }
22197
22230
  const updateCheck = await checkForUpdates(options.targetDir);
22231
+ if (versionDecision === "correction" && updateCheck.latestVersion !== cliVersion) {
22232
+ result.success = false;
22233
+ 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`;
22234
+ return result;
22235
+ }
22198
22236
  result.newVersion = updateCheck.latestVersion;
22199
22237
  if (!updateCheck.hasUpdates && !options.force) {
22200
22238
  info("update.no_updates");
@@ -22202,7 +22240,7 @@ async function update(options) {
22202
22240
  result.skippedComponents = options.components || getAllUpdateComponents();
22203
22241
  return result;
22204
22242
  }
22205
- await handleBackupIfRequested(options.targetDir, !!options.backup, result);
22243
+ await handleBackupIfRequested(options.targetDir, !!options.backup && !options.dryRun, result);
22206
22244
  const manifestCustomizations = await resolveManifestCustomizations(options, options.targetDir);
22207
22245
  const configPreserveFiles = resolveConfigPreserveFiles(options, config);
22208
22246
  const customizations = resolveCustomizations(manifestCustomizations, configPreserveFiles, options.targetDir);
@@ -22210,9 +22248,10 @@ async function update(options) {
22210
22248
  const components = options.components || getAllUpdateComponents();
22211
22249
  await updateAllComponents(options.targetDir, components, updateCheck, customizations, options, result, config, lockfile);
22212
22250
  await runFullUpdatePostProcessing(options, result, config, customizations);
22213
- await regenerateLockfile(options.targetDir, result);
22214
- checkAndInstallRtkAfterUpdate();
22215
- checkAndInstallCodexAfterUpdate();
22251
+ const retiredPaths = await retireRtkFiles(options, lockfile, result, config, customizations);
22252
+ await regenerateLockfile(options, result, retiredPaths);
22253
+ if (!options.dryRun)
22254
+ checkAndInstallCodexAfterUpdate();
22216
22255
  if (result.success && !options.dryRun) {
22217
22256
  await updateProjectRegistry(options.targetDir, result.newVersion);
22218
22257
  }
@@ -22367,22 +22406,19 @@ async function updateComponent(targetDir, component, customizations, options, co
22367
22406
  if (options.forceOverwriteAll) {
22368
22407
  warn("update.protected_file_force_overwrite", {
22369
22408
  file: protectedPath,
22370
- component,
22371
- hint: "File contains AI behavioral constraints. Overwriting because --force-overwrite-all was set."
22409
+ component
22372
22410
  });
22373
22411
  } else {
22374
22412
  warn("update.protected_file_skipped", {
22375
22413
  file: protectedPath,
22376
- component,
22377
- hint: "File was modified by user and preserved. Use --force-overwrite-all to override."
22414
+ component
22378
22415
  });
22379
22416
  }
22380
22417
  }
22381
22418
  for (const updatedPath of protectedUpdatedPaths) {
22382
22419
  info("update.protected_file_updated", {
22383
22420
  file: updatedPath,
22384
- component,
22385
- hint: "Protected file updated (unmodified by user, matches lockfile hash)."
22421
+ component
22386
22422
  });
22387
22423
  }
22388
22424
  skipPaths.push(...protectedSkipPaths);