oh-my-customcode 1.1.49 → 1.1.51

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/dist/index.js CHANGED
@@ -19,28 +19,28 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
19
19
  // src/utils/fs.ts
20
20
  var exports_fs = {};
21
21
  __export(exports_fs, {
22
- writeTextFile: () => writeTextFile,
23
- writeJsonFile: () => writeJsonFile,
24
- validatePreserveFilePath: () => validatePreserveFilePath,
25
- resolveTemplatePath: () => resolveTemplatePath,
26
- resolvePath: () => resolvePath,
27
- remove: () => remove,
28
- readTextFile: () => readTextFile,
29
- readJsonFile: () => readJsonFile,
30
- normalizePath: () => normalizePath,
31
- move: () => move,
32
- listFiles: () => listFiles,
33
- isAbsolutePath: () => isAbsolutePath,
34
- getRelativePath: () => getRelativePath,
35
- getPackageRoot: () => getPackageRoot,
36
- getFileStats: () => getFileStats,
37
- filesAreIdentical: () => filesAreIdentical,
38
- fileExists: () => fileExists,
39
- ensureDirectory: () => ensureDirectory,
40
- createTempDir: () => createTempDir,
41
- copyFile: () => copyFile,
22
+ calculateChecksum: () => calculateChecksum,
42
23
  copyDirectory: () => copyDirectory,
43
- calculateChecksum: () => calculateChecksum
24
+ copyFile: () => copyFile,
25
+ createTempDir: () => createTempDir,
26
+ ensureDirectory: () => ensureDirectory,
27
+ fileExists: () => fileExists,
28
+ filesAreIdentical: () => filesAreIdentical,
29
+ getFileStats: () => getFileStats,
30
+ getPackageRoot: () => getPackageRoot,
31
+ getRelativePath: () => getRelativePath,
32
+ isAbsolutePath: () => isAbsolutePath,
33
+ listFiles: () => listFiles,
34
+ move: () => move,
35
+ normalizePath: () => normalizePath,
36
+ readJsonFile: () => readJsonFile,
37
+ readTextFile: () => readTextFile,
38
+ remove: () => remove,
39
+ resolvePath: () => resolvePath,
40
+ resolveTemplatePath: () => resolveTemplatePath,
41
+ validatePreserveFilePath: () => validatePreserveFilePath,
42
+ writeJsonFile: () => writeJsonFile,
43
+ writeTextFile: () => writeTextFile
44
44
  });
45
45
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
46
46
  import { fileURLToPath } from "node:url";
@@ -294,14 +294,14 @@ var init_fs = () => {};
294
294
  // src/core/registry.ts
295
295
  var exports_registry = {};
296
296
  __export(exports_registry, {
297
- unregisterProject: () => unregisterProject,
298
- registryToList: () => registryToList,
299
- registerProject: () => registerProject,
300
- readRegistry: () => readRegistry,
301
- migrateFromLockfiles: () => migrateFromLockfiles,
302
- isTempPath: () => isTempPath,
297
+ _setRegistryDirForTesting: () => _setRegistryDirForTesting,
303
298
  cleanRegistry: () => cleanRegistry,
304
- _setRegistryDirForTesting: () => _setRegistryDirForTesting
299
+ isTempPath: () => isTempPath,
300
+ migrateFromLockfiles: () => migrateFromLockfiles,
301
+ readRegistry: () => readRegistry,
302
+ registerProject: () => registerProject,
303
+ registryToList: () => registryToList,
304
+ unregisterProject: () => unregisterProject
305
305
  });
306
306
  import { mkdir, readFile, writeFile } from "node:fs/promises";
307
307
  import { homedir, tmpdir } from "node:os";
@@ -2031,7 +2031,7 @@ var package_default = {
2031
2031
  workspaces: [
2032
2032
  "packages/*"
2033
2033
  ],
2034
- version: "1.1.49",
2034
+ version: "1.1.51",
2035
2035
  description: "Batteries-included agent harness for Claude Code",
2036
2036
  type: "module",
2037
2037
  bin: {
@@ -2079,7 +2079,7 @@ var package_default = {
2079
2079
  yaml: "^2.8.2"
2080
2080
  },
2081
2081
  devDependencies: {
2082
- "@anthropic-ai/sdk": "^0.115.0",
2082
+ "@anthropic-ai/sdk": "^0.117.1",
2083
2083
  "@biomejs/biome": "^2.3.12",
2084
2084
  "@types/bun": "^1.3.6",
2085
2085
  "@types/js-yaml": "^4.0.9",
@@ -5367,40 +5367,40 @@ var src_default = {
5367
5367
  VERSION
5368
5368
  };
5369
5369
  export {
5370
- writeJsonFile,
5371
- warn,
5372
- update,
5373
- success,
5374
- setLogLevel,
5375
- setLocale,
5376
- saveConfig,
5377
- resolveTemplatePath,
5378
- renderGitWorkflowKO,
5379
- renderGitWorkflowEN,
5380
- readJsonFile,
5381
- preserveCustomizations,
5382
- mergeConfig,
5383
- loadConfig,
5384
- install,
5385
- info,
5386
- getTemplateManifest,
5387
- getProviderLayout,
5388
- getPackageRoot,
5389
- getDefaultWorkflow,
5390
- getDefaultConfig,
5391
- getConfigPath,
5392
- fileExists,
5393
- error,
5394
- ensureDirectory,
5395
- detectProvider,
5396
- detectGitWorkflow,
5397
- src_default as default,
5398
- debug,
5399
- createLogger,
5400
- createDirectoryStructure,
5401
- copyTemplates,
5402
- copyDirectory,
5403
- checkForUpdates,
5370
+ VERSION,
5404
5371
  applyUpdates,
5405
- VERSION
5372
+ checkForUpdates,
5373
+ copyDirectory,
5374
+ copyTemplates,
5375
+ createDirectoryStructure,
5376
+ createLogger,
5377
+ debug,
5378
+ src_default as default,
5379
+ detectGitWorkflow,
5380
+ detectProvider,
5381
+ ensureDirectory,
5382
+ error,
5383
+ fileExists,
5384
+ getConfigPath,
5385
+ getDefaultConfig,
5386
+ getDefaultWorkflow,
5387
+ getPackageRoot,
5388
+ getProviderLayout,
5389
+ getTemplateManifest,
5390
+ info,
5391
+ install,
5392
+ loadConfig,
5393
+ mergeConfig,
5394
+ preserveCustomizations,
5395
+ readJsonFile,
5396
+ renderGitWorkflowEN,
5397
+ renderGitWorkflowKO,
5398
+ resolveTemplatePath,
5399
+ saveConfig,
5400
+ setLocale,
5401
+ setLogLevel,
5402
+ success,
5403
+ update,
5404
+ warn,
5405
+ writeJsonFile
5406
5406
  };
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "workspaces": [
4
4
  "packages/*"
5
5
  ],
6
- "version": "1.1.49",
6
+ "version": "1.1.51",
7
7
  "description": "Batteries-included agent harness for Claude Code",
8
8
  "type": "module",
9
9
  "bin": {
@@ -51,7 +51,7 @@
51
51
  "yaml": "^2.8.2"
52
52
  },
53
53
  "devDependencies": {
54
- "@anthropic-ai/sdk": "^0.115.0",
54
+ "@anthropic-ai/sdk": "^0.117.1",
55
55
  "@biomejs/biome": "^2.3.12",
56
56
  "@types/bun": "^1.3.6",
57
57
  "@types/js-yaml": "^4.0.9",
@@ -195,6 +195,16 @@
195
195
  }
196
196
  ],
197
197
  "description": "Advisory check for plugin cache directories missing node_modules (#1207)"
198
+ },
199
+ {
200
+ "matcher": "*",
201
+ "hooks": [
202
+ {
203
+ "type": "command",
204
+ "command": "bash .claude/hooks/scripts/claude-md-reinject.sh"
205
+ }
206
+ ],
207
+ "description": "Re-inject project CLAUDE.md into model context on session start/resume/clear and compact re-entry — matcher \"*\" covers all SessionStart sources including \"compact\" (#1617)"
198
208
  }
199
209
  ],
200
210
  "UserPromptSubmit": [
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env bash
2
+ # claude-md-reinject.sh — SessionStart: re-inject project CLAUDE.md into model context (#1617)
3
+ #
4
+ # 배경:
5
+ # 장기 세션에서 compact 이후 프로젝트 CLAUDE.md(강제 규칙 원문)가 모델 컨텍스트에서 유실되면
6
+ # 규칙 amnesia가 재발한다. 기존 PostCompact 훅(hooks.json)은 "prompt" 타입으로 요약 지침만
7
+ # 재주입할 뿐, CLAUDE.md 원문 전체를 재주입하지 않는다.
8
+ #
9
+ # 배선 이벤트 (Phase 1 판단, #1617):
10
+ # R006 실측 기준(이 저장소 rules/MUST-agent-design.md) additionalContext 지원 이벤트 목록에
11
+ # SessionStart는 있고 PostCompact는 없다. 한편 공식 문서(hooks.md, 2026-08-29 재확인)의
12
+ # SessionStart matcher는 startup / resume / clear / compact 네 가지이며, "compact"는
13
+ # auto/manual compact 직후를 가리킨다 — 즉 "session start 전체와 compact 재개 이후 재주입"
14
+ # 요구사항은 SessionStart 단일 이벤트(matcher "*")로 전부 커버된다. 별도 PostCompact 배선은
15
+ # 불필요 — 공식 문서 이벤트 목록(### 헤더 스캔)에 PostCompact 자체가 없다(PreCompact만 존재).
16
+ #
17
+ # 동작:
18
+ # stdin JSON의 .source(startup/resume/clear/compact/기타)를 로그 헤더에 포함해
19
+ # hookSpecificOutput.additionalContext로 CLAUDE.md 전체를 재주입한다.
20
+ #
21
+ # Opt-out: OMCUSTOM_CLAUDEMD_REINJECT=off (기본 on — 이 훅은 R021 advisory-first이되 기본 활성).
22
+ # Graceful degradation (R021): CLAUDE.md 부재 / jq 부재 / 초과 크기 → 무음 exit 0. 절대 exit 2 금지.
23
+
24
+ input=$(cat 2>/dev/null || echo '{}')
25
+
26
+ if [ "${OMCUSTOM_CLAUDEMD_REINJECT:-on}" = "off" ]; then
27
+ exit 0
28
+ fi
29
+
30
+ if ! command -v jq >/dev/null 2>&1; then
31
+ exit 0
32
+ fi
33
+
34
+ source_field=$(printf '%s' "$input" | jq -r '.source // "unknown"' 2>/dev/null) || source_field="unknown"
35
+
36
+ PROJECT_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
37
+ CLAUDE_MD="$PROJECT_ROOT/CLAUDE.md"
38
+
39
+ if [ ! -f "$CLAUDE_MD" ]; then
40
+ exit 0
41
+ fi
42
+
43
+ # 크기 가드 — 훅 출력 폭주로 세션을 wedge시키는 실패 클래스 방지 (R021).
44
+ MAX_BYTES=${OMCUSTOM_CLAUDEMD_REINJECT_MAX_BYTES:-102400}
45
+ file_size=$(wc -c < "$CLAUDE_MD" 2>/dev/null | tr -d ' ') || file_size=0
46
+ if [ -z "$file_size" ] || [ "$file_size" -gt "$MAX_BYTES" ] 2>/dev/null; then
47
+ exit 0
48
+ fi
49
+
50
+ claude_md_content=$(cat "$CLAUDE_MD" 2>/dev/null) || exit 0
51
+ if [ -z "$claude_md_content" ]; then
52
+ exit 0
53
+ fi
54
+
55
+ header="[claude-md-reinject] CLAUDE.md re-injection (source: ${source_field})"
56
+
57
+ # 사람이 보는 감사 추적용 (SessionStart의 stdout 자체가 컨텍스트로 들어가므로, 사람용 로그는
58
+ # stderr로만 남긴다 — additionalContext 본문에는 섞지 않는다).
59
+ printf '%s\n' "$header" >&2
60
+
61
+ jq -cn --arg header "$header" --arg body "$claude_md_content" \
62
+ '{hookSpecificOutput: {hookEventName: "SessionStart", additionalContext: ($header + "\n\n" + $body)}}' \
63
+ 2>/dev/null || exit 0
64
+
65
+ exit 0
@@ -32,6 +32,8 @@
32
32
 
33
33
  > **도구 이름 ≠ 그 프로그램 (#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.
34
34
 
35
+ > **v2.1.234+**: macOS/Linux 네이티브 빌드의 내장 `grep`이 pathological pattern에서 메모리 고갈 대신 fail fast하고, `-m N`과 `-A/-C` 옵션을 함께 쓸 때의 context 출력 정확도가 수정되었습니다(v2.1.235에서 추가 보강). 위 「도구 이름 ≠ 그 프로그램」(#1590) 조항과 인접한 함정입니다 — 이 저장소의 Bash 도구 `grep`은 셸 함수로 셰이딩돼 있으므로, 내장 `grep` 자체의 견고성 개선과 셰이딩 문제는 **별개 축**입니다. Darwin(이 저장소 실행 환경) 네이티브 빌드에 해당합니다.
36
+
35
37
  <!--
36
38
  > **v2.1.206+**: `/doctor`에 checked-in CLAUDE.md에서 코드베이스로부터 파생 가능한 내용을 잘라내도록 제안하는 체크가 추가되었습니다 — R005 "Context Optimization via HTML Comments"의 컨텍스트 절감 원칙과 정합(모델 불필요 메타데이터 축소).
37
39
  -->
@@ -42,7 +44,7 @@
42
44
 
43
45
  > **v2.1.212+**: MCP 도구 호출이 2분(기본값, `CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS`로 임계값 조정·비활성) 초과 시 자동으로 백그라운드로 이동해 세션이 계속 사용 가능해집니다 — 위 v2.1.210 Bash/PowerShell auto-background의 MCP 도구 확장. 느린 MCP 호출(ontology-rag `rebuild_ontology`, code-review-graph 인덱싱 등)을 hang으로 오판하지 말고, 2분 초과 시 백그라운드 전환을 전제로 후속 작업을 진행합니다.
44
46
 
45
- > **v2.1.233+**: `WebFetch`의 세션 URL 캐시 TTL이 `CLAUDE_CODE_WEBFETCH_CACHE_TTL_MS`로 조정 가능해졌습니다(기본 15분 불변). **재확인 함정**: 같은 URL을 TTL 내 재조회하면 캐시가 반환되므로 **독립적인 2차 확인이 아닙니다** — R020 Degraded-Output Re-Verification Gate가 요구하는 "결정론적 2차 소스"로 동일 URL의 WebFetch 재호출을 쓰지 말고, 다른 소스나 CLI 실측(`npm view`, `gh`)을 사용합니다.
47
+ > **v2.1.233+ (정정: v2.1.239에서 실제로 보장됨)**: `WebFetch`의 세션 URL 캐시 TTL이 `CLAUDE_CODE_WEBFETCH_CACHE_TTL_MS`로 조정 가능해졌습니다(기본 15분 — **단, v2.1.239 이전에는 이 15분이 지켜지지 않고 만료된 콘텐츠가 세션 전체 동안 메모리에 남아있었습니다**. v2.1.239가 이 결함을 수정해 이제야 15분 TTL이 실제로 보장됩니다). **재확인 함정**: 같은 URL을 TTL 내 재조회하면 캐시가 반환되므로 **독립적인 2차 확인이 아닙니다** — R020 Degraded-Output Re-Verification Gate가 요구하는 "결정론적 2차 소스"로 동일 URL의 WebFetch 재호출을 쓰지 말고, 다른 소스나 CLI 실측(`npm view`, `gh`)을 사용합니다. **회고적 함의**: v2.1.239 이전 세션에서는 이 재확인 함정이 "TTL 15분 이내"가 아니라 **세션 내내** 유효했으므로, 그 시기의 WebFetch 재조회 기반 판단은 15분보다 훨씬 오래된 stale 데이터에 의존했을 수 있습니다.
46
48
 
47
49
  > **v2.1.224+**: mid-turn에 연결된 MCP 도구가 **이름 고지 없이** tool search로 deferred되던 결함이 수정되었습니다. 구버전에서는 세션 도중 붙은 MCP 서버의 도구가 이름조차 노출되지 않아 "그런 도구 없음"으로 오판할 수 있었으므로, 위 tool-availability 주의(`command -v` 사전 확인과 동류)를 MCP 도구에도 적용합니다 — 도구 부재 결론 전에 `ToolSearch`로 실측합니다.
48
50
 
@@ -15,6 +15,8 @@ model: sonnet # CC-native alias (Tier 1) or full model ID (Tier 2)
15
15
  tools: [Read, Write, ...] # Allowed tools
16
16
  ```
17
17
 
18
+ > **v2.1.239+**: `.md` 파일이 UTF-8 BOM으로 시작하는 agent/skill/command 파일이 **조용히 무시**되던 결함이 수정되었습니다. 구버전에서는 BOM이 있는 `.claude/agents/*.md`, `.claude/skills/*/SKILL.md`가 에러 없이 로드에서 누락됐습니다 — "에이전트/스킬이 없다"는 관측이 실제로는 "BOM 때문에 무음 스킵"일 수 있었습니다. R017 Count Sync가 실측하는 카운트는 파일 **존재**를 세지만, BOM 파일은 CC가 실제로 **로드하지 않았으므로** 구버전에서는 카운트와 실제 로드된 에이전트/스킬 수가 어긋날 수 있었습니다(cross-ref R017 Count Sync).
19
+
18
20
  <!-- ARCHIVED CC version note (historical):
19
21
  > **v2.1.208+**: The Agent tool no longer launches with no tools when a subagent's `tools:` list resolves to nothing — it now returns a clear error naming the unrecognized entries, catching frontmatter `tools:` typos that previously failed silently.
20
22
  -->
@@ -66,10 +68,14 @@ Skill/rule text instructing "spawn with `model: opus`" refers to this tier — a
66
68
 
67
69
  > **v2.1.223+**: workflow agent · forked skill · slash command · 재개된 background agent가 **요청한 subagent 모델이 제한되어 parent model로 실행될 때 경고가 표시**됩니다. 위 v2.1.222 org step-down 노트의 직접 연장선으로, 이전에는 이 강등이 **무음**이었습니다 — 즉 "`model: opus`로 스폰했다"는 기록이 실제 실행 모델의 증거가 아니었습니다. 특정 모델을 확정하려면 frontmatter Tier-2 full ID를 쓰고, 실행 모델은 경고 표시 유무로 확인합니다(R020 "attempt ≠ outcome"의 모델 선택 각도).
68
70
 
71
+ > **v2.1.247+**: sub-agent가 첫 호출에서 model 404(인식 불가 model ID)를 만나면 죽던 결함이 수정되어, 이제 세션의 fallback model chain을 사용합니다. 부모 세션에 전달되는 에러에도 error type/status/request id/model이 포함됩니다. 위 v2.1.233 print모드 `unrecognized_model` 진단 노트가 관측성만 다뤘다면, 이 수정은 **실행 연속성**을 추가합니다 — 구버전에서는 서브에이전트 모델 해석 실패가 fallback 없이 그대로 죽음으로 이어졌습니다.
72
+
69
73
  > **v2.1.233+**: print 모드(`-p`) 진단이 추가되어, Claude Code가 **인식하지 못하는 model ID**로 요청이 나가면 stderr에 `[claude-code:unrecognized_model]` 라인이 기록됩니다(`modelOverrides`로 매핑하면 억제). 구버전에서는 오타·폐기된 full ID가 **무음으로 fallback 해석**되어 "frontmatter에 적힌 모델 = 실제 실행 모델"이라는 전제가 검증 불가능했습니다 — 위 v2.1.223 강등 경고와 같은 계열의 **관측성 보강**이며, 이 저장소는 다수 에이전트가 Tier-2 full ID를 쓰므로 `-p` 실행 시 이 라인 유무가 model ID 유효성의 결정론적 증거입니다(R020 "attempt ≠ outcome"). 무인 루프(`/fsd`)의 stderr를 버리면 이 신호도 함께 사라집니다.
70
74
 
71
75
  > **v2.1.223+**: `CLAUDE_CODE_DISABLE_1M_CONTEXT`가 **native 1M 창을 가진 모든 Claude 모델**을 auto-compaction으로 200K에 유지하도록 확대되었습니다(이전에는 고정 모델 목록). 미인식 model ID도 가정 컨텍스트 창 내로 유지되며 `CLAUDE_CODE_DISABLE_UNKNOWN_MODEL_WINDOW_ENFORCEMENT=1`로 복원할 수 있습니다. 위 Tier-2 표의 `claude-sonnet-5`/`claude-opus-5`(native 1M)와 `[1m]` 접미사는 이 env가 설정된 환경에서 **실효 200K로 동작**하므로, 1M 전제의 대용량 컨텍스트 위임 전에 env 설정 여부를 확인합니다(cross-ref R013 context budget).
72
76
 
77
+ > **v2.1.251+**: `CLAUDE_CODE_SUBAGENT_MODEL`이 이제 "모든 것을 override"가 아니라 **기본 subagent 모델만 설정**합니다 — 에이전트 정의의 `model:`(Tier 1/2)과 spawn 시점 명시적 `model`(Tier 3)이 이 env보다 **우선**합니다. 이 저장소는 다수 에이전트가 Tier-2 full ID로 model을 pin하므로, v2.1.251부터는 이 env var가 project의 model pin을 더 이상 깨뜨릴 수 없습니다(단, 이전 버전에서 실행된 세션은 여전히 영향받았을 수 있습니다).
78
+
73
79
  > **Claude Fable 5 (access via CC v2.1.170+)**: Mythos-class model, GA on the Claude API and positioned as a tier above Opus — its capabilities exceed any previously GA model. CC v2.1.170 is the client version that adds access (the model's GA is an API/platform property, not a CC-release milestone). Available via frontmatter full ID `claude-fable-5` (Tier 2) or Agent tool `model: fable` (Tier 3) — NOT via a Tier-1 frontmatter alias. Reserve for the most complex reasoning where its capability premium is warranted; `sonnet` remains the default for general tasks and `opus` for architecture (cost/latency awareness, R005). CC v2.1.170 also fixes session transcripts not saving (and not appearing in `--resume`) when launched from a VS Code integrated terminal or any shell inheriting Claude Code env vars — relevant to transcript-dependent skills (`homework`, `episodic-memory`). Closes #1352.
74
80
 
75
81
  <!-- ARCHIVED CC version notes (historical):
@@ -77,7 +83,7 @@ Skill/rule text instructing "spawn with `model: opus`" refers to this tier — a
77
83
 
78
84
  > **v2.1.197+**: Claude Sonnet 5가 Claude Code의 **기본 모델**로 도입되었습니다 — 네이티브 1M-token 컨텍스트, 프로모션 가격 $2/$10 per Mtok(2026-08-31까지). frontmatter에서 명시 opt-in하려면 Tier-2 full ID `claude-sonnet-5`를 사용합니다(`sonnet5`는 어느 계층에서도 유효한 값이 아님 — 위 3-Tier 구분 참조). **정정(실측)**: 이 조항이 이전에 "oh-my-customcode의 base `sonnet` alias는 안정성을 위해 `claude-sonnet-4-6`에 고정 유지"라고 서술했으나 사실이 아니다 — `sonnet` alias 해석 주체는 CC이며 프로젝트가 pin할 수 없다(Tier 1 참조); frontmatter `model: sonnet` 에이전트가 실측상 `claude-sonnet-5`로 실행되었다. Sonnet 5가 CC 신규 기본값이므로 명시 모델 없는 세션은 이제 Sonnet 5에서 동작합니다.
79
85
 
80
- > **v2.1.201+**: Claude Sonnet 5 세션이 harness reminder를 mid-conversation system role로 주입하지 않도록 변경되었습니다 — Sonnet 5 실행 시 하니스 리마인더(규칙 재주입 등) 전달 방식이 조정되었으며, PostCompact 규칙 재주입(R021)·세션 연속성 동작 자체에는 영향이 없습니다. Sonnet 5가 CC 기본 모델(v2.1.197+)이므로 명시 모델 없는 세션에 적용됩니다.
86
+ <!-- RETIRED (은퇴 릴리즈 v1.1.50, 보존 기준 v2.1.230 미만): > **v2.1.201+**: Claude Sonnet 5 세션이 harness reminder를 mid-conversation system role로 주입하지 않도록 변경되었습니다 — Sonnet 5 실행 시 하니스 리마인더(규칙 재주입 등) 전달 방식이 조정되었으며, PostCompact 규칙 재주입(R021)·세션 연속성 동작 자체에는 영향이 없습니다. Sonnet 5가 CC 기본 모델(v2.1.197+)이므로 명시 모델 없는 세션에 적용됩니다. -->
81
87
  -->
82
88
 
83
89
  > **Fable 5 Effort 전략**: Fable 5는 **high effort가 기본값**이며, `xhigh`는 capability-sensitive 작업(최고난도 아키텍처/추론)에 한정해야 합니다. Fable 5의 `low`/`medium` effort조차 이전 세대 모델의 `xhigh`를 상회하는 품질을 보이므로, Fable 5를 사용하는 실행 에이전트는 `effort` 필드를 신중히 명시하고 불필요한 `xhigh` 남용을 지양합니다(R005 비용/지연 인식과 정합).
@@ -126,7 +132,7 @@ This is a settings-level resilience mechanism, distinct from the per-agent `mode
126
132
 
127
133
  ### Optional Frontmatter
128
134
 
129
- Key optional fields: `memory`, `effort`, `skills`, `soul`, `isolation`, `background`, `maxTurns`, `maxTokens`, `mcpServers`, `hooks`, `permissionMode`, `disallowedTools`, `limitations`, `domain`, `disableSkillShellExecution`. Supported since CC v2.1.63+. See full optional frontmatter via Read tool.
135
+ Key optional fields: `memory`, `effort`, `skills`, `soul`, `isolation`, `background`, `maxTurns`, `maxTokens`, `mcpServers`, `hooks`, `permissionMode`, `disallowedTools`, `limitations`, `domain`, `disableSkillShellExecution`, `experimental.cacheTtl` (v2.1.248+). Supported since CC v2.1.63+. See full optional frontmatter via Read tool.
130
136
 
131
137
  ### Note on `skills:` field
132
138
 
@@ -167,6 +173,8 @@ limitations: # Negative capability declarations
167
173
  - "cannot modify code"
168
174
  domain: backend # backend | frontend | data-engineering | devops | universal
169
175
  disableSkillShellExecution: true # Disable inline shell execution in skills (v2.1.91+)
176
+ experimental:
177
+ cacheTtl: "5m" # "5m" | "1h" — subagent prompt cache TTL when not otherwise set (v2.1.248+)
170
178
  ```
171
179
 
172
180
  > **Note**: When `disableSkillShellExecution` is enabled (v2.1.91+), skills that rely on inline shell execution (e.g., `rtk-exec`) will have their shell blocks disabled. This is a security hardening option.
@@ -181,10 +189,13 @@ Hook JSON output `terminalSequence` field for desktop notifications, window titl
181
189
 
182
190
  ## Hook Event Types
183
191
 
184
- 31 event types supported: SessionStart, Setup, UserPromptSubmit, UserPromptExpansion, PreToolUse, PermissionRequest, PermissionDenied, PostToolUse, PostToolUseFailure, PostToolBatch, Notification, MessageDisplay, SubagentStart, SubagentStop, TaskCreated, TaskCompleted, Stop, StopFailure, TeammateIdle, InstructionsLoaded, ConfigChange, CwdChanged, DirectoryAdded, FileChanged, WorktreeCreate, WorktreeRemove, PreCompact, PostCompact, Elicitation, ElicitationResult, SessionEnd. 4 handler types: command, prompt, http, agent. See full reference table via Read tool.
192
+ 33 event types supported: SessionStart, Setup, UserPromptSubmit, UserPromptExpansion, PreToolUse, PermissionRequest, PermissionDenied, PostToolUse, PostToolUseFailure, PostToolBatch, Notification, MessageDisplay, SubagentStart, SubagentStop, TaskCreated, TaskCompleted, Stop, StopFailure, TeammateIdle, InstructionsLoaded, ConfigChange, CwdChanged, DirectoryAdded, FileChanged, WorktreeCreate, WorktreeRemove, PreCompact, PostCompact, Elicitation, ElicitationResult, SessionEnd, PreModelSwitch, PostModelSwitch (v2.1.251+). 4 handler types: command, prompt, http, agent. See full reference table via Read tool.
193
+ > **v2.1.251+**: 신규 훅 이벤트 `PreModelSwitch`/`PostModelSwitch`가 추가되어 model switch를 block/confirm/annotate할 수 있습니다. 또한 `SessionStart` resume 훅이 이제 session staleness와 예상 re-cache 비용을 인자로 받습니다.
185
194
 
186
195
  > **`MessageDisplay`는 표시 전용 — `additionalContext` 미지원**: `MessageDisplay`는 `hookSpecificOutput.displayContent`로 **화면 표시 텍스트만** 교체하며, 트랜스크립트와 Claude가 보는 내용은 원본이 유지된다. 따라서 advisory 훅을 `MessageDisplay`에 배선하면 **모델에 도달하지 않는다**. `additionalContext`(모델 컨텍스트 주입)를 지원하는 이벤트는 SessionStart, Setup, SubagentStart, UserPromptSubmit, UserPromptExpansion, PreToolUse, PostToolUse, PostToolUseFailure, PostToolBatch, Stop, SubagentStop이다. (이전 판이 나열하던 `PostMessage`는 문서화된 이벤트가 아니다 — 실제 이벤트명은 `MessageDisplay`.)
187
196
 
197
+ > **문서 시차 노트 — `PreModelSwitch`/`PostModelSwitch` 및 `PostCompact`**: 33종 중 `PreModelSwitch`/`PostModelSwitch`(v2.1.251)는 CHANGELOG(2026-08-28)에는 명시되나 공식 hooks.md "Hook events" 카탈로그 페이지에는 실측일(2026-08-29) 기준 헤더가 없다 — changelog→hooks.md 반영 시차로 판단(오류 아님, hook-events-audit 2026-08-29 실측). 또한 `PostCompact`는 공식 문서상 `additionalContext`/decision-control이 정의돼 있지 않다 — 재주입(compact 후 규칙 재주입) 용도로는 `PostCompact`가 아니라 `SessionStart`(matcher `*`)를 쓸 것(cross-ref R021 「Prompt-based」 각주).
198
+
188
199
  > **신규 이벤트 발동 시점**: `Setup` — `--init-only`, 또는 `-p` 모드에서 `--init`/`--maintenance`로 시작할 때. `UserPromptExpansion` — 사용자가 입력한 커맨드가 프롬프트로 확장될 때(모델 도달 전; 확장 차단 가능). `PostToolUseFailure` — 도구 호출이 실패한 뒤. `PostToolBatch` — 병렬 도구 호출 배치 전체가 끝난 뒤, 다음 모델 호출 전. `MessageDisplay` — assistant 메시지 텍스트가 표시되는 동안(실시간 스트리밍). `DirectoryAdded` (v2.1.219+) — `/add-dir` 또는 SDK `register_repo_root`로 작업 디렉토리가 세션 중 추가될 때. (그 밖의 신규 이벤트 — `PermissionRequest`, `StopFailure`, `InstructionsLoaded`, `ConfigChange`, `WorktreeCreate`, `WorktreeRemove` — 는 발동 시점을 미실측이므로 서술하지 않는다.)
189
200
 
190
201
  <!-- DETAIL: Hook Event Types Full Reference
@@ -277,7 +288,7 @@ Agent frontmatter `hooks:` now fire when the agent runs as a main-thread agent v
277
288
  -->
278
289
 
279
290
  <!-- ARCHIVED CC version note (historical):
280
- > **v2.1.204+**: headless 세션의 SessionStart hook 중 hook 이벤트가 스트리밍되지 않아 remote worker가 hook 도중 idle-reap되던 문제가 수정되었습니다. Hook Event Types/SessionStart 관련.
291
+ <!-- RETIRED (은퇴 릴리즈 v1.1.50, 보존 기준 v2.1.230 미만): > **v2.1.204+**: headless 세션의 SessionStart hook 중 hook 이벤트가 스트리밍되지 않아 remote worker가 hook 도중 idle-reap되던 문제가 수정되었습니다. Hook Event Types/SessionStart 관련. -->
281
292
  -->
282
293
 
283
294
  ## Permission Mode Guidance
@@ -411,6 +411,25 @@ Cross-reference: R020 ("actual outcome ≠ attempt" — verifying that a command
411
411
 
412
412
  > **v2.1.222+**: `SendMessage`가 긴 summary를 문자 수 제한으로 거부하던 동작이 **절단(truncate)**으로 변경되어 전송이 실패하지 않습니다. 전송 실패가 사라진 대신 **조용한 절단**이라는 새 실패 모드가 생겼으므로, 위 표의 "SendMessage report = Low reliability" 원칙이 오히려 강화됩니다. 긴 보고가 필요하면 SendMessage 본문 대신 아티팩트 파일 경로 전달(R006 Artifact Channel Protocol)로 대체합니다.
413
413
 
414
+ > **★★ v2.1.246+**: `maxTurns` 한도에 도달해 멈춘 서브에이전트의 결과가 이제 **partial로 표시**되고 `SendMessage`로 이어가라는 힌트가 붙습니다 — **이전에는 완료된 것처럼 보였습니다.**
415
+ >
416
+ > **확정된 것 (v1.1.50 세션 실측)**: `maxTurns` 절단은 R020 「Verification-Delegation Non-Termination」이 누적 14회로 기록한 "서브에이전트가 판정 없이 turn을 종료" 증상의 **실재하는 원인 중 하나**다 — 더 이상 가설이 아니다. v1.1.50 릴리즈 세션에서 오케스트레이터가 4개 그룹을 병렬 위임했고, 그중 **3개 그룹이 20턴 `maxTurns` 한도로 절단**되어 통지에 `stopped at its 20-turn limit (partial result)`이 명시됐다:
417
+ > - 한 건은 문장 중간에서 절단(진행도 불명 — 실측 필요).
418
+ > - 다른 한 건은 "Templates 미러를 동기화합니다" 직후 절단 — 오케스트레이터는 이 문구로 **미실행**을 추정했으나, 재개 후 실측 결과 작업은 **이미 완료돼 있었다**. 절단 위치(마지막 출력 문장)로부터 진행도를 추론하는 것 자체가 불가능함을 재확인한 사례다 — R020 「증상만으로 결과를 넘겨짚지 않는다」의 세 방향 중 "(c) 실제가 보고보다 앞섬"의 재현.
419
+ > - 세 번째 건은 "Now R020 — three items. Let's find suitable locations."라는 **다음 작업 예고 직후** 절단 — 착수 여부조차 미실측 상태로 끊겼다.
420
+ >
421
+ > **v2.1.246 이전이었다면 이 partial 표시가 없어 세 건 모두 완료 보고로 읽혔을 것이다.**
422
+ >
423
+ > **확정되지 않은 것**: R020이 기록한 과거 14회 각각이 이 원인이었는지는 미검증이다 — 사례별 귀속은 turn 수·소요 시간을 `maxTurns` 한도와 대조하는 별도 검증이 필요하다.
424
+ >
425
+ > **행동 함의**: 이 원인은 R020의 clause 강화("판정 없이 종료하지 말라")가 14회 내내 실패했던 이유를 설명한다 — **절단 주체가 에이전트의 판단이 아니라 플랫폼의 turn 한도이면, 에이전트를 향한 지시는 애초에 닿지 않는다.** 대칭적으로 R020 「위임 경계를 Phase 개수로 설계」(단일 목표로 분할)가 효과적이었던 이유도 설명된다 — 작업이 작으면 `maxTurns` 안에서 자연히 끝나기 때문이지, 에이전트가 더 순종적이어서가 아니다.
426
+ >
427
+ > **낮추지 말 것**: 원인이 확정됐다고 해서 위 표의 결정론적 ground-truth 검증 원칙을 낮추지 않는다 — 절단이 아닌 원인(위임 경계 미분할, 에이전트 자체 판단 종료)도 계속 존재한다. 또한 **partial 표시는 v2.1.246 이상에서만 나타나므로**, 그 이전 버전에서 관측된 mid-step 종료 사례를 재해석할 때는 이 신호 자체가 부재했다는 것을 전제로 한다 — "partial 표시가 없었다"가 "maxTurns 절단이 아니었다"의 증거는 아니다.
428
+
429
+ > **v2.1.251+**: Agent Teams 팀원의 최종 답변이 팀 리드에 도달하지 못하던 결함이 수정되어, 이제 idle notification에 실려 도착합니다(이전에는 내용 없는 "available" 알림만 떴습니다). 위 v2.1.224 "SendMessage teammate inbox 쓰기 실패 시에도 Message sent로 보고" 수정의 **후속 실증**입니다 — 구버전에서는 팀원이 정상 완료해도 리드가 그 답변을 못 받을 수 있었으므로, "결정론적 ground-truth로 확인"이라는 위 표의 원칙이 이 시점 이전 세션에서는 특히 중요했습니다.
430
+
431
+ > **v2.1.234+**: `/config`의 "Default teammate model" 설정이 **제거**되어, agent-team teammate는 이제 spawn이 모델을 지정하지 않는 한 **leader의 모델**을 사용합니다. 이전에는 teammate 모델을 전역 설정값으로 지정할 수 있었으므로, 과거 세션의 "teammate가 어떤 모델로 실행됐는지" 서술은 이 변경 이전 버전 기준일 수 있습니다.
432
+
414
433
  <!-- ARCHIVED CC version note (historical):
415
434
  > **CC v2.1.162+**: `claude agents --json` now includes a `waitingFor` field showing what a waiting session is blocked on (e.g. a permission prompt). Use it as an additional deterministic ground-truth signal — a member with a non-empty `waitingFor` is blocked on input (needs unblocking), NOT silently stalled (reassign per stall handling below). This distinguishes the two failure modes the verification is meant to separate.
416
435
 
@@ -126,6 +126,18 @@ Origin: #1443 (Session 126 회고 찐빠 #1) — v1.1.3 R017 검증에서 mgr-sa
126
126
 
127
127
  Cross-reference: R018 (Member Completion Verification), `feedback_release_delegation_phasing`, `feedback_orchestrator_direct_verify` (release delegation phasing을 verification 위임에도 확장).
128
128
 
129
+ #### maxTurns 절단 실증 (Origin: v1.1.50 세션)
130
+
131
+ **실측 (v1.1.50 세션)**: 오케스트레이터가 4개 그룹을 병렬 위임했고 **그중 3개가 20턴 `maxTurns` 한도로 절단**됐다. 세 건 모두 통지에 `stopped at its 20-turn limit (partial result)`이 명시됐고 출력이 작업 중간에서 끊겼다 — 한 건은 문장 중간에서 절단(진행도 불명, 실측 필요), 한 건은 "Templates 미러를 동기화합니다"라고 예고한 직후 절단(오케스트레이터는 미실행으로 추정했으나 **실측 결과 미러 동기화까지 이미 완료**돼 있었다 — 위 「증상만으로 결과를 넘겨짚지 않는다」의 "(c) 실제가 보고보다 앞섬" 재현), 한 건은 "Now R020 — three items"라고 다음 작업을 예고한 직후 절단(실측 결과 **편집은 완료, 검증만 미수행** 상태였다). 세 건 모두 **절단 위치 문장과 실제 진행도가 어긋났다** — 이것이 이 실증의 핵심이다.
132
+
133
+ 1. **확정**: `maxTurns` 절단은 이 조항이 누적 14회로 기록한 "판정 없이 종료" 증상의 **실재하는 원인 중 하나**다. CC v2.1.246부터 partial로 표시되므로 이제 **관측 가능**하다(그 이전에는 완료로 보였다 — R018 v2.1.246 노트 교차참조).
134
+ 2. **미확정**: 과거 14회 **각각**이 이 원인이었는지는 미검증이다. 사례별 귀속에는 turn 수·소요 시간 대조가 필요하다.
135
+ 3. **설명력**: 이것은 **왜 clause 강화가 14회 내내 실패했는지**를 설명한다 — 에이전트에게 "종료하지 말라"고 지시해도 **절단 주체가 에이전트가 아니면 지시가 닿지 않는다**. 역으로 「위임 경계를 Phase 개수로 설계」가 효과적이었던 이유도 설명된다: 작업이 작으면 턴 한도 안에 끝나기 때문이지 에이전트가 더 순종적이어서가 아니다.
136
+ 4. **정량 기준 신설**: 위임 크기 판정을 "Phase가 몇 개인가"에서 **"필요 tool call이 20턴 안에 들어가는가"**로 바꾼다. 파일 1개당 Read + Edit + 미러 Edit + diff 확인 = 약 4턴이므로, **파일 편집형 위임은 담당 파일 4~5개가 실질 상한**이다. v1.1.50 세션의 절단 3건은 담당 파일이 각각 4개·3개·7개였고 파일당 신규 노트 추가·은퇴 판정·미러 동기화를 함께 요구했다 — 산술적으로 20턴에 들어갈 수 없는 위임이었다. **이는 에이전트의 실패가 아니라 오케스트레이터의 위임 설계 결함이다.**
137
+ 5. **완화책**: 미러 동기화처럼 **후행 필수 작업은 마지막에 몰지 말고 파일 단위로 즉시 수행**한다 — 절단은 항상 마지막 작업을 자르므로, 마지막에 몰린 작업은 절단 시 전량 유실된다.
138
+
139
+ Cross-reference: R018 (v2.1.246 maxTurns partial-marking 노트), R009 (Member Prompt Size Cap — 프롬프트 토큰 상한과 별개로 턴 수 상한도 위임 크기 설계 변수임을 추가).
140
+
129
141
  <!--
130
142
  > **v2.1.199+**: subagent가 API 오류(usage limit reached 등)를 성공 결과로 오보하던 문제가 수정되어 이제 오류가 parent agent에 정확히 보고됩니다. 플랫폼 수정으로 false-success 자가보고 빈도는 줄지만, "actual outcome ≠ attempt" ground-truth 검증 원칙(R020 Core Rule)은 여전히 유지된다 — subagent 보고를 그대로 신뢰하지 말고 `git status`/`grep`/validation script로 재확인한다.
131
143
 
@@ -298,6 +310,8 @@ Origin: #1266 ④.
298
310
 
299
311
  실증: 2026-07-30 세션에서 자가 보고는 "직전 두 응답에서 누락"(2회)이었으나 transcript 실측은 **7회**였다 — 3.5배 과소 계상. Origin: #1553 찐빠 #2.
300
312
 
313
+ **계수를 수행하지 않았다면 그 사실을 명시할 것 (Origin: #1601, v1.1.49 세션)**: 시간·비용 제약으로 transcript 파싱 계수를 생략하는 경우, 회고 자체에 "전수 계수 미수행"임을 밝혀야 한다. 계수하지 않은 회고의 항목 목록은 위반 전수가 아니라 **"진행 중 자각했거나 서브에이전트가 지적한 항목"에 한정**되며, 이를 밝히지 않으면 독자가 목록을 전수로 오해해 후속 조치 우선순위가 왜곡된다. v1.1.49 세션 회고는 계수를 수행하지 않았고 그 사실을 스스로 명시했다(좋은 사례) — 대조적으로 그 이전 세션(위 실증)은 계수 미수행 여부를 밝히지 않은 기억 기반 자가 보고였고 실측 대비 3.5배 과소 계상이었다.
314
+
301
315
  이는 Read-Before-Characterize의 **자기 적용** 각도다 — 진단 대상이 외부 로그가 아니라 자기 자신의 transcript일 때에도 "읽기 전 특성화 금지"가 동일하게 적용된다.
302
316
 
303
317
  #### 자율 루프 세션의 턴 경계 정의 (계수 전 확정 필수)
@@ -375,6 +389,20 @@ Session 106: during 529 buffering, a CHANGELOG was misdiagnosed as "61x 중복
375
389
 
376
390
  Origin: #1269 ① (R020 self-violation, session 106).
377
391
 
392
+ ### Failure/Interrupt Report ≠ Actual Failure (reverse direction)
393
+
394
+ 위 항목들은 대체로 "성공 보고 ≠ 실제 성공"을 다루지만, **역방향**도 동일하게 검증 대상이다 — "실패/중단 보고"를 받았을 때도 ground-truth를 확인하기 전에는 실제로 실패했다고 단정하지 않는다.
395
+
396
+ | 증상 | 실제 상태 | 확인 수단 |
397
+ |------|-----------|-----------|
398
+ | **v2.1.246+**: 매우 큰 기존 파일을 덮어쓴 뒤 Write 도구가 "Out of memory"를 보고하거나 오래 멈춤 | **파일 자체는 정상적으로 쓰여 있었다** | 도구의 실패 보고 대신 파일 내용/크기를 직접 재확인 |
399
+ | **v2.1.246+**: 헤드리스/원격 세션에서 수신 메시지로 인터럽트된 MCP 도구 호출이 "출력 없이 완료됨"으로 보고됨(v2.1.246 이전) | 실제로는 **인터럽트**됐다 — 정상 완료가 아니었다 | v2.1.246+는 명시적 interrupted 에러로 보고하도록 수정됨; 구버전 세션의 "빈 출력 완료"는 무음 인터럽트였을 수 있음 |
400
+ | **v2.1.246+**: 실행 중 인터럽트된 셸 명령이 "Ran 1 shell command"로만 표시(잘렸다는 표시 없음, v2.1.246 이전) | 명령이 **완주하지 못했다** | 출력 완결성을 별도로 확인(예상 출력 패턴 대조) 없이 "실행됨"만으로 성공 단정 금지 |
401
+
402
+ > **v2.1.234+**: print/SDK 모드에서 SIGTERM 수신 시 더 이상 interrupted turn이나 synthetic tool denial을 기록하지 않는다(명령은 여전히 종료되고 프로세스는 exit code 143). 무인 실행(`-p` 모드) 강제 종료 후 트랜스크립트를 완료 판정 근거로 쓸 때, v2.1.234+에서는 SIGTERM에 의한 중단이 트랜스크립트 상에 "interrupted"로 남지 않는다는 점을 전제해야 한다 — 트랜스크립트가 깨끗해 보여도 실제로는 SIGTERM으로 잘렸을 수 있다.
403
+
404
+ **교훈**: 위 Core Rule("actual outcome ≠ attempt")은 방향이 없다 — 도구가 성공을 보고하든 실패를 보고하든, 보고 자체는 ground-truth가 아니다. 실패 보고를 받았다고 곧바로 재시도·롤백에 들어가지 말고, 먼저 실제 산출물 상태를 확인한다.
405
+
378
406
  ### CI Publish-Step Error vs Published-Artifact Ground Truth
379
407
 
380
408
  > Origin: #1332 — `npm publish --provenance` emitted a Sigstore `TLOG_CREATE_ENTRY_ERROR` 409, but the publish step's `|| npm view <pkg>@<ver>` fallback recovered (the package WAS published) and release.yml succeeded on all jobs. A subagent read the tlog error in the logs and prematurely declared the run "failed", recommending a re-run; deterministic ground-truth (`npm view`, `gh release view`) showed the release had fully succeeded.
@@ -95,21 +95,37 @@ R016의 승격 루프(위반 지적 → 규칙 조항 추가)는 코퍼스의 **
95
95
 
96
96
  ### 버전노트 보존정책
97
97
 
98
- - 규칙 내 CC 버전노트(`> **v2.1.NNN+**:`)는 최근 2-3개 마이너 릴리즈(현행 기준 v2.1.212 이상)만 visible 유지한다.
98
+ - 규칙 내 CC 버전노트(`> **v2.1.NNN+**:`)는 최근 2-3개 마이너 릴리즈(현행 기준 v2.1.230 이상)만 visible 유지한다.
99
99
  - 그 이하 버전노트는 HTML-comment화(무손실 중간 단계) 하거나 `guides/claude-code/15-version-compatibility.md`로 이관한다.
100
100
  - `claude-native` 스킬이 생성하는 버전 추적 이슈를 규칙에 반영할 때, 최신만 visible로 두고 구버전은 즉시 은닉한다.
101
+ - **기준선은 고정 상수가 아니라 최신 CC 대비 상대 폭으로 유지한다**: 기준선 v2.1.212가 설정될 당시 CC 최신은 v2.1.233이었으므로 보존 폭은 약 21 patch였다. 이번 상향(v1.1.50, `claude --version` = `npm view @anthropic-ai/claude-code version` 실측 = v2.1.251) 시점에 같은 폭을 유지하려면 기준선이 v2.1.230이어야 한다 — 기준선 갱신 시 "최신 실측값 − 약 20 patch"로 재계산할 것.
102
+
103
+ #### 은퇴 판정 기준 (기준선 미만 ≠ 자동 은퇴)
104
+
105
+ 기준선 미만은 은퇴 **검토 대상**을 정의할 뿐, 은퇴 **여부**를 자동으로 결정하지 않는다. 기준선 미만 노트는 다음 3개 조건을 **모두** 충족할 때만 은퇴(HTML-comment화)한다 — 하나라도 걸리면 **유지**하고, 유지 판정과 사유를 스윕 기록에 남긴다.
106
+
107
+ | 조건 | 판정 |
108
+ |------|------|
109
+ | (a) 인용 부재 | 다른 어떤 visible 노트도 그것을 "같은 계열"(cf., 연장선, 인접 등)로 인용하지 않는가 |
110
+ | (b) 비현행 | 현행 동작을 규정하지 않는가 (예: 이미 롤백/재수정된 과거 상태 서술) |
111
+ | (c) 진단 함의 소멸 | 회고적 진단 함의(과거 관측 재해석 근거)가 더 이상 없는가 |
112
+
113
+ 세 조건 모두 참 → 은퇴. 하나라도 거짓 → 유지(anchor로 인용되거나, 현행 동작을 서술하거나, 진단 함의가 살아있는 노트는 기준선 미만이어도 보존 가치가 있다). 이 기준은 v1.1.50 4개 병렬 그룹의 실제 판정에서 역추출한 것이다(아래 실적 참조) — "기준선 미만 = 즉시 은퇴"로 문자 그대로 읽으면 이 기준과 모순된다.
101
114
 
102
115
  #### 보존 기준 변경 = 전 룰 파일 스윕 (같은 릴리즈 내 필수)
103
116
 
104
- 보존 기준선을 상향하면 **같은 릴리즈에서 23개 룰 파일 전수를 스윕**해 기준 미만 노트를 HTML-comment화한다. 기준만 올리고 적용을 다음 릴리즈로 이월하면 코퍼스가 기준과 불일치한 상태로 남고, 그 불일치는 다음 회고에서 "잔존 N건" 부채로 재발견될 때까지 보이지 않는다. 스윕 범위는 `.claude/rules/**`와 `templates/.claude/rules/**` 양쪽이며, 잔존 여부는 **HTML 주석 안/밖을 구분해** 실측한다 — 단순 `grep`은 이미 은퇴한 주석 내부 노트까지 세어 판정을 왜곡한다.
117
+ 보존 기준선을 상향하면 **같은 릴리즈에서 23개 룰 파일 전수를 스윕**한다 — "스윕"은 기준 미만 노트의 **전량 HTML-comment화**가 아니라, 위 「은퇴 판정 기준」 3조건에 따른 **전수 검토**(각 노트를 은퇴/유지로 판정하고 유지 시 사유를 기록)를 의미한다. 기준만 올리고 검토 자체를 다음 릴리즈로 이월하면 코퍼스가 기준과 불일치한 상태로 남고, 그 불일치는 다음 회고에서 "잔존 N건" 부채로 재발견될 때까지 보이지 않는다 — **이월 금지는 변하지 않는다**, 변하는 것은 "전수 은퇴"가 아니라 "전수 판정"이 의무라는 점이다. 스윕 범위는 `.claude/rules/**`와 `templates/.claude/rules/**` 양쪽이며, 잔존 여부는 **HTML 주석 안/밖을 구분해** 실측한다 — 단순 `grep`은 이미 은퇴한 주석 내부 노트까지 세어 판정을 왜곡한다.
105
118
 
106
119
  | Anti-pattern | Required |
107
120
  |--------------|----------|
108
- | 보존 기준선만 상향하고 기존 노트 스윕을 다음 릴리즈로 이월 | 기준 상향과 전 룰 파일 스윕을 같은 릴리즈에서 완료 |
109
- | `grep -c` 히트 수로 잔존 판정 | 주석 안/밖을 구분해 **visible 잔존**만 계수 |
121
+ | 보존 기준선만 상향하고 기존 노트 검토를 다음 릴리즈로 이월 | 기준 상향과 전 룰 파일의 **전수 판정**(은퇴/유지 + 유지 사유 기록)을 같은 릴리즈에서 완료 |
122
+ | 기준선 미만 노트를 판정 없이 전량 HTML-comment화 | 「은퇴 판정 기준」 3조건(인용 부재 AND 비현행 AND 진단 함의 소멸)을 적용해 항목별 판정 |
123
+ | `grep -c` 히트 수로 잔존 판정 | 주석 안/밖을 구분해 **visible 잔존**만 계수 — 잔존 자체는 결함이 아니다(유지 판정의 결과일 수 있음) |
110
124
 
111
125
  Origin: #1563 찐빠 #4 — R016이 보존 기준을 v2.1.212로 규정했으나 R001/R005/R012에 visible v2.1.208 노트 3건이 잔존해 v1.1.44에서 뒤늦게 은퇴. Cross-reference: R005(HTML-comment 컨텍스트 최적화), R017(Count Sync — 전수 grep + 의미 판별).
112
126
 
127
+ **실적 (v1.1.50)**: 기준선을 v2.1.212→v2.1.230으로 상향할 때, 룰 파일 소유권을 4개 병렬 그룹으로 분배해 각 그룹이 자기 담당 파일만 스윕했다 — 스윕을 **작업 종류**(예: "은퇴 담당" vs "신규 노트 담당")가 아니라 **파일 소유권**으로 분배해야 병렬 에이전트 간 동일 파일 동시 편집 충돌이 발생하지 않는다(R009 File-Disjoint 원칙의 룰 코퍼스 자체 적용 사례). 스윕 결과는 **은퇴 2건 / 유지 다수**(기준선 미만 visible 노트 49건이 11개 파일에 잔존 — 전수 검토 후 유지 판정) — 은퇴된 2건은 어떤 visible 노트도 인용하지 않는 순수 이력 서사(R006 v2.1.201/204: v2.1.201 Sonnet 5 harness reminder 전달방식, v2.1.204 headless SessionStart 스트리밍)였고, 유지된 노트 대부분은 다른 visible 노트가 "같은 계열"로 인용하는 anchor이거나(예: v2.1.222가 v2.1.211/212/214를 인용) 현행 동작을 서술 중이었다. 은퇴 2건이 바로 "인용 없는 서사만 은퇴됐다"는 판정 기준의 양성 사례다. **판정이 버전 번호가 아니라 인용 관계로 이루어졌다는 뜻**이며, **이 판정 기준을 같은 릴리즈에서 정식 조항으로 승격했다**(sauron FAIL 지적 → 같은 커밋 내 정합화 — 위 「은퇴 판정 기준」참조). 잔존 49건/11파일은 결함이 아니라 3조건 판정에 따른 유지 결과이므로, 다음 회고가 이를 "잔존 N건" 부채로 오인하지 않도록 여기 고정 기록한다.
128
+
113
129
  ### Cross-References
114
130
 
115
131
  R005(HTML-comment 컨텍스트 최적화), R023(Deprecated-Platform-Feature Staleness Check — 폐기 참조를 결정론적으로 탐지하여 은퇴 후보를 조기 발굴), Origin #1473.
@@ -4,7 +4,9 @@
4
4
 
5
5
  ## Core Policy
6
6
 
7
- oh-my-customcode uses an **advisory-first enforcement model**. Most rules are enforced through prompt engineering (CLAUDE.md, rules/, PostCompact hook) rather than hard-blocking hooks. This is intentional — it preserves agent flexibility while maintaining behavioral standards.
7
+ oh-my-customcode uses an **advisory-first enforcement model**. Most rules are enforced through prompt engineering (CLAUDE.md, rules/, `SessionStart` re-injection[^postcompact]) rather than hard-blocking hooks. This is intentional — it preserves agent flexibility while maintaining behavioral standards.
8
+
9
+ [^postcompact]: compact 후 재주입의 문서상 보장 경로는 `SessionStart`(matcher `*`, `claude-md-reinject.sh` — v1.1.50 #1617)이다. 기존 PostCompact prompt 배선은 유지되나 공식 문서상 `additionalContext`가 정의돼 있지 않아 효과 미보장·발동 미검증(hook-events-audit 2026-08-29). Origin: #1619 #7 — 최초 보고는 'PostCompact 공식 부재'였으나 감사 실측 결과 실재하되 additionalContext 미정의로 정정됨. 서브에이전트 보고의 검증 없는 인용이 틀린 전제를 회고 이슈까지 전파시킨 사례 (R020 원인 분석 검증 조항의 실증).
8
10
 
9
11
  ## Enforcement Tiers
10
12
 
@@ -17,7 +19,7 @@ oh-my-customcode uses an **advisory-first enforcement model**. Most rules are en
17
19
  | Advisory (proactive) | UserPromptSubmit + SubagentStop + PostToolUse hooks | R007, R008 (`r007-r008-drift-advisor.sh` — #1229 UserPromptSubmit, #1545 SubagentStop, #1553 PostToolUse) | Reads last assistant turn; emits advisory if header/prefix absent. SubagentStop wiring (#1545) closes the no-user-input autonomous-loop gap (`/fsd`); PostToolUse (#1553) covers the orchestrator-only stretch before the first subagent spawn. Complements retroactive Stop-hook (`session-reflection.sh`, #1190). **v1.1.43부터 실제 발화 — 아래 각주 참조.** v1.1.49부터 역방향(announce > tool_use) 신호 포함, 기본 off 옵트인 (#1595 #6). |
18
20
  | Advisory (telemetry) | PostToolUseFailure hook | — (계측 전용, 규칙 강제 없음) | `failure-ledger.sh` (#1561, v1.1.44) — 도구 실패를 JSONL 원장에 append. stdout/stderr 무출력이라 모델에 도달하지 않으며 절대 차단하지 않음 |
19
21
  | Advisory (proactive) | UserPromptSubmit hook | R020 (원인 진단) | `fail-axis-cause-advisor.sh` (#1561, v1.1.44) — 원장에 실패 기록이 있는데 원인 진술 없는 재촉 프롬프트가 오면 `hookSpecificOutput.additionalContext`로 "원인 가설 되묻기" advisory 전달. 원장 부재 시 조용히 통과 |
20
- | Prompt-based | CLAUDE.md + rules/ + PostCompact | All MUST rules | Behavioral guidance in context |
22
+ | Prompt-based | CLAUDE.md + rules/ + `SessionStart` 재주입(matcher `*`; PostCompact 이벤트 배선은 유지되나 효과 미보장[^postcompact]) | All MUST rules | Behavioral guidance in context |
21
23
 
22
24
  > **Advisory (proactive/retroactive) 발화 결함과 해소 (실측)**: `hookSpecificOutput.additionalContext` **전달 경로 자체는 #1547(v1.1.40)에서 구현**됐으나, 그 앞단 **파서 셀렉터 결함**으로 advisory가 **v1.1.42까지 한 번도 발화하지 못했다** — `jq -r '.role'`로 읽었으나 트랜스크립트 최상위에 `role` 키가 없어(실제는 `.message.role`) `last_assistant`가 항상 비고 즉시 `exit 0`으로 종료됐다. 당시 실측: 트랜스크립트 771개 전수에서 `"additionalContext":` 출현 0건, 라이브 프로브 stdout/stderr 각 0바이트. **proactive(`r007-r008-drift-advisor.sh`)와 retroactive(`session-reflection.sh`, 동일 결함) 두 계층 모두 미발화**였다. **v1.1.43에서 양 계층 파서 복구 + `PostToolUse` 배선을 완료했고, 라이브 프로브로 최초 발화를 확인했다(#1553).** 후속으로 v1.1.44에서 R008 판정을 블록 인접 비교 → 턴 단위 개수 비교로 전환(#1563), v1.1.45에서 Skill 도구 면제를 추가했다(#1569). **v1.1.49에서 역방향 신호**(announce > tool_use — 도구 호출을 예고해 놓고 tool_use 블록 없이 턴을 종료한 방향)**를 추가했다(#1595 #6)** — 기존 판정식 `max(0, tool_use − announce)`는 이 방향을 **구조적으로 0으로 처리**해 원리적으로 탐지 불가였다. 역방향은 **전용 앵커 정규식**(`$an_anchored`, 줄 시작 앵커 있음 — forward의 `$an_tool`에는 적용하지 않는다. forward는 announce를 덜 세면 위반이 **늘어나기** 때문)을 쓰고, **Skill 포함 전체 tool_use가 0건**일 때만 계상한다(Skill 제외 카운트를 쓰면 Skill만 호출한 준수 턴에서 오발화). 기본 off 옵트인(`OMCUSTOM_R008_REVERSE=on`으로 활성)이다 — 482턴 실측에서 순진한 `announce − ntools > 0` 구현은 36턴에 발화해 advisory 총량을 2배로 만들었고(16건은 Skill 제외 아티팩트, 15건은 앵커 없는 정규식의 산문 매칭), 협소화 후 3/3 진양성·오탐 0(앵커 비용은 실제 announce 969줄 중 1줄, 0.1%)이 되었으나 표본이 3건이라 기본 활성은 보류했다. **배선 구조상 예방 효과가 없다는 점도 보류 근거다** — 결함 턴은 tool_use가 0이라 `PostToolUse`·`SubagentStop`이 발화하지 않고, `UserPromptSubmit`은 사용자가 이미 개입한 뒤 발화한다. 정시에 발화하는 유일한 이벤트는 `Stop`이며 거기 걸린 훅은 `session-reflection.sh`다. 역방향은 그래서 **의도적으로 advisor 전용**이며 `session-reflection.sh`에는 복제하지 않았다(같은 결함을 두 번 보고하면서 교정 기회는 여전히 0이 되고, 되돌릴 지점만 두 곳이 된다).
23
25
  >
@@ -35,12 +37,16 @@ oh-my-customcode uses an **advisory-first enforcement model**. Most rules are en
35
37
 
36
38
  > **v2.1.222+**: PreToolUse auto-allow 훅이 background agent task(summaries/compaction/renames)에서 tool restriction을 우회하던 문제가 수정되었습니다. 즉 위 Enforcement Tiers 표의 **Hard Block 계층(stage-blocker, dev-server tmux, rule-deletion-guard)이 background agent task 경로에서 우회될 수 있었다**는 뜻이며, background agent를 쓰는 장기 무인 루프에서 hard-block 훅이 실제로는 강제되지 않는 구간이 존재했습니다. v2.1.211/212/214 훅 결정 존중 체인의 연장선입니다.
37
39
 
40
+ > **v2.1.247+**: 훅 또는 background agent가 수 메가바이트의 error 출력을 찍어 대화를 overflow시켜 세션이 "Prompt is too long"으로 멈추던 결함이 수정되었습니다. 같은 릴리즈에서 hook/background task의 output file을 쓸 수 없을 때 무한 메모리 증가하던 문제도 수정되어, 이제 출력이 소실된 위치를 파일에 남깁니다. 이 저장소는 advisory 훅(`r007-r008-drift-advisor.sh`, `failure-ledger.sh`, `fail-axis-cause-advisor.sh` 등)을 다수 운용하므로, 훅 출력 폭주가 세션 자체를 wedge시키는 이 실패 클래스에 해당합니다 — v2.1.211/212/214/222 훅 신뢰성 계열의 연장선입니다.
41
+
42
+ > **v2.1.248+**: 훅 관측성이 두 건 강화되었습니다 — (a) `PermissionRequest`/`PreToolUse` 훅이 유효하지 않은 응답을 출력해 background session이 조용히 대기하던 결함이 수정되어, 이제 `claude agents` 행이 해당 훅 이름과 스키마 에러를 표시합니다. (b) 훅이 stdout으로 낸 `{…}` 객체가 유효한 JSON이 아닐 때 조용히 plain text로 처리하던 결함이 수정되어, 이제 parse 에러와 함께 훅 에러로 보고됩니다. 위 v2.1.214 "훅 stdout JSON이 스키마 검증에 실패할 때 exit code 2가 문서대로 차단하지 못하던 문제" 노트와 같은 계열 — 훅 실패가 무음에서 가시화되는 흐름의 연속입니다.
43
+
38
44
  ## Why Advisory-First
39
45
 
40
46
  1. **Agent flexibility**: Hard blocks can trap agents in unrecoverable states
41
47
  2. **Graceful degradation**: Missing dependencies (jq, etc.) don't break the session
42
48
  3. **Composability**: External skills and internal rules can coexist without deadlocks
43
- 4. **PostCompact reinforcement**: R007/R008/R009/R010/R018 are re-injected after context compaction
49
+ 4. **Post-compact reinforcement**: R007/R008/R009/R010/R018 are re-injected after context compaction via `SessionStart`(matcher `*`/`compact`) — not via the PostCompact event itself[^postcompact]
44
50
 
45
51
  ## Hard Enforcement Candidates — R010 git-delegation-guard (conditional), R007/R008 advisory **implemented & firing** (#1229 UserPromptSubmit, proactive) + **#1545 SubagentStop** (closes autonomous-loop gap) + **#1553 PostToolUse** + retroactive Stop-hook (#1190); `additionalContext` 전달 경로는 #1547(v1.1.40)에서 구현됐으나 파서 셀렉터 결함으로 v1.1.42까지 **양 계층 모두 미발화**였고, **v1.1.43에서 수정 완료·발화 확인**(#1553); hard-block variant still candidate if advisory insufficient (#1096). Promoted: rule-deletion-guard.sh (2026-04-08). See details via Read tool.
46
52
 
@@ -67,4 +73,4 @@ Promotion requires: (1) measured violation rate data, (2) user approval, (3) rol
67
73
  |------|-------------|
68
74
  | R010 | git-delegation-guard.sh is advisory; could promote to blocking |
69
75
  | R016 | Violations trigger rule updates, not enforcement changes |
70
- | PostCompact | Re-injects critical rules to combat context compaction amnesia |
76
+ | SessionStart (compact re-entry) | Re-injects critical rules to combat context compaction amnesia — see [^postcompact] |
@@ -388,6 +388,18 @@ Origin: #1595 #2 (v1.1.48 세션 — `git checkout -b release/v1.1.48 develop`
388
388
 
389
389
  > Origin: #1574 (v1.1.44 세션 대조 실증 — 동일 고지를 받은 3개 병렬 에이전트 중 [1]은 `bun test` 11 fail을 "형제가 그 파일 편집 중"으로 정황 귀속해 오답, [2]/[3]은 개입 실험으로 정확히 귀속). Cross-ref: R020 (Read-Before-Characterize — 정황으로 특성화 금지).
390
390
 
391
+ #### 형제 결과의 교차 서술 금지 (Origin: #1619 #3)
392
+
393
+ 병렬 위임서에 **형제 그룹의 결과를 서술·집계하는 작업을 포함하지 않는다** ("전 그룹 공통 결과는 X" 류). 형제 고지는 담당 범위 구분용이지, 형제 결과를 인용할 권한이 아니다 — 위 「고지는 귀속 후보를 늘릴 뿐 증거 등급을 올리지 않는다」와 같은 계열로, 고지가 형제에 대한 서술 권한까지 주지는 않는다.
394
+
395
+ 형제 결과의 집계·서술은 **전 그룹 완료 후 오케스트레이터가 대조해 직접 확정**하거나, 전 그룹 완료를 실측한 뒤 별도 위임으로 수행한다.
396
+
397
+ | Anti-pattern | Required |
398
+ |--------------|----------|
399
+ | 병렬 그룹 위임서에 "전 그룹 공통 결과" 서술 작업 포함 → 기재 시점 참이 완료 순서에 따라 거짓화 | 교차 서술은 전 그룹 완료 실측 후 오케스트레이터 대조 또는 후속 위임으로 |
400
+
401
+ > Origin: #1619 #3 (v1.1.50 세션 — 4개 병렬 룰편집 그룹 중 Group 4가 R016 실적 문단에 "은퇴 0건(전 그룹 공통)"을 기재. 기재 시점(Group 1·3 완료, Group 2 미완)에는 참이었으나 Group 2가 이후 2건을 은퇴시켜 서술이 거짓이 되었고 정정 왕복 1회 발생 — 교차 서술은 본질적으로 스냅샷이다). Cross-ref: 위 「고지는 귀속 후보를 늘릴 뿐 증거 등급을 올리지 않는다」.
402
+
391
403
  ##### "플래키"는 원인이 아니다 (Origin: #1598)
392
404
 
393
405
  간헐 실패에 **"플래키"·"부하 의존"이라는 판정을 결론으로 쓰지 않는다** — 그것은 "재현 조건을 아직 못 찾았다"는 뜻이지 "원인이 무작위"라는 뜻이 아니다. 각 서브에이전트는 격리 컨텍스트라 **형제가 같은 스위트를 동시에 도는 것을 구조적으로 볼 수 없으므로**, 형제 경합이 원인인 실패에 대해 각자 합리적이지만 틀린 "부하 의존 플래키" 결론에 도달한다. 간헐 실패는 개입 실험(단독 재실행 / 격리 `$TMPDIR` 재측정 / 형제 완료 후 재현)으로 귀속하고, 귀속에 실패하면 **"원인 미귀속 — 재현 조건 미확보"로 보고**한다.
@@ -488,6 +500,12 @@ Before spawning any agent:
488
500
 
489
501
  > **v2.1.232+**: interactive session의 **non-teammate 에이전트 스폰이 기본 background 실행**으로 바뀌었습니다(subagent forking 기본 활성화의 일부). 즉 Agent 도구 호출의 반환은 "작업 완료"가 아니라 **"백그라운드 착수"일 수 있으므로**, 오케스트레이터는 스폰 반환이나 완료 통지를 완료 근거로 삼지 않고 R020 ground-truth(`git status` / `grep` / 검증 스크립트)로 확인합니다 — 구버전에서는 동기 반환이 기본이라 "반환 = 완료"라는 암묵 전제가 대체로 성립했고, 그 전제가 이 버전부터 무너집니다. 위 v2.1.221 `/status` 표시와 v2.1.211(실행 중 agent 결과를 지어내지 않음)이 진단 보조 수단입니다. cross-ref R009(fork의 컨텍스트 상속), R018(Teams member는 non-teammate가 아니므로 이 변경 대상 밖).
490
502
 
503
+ > **★ v2.1.234+**: 세션 범위 permission 응답(**거부 포함**)이 background subagent의 tool permission 프롬프트에 응답할 때 **드롭**되던 결함이 수정되었습니다. 구버전에서는 background subagent에 대한 승인·거부가 **적용되지 않고 사라질 수 있었습니다** — 즉 "거부했다"가 "거부가 적용됐다"의 증거가 아니었습니다. 위 v2.1.232 non-teammate 기본 background 실행 서술과 결합하면, 과거 무인 루프에서 서브에이전트가 예상과 다르게 동작한 원인을 이것으로 재해석할 여지가 있습니다(단, 확정 진단이 아니라 원인 후보로만 취급 — R020 Diagnostic Hypothesis Verification).
504
+
505
+ > **v2.1.234+**: background task 알림(턴 사이에 전달되는 것)이 이제 mid-turn 전달과 동일하게 `<system-reminder>` 태그 안에 담겨 모델에 전달됩니다. 오케스트레이터가 background 에이전트 완료 통지를 받는 경로가 이것이므로, 그 통지는 **시스템 메시지이지 사용자 입력이 아닙니다** — R015 "다른 에이전트의 메시지는 결코 사용자의 승인이 아니다" 원칙과 마찬가지로, background 통지 역시 사용자 승인의 증거로 인용하지 않습니다. 이전에는 턴 사이 알림 형식이 mid-turn과 달라 이 구분이 덜 명확했습니다.
506
+
507
+ > **cross-ref (v1.1.50 실측)**: R018의 `maxTurns` partial 표시(v2.1.246)가 R020 「Verification-Delegation Non-Termination」 mid-step 종료 패턴의 **실재 원인 중 하나로 확정**되었다 — 위임 프롬프트에 종료 금지 clause를 아무리 강화해도, 절단 주체가 플랫폼 turn 한도이면 에이전트에 닿지 않는다. 위임 경계를 단일 목표로 분할하는 것(R020 해당 조항)이 여전히 1차 방어선인 이유다. 상세는 R018 (MUST-agent-teams.md) Member Completion Verification 섹션.
508
+
491
509
  ## Agent Capability Pre-Check
492
510
 
493
511
  Before delegating a task to a subagent, MUST verify the target agent's tool capabilities against the task requirements. Failure to pre-check causes round-trip waste (delegation → failure → re-delegation).