oh-my-customcode 0.49.0 → 0.50.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
@@ -13,7 +13,7 @@
13
13
 
14
14
  **[한국어 문서 (Korean)](./README_ko.md)**
15
15
 
16
- 45 agents. 89 skills. 21 rules. One command.
16
+ 45 agents. 90 skills. 21 rules. One command.
17
17
 
18
18
  ```bash
19
19
  npm install -g oh-my-customcode && cd your-project && omcustom init
@@ -138,7 +138,7 @@ Each agent declares its tools, model, memory scope, and limitations in YAML fron
138
138
 
139
139
  ---
140
140
 
141
- ### Skills (89)
141
+ ### Skills (90)
142
142
 
143
143
  | Category | Count | Includes |
144
144
  |----------|-------|----------|
@@ -274,7 +274,7 @@ your-project/
274
274
  ├── CLAUDE.md # Entry point
275
275
  ├── .claude/
276
276
  │ ├── agents/ # 45 agent definitions
277
- │ ├── skills/ # 89 skill modules
277
+ │ ├── skills/ # 90 skill modules
278
278
  │ ├── rules/ # 21 governance rules (R000-R021)
279
279
  │ ├── hooks/ # 15 lifecycle hook scripts
280
280
  │ ├── schemas/ # Tool input validation schemas
package/dist/cli/index.js CHANGED
@@ -9323,7 +9323,7 @@ var init_package = __esm(() => {
9323
9323
  package_default = {
9324
9324
  name: "oh-my-customcode",
9325
9325
  workspaces: ["packages/*"],
9326
- version: "0.49.0",
9326
+ version: "0.50.0",
9327
9327
  description: "Batteries-included agent harness for Claude Code",
9328
9328
  type: "module",
9329
9329
  bin: {
@@ -25838,6 +25838,7 @@ var MESSAGES = {
25838
25838
  "update.file_applied": "Applied update to {{path}}",
25839
25839
  "update.lockfile_regenerated": "Lockfile regenerated ({{files}} files tracked)",
25840
25840
  "update.lockfile_failed": "Failed to regenerate lockfile: {{error}}",
25841
+ "update.protected_file_updated": "⟳ Protected file {{file}} in {{component}} updated: {{hint}}",
25841
25842
  "config.load_failed": "Failed to load config: {{error}}",
25842
25843
  "config.not_found": "Config not found at {{path}}, using defaults",
25843
25844
  "config.saved": "Config saved to {{path}}",
@@ -25882,6 +25883,7 @@ var MESSAGES = {
25882
25883
  "update.file_applied": "{{path}} 업데이트 적용",
25883
25884
  "update.lockfile_regenerated": "잠금 파일 재생성 완료 ({{files}}개 파일 추적)",
25884
25885
  "update.lockfile_failed": "잠금 파일 재생성 실패: {{error}}",
25886
+ "update.protected_file_updated": "⟳ 보호 파일 {{file}} ({{component}}) 업데이트됨: {{hint}}",
25885
25887
  "config.load_failed": "설정 로드 실패: {{error}}",
25886
25888
  "config.not_found": "{{path}}에 설정 없음, 기본값 사용",
25887
25889
  "config.saved": "설정 저장: {{path}}",
@@ -29933,7 +29935,7 @@ async function handleBackupIfRequested(targetDir, backup, result) {
29933
29935
  result.backedUpPaths.push(backupPath);
29934
29936
  info("update.backup_created", { path: backupPath });
29935
29937
  }
29936
- async function processComponentUpdate(targetDir, component, updateCheck, customizations, options, result, config) {
29938
+ async function processComponentUpdate(targetDir, component, updateCheck, customizations, options, result, config, lockfile) {
29937
29939
  const componentUpdate = updateCheck.updatableComponents.find((c) => c.name === component);
29938
29940
  if (!componentUpdate && !options.force) {
29939
29941
  result.skippedComponents.push(component);
@@ -29945,7 +29947,7 @@ async function processComponentUpdate(targetDir, component, updateCheck, customi
29945
29947
  return;
29946
29948
  }
29947
29949
  try {
29948
- const preserved = await updateComponent(targetDir, component, customizations, options, config);
29950
+ const preserved = await updateComponent(targetDir, component, customizations, options, config, lockfile);
29949
29951
  result.updatedComponents.push(component);
29950
29952
  result.preservedFiles.push(...preserved);
29951
29953
  } catch (err) {
@@ -29954,9 +29956,9 @@ async function processComponentUpdate(targetDir, component, updateCheck, customi
29954
29956
  result.skippedComponents.push(component);
29955
29957
  }
29956
29958
  }
29957
- async function updateAllComponents(targetDir, components, updateCheck, customizations, options, result, config) {
29959
+ async function updateAllComponents(targetDir, components, updateCheck, customizations, options, result, config, lockfile) {
29958
29960
  for (const component of components) {
29959
- await processComponentUpdate(targetDir, component, updateCheck, customizations, options, result, config);
29961
+ await processComponentUpdate(targetDir, component, updateCheck, customizations, options, result, config, lockfile);
29960
29962
  }
29961
29963
  }
29962
29964
  function getEntryTemplateName2(language) {
@@ -30138,8 +30140,9 @@ async function update(options) {
30138
30140
  const manifestCustomizations = await resolveManifestCustomizations(options, options.targetDir);
30139
30141
  const configPreserveFiles = resolveConfigPreserveFiles(options, config);
30140
30142
  const customizations = resolveCustomizations(manifestCustomizations, configPreserveFiles, options.targetDir);
30143
+ const lockfile = await readLockfile(options.targetDir);
30141
30144
  const components = options.components || getAllUpdateComponents();
30142
- await updateAllComponents(options.targetDir, components, updateCheck, customizations, options, result, config);
30145
+ await updateAllComponents(options.targetDir, components, updateCheck, customizations, options, result, config, lockfile);
30143
30146
  await runFullUpdatePostProcessing(options, result, config);
30144
30147
  const lockfileResult = await generateAndWriteLockfileForDir(options.targetDir);
30145
30148
  if (lockfileResult.warning) {
@@ -30200,15 +30203,46 @@ async function componentHasUpdate(_targetDir, component, config) {
30200
30203
  const latestVersion = await getLatestVersion();
30201
30204
  return installedVersion !== latestVersion;
30202
30205
  }
30203
- async function collectProtectedSkipPaths(srcPath, destPath, componentPath, forceOverwriteAll) {
30206
+ async function shouldSkipProtectedFile(targetFilePath, lockfileKey, lockfile) {
30207
+ if (!lockfile) {
30208
+ return true;
30209
+ }
30210
+ const lockfileEntry = lockfile.files[lockfileKey];
30211
+ if (!lockfileEntry) {
30212
+ return false;
30213
+ }
30214
+ if (!await fileExists(targetFilePath)) {
30215
+ return false;
30216
+ }
30217
+ try {
30218
+ const currentHash = await computeFileHash(targetFilePath);
30219
+ return currentHash !== lockfileEntry.templateHash;
30220
+ } catch {
30221
+ return true;
30222
+ }
30223
+ }
30224
+ async function collectProtectedSkipPaths(srcPath, destPath, componentPath, forceOverwriteAll, lockfile, targetDir) {
30204
30225
  if (forceOverwriteAll) {
30205
- const warnedPaths = await findProtectedFilesInDir(srcPath, componentPath);
30206
- return { skipPaths: [], warnedPaths };
30226
+ const warnedPaths2 = await findProtectedFilesInDir(srcPath, componentPath);
30227
+ return { skipPaths: [], warnedPaths: warnedPaths2, updatedPaths: [] };
30207
30228
  }
30208
30229
  const protectedRelative = await findProtectedFilesInDir(srcPath, componentPath);
30209
30230
  const path3 = await import("node:path");
30210
- const skipPaths = protectedRelative.map((p) => path3.relative(destPath, join14(destPath, p)));
30211
- return { skipPaths, warnedPaths: protectedRelative };
30231
+ const skipPaths = [];
30232
+ const warnedPaths = [];
30233
+ const updatedPaths = [];
30234
+ for (const p of protectedRelative) {
30235
+ const targetFilePath = join14(targetDir, componentPath, p);
30236
+ const lockfileKey = `${componentPath}/${p}`.replace(/\\/g, "/");
30237
+ const shouldSkip = await shouldSkipProtectedFile(targetFilePath, lockfileKey, lockfile);
30238
+ if (shouldSkip) {
30239
+ skipPaths.push(path3.relative(destPath, join14(destPath, p)));
30240
+ warnedPaths.push(p);
30241
+ } else {
30242
+ updatedPaths.push(p);
30243
+ }
30244
+ }
30245
+ return { skipPaths, warnedPaths, updatedPaths };
30212
30246
  }
30213
30247
  function isEntryProtected(relPath, componentRelativePrefix) {
30214
30248
  if (isProtectedFile(relPath)) {
@@ -30244,7 +30278,7 @@ async function findProtectedFilesInDir(dirPath, componentRelativePrefix) {
30244
30278
  }
30245
30279
  return protected_;
30246
30280
  }
30247
- async function updateComponent(targetDir, component, customizations, options, config) {
30281
+ async function updateComponent(targetDir, component, customizations, options, config, lockfile) {
30248
30282
  const preservedFiles = [];
30249
30283
  const componentPath = getComponentPath2(component);
30250
30284
  const srcPath = resolveTemplatePath(componentPath);
@@ -30261,7 +30295,11 @@ async function updateComponent(targetDir, component, customizations, options, co
30261
30295
  skipPaths.push(cc.path);
30262
30296
  }
30263
30297
  }
30264
- const { skipPaths: protectedSkipPaths, warnedPaths: protectedWarnedPaths } = await collectProtectedSkipPaths(srcPath, destPath, componentPath, !!options.forceOverwriteAll);
30298
+ const {
30299
+ skipPaths: protectedSkipPaths,
30300
+ warnedPaths: protectedWarnedPaths,
30301
+ updatedPaths: protectedUpdatedPaths
30302
+ } = await collectProtectedSkipPaths(srcPath, destPath, componentPath, !!options.forceOverwriteAll, lockfile, targetDir);
30265
30303
  for (const protectedPath of protectedWarnedPaths) {
30266
30304
  if (options.forceOverwriteAll) {
30267
30305
  warn("update.protected_file_force_overwrite", {
@@ -30273,10 +30311,17 @@ async function updateComponent(targetDir, component, customizations, options, co
30273
30311
  warn("update.protected_file_skipped", {
30274
30312
  file: protectedPath,
30275
30313
  component,
30276
- hint: "File contains AI behavioral constraints and was not updated. Use --force-overwrite-all to override."
30314
+ hint: "File was modified by user and preserved. Use --force-overwrite-all to override."
30277
30315
  });
30278
30316
  }
30279
30317
  }
30318
+ for (const updatedPath of protectedUpdatedPaths) {
30319
+ info("update.protected_file_updated", {
30320
+ file: updatedPath,
30321
+ component,
30322
+ hint: "Protected file updated (unmodified by user, matches lockfile hash)."
30323
+ });
30324
+ }
30280
30325
  skipPaths.push(...protectedSkipPaths);
30281
30326
  const path3 = await import("node:path");
30282
30327
  const normalizedSkipPaths = skipPaths.map((p) => path3.relative(destPath, join14(targetDir, p)));
package/dist/index.js CHANGED
@@ -376,6 +376,7 @@ var MESSAGES = {
376
376
  "update.file_applied": "Applied update to {{path}}",
377
377
  "update.lockfile_regenerated": "Lockfile regenerated ({{files}} files tracked)",
378
378
  "update.lockfile_failed": "Failed to regenerate lockfile: {{error}}",
379
+ "update.protected_file_updated": "⟳ Protected file {{file}} in {{component}} updated: {{hint}}",
379
380
  "config.load_failed": "Failed to load config: {{error}}",
380
381
  "config.not_found": "Config not found at {{path}}, using defaults",
381
382
  "config.saved": "Config saved to {{path}}",
@@ -420,6 +421,7 @@ var MESSAGES = {
420
421
  "update.file_applied": "{{path}} 업데이트 적용",
421
422
  "update.lockfile_regenerated": "잠금 파일 재생성 완료 ({{files}}개 파일 추적)",
422
423
  "update.lockfile_failed": "잠금 파일 재생성 실패: {{error}}",
424
+ "update.protected_file_updated": "⟳ 보호 파일 {{file}} ({{component}}) 업데이트됨: {{hint}}",
423
425
  "config.load_failed": "설정 로드 실패: {{error}}",
424
426
  "config.not_found": "{{path}}에 설정 없음, 기본값 사용",
425
427
  "config.saved": "설정 저장: {{path}}",
@@ -1125,6 +1127,30 @@ function computeFileHash(filePath) {
1125
1127
  });
1126
1128
  });
1127
1129
  }
1130
+ async function readLockfile(targetDir) {
1131
+ const lockfilePath = join4(targetDir, LOCKFILE_NAME);
1132
+ const exists = await fileExists(lockfilePath);
1133
+ if (!exists) {
1134
+ debug("lockfile.not_found", { path: lockfilePath });
1135
+ return null;
1136
+ }
1137
+ try {
1138
+ const data = await readJsonFile(lockfilePath);
1139
+ if (typeof data !== "object" || data === null || data.lockfileVersion !== LOCKFILE_VERSION) {
1140
+ warn("lockfile.invalid_version", { path: lockfilePath });
1141
+ return null;
1142
+ }
1143
+ const record = data;
1144
+ if (typeof record.files !== "object" || record.files === null) {
1145
+ warn("lockfile.invalid_structure", { path: lockfilePath });
1146
+ return null;
1147
+ }
1148
+ return data;
1149
+ } catch (err) {
1150
+ warn("lockfile.read_failed", { path: lockfilePath, error: String(err) });
1151
+ return null;
1152
+ }
1153
+ }
1128
1154
  async function writeLockfile(targetDir, lockfile) {
1129
1155
  const lockfilePath = join4(targetDir, LOCKFILE_NAME);
1130
1156
  await writeJsonFile(lockfilePath, lockfile);
@@ -1642,7 +1668,7 @@ import { join as join6 } from "node:path";
1642
1668
  var package_default = {
1643
1669
  name: "oh-my-customcode",
1644
1670
  workspaces: ["packages/*"],
1645
- version: "0.49.0",
1671
+ version: "0.50.0",
1646
1672
  description: "Batteries-included agent harness for Claude Code",
1647
1673
  type: "module",
1648
1674
  bin: {
@@ -1875,7 +1901,7 @@ async function handleBackupIfRequested(targetDir, backup, result) {
1875
1901
  result.backedUpPaths.push(backupPath);
1876
1902
  info("update.backup_created", { path: backupPath });
1877
1903
  }
1878
- async function processComponentUpdate(targetDir, component, updateCheck, customizations, options, result, config) {
1904
+ async function processComponentUpdate(targetDir, component, updateCheck, customizations, options, result, config, lockfile) {
1879
1905
  const componentUpdate = updateCheck.updatableComponents.find((c) => c.name === component);
1880
1906
  if (!componentUpdate && !options.force) {
1881
1907
  result.skippedComponents.push(component);
@@ -1887,7 +1913,7 @@ async function processComponentUpdate(targetDir, component, updateCheck, customi
1887
1913
  return;
1888
1914
  }
1889
1915
  try {
1890
- const preserved = await updateComponent(targetDir, component, customizations, options, config);
1916
+ const preserved = await updateComponent(targetDir, component, customizations, options, config, lockfile);
1891
1917
  result.updatedComponents.push(component);
1892
1918
  result.preservedFiles.push(...preserved);
1893
1919
  } catch (err) {
@@ -1896,9 +1922,9 @@ async function processComponentUpdate(targetDir, component, updateCheck, customi
1896
1922
  result.skippedComponents.push(component);
1897
1923
  }
1898
1924
  }
1899
- async function updateAllComponents(targetDir, components, updateCheck, customizations, options, result, config) {
1925
+ async function updateAllComponents(targetDir, components, updateCheck, customizations, options, result, config, lockfile) {
1900
1926
  for (const component of components) {
1901
- await processComponentUpdate(targetDir, component, updateCheck, customizations, options, result, config);
1927
+ await processComponentUpdate(targetDir, component, updateCheck, customizations, options, result, config, lockfile);
1902
1928
  }
1903
1929
  }
1904
1930
  function getEntryTemplateName2(language) {
@@ -2080,8 +2106,9 @@ async function update(options) {
2080
2106
  const manifestCustomizations = await resolveManifestCustomizations(options, options.targetDir);
2081
2107
  const configPreserveFiles = resolveConfigPreserveFiles(options, config);
2082
2108
  const customizations = resolveCustomizations(manifestCustomizations, configPreserveFiles, options.targetDir);
2109
+ const lockfile = await readLockfile(options.targetDir);
2083
2110
  const components = options.components || getAllUpdateComponents();
2084
- await updateAllComponents(options.targetDir, components, updateCheck, customizations, options, result, config);
2111
+ await updateAllComponents(options.targetDir, components, updateCheck, customizations, options, result, config, lockfile);
2085
2112
  await runFullUpdatePostProcessing(options, result, config);
2086
2113
  const lockfileResult = await generateAndWriteLockfileForDir(options.targetDir);
2087
2114
  if (lockfileResult.warning) {
@@ -2163,15 +2190,46 @@ async function componentHasUpdate(_targetDir, component, config) {
2163
2190
  const latestVersion = await getLatestVersion();
2164
2191
  return installedVersion !== latestVersion;
2165
2192
  }
2166
- async function collectProtectedSkipPaths(srcPath, destPath, componentPath, forceOverwriteAll) {
2193
+ async function shouldSkipProtectedFile(targetFilePath, lockfileKey, lockfile) {
2194
+ if (!lockfile) {
2195
+ return true;
2196
+ }
2197
+ const lockfileEntry = lockfile.files[lockfileKey];
2198
+ if (!lockfileEntry) {
2199
+ return false;
2200
+ }
2201
+ if (!await fileExists(targetFilePath)) {
2202
+ return false;
2203
+ }
2204
+ try {
2205
+ const currentHash = await computeFileHash(targetFilePath);
2206
+ return currentHash !== lockfileEntry.templateHash;
2207
+ } catch {
2208
+ return true;
2209
+ }
2210
+ }
2211
+ async function collectProtectedSkipPaths(srcPath, destPath, componentPath, forceOverwriteAll, lockfile, targetDir) {
2167
2212
  if (forceOverwriteAll) {
2168
- const warnedPaths = await findProtectedFilesInDir(srcPath, componentPath);
2169
- return { skipPaths: [], warnedPaths };
2213
+ const warnedPaths2 = await findProtectedFilesInDir(srcPath, componentPath);
2214
+ return { skipPaths: [], warnedPaths: warnedPaths2, updatedPaths: [] };
2170
2215
  }
2171
2216
  const protectedRelative = await findProtectedFilesInDir(srcPath, componentPath);
2172
2217
  const path = await import("node:path");
2173
- const skipPaths = protectedRelative.map((p) => path.relative(destPath, join6(destPath, p)));
2174
- return { skipPaths, warnedPaths: protectedRelative };
2218
+ const skipPaths = [];
2219
+ const warnedPaths = [];
2220
+ const updatedPaths = [];
2221
+ for (const p of protectedRelative) {
2222
+ const targetFilePath = join6(targetDir, componentPath, p);
2223
+ const lockfileKey = `${componentPath}/${p}`.replace(/\\/g, "/");
2224
+ const shouldSkip = await shouldSkipProtectedFile(targetFilePath, lockfileKey, lockfile);
2225
+ if (shouldSkip) {
2226
+ skipPaths.push(path.relative(destPath, join6(destPath, p)));
2227
+ warnedPaths.push(p);
2228
+ } else {
2229
+ updatedPaths.push(p);
2230
+ }
2231
+ }
2232
+ return { skipPaths, warnedPaths, updatedPaths };
2175
2233
  }
2176
2234
  function isEntryProtected(relPath, componentRelativePrefix) {
2177
2235
  if (isProtectedFile(relPath)) {
@@ -2207,7 +2265,7 @@ async function findProtectedFilesInDir(dirPath, componentRelativePrefix) {
2207
2265
  }
2208
2266
  return protected_;
2209
2267
  }
2210
- async function updateComponent(targetDir, component, customizations, options, config) {
2268
+ async function updateComponent(targetDir, component, customizations, options, config, lockfile) {
2211
2269
  const preservedFiles = [];
2212
2270
  const componentPath = getComponentPath2(component);
2213
2271
  const srcPath = resolveTemplatePath(componentPath);
@@ -2224,7 +2282,11 @@ async function updateComponent(targetDir, component, customizations, options, co
2224
2282
  skipPaths.push(cc.path);
2225
2283
  }
2226
2284
  }
2227
- const { skipPaths: protectedSkipPaths, warnedPaths: protectedWarnedPaths } = await collectProtectedSkipPaths(srcPath, destPath, componentPath, !!options.forceOverwriteAll);
2285
+ const {
2286
+ skipPaths: protectedSkipPaths,
2287
+ warnedPaths: protectedWarnedPaths,
2288
+ updatedPaths: protectedUpdatedPaths
2289
+ } = await collectProtectedSkipPaths(srcPath, destPath, componentPath, !!options.forceOverwriteAll, lockfile, targetDir);
2228
2290
  for (const protectedPath of protectedWarnedPaths) {
2229
2291
  if (options.forceOverwriteAll) {
2230
2292
  warn("update.protected_file_force_overwrite", {
@@ -2236,10 +2298,17 @@ async function updateComponent(targetDir, component, customizations, options, co
2236
2298
  warn("update.protected_file_skipped", {
2237
2299
  file: protectedPath,
2238
2300
  component,
2239
- hint: "File contains AI behavioral constraints and was not updated. Use --force-overwrite-all to override."
2301
+ hint: "File was modified by user and preserved. Use --force-overwrite-all to override."
2240
2302
  });
2241
2303
  }
2242
2304
  }
2305
+ for (const updatedPath of protectedUpdatedPaths) {
2306
+ info("update.protected_file_updated", {
2307
+ file: updatedPath,
2308
+ component,
2309
+ hint: "Protected file updated (unmodified by user, matches lockfile hash)."
2310
+ });
2311
+ }
2243
2312
  skipPaths.push(...protectedSkipPaths);
2244
2313
  const path = await import("node:path");
2245
2314
  const normalizedSkipPaths = skipPaths.map((p) => path.relative(destPath, join6(targetDir, p)));
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "oh-my-customcode",
3
3
  "workspaces": ["packages/*"],
4
- "version": "0.49.0",
4
+ "version": "0.50.0",
5
5
  "description": "Batteries-included agent harness for Claude Code",
6
6
  "type": "module",
7
7
  "bin": {
@@ -0,0 +1,288 @@
1
+ ---
2
+ name: systematic-debugging
3
+ description: Use when encountering any bug, test failure, or unexpected behavior. Enforces a strict reproduce-first, root-cause-first, failing-test-first debugging workflow before fixing.
4
+ version: 1.0.0
5
+ user-invocable: false
6
+ ---
7
+
8
+ <!-- Source: https://github.com/tmdgusya/engineering-disciplines (MIT License) -->
9
+
10
+ # Systematic Debugging
11
+
12
+ 엄격한 디버깅 워크플로우다. 버그, 테스트 실패, 예기치 않은 동작을 다룰 때 사용한다.
13
+
14
+ 핵심 목적은 세 가지다.
15
+
16
+ 1. 증상이 아니라 원인을 고친다.
17
+ 2. 추측 기반 수정을 막는다.
18
+ 3. 실패를 테스트로 고정한 뒤 수정한다.
19
+
20
+ ## Hard Gates
21
+
22
+ 다음 규칙은 예외 없이 따른다.
23
+
24
+ 1. **재현 또는 관측 가능 상태를 만들기 전에는 수정하지 않는다.**
25
+ 2. **원인 가설을 명시하기 전에는 수정하지 않는다.**
26
+ 3. **실패 테스트 또는 동등한 재현 장치를 만들기 전에는 수정하지 않는다.**
27
+ 4. **한 번에 하나의 가설만 검증한다.**
28
+ 5. **수정 시 "while I'm here" 리팩터링을 금지한다.**
29
+ 6. **수정 시도가 3번 실패하면 추가 패치 전에 구조적 문제를 의심한다.**
30
+
31
+ 이 과정을 어기는 것은 디버깅 실패로 본다.
32
+
33
+ ## When To Use
34
+
35
+ 다음 상황이면 이 스킬을 사용한다.
36
+
37
+ - 테스트가 실패할 때
38
+ - 운영 또는 로컬에서 버그가 발생할 때
39
+ - 예상과 다른 응답, 상태, 렌더링, 쿼리 결과가 나올 때
40
+ - 성능 저하, 타임아웃, 레이스 컨디션, 간헐 실패를 조사할 때
41
+ - 이미 한 번 이상 고쳤는데 다시 깨졌을 때
42
+
43
+ 다음 핑계는 허용하지 않는다.
44
+
45
+ - "간단해 보여서 바로 고치면 된다"
46
+ - "시간이 없으니 일단 패치하고 보자"
47
+ - "이거 같으니까 그냥 바꿔보자"
48
+
49
+ ## Required Output Contract
50
+
51
+ 이 스킬을 사용할 때는 내부적으로 아래 항목을 반드시 고정한다.
52
+
53
+ 1. **Problem statement**: 무엇이 잘못되었는지 한 문장으로 정의
54
+ 2. **Reproduction path**: 어떻게 실패를 재현하거나 관측할지
55
+ 3. **Evidence**: 실제 관측 결과
56
+ 4. **Root-cause hypothesis**: 왜 이 문제가 발생한다고 보는지
57
+ 5. **Failing guard**: 실패 테스트, 재현 스크립트, 로그 검증 중 하나
58
+ 6. **Fix**: 원인에 대한 단일 수정
59
+ 7. **Verification**: 수정 후 재현 경로와 관련 테스트 결과
60
+
61
+ 이 7개 중 빠진 항목이 있으면 아직 끝난 일이 아니다.
62
+
63
+ ## Workflow
64
+
65
+ 반드시 아래 순서로 진행한다.
66
+
67
+ ### Phase 1. Define The Problem
68
+
69
+ 먼저 문제를 축약한다.
70
+
71
+ - 실제 기대 동작은 무엇인가
72
+ - 실제 관측 동작은 무엇인가
73
+ - 영향 범위는 어디까지인가
74
+ - 항상 재현되는가, 간헐적인가
75
+
76
+ 출력 형식:
77
+
78
+ ```text
79
+ Problem: <expected> but got <actual> under <condition>
80
+ ```
81
+
82
+ 증상과 추측을 섞지 않는다.
83
+
84
+ ```text
85
+ Good: Product detail API returns 500 when brand is null.
86
+ Bad: Serializer is broken because brand mapping seems wrong.
87
+ ```
88
+
89
+ ### Phase 2. Reproduce Or Instrument
90
+
91
+ 수정 전에 실패를 다시 볼 수 있어야 한다.
92
+
93
+ 우선순위:
94
+
95
+ 1. 기존 테스트로 재현
96
+ 2. 최소 통합 테스트로 재현
97
+ 3. 단위 테스트로 재현
98
+ 4. 재현 스크립트 또는 명령으로 관측
99
+ 5. 로그/계측 추가 후 관측
100
+
101
+ 규칙:
102
+
103
+ - 재현 경로는 가능한 한 가장 작게 만든다.
104
+ - UI에서만 보이는 버그라도 더 아래 계층에서 재현 가능하면 그쪽을 선호한다.
105
+ - 간헐 실패면 로그, 입력, 시간, 동시성 조건을 추가해 관측성을 높인다.
106
+ - 재현되지 않으면 수정으로 넘어가지 말고 관측 수단을 늘린다.
107
+
108
+ 재현 불가 상태에서 해야 할 일:
109
+
110
+ 1. 입력값 기록
111
+ 2. 환경 차이 확인
112
+ 3. 최근 변경점 확인
113
+ 4. 경계 지점별 로그 추가
114
+ 5. 동일 증상을 만드는 더 작은 조건 탐색
115
+
116
+ ### Phase 3. Gather Evidence
117
+
118
+ 관측 가능한 사실만 모은다.
119
+
120
+ 항상 확인할 것:
121
+
122
+ - 에러 메시지와 스택트레이스 전문
123
+ - 실패 입력값
124
+ - 최근 변경 파일 또는 커밋
125
+ - 환경/설정 차이
126
+ - 호출 경로와 데이터 흐름
127
+
128
+ 멀티 컴포넌트 문제에서는 경계마다 확인한다.
129
+
130
+ 예시:
131
+
132
+ - controller -> application -> service -> repository
133
+ - client -> API -> external service
134
+ - scheduler -> batch service -> database
135
+
136
+ 각 경계에서 확인할 것:
137
+
138
+ - 무엇이 들어왔는가
139
+ - 무엇이 나갔는가
140
+ - 어떤 값이 변형되었는가
141
+ - 어떤 조건에서만 깨지는가
142
+
143
+ 문제 위치를 특정하기 전에는 고치지 않는다.
144
+
145
+ ### Phase 4. Isolate Root Cause
146
+
147
+ 원인 후보를 하나만 세운다.
148
+
149
+ 형식:
150
+
151
+ ```text
152
+ Hypothesis: <root cause> because <evidence>
153
+ ```
154
+
155
+ 좋은 가설의 조건:
156
+
157
+ - 단일 원인을 가리킨다
158
+ - 관측 증거와 연결된다
159
+ - 작은 실험으로 반증 가능하다
160
+
161
+ 나쁜 가설의 예:
162
+
163
+ - "어딘가 비동기 문제가 있는 것 같다"
164
+ - "직렬화 쪽 전체가 불안정한 듯하다"
165
+
166
+ 원인을 소스까지 거슬러 올라간다. 오류가 깊은 스택에서 보이면 증상이 아니라 입력의 출처를 추적한다.
167
+
168
+ ### Phase 5. Lock The Failure
169
+
170
+ 수정 전에 실패를 고정한다.
171
+
172
+ 우선순위:
173
+
174
+ 1. 자동화된 failing test
175
+ 2. 기존 테스트에 회귀 케이스 추가
176
+ 3. 최소 재현 스크립트
177
+ 4. 로그/어설션 기반 임시 검증 장치
178
+
179
+ 규칙:
180
+
181
+ - 가능하면 자동화 테스트를 만든다.
182
+ - 수정 전에는 실패해야 한다.
183
+ - 수정 후에는 같은 경로에서 통과해야 한다.
184
+ - 테스트 이름은 무엇이 깨졌는지 드러내야 한다.
185
+
186
+ 자동화 테스트를 쓸 수 있으면 `test-driven-development` 스킬을 함께 사용한다.
187
+
188
+ ### Phase 6. Implement A Single Fix
189
+
190
+ 수정은 하나의 가설만 다룬다.
191
+
192
+ 허용:
193
+
194
+ - 원인에 직접 대응하는 최소 코드 변경
195
+ - 검증에 필요한 최소한의 보조 수정
196
+
197
+ 금지:
198
+
199
+ - 관련 있어 보이는 여러 수정 묶기
200
+ - 리팩터링 겸 수정
201
+ - 포맷/정리/이름 변경 끼워넣기
202
+ - 근거 없는 null-guard 추가
203
+ - 예외 삼키기
204
+
205
+ 실패하면 즉시 다시 Phase 1 또는 Phase 3으로 돌아간다. 이전 가설이 틀렸다는 뜻이다.
206
+
207
+ ### Phase 7. Verify And Close
208
+
209
+ 아래를 모두 만족해야 종료한다.
210
+
211
+ 1. 원래 재현 경로가 더 이상 실패하지 않는다.
212
+ 2. 새 failing guard가 통과한다.
213
+ 3. 관련 테스트가 깨지지 않는다.
214
+ 4. 수정이 증상이 아니라 원인을 막는다는 설명이 가능하다.
215
+
216
+ 간헐 버그라면 한 번 통과로 끝내지 않는다. 반복 실행 또는 조건 변화 하 검증이 필요하다.
217
+
218
+ ## Stop Conditions
219
+
220
+ 다음 상황이면 멈추고 프레임을 다시 잡는다.
221
+
222
+ ### 1. Reproduction Failed
223
+
224
+ 여러 번 시도해도 재현이 안 되면:
225
+
226
+ - 관측 수단이 부족한지 본다.
227
+ - 환경 차이가 있는지 본다.
228
+ - 문제 정의가 잘못되었는지 본다.
229
+
230
+ 재현이 안 되는데 코드를 바꾸는 것은 금지다.
231
+
232
+ ### 2. Three Failed Fixes
233
+
234
+ 세 번 연속으로 수정이 빗나가면 이렇게 판단한다.
235
+
236
+ - 현재 이해가 틀렸거나
237
+ - 문제가 공유 상태, 경계 설계, 책임 분리 같은 구조 문제일 가능성이 크다
238
+
239
+ 이 시점부터는 "네 번째 땜질"이 아니라 구조 논의가 필요하다.
240
+
241
+ ### 3. No Failing Guard
242
+
243
+ 실패 테스트나 동등한 재현 장치를 만들 수 없으면, 완료로 선언하지 않는다. 최소한 재현 명령과 관측 결과를 남긴다.
244
+
245
+ ## Red Flags
246
+
247
+ 아래 생각이 들면 즉시 멈추고 앞 단계로 돌아간다.
248
+
249
+ - "이 줄만 바꿔보면 될 것 같다"
250
+ - "로그는 나중에 보고 일단 수정해보자"
251
+ - "테스트는 나중에 추가하지 뭐"
252
+ - "한 번에 이것도 저것도 같이 고치자"
253
+ - "에러는 사라졌으니 원인은 몰라도 됐다"
254
+
255
+ ## Minimal Checklist
256
+
257
+ 실행 중에는 아래 체크리스트를 기준으로 스스로 검증한다.
258
+
259
+ - [ ] 문제를 한 문장으로 정의했다
260
+ - [ ] 실패를 재현하거나 관측 가능하게 만들었다
261
+ - [ ] 증거를 수집했다
262
+ - [ ] 단일 원인 가설을 만들었다
263
+ - [ ] 수정 전 실패 guard를 만들었다
264
+ - [ ] 단일 수정만 적용했다
265
+ - [ ] 같은 경로로 수정 후 검증했다
266
+
267
+ ## Completion Standard
268
+
269
+ 이 스킬의 완료 기준은 "코드가 바뀌었다"가 아니다.
270
+
271
+ 완료 기준:
272
+
273
+ - 문제 정의가 명확하다
274
+ - 실패가 수정 전에 고정되었다
275
+ - 수정이 원인과 연결된다
276
+ - 검증 결과가 남아 있다
277
+
278
+ 이 네 가지가 없으면 디버깅은 끝난 것이 아니다.
279
+
280
+ ## Reference Materials
281
+
282
+ This skill includes reference documents for specific debugging techniques:
283
+
284
+ - `root-cause-tracing.md` — Tracing bugs back through call chains to the original trigger
285
+ - `defense-in-depth.md` — Adding validation at every layer to make bugs structurally impossible
286
+ - `condition-based-waiting.md` — Replacing arbitrary delays with condition-based polling
287
+ - `find-polluter.sh` — Bisection script for finding test pollution sources
288
+ - `condition-based-waiting-example.ts` — Complete implementation of condition-based waiting utilities