oh-my-customcode 1.1.107 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -296,7 +296,7 @@ your-project/
296
296
 
297
297
  ## External Tool Integrations
298
298
 
299
- Starting with v1.1.107, project RTK integration is retired: the public `/rtk-exec` skill, automatic installation, doctor integration, and RTK hooks are removed. Separately installed RTK binaries remain on your machine. Updates automatically remove retired managed files only when their hashes match the previous lockfile. Modified files and files with unknown ownership are preserved and reported as conflicts; retained user skill files may still be discovered automatically. Other tools are optional:
299
+ Starting with v2.0.0, project RTK integration is retired: the public `/rtk-exec` skill, automatic installation, doctor integration, and RTK hooks are removed. Separately installed RTK binaries remain on your machine. Updates automatically remove retired managed files only when their hashes match the previous lockfile. Modified files and files with unknown ownership are preserved and reported as conflicts; retained user skill files may still be discovered automatically. Other tools are optional:
300
300
 
301
301
  | Tool | Purpose | Install | Status |
302
302
  |------|---------|---------|--------|
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.107",
338
+ version: "2.0.0",
339
339
  description: "Batteries-included agent harness for Claude Code",
340
340
  type: "module",
341
341
  bin: {
@@ -15800,7 +15800,7 @@ var MESSAGES = {
15800
15800
  "update.file_applied": "Applied update to {{path}}",
15801
15801
  "update.lockfile_regenerated": "Lockfile regenerated ({{files}} files tracked)",
15802
15802
  "update.lockfile_failed": "Failed to regenerate lockfile: {{error}}",
15803
- "update.protected_file_updated": "⟳ Protected file {{file}} in {{component}} selected for update: The protected-file policy allows this template update.",
15803
+ "update.protected_file_updated": "⟳ Protected file {{file}} in {{component}} updated: {{hint}}",
15804
15804
  "update.namespace_synced": "Namespace synced: {{file}} ({{component}})",
15805
15805
  "update.hook_commands_migrated": "Migrated {{count}} hook/statusLine command(s) to CLAUDE_PROJECT_DIR-anchored paths",
15806
15806
  "update.hook_commands_migration_failed": "Failed to migrate hook commands in {{path}}",
@@ -15836,8 +15836,8 @@ var MESSAGES = {
15836
15836
  "update.entry_doc_force_updated": "Entry document force-updated: {{path}}",
15837
15837
  "update.entry_merge_warning": "Entry document merge warning: {{warning}}",
15838
15838
  "update.entry_template_not_found": "Entry document template not found: {{template}}",
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.",
15839
+ "update.protected_file_force_overwrite": "Protected file {{file}} in {{component}} overwritten: {{hint}}",
15840
+ "update.protected_file_skipped": "Protected file {{file}} in {{component}} skipped: {{hint}}",
15841
15841
  "update.settings_local_backfill_failed": "Failed to backfill statusLine refreshInterval in {{path}}",
15842
15842
  "general.done": "Done!",
15843
15843
  "general.failed": "Failed",
@@ -15913,7 +15913,7 @@ var MESSAGES = {
15913
15913
  "update.file_applied": "{{path}} 업데이트 적용",
15914
15914
  "update.lockfile_regenerated": "잠금 파일 재생성 완료 ({{files}}개 파일 추적)",
15915
15915
  "update.lockfile_failed": "잠금 파일 재생성 실패: {{error}}",
15916
- "update.protected_file_updated": "⟳ 보호 파일 {{file}} ({{component}}) 업데이트 대상: 보호 파일 정책에 따라 템플릿 업데이트가 허용됩니다.",
15916
+ "update.protected_file_updated": "⟳ 보호 파일 {{file}} ({{component}}) 업데이트됨: {{hint}}",
15917
15917
  "update.namespace_synced": "네임스페이스 동기화: {{file}} ({{component}})",
15918
15918
  "update.hook_commands_migrated": "CLAUDE_PROJECT_DIR 기준 경로로 훅/상태줄 명령 {{count}}개를 마이그레이션했습니다",
15919
15919
  "update.hook_commands_migration_failed": "{{path}}의 훅 명령 마이그레이션에 실패했습니다",
@@ -15949,8 +15949,8 @@ var MESSAGES = {
15949
15949
  "update.entry_doc_force_updated": "진입 문서를 강제로 업데이트했습니다: {{path}}",
15950
15950
  "update.entry_merge_warning": "진입 문서 병합 경고: {{warning}}",
15951
15951
  "update.entry_template_not_found": "진입 문서 템플릿을 찾을 수 없습니다: {{template}}",
15952
- "update.protected_file_force_overwrite": "{{component}}의 보호 파일 {{file}} 덮어쓰기 대상: --force-overwrite-all 옵션에 따라 보호 파일 보존 정책을 적용하지 않습니다.",
15953
- "update.protected_file_skipped": "{{component}}의 보호 파일 {{file}}을(를) 건너뜁니다: 보호 파일 보존 정책에 따라 업데이트를 건너뜁니다. 덮어쓰려면 --force-overwrite-all 옵션을 사용하십시오.",
15952
+ "update.protected_file_force_overwrite": "{{component}}의 보호 파일 {{file}}을(를) 덮어썼습니다: {{hint}}",
15953
+ "update.protected_file_skipped": "{{component}}의 보호 파일 {{file}}을(를) 건너뜁니다: {{hint}}",
15954
15954
  "update.settings_local_backfill_failed": "{{path}}의 statusLine refreshInterval 보충에 실패했습니다",
15955
15955
  "general.done": "완료!",
15956
15956
  "general.failed": "실패",
@@ -22135,29 +22135,6 @@ async function runFullUpdatePostProcessing(options, result, config, customizatio
22135
22135
  });
22136
22136
  }
22137
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
- }
22161
22138
  function compareSemver2(a, b) {
22162
22139
  const pa = a.split(".").map(Number);
22163
22140
  const pb = b.split(".").map(Number);
@@ -22218,8 +22195,7 @@ async function update(options) {
22218
22195
  const config = await loadConfig(options.targetDir);
22219
22196
  result.previousVersion = config.version;
22220
22197
  const cliVersion = package_default.version;
22221
- const versionDecision = await getUpdateVersionDecision(result.previousVersion, cliVersion);
22222
- if (versionDecision === "blocked") {
22198
+ if (result.previousVersion !== "0.0.0" && compareSemver2(result.previousVersion, cliVersion) > 0) {
22223
22199
  result.success = false;
22224
22200
  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`;
22225
22201
  return result;
@@ -22228,11 +22204,6 @@ async function update(options) {
22228
22204
  return result;
22229
22205
  }
22230
22206
  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
- }
22236
22207
  result.newVersion = updateCheck.latestVersion;
22237
22208
  if (!updateCheck.hasUpdates && !options.force) {
22238
22209
  info("update.no_updates");
@@ -22406,19 +22377,22 @@ async function updateComponent(targetDir, component, customizations, options, co
22406
22377
  if (options.forceOverwriteAll) {
22407
22378
  warn("update.protected_file_force_overwrite", {
22408
22379
  file: protectedPath,
22409
- component
22380
+ component,
22381
+ hint: "File contains AI behavioral constraints. Overwriting because --force-overwrite-all was set."
22410
22382
  });
22411
22383
  } else {
22412
22384
  warn("update.protected_file_skipped", {
22413
22385
  file: protectedPath,
22414
- component
22386
+ component,
22387
+ hint: "File was modified by user and preserved. Use --force-overwrite-all to override."
22415
22388
  });
22416
22389
  }
22417
22390
  }
22418
22391
  for (const updatedPath of protectedUpdatedPaths) {
22419
22392
  info("update.protected_file_updated", {
22420
22393
  file: updatedPath,
22421
- component
22394
+ component,
22395
+ hint: "Protected file updated (unmodified by user, matches lockfile hash)."
22422
22396
  });
22423
22397
  }
22424
22398
  skipPaths.push(...protectedSkipPaths);
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}} selected for update: The protected-file policy allows this template update.",
419
+ "update.protected_file_updated": "⟳ Protected file {{file}} in {{component}} updated: {{hint}}",
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}}",
@@ -452,8 +452,8 @@ var MESSAGES = {
452
452
  "update.entry_doc_force_updated": "Entry document force-updated: {{path}}",
453
453
  "update.entry_merge_warning": "Entry document merge warning: {{warning}}",
454
454
  "update.entry_template_not_found": "Entry document template not found: {{template}}",
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.",
455
+ "update.protected_file_force_overwrite": "Protected file {{file}} in {{component}} overwritten: {{hint}}",
456
+ "update.protected_file_skipped": "Protected file {{file}} in {{component}} skipped: {{hint}}",
457
457
  "update.settings_local_backfill_failed": "Failed to backfill statusLine refreshInterval in {{path}}",
458
458
  "general.done": "Done!",
459
459
  "general.failed": "Failed",
@@ -529,7 +529,7 @@ var MESSAGES = {
529
529
  "update.file_applied": "{{path}} 업데이트 적용",
530
530
  "update.lockfile_regenerated": "잠금 파일 재생성 완료 ({{files}}개 파일 추적)",
531
531
  "update.lockfile_failed": "잠금 파일 재생성 실패: {{error}}",
532
- "update.protected_file_updated": "⟳ 보호 파일 {{file}} ({{component}}) 업데이트 대상: 보호 파일 정책에 따라 템플릿 업데이트가 허용됩니다.",
532
+ "update.protected_file_updated": "⟳ 보호 파일 {{file}} ({{component}}) 업데이트됨: {{hint}}",
533
533
  "update.namespace_synced": "네임스페이스 동기화: {{file}} ({{component}})",
534
534
  "update.hook_commands_migrated": "CLAUDE_PROJECT_DIR 기준 경로로 훅/상태줄 명령 {{count}}개를 마이그레이션했습니다",
535
535
  "update.hook_commands_migration_failed": "{{path}}의 훅 명령 마이그레이션에 실패했습니다",
@@ -565,8 +565,8 @@ var MESSAGES = {
565
565
  "update.entry_doc_force_updated": "진입 문서를 강제로 업데이트했습니다: {{path}}",
566
566
  "update.entry_merge_warning": "진입 문서 병합 경고: {{warning}}",
567
567
  "update.entry_template_not_found": "진입 문서 템플릿을 찾을 수 없습니다: {{template}}",
568
- "update.protected_file_force_overwrite": "{{component}}의 보호 파일 {{file}} 덮어쓰기 대상: --force-overwrite-all 옵션에 따라 보호 파일 보존 정책을 적용하지 않습니다.",
569
- "update.protected_file_skipped": "{{component}}의 보호 파일 {{file}}을(를) 건너뜁니다: 보호 파일 보존 정책에 따라 업데이트를 건너뜁니다. 덮어쓰려면 --force-overwrite-all 옵션을 사용하십시오.",
568
+ "update.protected_file_force_overwrite": "{{component}}의 보호 파일 {{file}}을(를) 덮어썼습니다: {{hint}}",
569
+ "update.protected_file_skipped": "{{component}}의 보호 파일 {{file}}을(를) 건너뜁니다: {{hint}}",
570
570
  "update.settings_local_backfill_failed": "{{path}}의 statusLine refreshInterval 보충에 실패했습니다",
571
571
  "general.done": "완료!",
572
572
  "general.failed": "실패",
@@ -2906,7 +2906,7 @@ var package_default = {
2906
2906
  workspaces: [
2907
2907
  "packages/*"
2908
2908
  ],
2909
- version: "1.1.107",
2909
+ version: "2.0.0",
2910
2910
  description: "Batteries-included agent harness for Claude Code",
2911
2911
  type: "module",
2912
2912
  bin: {
@@ -5901,29 +5901,6 @@ async function runFullUpdatePostProcessing(options, result, config, customizatio
5901
5901
  });
5902
5902
  }
5903
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
- }
5927
5904
  function compareSemver(a, b) {
5928
5905
  const pa = a.split(".").map(Number);
5929
5906
  const pb = b.split(".").map(Number);
@@ -5984,8 +5961,7 @@ async function update(options) {
5984
5961
  const config = await loadConfig(options.targetDir);
5985
5962
  result.previousVersion = config.version;
5986
5963
  const cliVersion = package_default.version;
5987
- const versionDecision = await getUpdateVersionDecision(result.previousVersion, cliVersion);
5988
- if (versionDecision === "blocked") {
5964
+ if (result.previousVersion !== "0.0.0" && compareSemver(result.previousVersion, cliVersion) > 0) {
5989
5965
  result.success = false;
5990
5966
  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`;
5991
5967
  return result;
@@ -5994,11 +5970,6 @@ async function update(options) {
5994
5970
  return result;
5995
5971
  }
5996
5972
  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
- }
6002
5973
  result.newVersion = updateCheck.latestVersion;
6003
5974
  if (!updateCheck.hasUpdates && !options.force) {
6004
5975
  info("update.no_updates");
@@ -6193,19 +6164,22 @@ async function updateComponent(targetDir, component, customizations, options, co
6193
6164
  if (options.forceOverwriteAll) {
6194
6165
  warn("update.protected_file_force_overwrite", {
6195
6166
  file: protectedPath,
6196
- component
6167
+ component,
6168
+ hint: "File contains AI behavioral constraints. Overwriting because --force-overwrite-all was set."
6197
6169
  });
6198
6170
  } else {
6199
6171
  warn("update.protected_file_skipped", {
6200
6172
  file: protectedPath,
6201
- component
6173
+ component,
6174
+ hint: "File was modified by user and preserved. Use --force-overwrite-all to override."
6202
6175
  });
6203
6176
  }
6204
6177
  }
6205
6178
  for (const updatedPath of protectedUpdatedPaths) {
6206
6179
  info("update.protected_file_updated", {
6207
6180
  file: updatedPath,
6208
- component
6181
+ component,
6182
+ hint: "Protected file updated (unmodified by user, matches lockfile hash)."
6209
6183
  });
6210
6184
  }
6211
6185
  skipPaths.push(...protectedSkipPaths);
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "workspaces": [
4
4
  "packages/*"
5
5
  ],
6
- "version": "1.1.107",
6
+ "version": "2.0.0",
7
7
  "description": "Batteries-included agent harness for Claude Code",
8
8
  "type": "module",
9
9
  "bin": {
@@ -38,13 +38,7 @@ fi
38
38
 
39
39
  source_field=$(printf '%s' "$input" | jq -r '.source // "unknown"' 2>/dev/null) || source_field="unknown"
40
40
 
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.
41
+ # #1770: honor CLAUDE_PROJECT_DIR first; fallback git toplevel (or pwd), not script location.
48
42
  PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
49
43
  CLAUDE_MD="$PROJECT_ROOT/CLAUDE.md"
50
44
 
@@ -29,67 +29,22 @@ if ! command -v jq >/dev/null 2>&1; then
29
29
  exit 0
30
30
  fi
31
31
 
32
- # ── 부모 필드 추출: 한 jq 생산자, 독립 오류 fallback 유지 ──
33
- # jq -j는 session_id의 원래 raw/pretty 출력을 유지한다. 문자열의 실제 NUL만
34
- # 제거하여 필드 구분자와 충돌하지 않게 하고, 각 필드의 끝 LF는 기존 command
35
- # substitution처럼 제거한다. JSON 문자열 안의 escaped NUL은 그대로 유지한다.
36
- # slurp 후에도 입력별 출력 순서와 오류 전 부분 출력 + fallback을 유지한다.
37
- # process substitution의 jq 실패를 잃지 않도록 별도 종료 상태 필드를 보낸다.
38
- parent_fields=()
39
- while IFS= read -r -d '' parent_field; do
40
- parent_fields[${#parent_fields[@]}]="$parent_field"
41
- done < <(
42
- if printf '%s\n' "$input" | jq -sj '
43
- . as $inputs
44
- | [try ($inputs[] | .background_tasks // [] | tojson) catch "[]"] as $bg
45
- | [try ($inputs[] | .session_crons // [] | tojson) catch "[]"] as $crons
46
- | [try ($bg[] | fromjson | length) catch 0] as $bg_count
47
- | [try ($crons[] | fromjson | length) catch 0] as $cron_count
48
- | [try ($bg[] | fromjson
49
- | [.[] | select(.status? == "running" or .status? == "pending" or .status? == "in_progress")]
50
- | length) catch 0] as $dangling
51
- | ($inputs[] | .session_id // empty
52
- | if type == "string" then gsub("\u0000"; "") else . end
53
- | (., "\n")),
54
- "\u0000",
55
- ($bg | join("\n")), "\u0000",
56
- ($crons | join("\n")), "\u0000",
57
- ($bg_count | map(tostring) | join("\n")), "\u0000",
58
- ($cron_count | map(tostring) | join("\n")), "\u0000",
59
- ($dangling | map(tostring) | join("\n")), "\u0000"
60
- ' 2>/dev/null; then
61
- parent_status=0
62
- else
63
- parent_status=$?
64
- fi
65
- # An extra separator terminates partial last fields from a failed producer.
66
- printf '\0%s\0' "$parent_status"
67
- )
68
- parent_status="${parent_fields[${#parent_fields[@]}-1]}"
69
- if [ "$parent_status" -ne 0 ]; then
70
- exit "$parent_status"
71
- fi
72
- if [ "${#parent_fields[@]}" -ne 8 ] || [ -n "${parent_fields[6]}" ]; then
73
- exit 1
74
- fi
75
- for parent_index in 0 1 2 3 4 5; do
76
- while [[ "${parent_fields[$parent_index]}" == *$'\n' ]]; do
77
- parent_fields[$parent_index]="${parent_fields[$parent_index]%$'\n'}"
78
- done
79
- done
80
- session_id="${parent_fields[0]}"
32
+ # ── session_id 추출 ──
33
+ session_id=$(printf '%s\n' "$input" | jq -r '.session_id // empty' 2>/dev/null)
81
34
  if [ -z "$session_id" ]; then
82
35
  printf '%s\n' "$input"
83
36
  exit 0
84
37
  fi
85
38
 
86
- # ── Phase 2 (#1196): background_tasks / session_crons (CC v2.1.145+) ──
87
- # Missing/null/false fields retain [], and each count retains its own fallback.
88
- bg_tasks_json="${parent_fields[1]}"
89
- session_crons_json="${parent_fields[2]}"
90
- bg_tasks_count="${parent_fields[3]}"
91
- session_crons_count="${parent_fields[4]}"
92
- bg_dangling_count="${parent_fields[5]}"
39
+ # ── Phase 2 (#1196): background_tasks / session_crons 추출 (CC v2.1.145+) ──
40
+ # 신규 필드 미존재 시 빈 배열로 처리 (graceful degrade).
41
+ bg_tasks_json=$(printf '%s\n' "$input" | jq -c '.background_tasks // []' 2>/dev/null || echo '[]')
42
+ session_crons_json=$(printf '%s\n' "$input" | jq -c '.session_crons // []' 2>/dev/null || echo '[]')
43
+ bg_tasks_count=$(printf '%s\n' "$bg_tasks_json" | jq 'length' 2>/dev/null || echo 0)
44
+ session_crons_count=$(printf '%s\n' "$session_crons_json" | jq 'length' 2>/dev/null || echo 0)
45
+
46
+ # 미완료 백그라운드 태스크: running/pending/in_progress 상태 카운트
47
+ bg_dangling_count=$(printf '%s\n' "$bg_tasks_json" | jq '[.[] | select(.status? == "running" or .status? == "pending" or .status? == "in_progress")] | length' 2>/dev/null || echo 0)
93
48
 
94
49
  # ── 경로 결정 (환경변수 override 지원) ──
95
50
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -284,8 +239,7 @@ WORKER_EOF
284
239
  chmod +x "$WORKER_SCRIPT"
285
240
 
286
241
  # ── 백그라운드로 실행 (Stop hook 시간 예산 <3s 준수) ──
287
- # 전체 subshell의 표준 입출력을 분리하여 부모의 파이프를 붙들지 않는다.
288
- # 분석은 비동기이며 완료 후 임시 파일을 제거한다. setsid/session 생존 보장은 없다.
242
+ # setsid로 부모 종료 후에도 실행 지속; 완료 후 임시 파일 자정리
289
243
  WORKER_ERR_LOG="/tmp/.claude-reflection-err-${PPID}.log"
290
244
 
291
245
  (
@@ -296,9 +250,10 @@ WORKER_ERR_LOG="/tmp/.claude-reflection-err-${PPID}.log"
296
250
  "$SCRIPT_DIR" \
297
251
  "$bg_tasks_json" \
298
252
  "$session_crons_json" \
299
- "$bg_dangling_count"
253
+ "$bg_dangling_count" \
254
+ 2>>"$WORKER_ERR_LOG"
300
255
  rm -f "$WORKER_SCRIPT"
301
- ) </dev/null >/dev/null 2>>"$WORKER_ERR_LOG" &
256
+ ) &
302
257
  disown $! 2>/dev/null || true
303
258
 
304
259
  # ── 즉시 stdin pass-through 후 exit 0 (Stop hook 체인 유지) ──
@@ -536,92 +536,8 @@ input=$(cat)
536
536
  # Swallow it instead: exit 0 with no stdout, exactly as if the hook had not
537
537
  # run. "jq -e" exits non-zero both for a parse error and for a false/null
538
538
  # result, so one guard covers every non-object shape.
539
- # Common string fields can share one jq producer. Unusual types and embedded
540
- # NUL retain the original extraction path: command substitution has platform-
541
- # specific NUL handling, and independent jq failures are part of the contract.
542
- extraction_mode=$(printf '%s' "$input" | jq -er '
543
- if type != "object" then false
544
- elif any(.. | strings; contains("\u0000")) then "legacy"
545
- elif ((.tool_name // "unknown" | type) != "string") then "legacy"
546
- elif ((.tool_input != null) and ((.tool_input | type) != "object")) then "legacy"
547
- elif ((.tool_output != null) and ((.tool_output | type) != "object")) then "legacy"
548
- elif ((.tool_input.file_path // "" | type) != "string")
549
- or ((.tool_input.command // "" | type) != "string")
550
- or ((.tool_input.old_string // .tool_input.content // .tool_input.new_string // "" | type) != "string")
551
- then "legacy" else "batched" end
552
- ' 2>/dev/null) || exit 0
553
-
554
- # NUL-delimited fields are safe only after the guard above excludes embedded
555
- # NUL. A final producer-status record makes jq failures observable; process
556
- # substitution alone would discard its exit status. jq -j preserves -r scalar
557
- # rendering without an added newline; trim actual trailing LF just as $(...) did.
558
- _read_stuck_fields() {
559
- local filter="$1" field status
560
- STUCK_FIELDS=()
561
- while IFS= read -r -d '' field; do
562
- while [[ "$field" == *$'\n' ]]; do field="${field%$'\n'}"; done
563
- STUCK_FIELDS[${#STUCK_FIELDS[@]}]="$field"
564
- done < <(
565
- if printf '%s' "$input" | jq -j "$filter"; then
566
- printf '%s\0' '__OMCUSTOM_JQ_STATUS__0'
567
- else
568
- printf '__OMCUSTOM_JQ_STATUS__%s\0' "$?"
569
- fi
570
- )
571
- status="${STUCK_FIELDS[${#STUCK_FIELDS[@]}-1]:-}"
572
- case "$status" in
573
- __OMCUSTOM_JQ_STATUS__*) status="${status#__OMCUSTOM_JQ_STATUS__}" ;;
574
- *) return 1 ;;
575
- esac
576
- unset "STUCK_FIELDS[$((${#STUCK_FIELDS[@]} - 1))]"
577
- return "$status"
578
- }
579
-
580
- TARGET_KEY_CAP=1000
581
-
582
- truncate_key() {
583
- local v="$1"
584
- if [ "${#v}" -gt "$TARGET_KEY_CAP" ]; then
585
- printf '%s' "${v:0:$TARGET_KEY_CAP}#len=${#v}"
586
- else
587
- printf '%s' "$v"
588
- fi
589
- }
539
+ printf '%s' "$input" | jq -e 'type == "object"' >/dev/null 2>&1 || exit 0
590
540
 
591
- # Batched extraction changes no classifier or detector inputs. Preview retains
592
- # its original byte-cap pipeline, including broken-pipe tolerance.
593
- if [ "$extraction_mode" = "batched" ]; then
594
- _read_stuck_fields '(.tool_name // "unknown"), "\u0000",
595
- (if (.tool_name // "unknown") == "Bash"
596
- then (.tool_input.command // .tool_input.file_path // "")
597
- else (.tool_input.file_path // "") end), "\u0000",
598
- (.tool_output.is_error // false), "\u0000"'
599
- [ "${#STUCK_FIELDS[@]}" -eq 3 ] || exit 1
600
- tool_name="${STUCK_FIELDS[0]}"
601
- if [ "$tool_name" = "Bash" ]; then
602
- file_path=""
603
- target_key=$(truncate_key "${STUCK_FIELDS[1]}")
604
- else
605
- file_path_full="${STUCK_FIELDS[1]}"
606
- file_path=$(printf '%s' "$file_path_full" | head -c "$TARGET_KEY_CAP" || true)
607
- target_key=$(truncate_key "$file_path_full")
608
- fi
609
- is_error="${STUCK_FIELDS[2]}"
610
- output_preview=$(printf '%s' "$input" \
611
- | jq -r '(.tool_output.output // "" | tostring)[0:200]' \
612
- | head -c 200 || true)
613
- _read_stuck_fields '(.tool_input.command // ""), "\u0000",
614
- (if (.tool_name == "Edit" or .tool_name == "Write")
615
- then ((.tool_input.old_string // .tool_input.content // .tool_input.new_string // "")
616
- | gsub("[[:space:]]+"; " ") | ltrimstr(" ") | rtrimstr(" ")
617
- | (length as $l | if $l <= 120 then "\($l)#\(.)"
618
- else ((explode | add // 0) as $sum
619
- | "\($l)#\($sum)#\(.[0:80])#\(.[-40:])") end))
620
- else "" end), "\u0000"'
621
- [ "${#STUCK_FIELDS[@]}" -eq 2 ] || exit 1
622
- raw_command="${STUCK_FIELDS[0]}"
623
- edit_hash="${STUCK_FIELDS[1]}"
624
- else
625
541
  # Extract tool info
626
542
  tool_name=$(printf '%s' "$input" | jq -r '.tool_name // "unknown"')
627
543
  # Bash commands are NOT files. Feeding tool_input.command into file_path made
@@ -636,6 +552,16 @@ tool_name=$(printf '%s' "$input" | jq -r '.tool_name // "unknown"')
636
552
  # remove that collision class, so truncate_key() ALSO appends the ORIGINAL
637
553
  # length: two values that share the capped prefix but differ in total length
638
554
  # no longer produce the same key (M-3).
555
+ TARGET_KEY_CAP=1000
556
+
557
+ truncate_key() {
558
+ local v="$1"
559
+ if [ "${#v}" -gt "$TARGET_KEY_CAP" ]; then
560
+ printf '%s' "${v:0:$TARGET_KEY_CAP}#len=${#v}"
561
+ else
562
+ printf '%s' "$v"
563
+ fi
564
+ }
639
565
 
640
566
  if [ "$tool_name" = "Bash" ]; then
641
567
  file_path=""
@@ -701,35 +627,13 @@ if [ "$tool_name" = "Edit" ] || [ "$tool_name" = "Write" ]; then
701
627
  end)')
702
628
  fi
703
629
 
704
- fi
705
-
706
630
  # History entries are written by jq, so their "path" values are JSON-ENCODED.
707
631
  # Matching them requires the SAME encoding, not the raw text: a raw value
708
632
  # containing a quote (common in Bash commands) can never match the encoded
709
633
  # form. The previous BRE escaping was doubly broken — in POSIX BRE "( ) + ? { |"
710
634
  # are literals, so escaping them turned them INTO operators and silently
711
635
  # stopped matching. Encode once here and match with grep -F (fixed string).
712
- # JSON-encoded literals contain no raw NUL or LF, including legacy fields.
713
- encoded_matches=()
714
- while IFS= read -r -d '' encoded_match; do
715
- encoded_matches[${#encoded_matches[@]}]="$encoded_match"
716
- done < <(
717
- if jq -nj --arg path "$target_key" --arg edit "$edit_hash" \
718
- '($path | tojson), "\u0000", ($edit | tojson), "\u0000"'; then
719
- printf '%s\0' '__OMCUSTOM_JQ_STATUS__0'
720
- else
721
- printf '__OMCUSTOM_JQ_STATUS__%s\0' "$?"
722
- fi
723
- )
724
- encoded_status="${encoded_matches[${#encoded_matches[@]}-1]:-}"
725
- case "$encoded_status" in
726
- __OMCUSTOM_JQ_STATUS__0) ;;
727
- __OMCUSTOM_JQ_STATUS__*) exit "${encoded_status#__OMCUSTOM_JQ_STATUS__}" ;;
728
- *) exit 1 ;;
729
- esac
730
- [ "${#encoded_matches[@]}" -eq 3 ] || exit 1
731
- target_key_json="${encoded_matches[0]}"
732
- edit_hash_json="${encoded_matches[1]}"
636
+ target_key_json=$(jq -n --arg v "$target_key" '$v')
733
637
  path_match="\"path\":${target_key_json}"
734
638
 
735
639
  # #1649 follow-up: edit_hash now preserves punctuation (see the edit_hash
@@ -738,7 +642,7 @@ path_match="\"path\":${target_key_json}"
738
642
  # (--arg ehash "$edit_hash" -> jq serializes it), so Check 1 / Check 3 below
739
643
  # must match it with the SAME JSON-encoded literal, not the raw string — same
740
644
  # fix as path_match above, applied to edit_hash.
741
- # edit_hash_json was encoded together with target_key_json above.
645
+ edit_hash_json=$(jq -n --arg v "$edit_hash" '$v')
742
646
  edit_hash_match="\"edit_hash\":${edit_hash_json}"
743
647
 
744
648
  is_readonly="false"
@@ -747,7 +651,7 @@ if [ "$tool_name" = "Bash" ]; then
747
651
  fi
748
652
 
749
653
  # Session-scoped history
750
- HISTORY_FILE="${OMCUSTOM_STUCK_HISTORY_FILE:-/tmp/.claude-tool-history-${PPID}}"
654
+ HISTORY_FILE="/tmp/.claude-tool-history-${PPID}"
751
655
 
752
656
  # Create entry
753
657
  timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)
@@ -86,7 +86,7 @@ zsh는 미인용 `$var`를 단어분할하지 않습니다(#1683 #5) — 분할
86
86
 
87
87
  도구 이름 ≠ 그 프로그램(#1590): 사용 전 `type <tool>`로 실체를 확인합니다 — 이 저장소 Bash의 `grep`은 `.gitignore`를 존중하는 셸 함수이므로 저장소 전수조사는 `git grep`을 표준으로 합니다.
88
88
 
89
- Bash `cd`는 세션 cwd로 남습니다(#1778, #1767): 훅 명령은 `CLAUDE_PROJECT_DIR`로 고정되어 지속 `cd`에 영향받지 않지만, 상대 경로를 내부에서 읽는 스크립트·도구와 이후 명령의 상대 경로는 어긋날 수 있습니다 — 절대 경로나 서브셸 `( cd <dir> && … )`만 사용하고 지속 `cd`는 남기지 않습니다. `cd X && ( … )`도 `cd`가 서브셸 밖에 놓인 지속 `cd`이므로 금지합니다(#1816) — 괄호는 `cd` 앞에서 열어야 합니다.
89
+ Bash `cd`는 세션 cwd로 남습니다(#1778, #1767): 훅 명령은 `CLAUDE_PROJECT_DIR`로 고정되어 지속 `cd`에 영향받지 않지만, 상대 경로를 내부에서 읽는 스크립트·도구와 이후 명령의 상대 경로는 어긋날 수 있습니다 — 절대 경로나 서브셸 `( cd <dir> && … )`만 사용하고 지속 `cd`는 남기지 않습니다.
90
90
 
91
91
  <!-- DETAIL: tool name vs program
92
92
  > **도구 이름 ≠ 그 프로그램 (#1590)**: 도구를 쓰기 전에 `type <tool>`로 실체를 확인한다. Bash 도구의 `grep`은 `~/.claude/shell-snapshots/snapshot-zsh-*.sh`의 **셸 함수**이며 `ugrep --ignore-files`에 위임한다. 그 결과 `.gitignore`의 리터럴 `CLAUDE.md` 패턴을 존중해, **force-tracked 파일을 재귀 탐색에서 조용히 누락**한다(에러 없이 exit 0). 명시 경로를 준 grep은 정상 동작하므로 **traversal만 영향**을 받는다. 실측(2026-08-15): 동일 패턴·동일 대상에 대해 셸 함수 36 / `command grep` 43 / `git grep` 38 히트 — 셸 함수만 `CLAUDE.md`를 0 히트로 놓쳤다. 진단 함정: `git check-ignore`는 **index-aware**라 tracked 파일에 "not ignored"(exit 1)를 반환한다 — 원인을 보려면 `git check-ignore --no-index`를 써야 한다. 처방: 저장소 전수 조사는 `git grep`을 표준으로 한다(R017 Count Sync cross-ref). Origin: #1590.
@@ -259,7 +259,7 @@ MEMORY.md supports an optional `## User Model` section (DISTINCT from `## Behavi
259
259
  - Focus: AI agent orchestration, CLI tooling
260
260
 
261
261
  ### Override Decisions [confidence: low]
262
- - Overrode /scout SKIP verdict → INTEGRATE for RTK (#756) (과거 결정 예시입니다. RTK 프로젝트 통합은 v1.1.107에서 퇴역하며 현재 실행 추천으로 해석하지 마십시오.)
262
+ - Overrode /scout SKIP verdict → INTEGRATE for RTK (#756) (과거 결정 예시입니다. RTK 프로젝트 통합은 v2.0.0에서 퇴역하며 현재 실행 추천으로 해석하지 마십시오.)
263
263
 
264
264
  ### Categories
265
265
 
@@ -101,15 +101,6 @@ Fable 5 실행 에이전트/스킬은 지시(instruction) 장문화가 오히려
101
101
  - [ ] Tier 1에서 잡을 수 있는 문제를 다루는가? (상위 tier 대신 시프트 권고)
102
102
  - [ ] Ladder 순서를 문서화했는가? (어떤 검사를 먼저 실행하는지)
103
103
 
104
- ### 문구 작성·정정
105
-
106
- 새 주석/JSDoc/문서의 문구 작성·정정 시 확인합니다(#1816 H1):
107
-
108
- - [ ] 새 문구의 사실 절마다 코드 앵커 또는 실제 관측 출력을 대응표에 기록했는가?
109
- - [ ] 독립 문구 검토로 사실 절과 근거의 대응을 확인했는가?
110
-
111
- lint·typecheck·테스트 통과만으로 문구의 사실 정확성을 확정하지 않습니다. pipeline auto-dev의 문구 검토 요구와 함께 확인합니다.
112
-
113
104
  ## Safety-Signal Rule Authoring — Carve-Out Pre-Check (shift-left)
114
105
 
115
106
  <!-- DETAIL: Origin note
@@ -1446,11 +1446,9 @@ steps:
1446
1446
  re-running step d then step e, re-staging, and re-running this assertion.
1447
1447
 
1448
1448
  Version decision (semver) — PATCH-PREFERRED policy (post-1.0.0):
1449
- - The user's explicit version or bump-level judgment takes precedence. Approval to remove a feature or change hooks is not approval for a major or minor version.
1450
1449
  - No existing tags → v0.1.0
1451
1450
  - Previous tag exists → DEFAULT to patch. When in doubt, patch — do NOT reflexively bump minor for rule/doc/skill edits.
1452
1451
  - patch (DEFAULT): rule/doc/skill/config/wiki/workflow changes, bugfixes, hardening, CC-compat notes — the vast majority of this project's changes
1453
- - Public skill/agent/integration additions and removals follow this repository's PATCH-PREFERRED lifecycle policy, including RTK integration and /rtk-exec retirement. Names, files, or counts appearing or disappearing alone do not justify a major or minor bump.
1454
1452
  - minor: ONLY a new user-facing capability that changes HOW the harness is used — a new
1455
1453
  workflow axis, a new command surface users must learn, or a contract other components
1456
1454
  depend on. NOT "a file was added under .claude/skills/ or .claude/agents/".
@@ -1462,7 +1460,7 @@ steps:
1462
1460
  baseline rate of change, NOT a minor signal. If a version bump would follow
1463
1461
  mechanically from "a new file exists", it is patch. Ask instead: does a user's
1464
1462
  workflow change because of this?
1465
- - major: actual compatibility breaks in existing CLI flags or public APIs, or an explicit major milestone declaration (e.g., v1.0.0). Skill/agent/integration lifecycle changes alone are excluded; the user's explicit version or bump-level judgment takes precedence.
1463
+ - major: breaking changes to user-facing contracts (CLI flags, public skill/command names, public API), or a deliberate milestone declaration (e.g., v1.0.0)
1466
1464
  - Previous tag is ahead of source version (e.g., tag v0.136.1, package.json 0.136.0): use next available skip-version (0.136.2)
1467
1465
 
1468
1466
  2. Release notes via omcustom-release-notes skill
@@ -183,7 +183,7 @@ v1.1.29(#1484)에서 제거되었습니다.
183
183
 
184
184
  DEFER 기간 동안의 cross-tool integration 우회 경로:
185
185
 
186
- 아래 대안과 use case 범위는 당시 결정 이력으로 보존합니다. RTK 프로젝트 통합은 v1.1.107에서 퇴역하므로 RTK proxy skill을 현재 실행 경로로 사용하지 마십시오. 이 기록의 효과 범위를 현재 버전에서 새로 실측했다는 뜻은 아닙니다.
186
+ 아래 대안과 use case 범위는 당시 결정 이력으로 보존합니다. RTK 프로젝트 통합은 v2.0.0에서 퇴역하므로 RTK proxy skill을 현재 실행 경로로 사용하지 마십시오. 이 기록의 효과 범위를 현재 버전에서 새로 실측했다는 뜻은 아닙니다.
187
187
 
188
188
  - **RTK proxy skill**: 기존 infra 활용, Cursor 등에서 oh-my-customcode skill 호출
189
189
  - **Codex/Gemini exec skill**: 외부 LLM executor를 skill로 래핑, harness 종속성 없음
@@ -8,7 +8,7 @@ Inspired by OpenHarness's provider profile switching pattern, adapted for oh-my-
8
8
 
9
9
  ## Provider Matrix
10
10
 
11
- v1.1.107에서는 사용자 결정에 따라 프로젝트 RTK 통합과 공개 `rtk-exec` 스킬을 퇴역합니다. 이 가이드의 RTK 실행 예시·추천을 사용하지 마십시오. 머신에 별도로 설치한 RTK 바이너리는 이 변경으로 제거하지 않습니다.
11
+ v2.0.0에서는 사용자 결정에 따라 프로젝트 RTK 통합과 공개 `rtk-exec` 스킬을 퇴역합니다. 이 가이드의 RTK 실행 예시·추천을 사용하지 마십시오. 머신에 별도로 설치한 RTK 바이너리는 이 변경으로 제거하지 않습니다.
12
12
 
13
13
  ## Availability Detection
14
14
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.1.107",
2
+ "version": "2.0.0",
3
3
  "lastUpdated": "2026-10-06",
4
4
  "omcustomMinClaudeCode": "2.1.121",
5
5
  "omcustomMinClaudeCodeReason": "Sensitive-path direct Write/Edit on .claude/** under bypassPermissions (R010 deprecation, #1101)",
@@ -1446,11 +1446,9 @@ steps:
1446
1446
  re-running step d then step e, re-staging, and re-running this assertion.
1447
1447
 
1448
1448
  Version decision (semver) — PATCH-PREFERRED policy (post-1.0.0):
1449
- - The user's explicit version or bump-level judgment takes precedence. Approval to remove a feature or change hooks is not approval for a major or minor version.
1450
1449
  - No existing tags → v0.1.0
1451
1450
  - Previous tag exists → DEFAULT to patch. When in doubt, patch — do NOT reflexively bump minor for rule/doc/skill edits.
1452
1451
  - patch (DEFAULT): rule/doc/skill/config/wiki/workflow changes, bugfixes, hardening, CC-compat notes — the vast majority of this project's changes
1453
- - Public skill/agent/integration additions and removals follow this repository's PATCH-PREFERRED lifecycle policy, including RTK integration and /rtk-exec retirement. Names, files, or counts appearing or disappearing alone do not justify a major or minor bump.
1454
1452
  - minor: ONLY a new user-facing capability that changes HOW the harness is used — a new
1455
1453
  workflow axis, a new command surface users must learn, or a contract other components
1456
1454
  depend on. NOT "a file was added under .claude/skills/ or .claude/agents/".
@@ -1462,7 +1460,7 @@ steps:
1462
1460
  baseline rate of change, NOT a minor signal. If a version bump would follow
1463
1461
  mechanically from "a new file exists", it is patch. Ask instead: does a user's
1464
1462
  workflow change because of this?
1465
- - major: actual compatibility breaks in existing CLI flags or public APIs, or an explicit major milestone declaration (e.g., v1.0.0). Skill/agent/integration lifecycle changes alone are excluded; the user's explicit version or bump-level judgment takes precedence.
1463
+ - major: breaking changes to user-facing contracts (CLI flags, public skill/command names, public API), or a deliberate milestone declaration (e.g., v1.0.0)
1466
1464
  - Previous tag is ahead of source version (e.g., tag v0.136.1, package.json 0.136.0): use next available skip-version (0.136.2)
1467
1465
 
1468
1466
  2. Release notes via omcustom-release-notes skill