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/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.106",
2909
+ version: "1.1.107",
2947
2910
  description: "Batteries-included agent harness for Claude Code",
2948
2911
  type: "module",
2949
2912
  bin: {
@@ -5589,6 +5552,86 @@ ${MANAGED_END}`;
5589
5552
 
5590
5553
  // src/core/updater.ts
5591
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
+ }
5592
5635
  function createUpdateResult() {
5593
5636
  return {
5594
5637
  success: false,
@@ -5776,32 +5819,52 @@ async function backfillStatusLineRefreshInterval(targetDir, options) {
5776
5819
  });
5777
5820
  }
5778
5821
  }
5779
- async function migrateHookCommandsInSettingsLocal(targetDir, options) {
5780
- if (options.dryRun) {
5781
- 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);
5782
5825
  }
5826
+ }
5827
+ async function migrateRtkSettingsFile(targetDir, options, result, name) {
5783
5828
  const layout = getProviderLayout();
5784
- const settingsPath = join8(targetDir, layout.rootDir, "settings.local.json");
5785
- if (!await fileExists(settingsPath)) {
5786
- return;
5787
- }
5829
+ const path = `${layout.rootDir}/${name}`;
5830
+ const settingsPath = join8(targetDir, path);
5831
+ const retirement = getRtkRetirement(result);
5788
5832
  try {
5833
+ await validateRtkTarget(targetDir, path);
5789
5834
  const raw = await readTextFile(settingsPath);
5790
5835
  const bom = raw.startsWith("\uFEFF") ? "\uFEFF" : "";
5791
5836
  const parsed = JSON.parse(bom ? raw.slice(bom.length) : raw);
5792
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) {
5793
5850
  return;
5794
5851
  }
5795
- const { settings, rewritten } = migrateHookCommands(parsed);
5796
- if (rewritten === 0) {
5852
+ if (options.dryRun) {
5853
+ retirement.wouldChangeSettings.push(path);
5797
5854
  return;
5798
5855
  }
5799
5856
  const trailingNewline = raw.endsWith(`
5800
5857
  `) ? `
5801
5858
  ` : "";
5859
+ await validateRtkTarget(targetDir, path);
5802
5860
  await writeTextFile(settingsPath, `${bom}${JSON.stringify(settings, null, 2)}${trailingNewline}`);
5861
+ retirement.settingsChanged.push(path);
5803
5862
  info("update.hook_commands_migrated", { count: String(rewritten) });
5804
- } 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}`);
5805
5868
  warn("update.hook_commands_migration_failed", {
5806
5869
  path: settingsPath
5807
5870
  });
@@ -5821,7 +5884,7 @@ async function runFullUpdatePostProcessing(options, result, config, customizatio
5821
5884
  }
5822
5885
  }
5823
5886
  if (isFullUpdate || options.components?.includes("hooks")) {
5824
- await migrateHookCommandsInSettingsLocal(options.targetDir, options);
5887
+ await migrateHookCommandsInSettingsLocal(options.targetDir, options, result);
5825
5888
  }
5826
5889
  if (!options.dryRun) {
5827
5890
  config.version = result.newVersion;
@@ -5838,6 +5901,29 @@ async function runFullUpdatePostProcessing(options, result, config, customizatio
5838
5901
  });
5839
5902
  }
5840
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
+ }
5841
5927
  function compareSemver(a, b) {
5842
5928
  const pa = a.split(".").map(Number);
5843
5929
  const pb = b.split(".").map(Number);
@@ -5848,24 +5934,16 @@ function compareSemver(a, b) {
5848
5934
  }
5849
5935
  return 0;
5850
5936
  }
5851
- function checkAndInstallRtkAfterUpdate() {
5852
- if (!isRtkInstalled()) {
5853
- warn("update.rtk_missing");
5854
- console.log(i18n.t("cli.update.rtkMissing"));
5855
- const rtkInstalled = installRtk();
5856
- if (rtkInstalled) {
5857
- console.log(i18n.t("cli.update.rtkInstalled"));
5858
- }
5859
- }
5860
- }
5861
5937
  async function updateProjectRegistry(targetDir, newVersion) {
5862
5938
  try {
5863
5939
  await Promise.resolve().then(() => init_registry());
5864
5940
  await registerProject(targetDir, newVersion);
5865
5941
  } catch {}
5866
5942
  }
5867
- async function regenerateLockfile(targetDir, result) {
5868
- 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 });
5869
5947
  if (lockfileResult.warning) {
5870
5948
  result.warnings.push(lockfileResult.warning);
5871
5949
  warn("update.lockfile_failed", { error: lockfileResult.warning });
@@ -5906,7 +5984,8 @@ async function update(options) {
5906
5984
  const config = await loadConfig(options.targetDir);
5907
5985
  result.previousVersion = config.version;
5908
5986
  const cliVersion = package_default.version;
5909
- if (result.previousVersion !== "0.0.0" && compareSemver(result.previousVersion, cliVersion) > 0) {
5987
+ const versionDecision = await getUpdateVersionDecision(result.previousVersion, cliVersion);
5988
+ if (versionDecision === "blocked") {
5910
5989
  result.success = false;
5911
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`;
5912
5991
  return result;
@@ -5915,6 +5994,11 @@ async function update(options) {
5915
5994
  return result;
5916
5995
  }
5917
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
+ }
5918
6002
  result.newVersion = updateCheck.latestVersion;
5919
6003
  if (!updateCheck.hasUpdates && !options.force) {
5920
6004
  info("update.no_updates");
@@ -5922,7 +6006,7 @@ async function update(options) {
5922
6006
  result.skippedComponents = options.components || getAllUpdateComponents();
5923
6007
  return result;
5924
6008
  }
5925
- await handleBackupIfRequested(options.targetDir, !!options.backup, result);
6009
+ await handleBackupIfRequested(options.targetDir, !!options.backup && !options.dryRun, result);
5926
6010
  const manifestCustomizations = await resolveManifestCustomizations(options, options.targetDir);
5927
6011
  const configPreserveFiles = resolveConfigPreserveFiles(options, config);
5928
6012
  const customizations = resolveCustomizations(manifestCustomizations, configPreserveFiles, options.targetDir);
@@ -5930,9 +6014,10 @@ async function update(options) {
5930
6014
  const components = options.components || getAllUpdateComponents();
5931
6015
  await updateAllComponents(options.targetDir, components, updateCheck, customizations, options, result, config, lockfile);
5932
6016
  await runFullUpdatePostProcessing(options, result, config, customizations);
5933
- await regenerateLockfile(options.targetDir, result);
5934
- checkAndInstallRtkAfterUpdate();
5935
- checkAndInstallCodexAfterUpdate();
6017
+ const retiredPaths = await retireRtkFiles(options, lockfile, result, config, customizations);
6018
+ await regenerateLockfile(options, result, retiredPaths);
6019
+ if (!options.dryRun)
6020
+ checkAndInstallCodexAfterUpdate();
5936
6021
  if (result.success && !options.dryRun) {
5937
6022
  await updateProjectRegistry(options.targetDir, result.newVersion);
5938
6023
  }
@@ -6108,22 +6193,19 @@ async function updateComponent(targetDir, component, customizations, options, co
6108
6193
  if (options.forceOverwriteAll) {
6109
6194
  warn("update.protected_file_force_overwrite", {
6110
6195
  file: protectedPath,
6111
- component,
6112
- hint: "File contains AI behavioral constraints. Overwriting because --force-overwrite-all was set."
6196
+ component
6113
6197
  });
6114
6198
  } else {
6115
6199
  warn("update.protected_file_skipped", {
6116
6200
  file: protectedPath,
6117
- component,
6118
- hint: "File was modified by user and preserved. Use --force-overwrite-all to override."
6201
+ component
6119
6202
  });
6120
6203
  }
6121
6204
  }
6122
6205
  for (const updatedPath of protectedUpdatedPaths) {
6123
6206
  info("update.protected_file_updated", {
6124
6207
  file: updatedPath,
6125
- component,
6126
- hint: "Protected file updated (unmodified by user, matches lockfile hash)."
6208
+ component
6127
6209
  });
6128
6210
  }
6129
6211
  skipPaths.push(...protectedSkipPaths);
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "workspaces": [
4
4
  "packages/*"
5
5
  ],
6
- "version": "1.1.106",
6
+ "version": "1.1.107",
7
7
  "description": "Batteries-included agent harness for Claude Code",
8
8
  "type": "module",
9
9
  "bin": {
@@ -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": [
@@ -38,7 +38,13 @@ fi
38
38
 
39
39
  source_field=$(printf '%s' "$input" | jq -r '.source // "unknown"' 2>/dev/null) || source_field="unknown"
40
40
 
41
- # #1770: honor CLAUDE_PROJECT_DIR first; fallback git toplevel (or pwd), not script location.
41
+ # #1770 / B-L1: nonempty CLAUDE_PROJECT_DIR selects the anchor. When manually
42
+ # running inside a Git monorepo package, set it to that package to select its
43
+ # CLAUDE.md; unset/empty instead selects the Git toplevel, then pwd if Git fails.
44
+ # The script location is never the fallback project anchor.
45
+ # B-L2: a nonempty invalid anchor is authoritative; there is no Git/cwd retry.
46
+ # If its CLAUDE.md is missing (including a missing directory or a file anchor),
47
+ # the file check below returns silently with exit 0, preserving advisory behavior.
42
48
  PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
43
49
  CLAUDE_MD="$PROJECT_ROOT/CLAUDE.md"
44
50