oh-my-customcode 1.1.42 → 1.1.43

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/cli/index.js CHANGED
@@ -241,7 +241,7 @@ var init_package = __esm(() => {
241
241
  workspaces: [
242
242
  "packages/*"
243
243
  ],
244
- version: "1.1.42",
244
+ version: "1.1.43",
245
245
  description: "Batteries-included agent harness for Claude Code",
246
246
  type: "module",
247
247
  bin: {
package/dist/index.js CHANGED
@@ -2031,7 +2031,7 @@ var package_default = {
2031
2031
  workspaces: [
2032
2032
  "packages/*"
2033
2033
  ],
2034
- version: "1.1.42",
2034
+ version: "1.1.43",
2035
2035
  description: "Batteries-included agent harness for Claude Code",
2036
2036
  type: "module",
2037
2037
  bin: {
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "workspaces": [
4
4
  "packages/*"
5
5
  ],
6
- "version": "1.1.42",
6
+ "version": "1.1.43",
7
7
  "description": "Batteries-included agent harness for Claude Code",
8
8
  "type": "module",
9
9
  "bin": {
@@ -455,6 +455,17 @@
455
455
  ],
456
456
  "description": "Context budget advisor \u2014 track tool usage patterns and advise ecomode activation"
457
457
  },
458
+ {
459
+ "matcher": "tool == \"Edit\" || tool == \"Write\" || tool == \"Bash\" || tool == \"Task\" || tool == \"Agent\" || tool == \"Read\" || tool == \"Glob\" || tool == \"Grep\"",
460
+ "hooks": [
461
+ {
462
+ "type": "command",
463
+ "command": "bash .claude/hooks/scripts/r007-r008-drift-advisor.sh",
464
+ "continueOnBlock": true
465
+ }
466
+ ],
467
+ "description": "Proactive R007/R008 drift advisory on tool use \u2014 covers the orchestrator-only stretch before the first subagent spawn, where neither UserPromptSubmit nor SubagentStop fires (#1553). Turn-deduplicated; exit 0 advisory only."
468
+ },
458
469
  {
459
470
  "matcher": "tool == \"Edit\" || tool == \"Write\" || tool == \"Bash\" || tool == \"Task\" || tool == \"Agent\"",
460
471
  "hooks": [
@@ -1,47 +1,83 @@
1
1
  #!/usr/bin/env bash
2
- # r007-r008-drift-advisor.sh — PROACTIVE R007/R008 drift advisory (#1229, #1545, #1547)
2
+ # r007-r008-drift-advisor.sh — PROACTIVE R007/R008 drift advisory (#1229, #1545, #1547, #1553)
3
3
  #
4
- # Wired to TWO trigger points:
4
+ # Wired to THREE trigger points:
5
5
  # 1. UserPromptSubmit — fires before Claude responds to a user-typed prompt (#1229).
6
6
  # 2. SubagentStop — fires when a background subagent (Agent tool) completes, covering
7
7
  # autonomous-loop re-entry (e.g. /fsd) where the orchestrator resumes WITHOUT a
8
8
  # UserPromptSubmit event. Prior to #1545, autonomous-loop re-entry had zero R007/R008
9
9
  # advisory coverage since UserPromptSubmit never fires in that path.
10
+ # 3. PostToolUse — fires after every tool call, covering the remaining structural gap
11
+ # (#1553): an orchestrator-only stretch BEFORE the first subagent spawn satisfies
12
+ # neither UserPromptSubmit (no user input) nor SubagentStop (no subagent yet).
13
+ # Measured 2026-08-05: 7/8 responses in such a stretch had missing R007 headers with
14
+ # zero advisory fires.
10
15
  #
11
- # Inspects the LAST completed assistant turn in the session transcript for R007/R008
12
- # compliance BEFORE Claude responds. If the previous turn drifted (missing identification
13
- # header / tool prefix), delivers an advisory so the upcoming response self-corrects.
16
+ # Inspects the LAST completed assistant TURN in the session transcript for R007/R008
17
+ # compliance. If the turn drifted (missing identification header / tool prefix), delivers
18
+ # an advisory so the next response self-corrects.
14
19
  #
15
20
  # This is the PROACTIVE complement to the retroactive session-reflection.sh (Stop hook, #1190).
16
- # Detection patterns are reused from session-reflection.sh.
21
+ # Detection patterns are shared with session-reflection.sh.
17
22
  #
18
23
  # Advisory-only: ALWAYS exits 0, NEVER blocks.
19
- # Performance: parses ONLY the last assistant turn (not the whole transcript).
20
- # Input-schema note: session_id/transcript_path are COMMON fields present on both
21
- # UserPromptSubmit and SubagentStop hook payloads, so the detection logic below is
22
- # event-agnostic and required no functional changes for the SubagentStop wiring.
23
24
  #
24
- # Delivery mechanism (#1547 fix):
25
- # Prior to this fix, the advisory was written to stderr with exit 0. Per the official
26
- # Claude Code hook spec, stderr on exit 0 is NEVER fed into the model's context for ANY
27
- # hook event (it is only visible in transcript debug mode, i.e. to a human, not Claude) —
28
- # so #1545's SubagentStop wiring never actually reached the model despite firing correctly.
29
- # The confirmed non-blocking delivery path for BOTH UserPromptSubmit and SubagentStop is
30
- # `hookSpecificOutput.additionalContext` in JSON stdout with exit 0:
25
+ # ── Transcript schema (MEASURED 2026-08-05, #1553) ────────────────────────────────────
26
+ # Claude Code JSONL lines do NOT carry a TOP-LEVEL `role`/`content`. The measured top-level
27
+ # key set is:
28
+ # attributionSkill, cwd, effort, entrypoint, gitBranch, isSidechain, message, parentUuid,
29
+ # requestId, sessionId, session_id, timestamp, type, userType, uuid, version
30
+ # The role lives at `.message.role` and the content blocks at `.message.content`.
31
+ # The previous implementation selected `.role` / `.content`, which ALWAYS evaluated to empty
32
+ # — so this advisor exited before ever reaching its detection logic and had NEVER fired
33
+ # (verified: 0 occurrences of `"additionalContext":` across 771 transcripts; a live probe
34
+ # produced 0 bytes on both stdout and stderr).
35
+ #
36
+ # Additional measured facts that shape this implementation:
37
+ # * ONE content block per JSONL line (867 assistant lines across 3 sessions, 0 multi-block).
38
+ # A single assistant TURN therefore spans MULTIPLE consecutive lines. Treating "the last
39
+ # assistant line" as "the last turn" makes the R008 adjacency test (`i > 0`) permanently
40
+ # false — every tool call would be reported as a violation. Turn reconstruction is a
41
+ # PRECONDITION for the PostToolUse wiring, not an optimization.
42
+ # * `isSidechain: true` marks subagent turns. They MUST be excluded or a subagent's turn
43
+ # is misattributed to the orchestrator. (Note: jq's `//` treats `false` as empty, so the
44
+ # filter is written as `(.isSidechain // false) != true`.)
45
+ # * User lines are NOT all turn boundaries — tool results arrive as `.message.role == "user"`
46
+ # with `tool_result` content blocks. Only a genuine prompt (string content, or an array
47
+ # with no tool_result block) ends a turn.
48
+ # * `thinking` blocks are interleaved with text/tool_use and never carry an R008 prefix;
49
+ # they are filtered out before adjacency analysis.
50
+ #
51
+ # ── Performance ───────────────────────────────────────────────────────────────────────
52
+ # The previous implementation forked jq once PER LINE inside a `while read` loop (measured
53
+ # 2.80s and 8.64s on real transcripts). PostToolUse fires on EVERY tool call, so that cost
54
+ # would stall the session. This version reads a bounded `tail -n 200` window and forks jq
55
+ # exactly ONCE for the whole analysis.
56
+ #
57
+ # ── Dedup ─────────────────────────────────────────────────────────────────────────────
58
+ # PostToolUse fires repeatedly within a single turn. The turn's first assistant line `uuid`
59
+ # is recorded in a per-session marker file; the same turn never produces a second advisory.
60
+ # The marker is derived purely from transcript content (no `date`, no randomness) so the
61
+ # behavior is idempotent and testable.
62
+ #
63
+ # ── Delivery mechanism (#1547 fix) ────────────────────────────────────────────────────
64
+ # Per the official Claude Code hook spec, stderr on exit 0 is NEVER fed into the model's
65
+ # context for ANY hook event (it is only visible in transcript debug mode, i.e. to a human).
66
+ # The confirmed non-blocking delivery path is `hookSpecificOutput.additionalContext` in
67
+ # JSON stdout with exit 0:
31
68
  # {"hookSpecificOutput": {"hookEventName": "<event>", "additionalContext": "<text>"}}
69
+ # `hookEventName` MUST echo the ACTUAL firing event — a wrong value invalidates the output,
70
+ # so a missing `hook_event_name` field is a hard `exit 0` (no default is guessed).
32
71
  # This is NOT the same as `"decision": "block"` — that would force Stop/SubagentStop to
33
- # block (refuse to stop), which is exactly the blocking behavior R021 (advisory-first
34
- # enforcement) forbids for this hook. additionalContext alone (no `decision` field) is
35
- # non-blocking: Claude is allowed to stop/continue normally and simply sees the extra
36
- # context on its next turn. Exit code MUST stay 0 — exit 2 causes Claude Code to discard
37
- # any JSON output and treat stderr as a blocking error instead (see Common JSON Fields /
38
- # Exit Code Behavior in the official hook reference).
39
- # The stderr line is kept for human-visible audit trail (harmless on exit 0) but is no
40
- # longer the delivery mechanism.
72
+ # block, exactly the behavior R021 (advisory-first enforcement) forbids here. Exit code MUST
73
+ # stay 0 — exit 2 causes Claude Code to discard JSON output and treat stderr as a blocking
74
+ # error instead.
75
+ # The stderr line is kept purely as a human-visible audit trail.
41
76
  #
42
77
  # 환경변수 override (테스트/디버깅용):
43
78
  # OMCUSTOM_R007_ADVISOR=off — advisory 완전 비활성화 (pass-through)
44
- # OMCUSTOM_TRANSCRIPT_BASE — transcript 디렉토리 경로 override
79
+ # OMCUSTOM_TRANSCRIPT_BASE — transcript 디렉토리 경로 override (설정 시 최우선)
80
+ # OMCUSTOM_R007_MARKER_DIR — dedup 마커 디렉토리 override (기본 ${TMPDIR:-/tmp})
45
81
 
46
82
  set -euo pipefail
47
83
 
@@ -58,98 +94,116 @@ if ! command -v jq >/dev/null 2>&1; then
58
94
  exit 0
59
95
  fi
60
96
 
61
- # ── session_id 추출 ──
62
- session_id=$(echo "$input" | jq -r '.session_id // empty' 2>/dev/null)
97
+ # ── 입력 필드 추출 (jq 1회 fork) ──
98
+ meta=$(printf '%s' "$input" | jq -r '[(.session_id // ""), (.hook_event_name // ""), (.transcript_path // "")] | @tsv' 2>/dev/null) || exit 0
99
+ session_id=$(printf '%s' "$meta" | cut -f1)
100
+ hook_event_name=$(printf '%s' "$meta" | cut -f2)
101
+ transcript_in=$(printf '%s' "$meta" | cut -f3)
102
+
63
103
  if [ -z "$session_id" ]; then
64
104
  exit 0
65
105
  fi
66
106
 
67
- # ── hook_event_name 추출 (hookSpecificOutput.hookEventName에 되돌려줄 값) ──
68
- hook_event_name=$(echo "$input" | jq -r '.hook_event_name // empty' 2>/dev/null)
107
+ # hook_event_name이 없으면 hookSpecificOutput.hookEventName을 정확히 채울 수 없다.
108
+ # 잘못된 기본값은 출력을 무효화하므로 추측하지 않고 즉시 종료한다 (fallback 제거).
69
109
  if [ -z "$hook_event_name" ]; then
70
- hook_event_name="UserPromptSubmit"
110
+ exit 0
71
111
  fi
72
112
 
73
- # ── 경로 결정 (환경변수 override 지원) ──
74
- TRANSCRIPT_BASE="${OMCUSTOM_TRANSCRIPT_BASE:-${HOME}/.claude/projects/-Users-sangyi-workspace-projects-oh-my-customcode}"
75
- TRANSCRIPT_PATH="${TRANSCRIPT_BASE}/${session_id}.jsonl"
113
+ # ── transcript 경로 결정 ──
114
+ # 우선순위: 테스트 override > 훅 페이로드의 transcript_path > 기본 경로
115
+ if [ -n "${OMCUSTOM_TRANSCRIPT_BASE:-}" ]; then
116
+ TRANSCRIPT_PATH="${OMCUSTOM_TRANSCRIPT_BASE}/${session_id}.jsonl"
117
+ elif [ -n "$transcript_in" ]; then
118
+ TRANSCRIPT_PATH="$transcript_in"
119
+ else
120
+ TRANSCRIPT_PATH="${HOME}/.claude/projects/-Users-sangyi-workspace-projects-oh-my-customcode/${session_id}.jsonl"
121
+ fi
76
122
 
77
123
  if [ ! -f "$TRANSCRIPT_PATH" ]; then
78
124
  exit 0
79
125
  fi
80
126
 
81
- # ── 마지막 assistant 메시지 추출 (성능: 전체 transcript 스캔 회피) ──
82
- # 파일을 역순으로 읽으며 첫 번째 role=="assistant" 라인을 찾는다.
83
- last_assistant=""
84
- while IFS= read -r line; do
85
- role=$(echo "$line" | jq -r '.role // empty' 2>/dev/null) || continue
86
- if [ "$role" = "assistant" ]; then
87
- last_assistant="$line"
88
- break
89
- fi
90
- done < <(tail -r "$TRANSCRIPT_PATH" 2>/dev/null || tac "$TRANSCRIPT_PATH" 2>/dev/null)
91
-
92
- if [ -z "$last_assistant" ]; then
127
+ # ── 마지막 assistant 턴 재구성 + R007/R008 판정 (jq 1회 fork) ──
128
+ # 출력: "<turn-uuid>\t<r007-count>\t<r008-count>" (턴이 없으면 무출력)
129
+ JQ_LAST_TURN='
130
+ split("\n")
131
+ | map(select(length > 0) | (fromjson? // empty))
132
+ | map(select((.isSidechain // false) != true))
133
+ | . as $L
134
+ | ($L | length) as $n
135
+ | [ range(0; $n) | select($L[.].message.role? == "assistant") ] as $ai
136
+ | if ($ai | length) == 0 then empty
137
+ else
138
+ ($ai[-1]) as $last
139
+ | [ range(0; $last + 1)
140
+ | select( ($L[.].message.role? == "user")
141
+ and ( (($L[.].message.content | type) == "string")
142
+ or (([ $L[.].message.content[]? | select(.type? == "tool_result") ] | length) == 0) ) ) ] as $bi
143
+ | (if ($bi | length) > 0 then $bi[-1] else -1 end) as $b
144
+ | [ range($b + 1; $last + 1) | $L[.] | select(.message.role? == "assistant") ] as $turn
145
+ | [ $turn[] | .message.content[]? | select(.type? != "thinking") ] as $blocks
146
+ | ($turn[0].uuid? // "") as $tuuid
147
+ | ([ $blocks[] | select(.type? == "text") ][0].text? // "") as $ftext
148
+ | (($ftext | split("\n") | .[0]) // "") as $fline
149
+ | (if ($ftext | length) == 0 then 0
150
+ elif ($fline | test("^┌─ Agent:")) or ($fline | test("^\\[.+\\]")) then 0
151
+ else 1 end) as $r007
152
+ | ([ range(0; ($blocks | length))
153
+ | select($blocks[.].type? == "tool_use")
154
+ | select( (. == 0)
155
+ or ($blocks[. - 1].type? != "text")
156
+ or (((($blocks[. - 1].text?) // "") | test("\\[.+\\]\\[.+\\] ?(→|->|—>) ?(Tool|Target):")) | not) ) ] | length) as $r008
157
+ | [$tuuid, ($r007 | tostring), ($r008 | tostring)] | @tsv
158
+ end
159
+ '
160
+
161
+ result=$(tail -n 200 "$TRANSCRIPT_PATH" 2>/dev/null | jq -Rsr "$JQ_LAST_TURN" 2>/dev/null) || result=""
162
+
163
+ if [ -z "$result" ]; then
93
164
  exit 0
94
165
  fi
95
166
 
96
- # ── content 배열 파싱 ──
97
- content_raw=$(echo "$last_assistant" | jq -c '.content // []' 2>/dev/null) || content_raw="[]"
167
+ turn_uuid=$(printf '%s' "$result" | cut -f1)
168
+ r007_violations=$(printf '%s' "$result" | cut -f2)
169
+ r008_violations=$(printf '%s' "$result" | cut -f3)
98
170
 
99
- r007_violations=0
100
- r008_violations=0
171
+ : "${r007_violations:=0}"
172
+ : "${r008_violations:=0}"
101
173
 
102
- # ── R007: 첫 번째 text 블록의 첫 줄 체크 ──
103
- first_text=$(echo "$content_raw" | jq -r '[.[] | select(.type == "text")][0].text // empty' 2>/dev/null) || first_text=""
104
- if [ -n "$first_text" ]; then
105
- first_line=$(printf '%s' "$first_text" | head -1)
106
- # R007 패턴: '┌─ Agent:' 또는 '[anything]' 단축 형태
107
- if ! printf '%s' "$first_line" | grep -qE '(^┌─ Agent:|^\[.+\])'; then
108
- r007_violations=$((r007_violations + 1))
109
- fi
174
+ # ── 위반이 없으면 아무것도 출력하지 않는다 (오탐 방지) ──
175
+ if [ "$r007_violations" -eq 0 ] && [ "$r008_violations" -eq 0 ]; then
176
+ exit 0
110
177
  fi
111
178
 
112
- # ── R008: tool_use 블록 직전 text에 prefix 체크 ──
113
- content_length=$(echo "$content_raw" | jq 'length' 2>/dev/null) || content_length=0
114
- i=0
115
- while [ "$i" -lt "$content_length" ]; do
116
- block_type=$(echo "$content_raw" | jq -r ".[$i].type // empty" 2>/dev/null) || { i=$((i+1)); continue; }
117
-
118
- if [ "$block_type" = "tool_use" ]; then
119
- has_prefix=false
120
- if [ "$i" -gt 0 ]; then
121
- prev_type=$(echo "$content_raw" | jq -r ".[$(( i - 1 ))].type // empty" 2>/dev/null) || true
122
- if [ "$prev_type" = "text" ]; then
123
- prev_text=$(echo "$content_raw" | jq -r ".[$(( i - 1 ))].text // empty" 2>/dev/null) || true
124
- # R008 패턴: '[agent-name][model] → Tool:' 또는 '→ Target:'
125
- if printf '%s' "$prev_text" | grep -qE '\[.+\]\[.+\] ?(→|->|—>) ?(Tool|Target):'; then
126
- has_prefix=true
127
- fi
128
- fi
129
- fi
130
- if [ "$has_prefix" = "false" ]; then
131
- r008_violations=$((r008_violations + 1))
132
- fi
179
+ # ── dedup: 같은 턴에 대해 두 번 발화하지 않는다 ──
180
+ # 마커 키는 transcript 내용(턴의 첫 assistant uuid)에서만 파생 — date/랜덤 사용 금지.
181
+ MARKER_DIR="${OMCUSTOM_R007_MARKER_DIR:-${TMPDIR:-/tmp}}"
182
+ marker_key=$(printf '%s' "$session_id" | tr -c 'A-Za-z0-9._-' '_')
183
+ MARKER_FILE="${MARKER_DIR}/.omcustom-r007-advisor-${marker_key}"
184
+
185
+ if [ -n "$turn_uuid" ] && [ -f "$MARKER_FILE" ]; then
186
+ prev_uuid=$(cat "$MARKER_FILE" 2>/dev/null || printf '')
187
+ if [ "$prev_uuid" = "$turn_uuid" ]; then
188
+ exit 0
133
189
  fi
190
+ fi
134
191
 
135
- i=$((i+1))
136
- done
192
+ advisory_text=$(printf '[R007/R008 Advisory] 직전 응답에서 식별 누락 감지 (R007 헤더=%s, R008 접두사=%s). 이번 응답은 ┌─ Agent: 헤더로 시작하고, 모든 도구 호출에 [agent][model] → Tool: 접두사를 포함하십시오.' \
193
+ "$r007_violations" "$r008_violations")
137
194
 
138
- # ── advisory 전달 (위반 시에만) ──
139
- if [ "$r007_violations" -gt 0 ] || [ "$r008_violations" -gt 0 ]; then
140
- advisory_text=$(printf '[R007/R008 Advisory] 직전 응답에서 식별 누락 감지 (R007 헤더=%d, R008 접두사=%d). 이번 응답은 ┌─ Agent: 헤더로 시작하고, 모든 도구 호출에 [agent][model] → Tool: 접두사를 포함하십시오.' \
141
- "$r007_violations" "$r008_violations")
195
+ # 사람이 보는 감사 추적용 (exit 0에서는 모델에 전달되지 않음 — #1547 참고)
196
+ printf '%s\n' "$advisory_text" >&2
142
197
 
143
- # 사람이 보는 감사 추적용 (exit 0에서는 모델에 전달되지 않음 — #1547 참고)
144
- printf '%s\n' "$advisory_text" >&2
198
+ # #1547 fix: hookSpecificOutput.additionalContext로 모델 컨텍스트에 실제 전달.
199
+ # decision 필드는 절대 포함하지 않는다 — "block"을 쓰면 Stop/SubagentStop 정지를
200
+ # 강제로 막아버려 advisory-only 원칙(R021)을 위반하게 된다. exit code는 반드시 0.
201
+ jq -cn --arg event "$hook_event_name" --arg ctx "$advisory_text" \
202
+ '{hookSpecificOutput: {hookEventName: $event, additionalContext: $ctx}}'
145
203
 
146
- # #1547 fix: hookSpecificOutput.additionalContext로 모델 컨텍스트에 실제 전달.
147
- # decision 필드는 절대 포함하지 않는다 — "block"을 쓰면 Stop/SubagentStop 정지를
148
- # 강제로 막아버려 advisory-only 원칙(R021)을 위반하게 된다. exit code는 반드시 0.
149
- jq -cn --arg event "$hook_event_name" --arg ctx "$advisory_text" \
150
- '{hookSpecificOutput: {hookEventName: $event, additionalContext: $ctx}}'
204
+ if [ -n "$turn_uuid" ]; then
205
+ printf '%s' "$turn_uuid" > "$MARKER_FILE" 2>/dev/null || true
151
206
  fi
152
207
 
153
- # ── 위반이 없으면 아무것도 출력하지 않는다 (오탐 방지) ──
154
208
  # ── 항상 exit 0 (advisory는 절대 차단 금지) ──
155
209
  exit 0
@@ -84,76 +84,76 @@ LOG_FILE="${OUTPUT_DIR}/$(date +%Y-%m-%d).md"
84
84
  ISO8601="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
85
85
 
86
86
  # ── transcript 파싱 ──
87
- r007_violations=0
88
- r008_violations=0
89
- total_turns=0
90
- sample_count=0
91
- sample_lines=""
92
-
93
- while IFS= read -r line; do
94
- role=$(echo "$line" | jq -r '.role // empty' 2>/dev/null) || continue
95
- [ "$role" = "assistant" ] || continue
96
-
97
- total_turns=$((total_turns + 1))
98
- turn_idx=$total_turns
99
-
100
- # content 배열 파싱
101
- content_raw=$(echo "$line" | jq -c '.content // []' 2>/dev/null) || continue
102
-
103
- # ── R007: 첫 번째 text 블록의 첫 줄 체크 ──
104
- first_text=$(echo "$content_raw" | jq -r '[.[] | select(.type == "text")][0].text // empty' 2>/dev/null) || true
105
- if [ -n "$first_text" ]; then
106
- first_line=$(printf '%s' "$first_text" | head -1)
107
- # R007 패턴: '┌─ Agent:' 또는 '[anything]' 단축 형태
108
- if ! printf '%s' "$first_line" | grep -qE '(^┌─ Agent:|^\[.+\])'; then
109
- r007_violations=$((r007_violations + 1))
110
- if [ $sample_count -lt 3 ]; then
111
- # 120자 truncate (secret-filter.sh는 PostToolUse용이라 여기서는 단순 truncate)
112
- safe_text=$(printf '%s' "$first_line" | head -c 120)
113
- sample_lines="${sample_lines}
114
- - [R007 turn ${turn_idx}]: ${safe_text}"
115
- sample_count=$((sample_count + 1))
116
- fi
117
- fi
118
- fi
119
-
120
- # ── R008: tool_use 블록 직전 text에 prefix 체크 ──
121
- content_length=$(echo "$content_raw" | jq 'length' 2>/dev/null) || continue
122
-
123
- i=0
124
- while [ $i -lt "$content_length" ]; do
125
- block_type=$(echo "$content_raw" | jq -r ".[$i].type // empty" 2>/dev/null) || { i=$((i+1)); continue; }
126
-
127
- if [ "$block_type" = "tool_use" ]; then
128
- tool_name=$(echo "$content_raw" | jq -r ".[$i].name // empty" 2>/dev/null) || true
129
-
130
- # 직전 블록이 text이고 R008 prefix를 포함하는지 체크
131
- has_prefix=false
132
- if [ $i -gt 0 ]; then
133
- prev_type=$(echo "$content_raw" | jq -r ".[$(( i - 1 ))].type // empty" 2>/dev/null) || true
134
- if [ "$prev_type" = "text" ]; then
135
- prev_text=$(echo "$content_raw" | jq -r ".[$(( i - 1 ))].text // empty" 2>/dev/null) || true
136
- # R008 패턴: '[agent-name][model] → Tool:' 또는 '→ Target:'
137
- if printf '%s' "$prev_text" | grep -qE '\[.+\]\[.+\] ?(→|->|—>) ?(Tool|Target):'; then
138
- has_prefix=true
139
- fi
140
- fi
141
- fi
142
-
143
- if [ "$has_prefix" = "false" ]; then
144
- r008_violations=$((r008_violations + 1))
145
- if [ $sample_count -lt 3 ]; then
146
- sample_lines="${sample_lines}
147
- - [R008 turn ${turn_idx}]: ${tool_name}, missing prefix"
148
- sample_count=$((sample_count + 1))
149
- fi
150
- fi
151
- fi
152
-
153
- i=$((i+1))
154
- done
155
-
156
- done < "$TRANSCRIPT_PATH"
87
+ #
88
+ # 스키마 주의 (MEASURED 2026-08-05, #1553): Claude Code JSONL 라인에는 최상위 `role`/`content`가
89
+ # 없다. 실제 경로는 `.message.role` / `.message.content`다. 종전 구현은 `.role`을 읽어 항상 빈 값을
90
+ # 얻었고, 그 결과 assistant 라인이 하나도 매칭되지 않아 이 분석기는 사실상 0계층 탐지였다.
91
+ #
92
+ # 그리고 content 블록은 라인당 1개다 — 한 assistant 턴이 여러 줄에 걸친다. 따라서 라인 단위로
93
+ # R008 인접성(직전 블록이 text인지)을 보면 모든 tool_use가 영구 위반으로 집계된다. 턴을 먼저
94
+ # 복원한 뒤 블록 인접성을 본다. 턴 경계는 "진짜 user 프롬프트"(문자열 content 또는 tool_result가
95
+ # 없는 배열)이며, tool_result user 라인은 경계가 아니다. `isSidechain: true`(서브에이전트 턴)은
96
+ # 제외한다. `thinking` 블록은 R008 접두사를 가질 수 없으므로 인접성 판정 전에 제거한다.
97
+ #
98
+ # 성능: 줄마다 jq를 포크하던 구조를 jq 1회 포크로 교체.
99
+ JQ_REFLECT='
100
+ split("\n")
101
+ | map(select(length > 0) | (fromjson? // empty))
102
+ | map(select((.isSidechain // false) != true))
103
+ | [ foreach .[] as $l (0;
104
+ (if ($l.message.role? == "user")
105
+ and ( (($l.message.content | type) == "string")
106
+ or (([ $l.message.content[]? | select(.type? == "tool_result") ] | length) == 0) )
107
+ then . + 1 else . end);
108
+ {g: ., l: $l}) ]
109
+ | map(select(.l.message.role? == "assistant"))
110
+ | group_by(.g)
111
+ | map({ blocks: [ .[].l.message.content[]? | select(.type? != "thinking") ] })
112
+ | . as $turns
113
+ | ($turns | length) as $total
114
+ | [ range(0; $total)
115
+ | . as $ti
116
+ | ($turns[$ti].blocks) as $blocks
117
+ | ([ $blocks[] | select(.type? == "text") ][0].text? // "") as $ftext
118
+ | (($ftext | split("\n") | .[0]) // "") as $fline
119
+ | ( if ($ftext | length) > 0
120
+ and ((($fline | test("^┌─ Agent:")) or ($fline | test("^\\[.+\\]"))) | not)
121
+ then [ {k: "R007", turn: ($ti + 1), s: ($fline[0:120])} ]
122
+ else [] end )
123
+ + [ range(0; ($blocks | length))
124
+ | select($blocks[.].type? == "tool_use")
125
+ | select( (. == 0)
126
+ or ($blocks[. - 1].type? != "text")
127
+ or (((($blocks[. - 1].text?) // "") | test("\\[.+\\]\\[.+\\] ?(→|->|—>) ?(Tool|Target):")) | not) )
128
+ | {k: "R008", turn: ($ti + 1), s: ($blocks[.].name? // "")} ]
129
+ ]
130
+ | flatten
131
+ | . as $viol
132
+ | ( [ ($total | tostring),
133
+ ([ $viol[] | select(.k == "R007") ] | length | tostring),
134
+ ([ $viol[] | select(.k == "R008") ] | length | tostring) ] | @tsv ),
135
+ ( $viol[0:3][]
136
+ | if .k == "R007" then "- [R007 turn \(.turn)]: \(.s)"
137
+ else "- [R008 turn \(.turn)]: \(.s), missing prefix" end )
138
+ '
139
+
140
+ analysis=$(jq -Rsr "$JQ_REFLECT" < "$TRANSCRIPT_PATH" 2>/dev/null) || analysis=""
141
+
142
+ counts_line=$(printf '%s\n' "$analysis" | head -1)
143
+ total_turns=$(printf '%s' "$counts_line" | cut -f1)
144
+ r007_violations=$(printf '%s' "$counts_line" | cut -f2)
145
+ r008_violations=$(printf '%s' "$counts_line" | cut -f3)
146
+ sample_lines=$(printf '%s\n' "$analysis" | tail -n +2)
147
+
148
+ [ -n "$total_turns" ] || total_turns=0
149
+ [ -n "$r007_violations" ] || r007_violations=0
150
+ [ -n "$r008_violations" ] || r008_violations=0
151
+
152
+ if [ -n "$sample_lines" ]; then
153
+ sample_count=$(printf '%s\n' "$sample_lines" | grep -c . || true)
154
+ else
155
+ sample_count=0
156
+ fi
157
157
 
158
158
  # ── Phase 2 (#1196): background_tasks / session_crons 분석 ──
159
159
  bg_total=$(echo "$BG_TASKS_JSON" | jq 'length' 2>/dev/null || echo 0)
@@ -26,7 +26,7 @@
26
26
 
27
27
  > **파이프 뒤 `$?`는 마지막 명령의 exit code (#1492, zsh 변형 #1540)**: `script.sh | tail -N; echo $?`처럼 검증 스크립트를 파이프에 연결한 뒤 `$?`로 읽으면 파이프라인 **마지막 명령**(`tail`)의 종료코드를 얻는다 — 스크립트 자체가 실패(exit 1)해도 `tail`이 성공(exit 0)하면 `$?=0`으로 "통과"를 오판한다. **1차 지침**: 검증 스크립트는 파이프 없이 단독 실행한다. 부득이 파이프를 써야 한다면, 원본 exit code를 읽는 문법은 **셸마다 다르다** — bash는 `${PIPESTATUS[0]}`(대문자, 0-indexed), zsh는 `$pipestatus[1]`(소문자, 1-indexed)이며 서로 호환되지 않는다. **이 저장소의 기본 셸이자 Claude Code Bash 도구 실행 셸은 zsh**이므로, bash 문법 `${PIPESTATUS[0]}`을 그대로 쓰면 zsh에서는 미정의 변수로 취급되어 **오류 없이 빈 값**을 반환한다 — 조건문에서 빈 값은 거짓으로 평가돼 "검증 통과"처럼 보이는 조용한 오판을 재생산한다. 셸을 사전 확인(`echo $SHELL` / `$BASH_VERSION` 존재 여부)한 뒤 해당 셸의 문법을 쓴다. **주의**: `${PIPESTATUS[0]}` 자체는 R023 Workflow JS 템플릿 리터럴 이스케이프 이슈(#1438, `${...}`를 JS가 평가해 ReferenceError)와 별개 문제 — 본 항목은 셸에서 파이프 뒤 exit code를 읽는 각도다. Origin: #1492 (Session 132 회고 찐빠 #3); zsh 변형은 #1540 (Session 138 회고 찐빠 #6) — `gh run watch ... | tail` 뒤 `${PIPESTATUS[0]}`가 zsh에서 빈 값을 반환해 CI 결론을 재실측해야 했음. Cross-ref: R020 ("command executed" ≠ "succeeded").
28
28
 
29
- > **계수/매칭 방법 확인 (#1521)**: 카운트를 대조하기 전에 **비교 대상이 무엇을 어떻게 세는지** 먼저 확인한다 — 같은 지표라도 계수 방법이 다르면 값이 달라진다. 대표 함정 3종: (a) glob(`ls *.md`, 최상위만) vs 재귀 `find`(하위 디렉토리 포함), (b) 부분 문자열 grep(`grep "sdd"`가 `sdd-dev`까지 매칭), (c) 확장자 필터(`--include='*.md'`가 `CLAUDE.md.en`을 미매칭). 검증 스크립트와 대조할 때는 **스크립트의 실제 계수 로직을 읽고** 같은 방법으로 센다. 위 `ls | tail` 시계열 오판(#1417)과 동류로, 도구의 기본 동작을 확인하지 않은 채 결과를 해석해 오탐에 이르는 패턴이다. Origin: #1521 (2026-07-20 세션에서 3회 반복; 두 서브에이전트가 독립적으로 동일 오탐에 도달).
29
+ > **계수/매칭 방법 확인 (#1521)**: 카운트를 대조하기 전에 **비교 대상이 무엇을 어떻게 세는지** 먼저 확인한다 — 같은 지표라도 계수 방법이 다르면 값이 달라진다. 대표 함정 4종: (a) glob(`ls *.md`, 최상위만) vs 재귀 `find`(하위 디렉토리 포함), (b) 부분 문자열 grep(`grep "sdd"`가 `sdd-dev`까지 매칭), (c) 확장자 필터(`--include='*.md'`가 `CLAUDE.md.en`을 미매칭), (d) **머지 커밋 diff 기본 생략** — `git show --name-only <머지커밋>`은 diff를 기본적으로 출력하지 않아 변경 파일 0개로 오독된다. 머지 커밋의 변경 파일을 세려면 `--first-parent`(1차 부모 대비) 또는 `-m`(각 부모별 diff)을 명시한다. 검증 스크립트와 대조할 때는 **스크립트의 실제 계수 로직을 읽고** 같은 방법으로 센다. 위 `ls | tail` 시계열 오판(#1417)과 동류로, 도구의 기본 동작을 확인하지 않은 채 결과를 해석해 오탐에 이르는 패턴이다. Origin: #1521 (2026-07-20 세션에서 3회 반복; 두 서브에이전트가 독립적으로 동일 오탐에 도달); (d)는 #1553 찐빠 #4 (2026-07-30 세션에서 머지 커밋 `--name-only` 0파일을 "변경 없음"으로 오독).
30
30
 
31
31
  <!--
32
32
  > **v2.1.206+**: `/doctor`에 checked-in CLAUDE.md에서 코드베이스로부터 파생 가능한 내용을 잘라내도록 제안하는 체크가 추가되었습니다 — R005 "Context Optimization via HTML Comments"의 컨텍스트 절감 원칙과 정합(모델 불필요 메타데이터 축소).
@@ -15,7 +15,9 @@ 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
+ <!-- ARCHIVED CC version note (historical):
18
19
  > **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
+ -->
19
21
 
20
22
  ### Model Specification — 3 Tiers
21
23
 
@@ -60,6 +62,8 @@ Skill/rule text instructing "spawn with `model: opus`" refers to this tier — a
60
62
 
61
63
  > **v2.1.219+**: Claude Opus 5 (`claude-opus-5`) added. Opt in via the Tier-2 full ID in frontmatter; Tier-1 `opus`/`sonnet` alias resolution is CC-controlled (see Tier 1 above — this project does not pin it). Relative standing vs Fable 5 is not yet confirmed officially — do not assert an ordering.
62
64
 
65
+ > **v2.1.222+**: **org-restricted 환경에서** `model: opus` 계열 subagent/teammate의 family alias가 parent model로 떨어지던 문제가 수정되어, 이제 해당 family 내에서 org가 허용한 **최신 모델로 step-down**합니다. 이는 Tier 1의 "CC resolves these, not this project" 원칙을 강화하는 사례입니다 — Tier-1 alias 해석에는 **org 제한이라는 추가 변수**가 있어 프로젝트가 pin할 수 없으므로, 특정 모델을 확정하려면 frontmatter에 **Tier-2 full ID**를 씁니다. Agent 도구 spawn 파라미터(Tier 3)는 full ID를 받지 않으므로 이 경로에서는 alias 해석이 org 설정에 좌우됩니다. (본 저장소의 org 제한 여부는 미실측 — 위 조건절이 적용 범위입니다.)
66
+
63
67
  > **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.
64
68
 
65
69
  <!-- ARCHIVED CC version notes (historical):
@@ -171,9 +175,11 @@ Hook JSON output `terminalSequence` field for desktop notifications, window titl
171
175
 
172
176
  ## Hook Event Types
173
177
 
174
- 21 event types supported: PreToolUse, PostToolUse, PreCompact, PostCompact, Stop, SessionStart, SessionEnd, SubagentStart, SubagentStop, UserPromptSubmit, Notification, CwdChanged, FileChanged, Elicitation, ElicitationResult, PostMessage, PermissionDenied, TeammateIdle, TaskCreated, TaskCompleted, DirectoryAdded. 4 handler types: command, prompt, http, agent. See full reference table via Read tool.
178
+ 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.
175
179
 
176
- > **v2.1.219+**: `DirectoryAdded` hook event added — fires after `/add-dir` or an SDK `register_repo_root` control request registers a new working directory mid-session.
180
+ > **`MessageDisplay`는 표시 전용 — `additionalContext` 미지원**: `MessageDisplay`는 `hookSpecificOutput.displayContent`로 **화면 표시 텍스트만** 교체하며, 트랜스크립트와 Claude가 보는 내용은 원본이 유지된다. 따라서 advisory 훅을 `MessageDisplay`에 배선하면 **모델에 도달하지 않는다**. `additionalContext`(모델 컨텍스트 주입)를 지원하는 이벤트는 SessionStart, Setup, SubagentStart, UserPromptSubmit, UserPromptExpansion, PreToolUse, PostToolUse, PostToolUseFailure, PostToolBatch, Stop, SubagentStop이다. (이전 판이 나열하던 `PostMessage`는 문서화된 이벤트가 아니다 — 실제 이벤트명은 `MessageDisplay`.)
181
+
182
+ > **신규 이벤트 발동 시점**: `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` — 는 발동 시점을 미실측이므로 서술하지 않는다.)
177
183
 
178
184
  <!-- DETAIL: Hook Event Types Full Reference
179
185
 
@@ -194,7 +200,7 @@ Hook JSON output `terminalSequence` field for desktop notifications, window titl
194
200
  | `FileChanged` | External file modification | file_path, change_type | command | v2.1.83+ |
195
201
  | `Elicitation` | Agent requests user input | question | command, prompt | v2.1.76+ |
196
202
  | `ElicitationResult` | User responds to elicitation | answer | command, prompt | v2.1.76+ |
197
- | `PostMessage` | After message sent | message_type | command | v2.1.76+ |
203
+ | `MessageDisplay` | While assistant message text is displayed (live streaming); `displayContent` only — does NOT support `additionalContext` | — (not stated) | — (not stated) | not stated |
198
204
  | `PermissionDenied` | Auto mode classifier denial | tool, tool_input, denial_reason | command, prompt | v2.1.88+ |
199
205
  | `TeammateIdle` | Agent Teams member idle | teammate_id | command | v2.1.83+ |
200
206
  | `TaskCreated` | Task created | task_id, description | command | v2.1.83+ |
@@ -480,6 +486,8 @@ Key optional fields: `scope`, `context`, `version`, `effort`, `model`, `agent`,
480
486
 
481
487
  > **v2.1.210+**: 스킬/커맨드 본문에서 인자 없이 호출된(unmatched) `$1`/`$2` positional placeholder가 조용히 제거되던(silently stripped) 동작이 수정되어 이제 리터럴 `$1`로 verbatim 보존됩니다 — 인자 부재 시 `$1`이 확장된 프롬프트에 그대로 남아 지시가 깨지므로, silent stripping에 옵션-인자 처리를 의존하지 말고 인자 부재 케이스를 명시 처리(default text / `$ARGUMENTS` guard / `argument-hint`)해야 합니다. (위 v2.1.163+ `\$1` escape는 항상 리터럴 `$` 출력용 별개 메커니즘으로 이번 변경 대상이 아니며, 이번 수정은 치환 의도의 bare `$1`이 unmatched일 때만 적용됩니다.)
482
488
 
489
+ > **v2.1.222+**: 스킬 frontmatter의 `disable-model-invocation: true`(모델이 스스로 그 스킬을 호출하지 못하게 막고 사용자/파이프라인의 명시적 호출만 허용하는 필드)가 설정된 스킬을 모델이 호출하려 할 때의 refusal 문구가 개선되어, 모델에게 **워크플로우를 스스로 복제하지 말고 사용자에게 실행을 요청하라**고 지시합니다. 무인 루프(`/fsd` 등)가 이런 스킬을 모델 호출 경로에 두면 실행 대신 refusal이 반환되므로, 해당 스킬은 **사용자/파이프라인 명시 호출**로 설계합니다.
490
+
483
491
  <!-- DETAIL: Skill Optional Fields (full yaml block)
484
492
  ```yaml
485
493
  scope: core # core | harness | package (default: core)
@@ -130,6 +130,19 @@ Reference issue: #1096.
130
130
 
131
131
  Origin: #1507 (장기 자율 릴리즈 루프 몰입 중 상태줄 브래킷이 에이전트-id 브래킷 대체 — R007 External-Project/Debugging Session Vigilance의 자율-루프 각도 확장).
132
132
 
133
+ #### Insight/분석 블록 선행 ≠ 헤더 면제
134
+
135
+ 응답을 `★ Insight` 블록, 분석 서술, 표, 코드 블록 등으로 시작하더라도 R007 에이전트 식별 헤더는 **그보다 먼저** 와야 한다. 헤더는 응답의 첫 줄이며, 분석·통찰의 밀도가 높다는 이유로 뒤로 밀리거나 생략될 수 없다.
136
+
137
+ **바로 위 "Status-Line Bracket ≠ Agent Identification Header"와는 별개 실패 모드다.** 그 조항은 *형태 혼동* 축이다 — 상태줄 브래킷(`[FSD Iteration N]`)을 에이전트-id 브래킷으로 착각해 헤더를 이미 쓴 것으로 오인한다. 이 조항은 *작성 순서* 축이다 — 헤더가 무엇인지 알고 있으나 분석 블록을 먼저 쓰기 시작해 헤더가 아예 등장하지 않는다. 형태를 아무리 정확히 구분해도 순서가 틀리면 동일하게 위반이다.
138
+
139
+ | Anti-pattern | Required |
140
+ |--------------|----------|
141
+ | `★ Insight ─────` 블록이나 분석 표/서술로 응답 시작 (헤더 부재) | `┌─ Agent: {name}` 또는 `[{agent-name}]` 헤더를 **첫 줄로** 출력한 뒤 Insight/분석 블록 |
142
+ | "이번엔 분석이 본론이라 헤더는 뒤에 붙이겠다" | 헤더는 항상 선행 — 본론 밀도와 무관 |
143
+
144
+ 실증: 2026-07-30 세션에서 R007 헤더 누락 7건 중 **5건이 `★ Insight` 블록으로 시작**했다 — 단일 최대 누락 경로다. Origin: #1553 찐빠 #2.
145
+
133
146
  ### External-Project / Debugging Session Vigilance
134
147
 
135
148
  R007 헤더 누락은 외부 프로젝트 디버깅, SSH 진단, 배포 작업 등 기술적 몰입 세션에서 가장 자주 발생한다. 이 규칙은 프로젝트 종류와 무관하게 모든 상황에 적용된다.
@@ -54,6 +54,8 @@ These are distinct mechanisms. Agent Teams `SendMessage` requires `TeamCreate` a
54
54
 
55
55
  This hardens cross-session coordination (claude-peers-mcp `send_message`, see Scope table above) against privilege escalation — a relayed message from session A cannot grant session B permissions the user did not authorize on B. Aligns with R001 (credential/privileged-scope guardrails) and R010 (out-of-scope privileged chaining). Intra-session Agent Teams `SendMessage` between peers in the same session is unaffected.
56
56
 
57
+ > **v2.1.222+**: auto mode 안전성 개선 — 다른 agent session으로 `SendMessage`가 보내는 메시지가 dispatch 전에 permission classifier로 평가됩니다. v2.1.166의 relay authority hardening이 **수신** 경로를 막았다면, 이번 변경은 **발신** 경로를 게이트합니다. 따라서 SendMessage 전송 자체를 조율 성공의 증거로 삼지 말고, 아래 Member Completion Verification의 결정론적 ground-truth로 확인합니다. classifier가 2회 걸리면 R010 Subagent Scope-Creep STOP Protocol을 적용해 재전송 대신 범위를 재설계합니다.
58
+
57
59
  <!-- ARCHIVED CC version note (historical):
58
60
  > **v2.1.183+**: Fixed tmux teammate panes failing to launch when the shell has slow rc-file initialization — a slow `.zshrc`/`.bashrc` no longer prevents Agent Teams teammate panes from launching in tmux. Also fixed WebSearch returning empty results in subagents: a subagent (including a Teams member) using WebSearch now returns results instead of silently empty.
59
61
  -->
@@ -379,6 +381,8 @@ Agent Teams member completion MUST be verified by deterministic ground-truth —
379
381
 
380
382
  Cross-reference: R020 ("actual outcome ≠ attempt" — verifying that a command ran is not the same as verifying it succeeded).
381
383
 
384
+ > **v2.1.222+**: `SendMessage`가 긴 summary를 문자 수 제한으로 거부하던 동작이 **절단(truncate)**으로 변경되어 전송이 실패하지 않습니다. 전송 실패가 사라진 대신 **조용한 절단**이라는 새 실패 모드가 생겼으므로, 위 표의 "SendMessage report = Low reliability" 원칙이 오히려 강화됩니다. 긴 보고가 필요하면 SendMessage 본문 대신 아티팩트 파일 경로 전달(R006 Artifact Channel Protocol)로 대체합니다.
385
+
382
386
  <!-- ARCHIVED CC version note (historical):
383
387
  > **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.
384
388
 
@@ -248,6 +248,19 @@ triage-dispatch.yml 실패 원인을 파일 Read 전에 "triaged 라벨 부재 +
248
248
 
249
249
  Origin: #1266 ④.
250
250
 
251
+ ### Self-Violation Counting Is Also Diagnosis (#1553 ②)
252
+
253
+ **자기 위반 횟수를 세는 것도 진단이다.** 회고·자가 보고에서 "몇 번 위반했는가"를 기억(recall)으로 세면 체계적으로 **과소 계상**된다 — 위반 순간은 정의상 자각 없이 지나간 순간이므로, 기억에는 나중에 스스로 알아챈 소수만 남는다. 위반 횟수는 추정하지 말고 **transcript를 실제로 파싱해** 센다(예: 응답 시작 라인에 R007 헤더 패턴이 없는 assistant turn 수를 grep/스크립트로 집계).
254
+
255
+ | Anti-pattern | Required |
256
+ |--------------|----------|
257
+ | 회고에서 "직전 두 응답에서 누락했습니다"처럼 기억 기반으로 위반 횟수 보고 | transcript를 파싱해 실측 집계 후 보고 (예: 헤더 패턴 미매칭 turn 수 grep) |
258
+ | 실측 없이 "몇 회 정도" 추정치로 위반 심각도를 특성화 | 실측값으로 심각도 판정 — 과소 계상은 후속 조치 우선순위를 왜곡한다 |
259
+
260
+ 실증: 2026-07-30 세션에서 자가 보고는 "직전 두 응답에서 누락"(2회)이었으나 transcript 실측은 **7회**였다 — 3.5배 과소 계상. Origin: #1553 찐빠 #2.
261
+
262
+ 이는 Read-Before-Characterize의 **자기 적용** 각도다 — 진단 대상이 외부 로그가 아니라 자기 자신의 transcript일 때에도 "읽기 전 특성화 금지"가 동일하게 적용된다.
263
+
251
264
  ### Proxy Signal vs Canonical Ground-Truth (#1336 ①②)
252
265
 
253
266
  > Origin: #1336 ①② — transcription was alarmed as "stopped" because `.txt` files looked stale, but the canonical DB had transcripts current to 06-09 21:30 (.txt is not the whisper collector's output — it emits only to the DB). Separately, SMS was over-diagnosed as "fully blocked" from one empty OneDrive XML path + a single 401, while the DB held 17 SMS rows ingested via the app path.
@@ -95,7 +95,7 @@ R016의 승격 루프(위반 지적 → 규칙 조항 추가)는 코퍼스의 **
95
95
 
96
96
  ### 버전노트 보존정책
97
97
 
98
- - 규칙 내 CC 버전노트(`> **v2.1.NNN+**:`)는 최근 2-3개 마이너 릴리즈(현행 기준 v2.1.208 이상)만 visible 유지한다.
98
+ - 규칙 내 CC 버전노트(`> **v2.1.NNN+**:`)는 최근 2-3개 마이너 릴리즈(현행 기준 v2.1.212 이상)만 visible 유지한다.
99
99
  - 그 이하 버전노트는 HTML-comment화(무손실 중간 단계) 하거나 `guides/claude-code/15-version-compatibility.md`로 이관한다.
100
100
  - `claude-native` 스킬이 생성하는 버전 추적 이슈를 규칙에 반영할 때, 최신만 visible로 두고 구버전은 즉시 은닉한다.
101
101
 
@@ -14,9 +14,13 @@ oh-my-customcode uses an **advisory-first enforcement model**. Most rules are en
14
14
  | Soft Block | Stop hook prompt | R011 session-end saves | Auto-performs then approves |
15
15
  | Conversation Block | PostToolUse hook + `continueOnBlock` (CC v2.1.139+), exit 2 | stuck-detector, context-budget-advisor, cost-cap-advisor | Feeds rejection reason into conversation; Claude continues with awareness |
16
16
  | Advisory | PostToolUse hooks | R007, R008, R009, R010, R018 | Warns via stderr, never blocks |
17
- | Advisory (proactive) | UserPromptSubmit + SubagentStop hooks | R007, R008 (`r007-r008-drift-advisor.sh`, #1229 UserPromptSubmit; #1545 added SubagentStop) | Reads last assistant turn; emits stderr advisory if header/prefix absent. SubagentStop wiring (#1545) closes a structural gap: UserPromptSubmit fires only on user input, so long-lived autonomous loops (e.g. `/fsd`) that re-enter via task notifications (no user input) never reached the advisor — measured 2026-07-30: 6 responses / 12 tool calls with missing R007/R008 and zero advisory fires. Complements retroactive Stop-hook (`session-reflection.sh`, #1190). **Delivery gap resolved (#1547, v1.1.40)**: stderr on exit 0 is never injected into model context by any hook event, so #1545's wiring fired without reaching the model. The advisory is now delivered via `hookSpecificOutput.additionalContext` (JSON stdout, exit 0) — the confirmed non-blocking context-injection path for both events; stderr is retained only as a human-visible audit trail. No top-level `decision`/`continue`/`stopReason` field is emitted, so the hook stays advisory (never blocks a Stop/SubagentStop). |
17
+ | Advisory (proactive) | UserPromptSubmit + SubagentStop hooks | R007, R008 (`r007-r008-drift-advisor.sh` — #1229 UserPromptSubmit, #1545 SubagentStop) | Reads last assistant turn; emits advisory if header/prefix absent. SubagentStop wiring (#1545) closes the no-user-input autonomous-loop gap (`/fsd`). Complements retroactive Stop-hook (`session-reflection.sh`, #1190). **현재 미발화 — 아래 각주 참조.** |
18
18
  | Prompt-based | CLAUDE.md + rules/ + PostCompact | All MUST rules | Behavioral guidance in context |
19
19
 
20
+ > **Advisory (proactive/retroactive) 실태 정정 (실측)**: `hookSpecificOutput.additionalContext` **전달 경로 자체는 #1547(v1.1.40)에서 구현**됐으나, 그 앞단 **파서 셀렉터 결함**으로 advisory가 **한 번도 발화한 적이 없다** — `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` 배선으로 수정 중(Refs #1553).
21
+ >
22
+ > 교훈: **배선 확인 ≠ 전달 확인 ≠ 발화 확인** — R020 "actual outcome ≠ attempt"의 훅 도메인 재현 사례.
23
+
20
24
  <!--
21
25
  > **v2.1.163+**: Stop and SubagentStop hooks can return `hookSpecificOutput.additionalContext` (JSON) to feed structured feedback back into Claude's context without triggering a hook error label. This enables advisory-style enforcement via Stop/SubagentStop hooks (e.g., `session-reflection.sh`, omcustom-loop SubagentStop) to pass richer context — replacing plain stderr text — without disrupting the turn continuation behavior that advisory-first enforcement relies on.
22
26
 
@@ -27,6 +31,8 @@ oh-my-customcode uses an **advisory-first enforcement model**. Most rules are en
27
31
 
28
32
  > **v2.1.211/212/214+**: 훅의 enforcement 결정이 auto/unattended 모드에서 안정적으로 존중되도록 세 건이 수정되었습니다 — (211) auto mode가 unsandboxed Bash에 대한 PreToolUse 훅의 `ask` 결정을 덮어쓰던 문제가 수정되어 훅 `ask`가 최소 prompt로 floor되고, (212) `continue:false` 훅의 halt가 도구 실패·중간 완료 시 누락되던 문제 및 훅 인프라 오류가 user rejection으로 오보고되던 문제가 수정되었으며, (214) 훅 stdout JSON이 스키마 검증에 실패할 때 exit code 2가 문서대로 차단하지 못하던 문제가 수정되었습니다. R021 Enforcement Tiers(Hard Block=exit 2, Conversation Block=continueOnBlock exit 2, Advisory)가 훅의 block/ask 결정 존중에 의존하므로, 세 수정 모두 hard-block·advisory 훅(stage-blocker, rule-deletion-guard, stuck-detector 등)의 강제 신뢰성을 강화합니다 — v2.1.210 훅 timeout phantom-rejection 수정의 연장선.
29
33
 
34
+ > **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 훅 결정 존중 체인의 연장선입니다.
35
+
30
36
  ## Why Advisory-First
31
37
 
32
38
  1. **Agent flexibility**: Hard blocks can trap agents in unrecoverable states
@@ -34,7 +40,7 @@ oh-my-customcode uses an **advisory-first enforcement model**. Most rules are en
34
40
  3. **Composability**: External skills and internal rules can coexist without deadlocks
35
41
  4. **PostCompact reinforcement**: R007/R008/R009/R010/R018 are re-injected after context compaction
36
42
 
37
- ## Hard Enforcement Candidates — R010 git-delegation-guard (conditional), R007/R008 advisory **implemented** (#1229 UserPromptSubmit, proactive) + **#1545 SubagentStop** (closes autonomous-loop gap) + retroactive Stop-hook (#1190); model-context delivery fixed via `hookSpecificOutput.additionalContext` (#1547, v1.1.40); hard-block variant still candidate if advisory insufficient (#1096). Promoted: rule-deletion-guard.sh (2026-04-08). See details via Read tool.
43
+ ## Hard Enforcement Candidates — R010 git-delegation-guard (conditional), R007/R008 advisory **implemented** (#1229 UserPromptSubmit, proactive) + **#1545 SubagentStop** (closes autonomous-loop gap) + retroactive Stop-hook (#1190); `additionalContext` 전달 경로는 #1547(v1.1.40)에서 구현됐으나 파서 셀렉터 결함으로 **양 계층 모두 미발화** — v1.1.43에서 수정 중(Refs #1553); hard-block variant still candidate if advisory insufficient (#1096). Promoted: rule-deletion-guard.sh (2026-04-08). See details via Read tool.
38
44
 
39
45
  <!-- DETAIL: Hard Enforcement Candidates (Future)
40
46
  If advisory enforcement proves insufficient for specific rules, these are candidates for promotion to hard-block:
@@ -42,7 +48,7 @@ If advisory enforcement proves insufficient for specific rules, these are candid
42
48
  | Rule | Candidate Hook | Status | Condition for Promotion |
43
49
  |------|---------------|--------|------------------------|
44
50
  | R010 | git-delegation-guard.sh | Candidate | If orchestrator-direct-write violations exceed 3/session |
45
- | R007/R008 | `r007-r008-drift-advisor.sh` (UserPromptSubmit #1229 + SubagentStop #1545) | **Advisory implemented** — proactive pre-response check now wired to both UserPromptSubmit and SubagentStop; the SubagentStop leg (#1545) closes the no-user-input autonomous-loop gap (`/fsd` etc.). Retroactive: `session-reflection.sh` (Stop, #1190). Two-layer drift detection: proactive (#1229/#1545) + retroactive (#1190). **Delivery mechanism fixed (#1547, v1.1.40)** — stderr on exit 0 reaches only a human (transcript debug mode), never the model, so wiring alone did not prove advisory reach; the advisory now travels via `hookSpecificOutput.additionalContext` on JSON stdout with exit 0, and emits no top-level `decision`/`continue`/`stopReason` so it remains non-blocking. | Promote to hard-block if advisory proves insufficient (#1096) |
51
+ | R007/R008 | `r007-r008-drift-advisor.sh` (UserPromptSubmit #1229 + SubagentStop #1545) | **Advisory implemented** — proactive pre-response check now wired to both UserPromptSubmit and SubagentStop; the SubagentStop leg (#1545) closes the no-user-input autonomous-loop gap (`/fsd` etc.). Retroactive: `session-reflection.sh` (Stop, #1190). Two-layer drift detection: proactive (#1229/#1545) + retroactive (#1190). **Delivery path implemented but never fired (측정 정정)** — #1547 (v1.1.40) switched delivery from stderr (never model-visible on exit 0) to `hookSpecificOutput.additionalContext` on JSON stdout, non-blocking (no top-level `decision`/`continue`/`stopReason`). However both scripts short-circuit BEFORE emitting: `jq -r '.role'` reads a key absent at transcript top level (it is `.message.role`), so `last_assistant` is always empty and the script exits 0 silently. Measured: 0 `"additionalContext":` occurrences across 771 transcripts; live probe emits 0 bytes on both streams. Parser fix + `PostToolUse` wiring in v1.1.43 (Refs #1553). | Promote to hard-block if advisory proves insufficient (#1096) |
46
52
 
47
53
  Promotion requires: (1) measured violation rate data, (2) user approval, (3) rollback plan.
48
54
 
@@ -276,12 +276,29 @@ The Subagent Scope-Creep STOP Protocol (above) is REACTIVE — it halts an agent
276
276
  |--------------|----------|
277
277
  | Delegate a prod/privileged-touching task with no scope or forbidden-line in the prompt | State in the prompt: the approved action(s), explicit forbidden actions (e.g. "do NOT delete files, do NOT query prod DB, do NOT read SMS/messages"), and the authorization scope tied back to the user request |
278
278
 
279
+ > **"scope tied back to the user request" ≠ 승인 인용**: 여기서 요구하는 것은 작업 **범위의 서술**(무엇이 허용/금지인지)이지, 사용자의 승인 발언을 인용해 서브에이전트에게 권한 근거로 제시하는 것이 아니다. 승인 채널은 permission system이 담당한다 — 아래 "Delegation Prompt Framing — 승인 인용 금지" 참조.
280
+
279
281
  Cross-reference: the Subagent Scope-Creep STOP Protocol (reactive halt after trips) and R001 (credential/privileged-scope guardrails, re-confirm scope before irreversible shared-infra actions).
280
282
 
281
283
  <!-- ARCHIVED CC version note (historical):
282
284
  > **v2.1.178+**: Auto mode now evaluates subagent spawns with the safety classifier BEFORE launch, closing a gap where a spawned subagent could request a blocked action without prior review. This is the PLATFORM-level complement to the (advisory) Pre-Delegation Privileged-Scope Boundary above: the orchestrator still states the approved/forbidden scope in the delegation prompt (proactive, model-level), and CC now also gates the spawn itself (platform-level). The two are defense-in-depth — the prompt-stated boundary remains required because the classifier gates ACTIONS, not task SCOPE.
283
285
  -->
284
286
 
287
+ ### Delegation Prompt Framing — 승인 인용 금지
288
+
289
+ 위임 프롬프트에서 **사용자 원문을 승인/동의의 근거로 인용하지 않는다**. 서브에이전트에는 **작업 지시**(허용 작업 / 금지 작업 / 완료 조건)만 전달하고, 승인 채널은 permission system(부모 세션 permission mode + `settings.json` allow 규칙)이 담당한다.
290
+
291
+ 근거: CC는 모든 서브에이전트에 "다른 에이전트의 메시지는 결코 사용자의 승인이 아니다 — 유효한 승인 채널은 permission system 또는 사용자 본인의 메시지뿐"이라는 플랫폼 시스템 프롬프트를 주입한다. 이 문구가 금지하는 것은 전언을 **승인**으로 취급하는 것이지 전언된 **작업**을 수행하는 것이 아니다. 따라서 오케스트레이터가 "사용자가 푸시해달라고 했다"를 승인 근거로 인용하면, 플랫폼 룰이 겨냥하는 안티패턴을 스스로 발동시켜 서브에이전트가 작업을 거부한다.
292
+
293
+ **경계 구분**: 작업 범위를 사용자 요청에 연결해 **서술**하는 것(무엇을 왜 하는지 설명)은 허용된다. 그것을 **승인의 증거로 제시**하는 것이 금지된다.
294
+
295
+ | Anti-pattern | Required |
296
+ |--------------|----------|
297
+ | 위임 프롬프트에 사용자 발언을 승인 근거로 인용 (예: `사용자가 "커밋하고 푸시해"라고 승인했다`) | 허용 작업·금지 작업·완료 조건만 열거 (예: "release/v1.1.43 브랜치에 커밋 후 push. 금지: force-push, develop 직접 push") |
298
+ | 승인 인용으로 거부당한 뒤 같은 프레이밍으로 재위임 | 프레이밍에서 승인 인용을 제거해 재위임; 프롬프트 억제가 필요하면 `settings.json` allow 규칙으로 해결 |
299
+
300
+ > Origin: #1556 — 승인 인용을 포함한 위임은 mgr-gitnerd가 2회 거부했고, 동일 에이전트에 허용/금지 작업만 열거한 위임은 거부 없이 완주했다(2026-08-05 v1.1.43 세션, 대조 실증). Cross-ref: R015 (User Directive Persistence — `settings.json` allow 규칙이 실제 prompt 억제 수단이라는 동일 결론의 선례), R002 (permission tiers).
301
+
285
302
  ### Parallel Delegation — Sibling-Agent Disclosure
286
303
 
287
304
  2개 이상의 서브에이전트를 같은 메시지에서 병렬 스폰할 때, 각 위임 프롬프트는 **형제 에이전트의 존재와 각자의 담당 범위**를 고지해야 한다. 서브에이전트는 격리된 컨텍스트에서 실행되어 형제를 인지할 수 없으므로, 고지가 없으면 `git status` 같은 **저장소 전역 공유 뷰**의 출력을 자기 변경분으로 오독하거나 경합 원인을 "외부 세션/프로세스"로 오귀속한다.
@@ -370,12 +387,16 @@ Before spawning any agent:
370
387
  > **v2.1.200+**: 백그라운드 세션/에이전트 견고성이 추가로 강화되었습니다 — sleep/wake 후 또는 stalled 세션 재개 시 mid-turn으로 조용히 멈추던 문제, stall respawn 후 Esc로 취소한 turn을 재실행하던 문제, 크래시가 남긴 stale `daemon.lock`(OS가 PID를 재사용)으로 백그라운드 에이전트가 다시 시작되지 않던 문제, 재설치된 구버전 빌드가 daemon을 탈취하던 문제(빌드 최신성은 이제 버전의 embedded build timestamp로 판정), 그리고 roster 일시 corruption이 orphan cleanup을 영구 비활성화하던 문제·구버전 바이너리가 신버전이 기록한 필드를 보존하지 못하던 문제·daemon 재시작 중 socket auth token이 제거되던 문제를 수정했습니다. v2.1.195~199 백그라운드-에이전트 lifecycle 견고성 체인의 연장입니다. `mode: "bypassPermissions"`는 모든 Agent tool 호출에 여전히 필수입니다.
371
388
  -->
372
389
 
390
+ <!-- ARCHIVED CC version note (historical):
373
391
  > **v2.1.208+**: Added `CLAUDE_CODE_PROCESS_WRAPPER` — the background service and agent view now honor a corporate launcher by routing every Claude Code self-spawn through a required wrapper executable. Also fixed: replies typed to a background agent being lost when delivery fails (now saved and delivered on session restart), background-session attach failing permanently ("Couldn't start the background daemon") after an update replaced the binary a running session was launched from, and an older daemon no longer silently restarting workers spawned by a newer version onto the older binary. Extends the v2.1.195~200 background-agent lifecycle robustness chain. `mode: "bypassPermissions"` remains required on every Agent tool call.
374
392
 
375
393
  > **v2.1.209+**: Fixed `/model` and other dialogs being blocked in `claude agents` background sessions (reverts an overly broad guard). Continuation of the background-agent lifecycle chain above (cf. v2.1.208). `mode: "bypassPermissions"` remains required.
394
+ -->
376
395
 
377
396
  > **v2.1.212+**: CC가 Task(=Agent) 도구의 `mode` 파라미터를 deprecated(이제 무시)했습니다 — subagent는 기본적으로 **부모(오케스트레이터) 세션의 permission mode를 상속**합니다. 따라서 이 섹션이 요구하는 per-call `mode: "bypassPermissions"`는 v2.1.212+에서 no-op이며, 무인 위임이 프롬프트 없이 돌게 하는 통제점은 per-call 파라미터가 아니라 **부모 세션의 permission mode**입니다(안전 완화 아님 — 부모가 bypassPermissions면 subagent도 상속). 단 CC < v2.1.212에서는 여전히 per-call `mode` 명시가 필요하므로(위 History #926/#947/#955) 하위 호환을 위해 계속 포함하되, 신버전에서 프롬프트 발생 시 진단은 위 Self-Check("mode 있는지 확인")가 아니라 **부모 세션 모드**를 확인합니다. cross-ref R002/R006(이 섹션을 canonical source로 참조).
378
397
 
398
+ > **v2.1.221+**: background session이 작업 보존을 위해 commit·push를 수행하고, draft PR은 작업이 요구할 때만 열며, 사용자의 CLAUDE.md git 지침을 따르고, 항상 작업 위치를 보고하며 종료하도록 변경되었습니다. 이 저장소의 R010은 모든 git 작업을 mgr-gitnerd 위임으로 요구하므로 background session은 그 지침을 읽고 동작하지만, **R020 기준 ground-truth(`git log` / `gh pr view`) 실측 없이 background session의 커밋/푸시 완료 보고를 신뢰하지 않습니다**. 또한 v2.1.221에서 `/status`가 세션 종류(interactive / background attached / background unattended)를 표시하므로 무인 실행 여부를 결정론적으로 확인할 수 있습니다.
399
+
379
400
  ## Agent Capability Pre-Check
380
401
 
381
402
  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).
@@ -45,7 +45,7 @@ On insufficient permission: do not attempt, notify user, suggest alternative.
45
45
 
46
46
  Use a `"*"` deny rule in `settings.json` to enforce a deny-by-default posture, then add specific allow rules. Complements the Tier-based policy above — settings.json deny rules are evaluated by the CC platform, independent of the advisory tier table.
47
47
 
48
- > **v2.1.208+**: Permission rule matchers (deny/ask rules) are now compiled once and cached, fixing multi-second per-turn slowdowns in sessions with many rules. Complements the deny-by-default posture above — a large deny/allow rule set no longer costs per-turn latency.
48
+ <!-- ARCHIVED CC version note (historical): v2.1.208+: Permission rule matchers (deny/ask rules) are now compiled once and cached, fixing multi-second per-turn slowdowns in sessions with many rules. Complements the deny-by-default posture above — a large deny/allow rule set no longer costs per-turn latency. -->
49
49
 
50
50
  <!-- ARCHIVED CC version note (historical): v2.1.183+: Fixed MCP servers requiring authentication exposing auth-stub tools to the model in headless/SDK mode — unauthenticated MCP auth-stub tools are no longer surfaced to the model in `-p` / SDK runs (they would fail on call). Relevant to the Tier-6 MCP tier: a headless run no longer offers auth-stub MCP tools. Separately, v2.1.181 added the `sandbox.allowAppleEvents` opt-in setting, letting sandboxed commands send Apple Events on macOS (default off) — a deliberate sandbox-scope widening, complementing the Tier-based policy above. -->
51
51
 
@@ -75,6 +75,11 @@ Use a `"*"` deny rule in `settings.json` to enforce a deny-by-default posture, t
75
75
 
76
76
  > **v2.1.214+**: 단일 세그먼트 `dir/**` allow rule(예: `Edit(src/**)`)이 트리 어디에나 있는 중첩 `dir/`까지 auto-approve하던 버그가 수정되어 이제 `<cwd>/dir`에만 매칭됩니다(hook `if:` 조건도 동일 — 임의 깊이 매칭이 필요하면 `**/dir/**`로 작성). **`deny`/`ask` permission rule은 any-depth 매칭을 유지**(allow만 `<cwd>`로 좁아짐). settings.json 스코프 설계 시 이 비대칭(allow 좁게 / deny·ask 넓게)을 전제로 삼습니다. 위 v2.1.210 `Edit(path)`/`Read(path)` matcher 권고의 연장선.
77
77
 
78
+ > **v2.1.221/222+**: 세 건이 Tier-3/4 권한 흐름에 영향을 줍니다.
79
+ > 1. **(v2.1.221) Bash 도구 권한 검사 우회 수정** — zsh가 `[[ ]]` 정규식 조건문 안에서 숨겨진 명령을 실행할 수 있었고, 해당 명령들은 이제 권한 프롬프트를 발생시킵니다. **이 저장소의 Claude Code Bash 도구 실행 셸이 zsh**이므로(R005 #1540), `[[ ... =~ ... ]]` 안에 명령을 포함하는 형태는 Tier-4 프롬프트 대상이며 무인 흐름의 새 프롬프트 발생원이 될 수 있습니다. Windows의 따옴표 포함 경로 PowerShell 권한 검사도 같은 방향으로 수정되었습니다.
80
+ > 2. **(v2.1.221) auto mode 병렬 권한 검사 최적화** — 병렬 tool call의 권한 검사가 cache-efficient해지고 캐시된 대화 prefix 재사용으로 비용이 감소했습니다(R009 병렬 배치의 부담 완화). 검사 대기 중 모드를 전환하면 stale 결과를 적용하지 않고 재프롬프트합니다.
81
+ > 3. **(v2.1.222) Remote Control auto-start 스코프 축소** — repo-local 설정(`.claude/settings.json` / `.claude/settings.local.json`)으로는 **켤 수 없고**(끄는 것은 가능), 활성화는 user scope `/config`에서만 가능합니다.
82
+
78
83
  ## Agent Tool Permission Mode
79
84
 
80
85
  > Canonical source: R010 (MUST-orchestrator-coordination.md) "Universal bypassPermissions" owns the full requirement, rationale, self-check, and version history. Core rule: always pass `mode: "bypassPermissions"` explicitly on every Agent tool call — the Agent tool's default `mode` (`acceptEdits`) overrides agent frontmatter `permissionMode` and causes prompts during unattended execution. Skills that spawn agents MUST include this in their Agent tool call instructions. See R010 for details.
@@ -31,6 +31,8 @@ The following git commands have caused working tree loss in past sessions (#1146
31
31
 
32
32
  > **v2.1.208+**: Catastrophic removals (e.g. `rm -rf ~`) wrapped in `$(…)`/backticks/`<(…)` now trigger the same prompt as the plain form in `--dangerously-skip-permissions` and auto mode — closes a subshell-obfuscation gap in the v2.1.183 platform-level destructive-command block above.
33
33
 
34
+ > **v2.1.221/222+**: v2.1.222에서 worktree-isolated 세션과 그 subagent가 main checkout에 대해 파괴적 git 명령을 실행할 수 있던 문제가 수정되어, isolation이 모든 세션 타입의 file edit과 Bash에 적용됩니다. v2.1.221에서는 `/fork` 세션이 원본 세션 checkout이 아니라 자체 worktree를 생성하도록 변경되었습니다. **완화 아님**: 위 Destructive Git Commands 표의 per-invocation 승인 요구와 아래 Pre-Delegation Blast-Radius Enumeration은 그대로 유지됩니다. 플랫폼 isolation은 격리 경계를 강화할 뿐, 사용자가 판단하는 데 필요한 blast-radius 열거를 대체하지 않습니다(v2.1.183/208 플랫폼 블록과 동일한 defense-in-depth 관계).
35
+
34
36
  ### Pre-Delegation Blast-Radius Enumeration
35
37
 
36
38
  > Origin: #1307 찐빠 #1 (High) — user chose "discard local changes and pull", and `git reset --hard origin/develop` was delegated immediately → user rejected (interrupt). The blast radius — that "discard local changes" included 18 files of *intended* uncommitted work (rule edits, new skills, new guides), not just a version downgrade — was never enumerated for the user.
@@ -82,6 +82,20 @@ steps:
82
82
  - If not existing:
83
83
  Create new milestone: gh api repos/{owner}/{repo}/milestones --method POST --field title="vX.Y.Z"
84
84
 
85
+ ⚠ This step is a 3-BRANCH STATE MACHINE (closed → HALT / open → reuse / absent → CREATE),
86
+ NOT a gate-only pre-check. The "absent → create" branch is a STATE CHANGE and is therefore
87
+ NEVER skipped by compression (see "Cross-tier — State-Change Side Effects Are NEVER
88
+ Compressed" in compression-mode-eval). Reading this as "just a milestone existence gate"
89
+ and skipping it under lite/docs-only leaves the release with no milestone (#1553 찐빠 #3).
90
+
91
+ 3. Post-condition — MANDATORY re-query before proceeding:
92
+ gh api 'repos/{owner}/{repo}/milestones?state=all&per_page=100' --paginate \
93
+ --jq '.[] | select(.title == "vX.Y.Z") | "\(.number)|\(.state)"'
94
+ The milestone MUST exist and be open. If the re-query returns nothing, the create branch
95
+ did not take effect — HALT; do NOT proceed to Step 1 on an unverified milestone.
96
+ `--paginate` is REQUIRED: with 100+ milestones in the repo a single page silently drops
97
+ the newest entries, so a just-created milestone can read back as absent.
98
+
85
99
  ## Step 1 — Label-based filtering (G4)
86
100
 
87
101
  Reference label semantics: .claude/skills/pipeline/labels.md
@@ -131,6 +145,9 @@ steps:
131
145
  - deep-plan step: skip deep-plan skill; single-response implementation notes instead
132
146
  - deep-verify step: skip deep-verify skill; perform self-review checklist instead
133
147
  - Log: "[compression-mode] docs-only compression activated (scope={n}, all docs/yaml labels)"
148
+ - ⚠ Analysis artifacts only. Every step's state-change side effect (milestone create/assign,
149
+ label add/remove, issue assign/comment/close) still runs in full — see "Cross-tier —
150
+ State-Change Side Effects Are NEVER Compressed" below.
134
151
 
135
152
  ## Tier 2 — lite (intermediate)
136
153
 
@@ -158,6 +175,9 @@ steps:
158
175
  "[compression-mode] lite — skill 단계 통합 분석 대체. 정당화: scope={n}, 모든 이슈 저위험(라벨 {labels}), 구현 대상 이슈 본문 명시 {issue_refs}"
159
176
  - If the justification cannot be stated concretely (e.g., a stage cannot be safely integrated),
160
177
  do NOT compress that stage — fall back to full skill spawn for it.
178
+ - ⚠ "통합 분석 대체" replaces analysis output ONLY. Every step's state-change side effect
179
+ (milestone create/assign, label add/remove, issue assign/comment/close) still runs in full
180
+ — see "Cross-tier — State-Change Side Effects Are NEVER Compressed" below.
161
181
 
162
182
  ## Tier 3 — standard (fallback)
163
183
 
@@ -165,6 +185,42 @@ steps:
165
185
  - All pipeline steps execute normally with full skill spawns
166
186
  - Log: "[compression-mode] standard mode (scope={n}, mixed/high-risk labels, large scope, or code logic change)"
167
187
 
188
+ ## Cross-tier — State-Change Side Effects Are NEVER Compressed
189
+
190
+ Compression (docs-only / lite) substitutes ANALYSIS ARTIFACTS ONLY. A step's
191
+ STATE-CHANGE SIDE EFFECTS — milestone create/assign, label add/remove, issue
192
+ assign, issue comment, issue/milestone close — are NOT analysis output and MUST
193
+ be performed regardless of compression mode. "통합 분석 대체" replaces how the
194
+ step THINKS, never what the step CHANGES.
195
+
196
+ A step is not a monolith: "skip the professor-triage skill spawn" does NOT
197
+ authorize skipping that step's gh state mutations. Before compressing any step,
198
+ consult the inventory below and execute every side effect it lists.
199
+
200
+ ### Step side-effect inventory (state changes; MUST run in every compression mode)
201
+
202
+ | Step | State-change side effects (MANDATORY regardless of compression_mode) |
203
+ |------|---------------------------------------------------------------------|
204
+ | pre-triage | `gh label create in-progress / verify-ready / needs-review --force` (idempotent label bootstrap) |
205
+ | scope-selection | Milestone 3-branch state machine (Step 0): closed → HALT / open → reuse / absent → `gh api repos/{owner}/{repo}/milestones --method POST`. Then: assign every scoped issue to that milestone. |
206
+ | compression-mode-eval | none (pure evaluation + justification logs) |
207
+ | triage | none (analysis only) — compressible |
208
+ | plan | none (analysis only) — compressible |
209
+ | deep-plan | none (analysis only) — compressible |
210
+ | implement | `gh issue edit <N> --add-label in-progress --assignee @me`; `gh issue comment <N>` (start notice); on success remove in-progress + add verify-ready; on failure remove in-progress + add needs-review + comment error summary |
211
+ | verify-build | none (gate only) |
212
+ | deep-verify | none (analysis only) — compressible |
213
+ | release | PR body MUST carry `Closes #N` for every resolved issue (auto-tag.yml greps the PR body); non-auto-tag projects additionally: `gh api .../milestones/{n} --method PATCH --field state=closed`, `gh issue close {n}`, label needs-review issues "Deferred from v{version}" |
214
+ | ci-check | none (verification only) |
215
+ | post-release-followup | new issue registration for release follow-ups (genuine defects auto-registered, no-ask per R016) |
216
+
217
+ Only the rows marked "none (analysis only)" are compressible. Any step with a
218
+ non-empty side-effect cell executes those effects in FULL under docs-only and lite.
219
+
220
+ Origin: #1553 찐빠 #3 — v1.1.41 릴리즈에서 lite 압축이 scope-selection의
221
+ "마일스톤 미존재 → 생성" 분기까지 함께 생략해 마일스톤이 만들어지지 않았다.
222
+ 압축 대상은 분석 산출물이었으나 상태 변경 분기가 동반 생략됐다.
223
+
168
224
  ## Cross-tier — Pre-Existing Converged Artifact Substitution
169
225
 
170
226
  Independent of the tier selected above, an INDIVIDUAL planning/verification step (triage / plan / deep-plan / deep-verify) MAY be satisfied by a pre-existing converged artifact instead of a fresh skill spawn — even in standard mode — when ALL of the following hold for that step:
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.1.42",
2
+ "version": "1.1.43",
3
3
  "lastUpdated": "2026-07-14T00:00:00.000Z",
4
4
  "omcustomMinClaudeCode": "2.1.121",
5
5
  "omcustomMinClaudeCodeReason": "Sensitive-path direct Write/Edit on .claude/** under bypassPermissions (R010 deprecation, #1101)",
@@ -82,6 +82,20 @@ steps:
82
82
  - If not existing:
83
83
  Create new milestone: gh api repos/{owner}/{repo}/milestones --method POST --field title="vX.Y.Z"
84
84
 
85
+ ⚠ This step is a 3-BRANCH STATE MACHINE (closed → HALT / open → reuse / absent → CREATE),
86
+ NOT a gate-only pre-check. The "absent → create" branch is a STATE CHANGE and is therefore
87
+ NEVER skipped by compression (see "Cross-tier — State-Change Side Effects Are NEVER
88
+ Compressed" in compression-mode-eval). Reading this as "just a milestone existence gate"
89
+ and skipping it under lite/docs-only leaves the release with no milestone (#1553 찐빠 #3).
90
+
91
+ 3. Post-condition — MANDATORY re-query before proceeding:
92
+ gh api 'repos/{owner}/{repo}/milestones?state=all&per_page=100' --paginate \
93
+ --jq '.[] | select(.title == "vX.Y.Z") | "\(.number)|\(.state)"'
94
+ The milestone MUST exist and be open. If the re-query returns nothing, the create branch
95
+ did not take effect — HALT; do NOT proceed to Step 1 on an unverified milestone.
96
+ `--paginate` is REQUIRED: with 100+ milestones in the repo a single page silently drops
97
+ the newest entries, so a just-created milestone can read back as absent.
98
+
85
99
  ## Step 1 — Label-based filtering (G4)
86
100
 
87
101
  Reference label semantics: .claude/skills/pipeline/labels.md
@@ -131,6 +145,9 @@ steps:
131
145
  - deep-plan step: skip deep-plan skill; single-response implementation notes instead
132
146
  - deep-verify step: skip deep-verify skill; perform self-review checklist instead
133
147
  - Log: "[compression-mode] docs-only compression activated (scope={n}, all docs/yaml labels)"
148
+ - ⚠ Analysis artifacts only. Every step's state-change side effect (milestone create/assign,
149
+ label add/remove, issue assign/comment/close) still runs in full — see "Cross-tier —
150
+ State-Change Side Effects Are NEVER Compressed" below.
134
151
 
135
152
  ## Tier 2 — lite (intermediate)
136
153
 
@@ -158,6 +175,9 @@ steps:
158
175
  "[compression-mode] lite — skill 단계 통합 분석 대체. 정당화: scope={n}, 모든 이슈 저위험(라벨 {labels}), 구현 대상 이슈 본문 명시 {issue_refs}"
159
176
  - If the justification cannot be stated concretely (e.g., a stage cannot be safely integrated),
160
177
  do NOT compress that stage — fall back to full skill spawn for it.
178
+ - ⚠ "통합 분석 대체" replaces analysis output ONLY. Every step's state-change side effect
179
+ (milestone create/assign, label add/remove, issue assign/comment/close) still runs in full
180
+ — see "Cross-tier — State-Change Side Effects Are NEVER Compressed" below.
161
181
 
162
182
  ## Tier 3 — standard (fallback)
163
183
 
@@ -165,6 +185,42 @@ steps:
165
185
  - All pipeline steps execute normally with full skill spawns
166
186
  - Log: "[compression-mode] standard mode (scope={n}, mixed/high-risk labels, large scope, or code logic change)"
167
187
 
188
+ ## Cross-tier — State-Change Side Effects Are NEVER Compressed
189
+
190
+ Compression (docs-only / lite) substitutes ANALYSIS ARTIFACTS ONLY. A step's
191
+ STATE-CHANGE SIDE EFFECTS — milestone create/assign, label add/remove, issue
192
+ assign, issue comment, issue/milestone close — are NOT analysis output and MUST
193
+ be performed regardless of compression mode. "통합 분석 대체" replaces how the
194
+ step THINKS, never what the step CHANGES.
195
+
196
+ A step is not a monolith: "skip the professor-triage skill spawn" does NOT
197
+ authorize skipping that step's gh state mutations. Before compressing any step,
198
+ consult the inventory below and execute every side effect it lists.
199
+
200
+ ### Step side-effect inventory (state changes; MUST run in every compression mode)
201
+
202
+ | Step | State-change side effects (MANDATORY regardless of compression_mode) |
203
+ |------|---------------------------------------------------------------------|
204
+ | pre-triage | `gh label create in-progress / verify-ready / needs-review --force` (idempotent label bootstrap) |
205
+ | scope-selection | Milestone 3-branch state machine (Step 0): closed → HALT / open → reuse / absent → `gh api repos/{owner}/{repo}/milestones --method POST`. Then: assign every scoped issue to that milestone. |
206
+ | compression-mode-eval | none (pure evaluation + justification logs) |
207
+ | triage | none (analysis only) — compressible |
208
+ | plan | none (analysis only) — compressible |
209
+ | deep-plan | none (analysis only) — compressible |
210
+ | implement | `gh issue edit <N> --add-label in-progress --assignee @me`; `gh issue comment <N>` (start notice); on success remove in-progress + add verify-ready; on failure remove in-progress + add needs-review + comment error summary |
211
+ | verify-build | none (gate only) |
212
+ | deep-verify | none (analysis only) — compressible |
213
+ | release | PR body MUST carry `Closes #N` for every resolved issue (auto-tag.yml greps the PR body); non-auto-tag projects additionally: `gh api .../milestones/{n} --method PATCH --field state=closed`, `gh issue close {n}`, label needs-review issues "Deferred from v{version}" |
214
+ | ci-check | none (verification only) |
215
+ | post-release-followup | new issue registration for release follow-ups (genuine defects auto-registered, no-ask per R016) |
216
+
217
+ Only the rows marked "none (analysis only)" are compressible. Any step with a
218
+ non-empty side-effect cell executes those effects in FULL under docs-only and lite.
219
+
220
+ Origin: #1553 찐빠 #3 — v1.1.41 릴리즈에서 lite 압축이 scope-selection의
221
+ "마일스톤 미존재 → 생성" 분기까지 함께 생략해 마일스톤이 만들어지지 않았다.
222
+ 압축 대상은 분석 산출물이었으나 상태 변경 분기가 동반 생략됐다.
223
+
168
224
  ## Cross-tier — Pre-Existing Converged Artifact Substitution
169
225
 
170
226
  Independent of the tier selected above, an INDIVIDUAL planning/verification step (triage / plan / deep-plan / deep-verify) MAY be satisfied by a pre-existing converged artifact instead of a fresh skill spawn — even in standard mode — when ALL of the following hold for that step: