oh-my-customcode 1.1.57 → 1.1.59

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
@@ -243,7 +243,7 @@ var init_package = __esm(() => {
243
243
  workspaces: [
244
244
  "packages/*"
245
245
  ],
246
- version: "1.1.57",
246
+ version: "1.1.59",
247
247
  description: "Batteries-included agent harness for Claude Code",
248
248
  type: "module",
249
249
  bin: {
@@ -291,7 +291,7 @@ var init_package = __esm(() => {
291
291
  yaml: "^2.8.2"
292
292
  },
293
293
  devDependencies: {
294
- "@anthropic-ai/sdk": "^0.117.1",
294
+ "@anthropic-ai/sdk": "^0.122.0",
295
295
  "@biomejs/biome": "^2.3.12",
296
296
  "@types/bun": "^1.3.6",
297
297
  "@types/js-yaml": "^4.0.9",
package/dist/index.js CHANGED
@@ -2539,7 +2539,7 @@ var package_default = {
2539
2539
  workspaces: [
2540
2540
  "packages/*"
2541
2541
  ],
2542
- version: "1.1.57",
2542
+ version: "1.1.59",
2543
2543
  description: "Batteries-included agent harness for Claude Code",
2544
2544
  type: "module",
2545
2545
  bin: {
@@ -2587,7 +2587,7 @@ var package_default = {
2587
2587
  yaml: "^2.8.2"
2588
2588
  },
2589
2589
  devDependencies: {
2590
- "@anthropic-ai/sdk": "^0.117.1",
2590
+ "@anthropic-ai/sdk": "^0.122.0",
2591
2591
  "@biomejs/biome": "^2.3.12",
2592
2592
  "@types/bun": "^1.3.6",
2593
2593
  "@types/js-yaml": "^4.0.9",
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "workspaces": [
4
4
  "packages/*"
5
5
  ],
6
- "version": "1.1.57",
6
+ "version": "1.1.59",
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.117.1",
54
+ "@anthropic-ai/sdk": "^0.122.0",
55
55
  "@biomejs/biome": "^2.3.12",
56
56
  "@types/bun": "^1.3.6",
57
57
  "@types/js-yaml": "^4.0.9",
@@ -50,6 +50,27 @@ Types: feat, fix, docs, style, refactor, test, chore
50
50
  - ALWAYS check `git reflog` before declaring work lost — most destructive ops are recoverable for 30 days
51
51
  - Reference: R001 Destructive Git Commands section, #1146 (v0.136.0 working tree loss incident)
52
52
 
53
+ ## Commit Timeout Budget (#1645)
54
+
55
+ `.husky/pre-commit` 는 **메인 워크트리**에서 typecheck + `bun run lint` + `bun test --coverage`
56
+ 전체 스위트(실측 약 **165초**) + 커버리지 임계값 + CLAUDE.md 카운트 검증까지 순차 실행한다.
57
+ Bash 도구 기본 타임아웃은 **120000ms(2분)** 이라 `git commit` 이 훅 실행 도중
58
+ **exit 143(SIGTERM)** 으로 끊긴다.
59
+
60
+ | 상황 | Bash `timeout` |
61
+ |---|---|
62
+ | 메인 워크트리에서 `git commit` | **≥ 400000** (약 6.7분) |
63
+ | git worktree 에서 `git commit` | 기본값으로 충분 (`.husky/pre-commit` **7-12행**이 `[ -f .git ]` 로 분기해 `bun run typecheck` 만 실행하고 `exit 0` — 전체 스위트는 CI 담당) |
64
+ | `git push` / `gh pr` 등 훅 없는 명령 | 기본값 |
65
+
66
+ **금지**: 타임아웃 회피 목적의 `--no-verify` 사용. 품질 게이트 우회는 상시 금지이며
67
+ 오케스트레이터의 사전 승인이 있을 때만 예외다(R010 「품질 게이트 우회 금지」).
68
+ 훅이 차단하면 우회하지 말고 **차단 사실과 원인을 보고하고 대기**한다.
69
+
70
+ **타임아웃으로 끊긴 경우**: exit 143 은 "커밋 실패"의 증거가 아니다 — 훅이 통과한 뒤
71
+ 커밋이 성사됐을 수도 있다. 재시도 전 `git log -1 --format=%H%n%s` 로 **실제 HEAD 를 실측**하라
72
+ (R020 「Failure/Interrupt Report ≠ Actual Failure」).
73
+
53
74
  ## Push Rules (R016)
54
75
 
55
76
  All pushes require prior mgr-sauron:watch verification. If sauron was not run, REFUSE the push.
@@ -304,8 +304,33 @@ if [ -n "$turn_uuid" ] && [ -f "$MARKER_FILE" ]; then
304
304
  fi
305
305
  fi
306
306
 
307
- advisory_text=$(printf '[R007/R008 Advisory] 직전 응답에서 식별 누락 감지 (R007 헤더=%s, R008 접두사=%s). 이번 응답은 ┌─ Agent: 헤더로 시작하고, 모든 도구 호출에 [agent][model] → Tool: 접두사를 포함하십시오.' \
308
- "$r007_violations" "$r008_violations")
307
+ # ── 위반 문구 조립 (#1643 — 판정 로직 불변, 레이블 의미를 문구 자체가 드러내도록 정정) ──
308
+ # 이전 문구 "R007 헤더=%s, R008 접두사=%s"의 %s는 위반 "건수"(0=컴플라이언트)였는데, 레이블
309
+ # "헤더"/"접두사"가 "개수"로 오독되어 "R007 헤더=0"이 "헤더가 없다"로 잘못 읽혔다(#1643 진단
310
+ # 확정 — 실제로는 헤더가 정상 존재하는 컴플라이언트 상태였다). 값이 위반 건수라는 것을 문구
311
+ # 자체가 드러내도록 "누락 N건"으로 표현하고, 위반이 없는 차원은 문구에서 아예 제외한다 — 0건인
312
+ # 차원을 언급하면 그 차원도 위반처럼 오독될 수 있기 때문이다.
313
+ violation_desc=""
314
+ instruction=""
315
+ if [ "$r007_violations" -gt 0 ]; then
316
+ violation_desc="R007 에이전트 식별 헤더 누락 ${r007_violations}건"
317
+ instruction="이번 응답은 ┌─ Agent: 헤더로 시작하십시오."
318
+ fi
319
+ if [ "$r008_violations" -gt 0 ]; then
320
+ if [ -n "$violation_desc" ]; then
321
+ violation_desc="${violation_desc}, R008 도구 식별 접두사 누락 ${r008_violations}건"
322
+ instruction="이번 응답은 ┌─ Agent: 헤더로 시작하고, 모든 도구 호출에 [agent][model] → Tool: 접두사를 포함하십시오."
323
+ else
324
+ violation_desc="R008 도구 식별 접두사 누락 ${r008_violations}건"
325
+ instruction="모든 도구 호출에 [agent][model] → Tool: 접두사를 포함하십시오."
326
+ fi
327
+ fi
328
+
329
+ if [ -n "$violation_desc" ]; then
330
+ advisory_text="[R007/R008 Advisory] 직전 응답에서 ${violation_desc} 감지. ${instruction}"
331
+ else
332
+ advisory_text="[R007/R008 Advisory]"
333
+ fi
309
334
 
310
335
  if [ "$r008_reverse" -gt 0 ]; then
311
336
  advisory_text="${advisory_text} [R009 Self-Check #6] 직전 턴은 도구 호출 ${r008_reverse}건을 예고하고도 tool_use 블록 없이 종료했습니다 — 예고한 호출을 지금 실행하거나, 실행하지 않기로 한 사유를 명시하십시오."
@@ -96,18 +96,66 @@ if [ -d "wiki" ]; then
96
96
  fi
97
97
 
98
98
  # ─── Check 6: Broken skill references (lightweight) ───
99
+ # Parses BOTH frontmatter forms (#1640):
100
+ # inline : skills: [a, b]
101
+ # block : skills:
102
+ # - a
103
+ # - b
104
+ # Emits one skill name per line. Scans the YAML frontmatter only (stops at the
105
+ # closing "---"), so a "skills:" line in the agent body is never parsed.
106
+ parse_agent_skills() {
107
+ awk '
108
+ BEGIN {
109
+ SQ = sprintf("%c", 39); DQ = sprintf("%c", 34)
110
+ infm = 0; inskills = 0
111
+ }
112
+ function clean(s) {
113
+ sub(/[[:space:]]*#.*$/, "", s)
114
+ gsub(SQ, "", s); gsub(DQ, "", s)
115
+ gsub(/[[:space:]]/, "", s)
116
+ return s
117
+ }
118
+ NR == 1 { if ($0 ~ /^---[[:space:]]*$/) infm = 1; next }
119
+ infm == 0 { exit }
120
+ /^---[[:space:]]*$/ { exit }
121
+ inskills == 1 {
122
+ if ($0 ~ /^[[:space:]]+-[[:space:]]*/) {
123
+ item = $0
124
+ sub(/^[[:space:]]+-[[:space:]]*/, "", item)
125
+ item = clean(item)
126
+ if (item != "") print item
127
+ next
128
+ }
129
+ if ($0 ~ /^[[:space:]]*$/) next
130
+ inskills = 0
131
+ }
132
+ /^skills:/ {
133
+ rest = $0
134
+ sub(/^skills:[[:space:]]*/, "", rest)
135
+ sub(/[[:space:]]*#.*$/, "", rest)
136
+ if (rest ~ /^\[/) {
137
+ sub(/^\[/, "", rest); sub(/\][[:space:]]*$/, "", rest)
138
+ n = split(rest, arr, ",")
139
+ for (i = 1; i <= n; i++) {
140
+ s = clean(arr[i])
141
+ if (s != "") print s
142
+ }
143
+ } else if (rest == "") {
144
+ inskills = 1
145
+ }
146
+ next
147
+ }
148
+ ' "$1"
149
+ }
150
+
99
151
  broken_refs=0
100
152
  for agent in .claude/agents/*.md; do
101
- skills_line=$(grep -E '^skills:' "$agent" 2>/dev/null | head -1)
102
- if [ -n "$skills_line" ]; then
103
- skills=$(echo "$skills_line" | sed 's/skills: *\[//;s/\]//;s/,/ /g;s/"//g' | tr -d "'")
104
- for skill in $skills; do
105
- skill=$(echo "$skill" | tr -d ' ')
106
- if [ -n "$skill" ] && [ ! -f ".claude/skills/${skill}/SKILL.md" ]; then
107
- broken_refs=$((broken_refs + 1))
108
- fi
109
- done
110
- fi
153
+ [ -f "$agent" ] || continue
154
+ while IFS= read -r skill; do
155
+ if [ -n "$skill" ] && [ ! -f ".claude/skills/${skill}/SKILL.md" ]; then
156
+ broken_refs=$((broken_refs + 1))
157
+ fi
158
+ done <<< "$(parse_agent_skills "$agent")"
111
159
  done
112
160
  if [ "$broken_refs" -gt 0 ]; then
113
161
  add_issue "broken-refs:${broken_refs} broken skill reference(s) in agent frontmatter"
@@ -44,6 +44,23 @@ is_readonly_single_command() {
44
44
  local w2="${parts[1]:-}"
45
45
  local w3="${parts[2]:-}"
46
46
 
47
+ # Variable assignment prefix: "VAR=value" or "VAR=value cmd args".
48
+ # A bare assignment has no side effect outside the shell; when followed by a
49
+ # command, classify that command instead. Command substitutions inside the
50
+ # RHS were already validated and replaced by is_readonly_bash_command (#1641).
51
+ case "$w1" in
52
+ [A-Za-z_]*=*)
53
+ if [ "${#parts[@]}" -le 1 ]; then
54
+ echo "true"
55
+ return 0
56
+ fi
57
+ local rest_cmd="${cmd#*"$w1"}"
58
+ rest_cmd="$(printf '%s' "$rest_cmd" | sed -e 's/^[[:space:]]*//')"
59
+ is_readonly_single_command "$rest_cmd"
60
+ return 0
61
+ ;;
62
+ esac
63
+
47
64
  case "$w1" in
48
65
  ls|cat|head|tail|grep|rg|wc|jq|md5|md5sum|type|command|which|echo|printf|pwd|date)
49
66
  echo "true"
@@ -77,7 +94,7 @@ is_readonly_single_command() {
77
94
  *) ok="false" ;;
78
95
  esac
79
96
  done
80
- echo "$ok"
97
+ printf '%s\n' "$ok"
81
98
  ;;
82
99
  *)
83
100
  # fetch and all other subcommands (checkout/merge/rebase/push/...) => write
@@ -116,29 +133,172 @@ is_readonly_single_command() {
116
133
  esac
117
134
  }
118
135
 
136
+ # Maximum recursion depth for nested command substitution / loop bodies.
137
+ _RO_MAX_DEPTH=5
138
+ # Maximum command length that is parsed at all (see Step 0 below).
139
+ _RO_MAX_COMMAND_LEN=4000
140
+
119
141
  is_readonly_bash_command() {
120
142
  local cmd="$1"
143
+ local depth="${2:-0}"
144
+
145
+ if [ "$depth" -gt "$_RO_MAX_DEPTH" ]; then
146
+ echo "false"
147
+ return 0
148
+ fi
149
+
150
+ # --- Step 0: length cap (M-4). The "$(...)" balance scan below walks the
151
+ # remainder of the command ONE CHARACTER AT A TIME and rebuilds "$inner" on
152
+ # every iteration, which is O(n^2) in the substitution's length (measured:
153
+ # 5 KB => 0.9 s, 20 KB => 13.6 s of latency on a PostToolUse hook).
154
+ # "$raw_command" is the only unbounded field left after the cap on
155
+ # target_key, so cap it here. Nothing legitimately read-only is 4 KB long;
156
+ # anything over the cap is classified as a write, i.e. the conservative side
157
+ # (a read-only classification only ever RELAXES blocking).
158
+ if [ "${#cmd}" -gt "$_RO_MAX_COMMAND_LEN" ]; then
159
+ echo "false"
160
+ return 0
161
+ fi
162
+
121
163
  cmd="$(printf '%s' "$cmd" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
122
164
  if [ -z "$cmd" ]; then
123
165
  echo "false"
124
166
  return 0
125
167
  fi
126
168
 
127
- # Truly ambiguous constructs: redirection, command/process substitution, or
128
- # "||" (conditional-OR — intentionally NOT decomposed into segments, since
129
- # its branch that actually runs depends on the first segment's exit code)
130
- # => always treated as write.
169
+ # --- Step 1: process substitution "<(...)" stays ambiguous => write. ---
170
+ case "$cmd" in
171
+ *'<('*)
172
+ echo "false"
173
+ return 0
174
+ ;;
175
+ esac
176
+
177
+ # --- Step 2: command substitution "$(...)" / backticks.
178
+ # Rather than treating them as an automatic write (#1641), extract the inner
179
+ # command, classify it RECURSIVELY, and replace the substitution with the
180
+ # inert placeholder "SUBST". A write inside the substitution (e.g.
181
+ # "$(rm -rf /tmp/x)", "$(git push)") still makes the whole command a write.
182
+ while :; do
183
+ case "$cmd" in
184
+ *'$('*)
185
+ local before="${cmd%%'$('*}"
186
+ local after="${cmd#*'$('}"
187
+ # Balance-aware scan for the matching ")".
188
+ local inner="" rest="" d=1 i ch
189
+ for ((i = 0; i < ${#after}; i++)); do
190
+ ch="${after:$i:1}"
191
+ if [ "$ch" = "(" ]; then
192
+ d=$((d + 1))
193
+ elif [ "$ch" = ")" ]; then
194
+ d=$((d - 1))
195
+ if [ "$d" -eq 0 ]; then
196
+ rest="${after:$((i + 1))}"
197
+ break
198
+ fi
199
+ fi
200
+ inner="${inner}${ch}"
201
+ done
202
+ if [ "$d" -ne 0 ]; then
203
+ # Unbalanced => unparseable => conservative write.
204
+ echo "false"
205
+ return 0
206
+ fi
207
+ if [ "$(is_readonly_bash_command "$inner" $((depth + 1)))" != "true" ]; then
208
+ echo "false"
209
+ return 0
210
+ fi
211
+ cmd="${before}SUBST${rest}"
212
+ ;;
213
+ *'`'*)
214
+ # Backticks do not nest; take the text between the first pair.
215
+ local b_before="${cmd%%'`'*}"
216
+ local b_after="${cmd#*'`'}"
217
+ case "$b_after" in
218
+ *'`'*) ;;
219
+ *) echo "false"; return 0 ;;
220
+ esac
221
+ local b_inner="${b_after%%'`'*}"
222
+ local b_rest="${b_after#*'`'}"
223
+ if [ "$(is_readonly_bash_command "$b_inner" $((depth + 1)))" != "true" ]; then
224
+ echo "false"
225
+ return 0
226
+ fi
227
+ cmd="${b_before}SUBST${b_rest}"
228
+ ;;
229
+ *)
230
+ break
231
+ ;;
232
+ esac
233
+ done
234
+
235
+ # --- Step 3: strip harmless redirections BEFORE the ">" write check.
236
+ # "2>&1", ">/dev/null", "2>/dev/null", "1>/dev/null", "&>/dev/null" produce no
237
+ # observable side effect. Any OTHER ">" (a real file write) remains a write.
238
+ local stripped
239
+ stripped="$(printf '%s' "$cmd" \
240
+ | sed -E 's/[0-9]?>&[0-9]/ /g; s#[0-9]?&?>[[:space:]]*/dev/null# #g')"
241
+ case "$stripped" in
242
+ *'>'*)
243
+ echo "false"
244
+ return 0
245
+ ;;
246
+ esac
247
+ cmd="$stripped"
248
+
249
+ # --- Step 4: "||" is still NOT decomposed (which branch runs depends on the
250
+ # first segment's exit code) => write. Preserved from #1629.
131
251
  case "$cmd" in
132
- *'>'*|*'||'*|*'$('*|*'`'*|*'<('*)
252
+ *'||'*)
133
253
  echo "false"
134
254
  return 0
135
255
  ;;
136
256
  esac
137
257
 
138
- # Compound command: split on "&&", ";", or a single "|" into segments and
258
+ # --- Step 5: for / while loops (#1641).
259
+ # Normalize ";do" / ";done" spacing, then classify the header and the
260
+ # "do ... done" body separately. Both must be read-only.
261
+ case "$cmd" in
262
+ for\ *|while\ *)
263
+ local norm
264
+ norm="$(printf '%s' "$cmd" | sed -E 's/;[[:space:]]*do([[:space:]]|$)/ ; do /g; s/;[[:space:]]*done/ ; done/g')"
265
+ case "$norm" in
266
+ *' do '*'done'*) ;;
267
+ *) echo "false"; return 0 ;;
268
+ esac
269
+ local head="${norm%% do *}"
270
+ local tail_part="${norm#* do }"
271
+ local body="${tail_part%done*}"
272
+ head="$(printf '%s' "$head" | sed -e 's/[[:space:]]*;[[:space:]]*$//')"
273
+ body="$(printf '%s' "$body" | sed -e 's/[[:space:]]*;[[:space:]]*$//')"
274
+
275
+ case "$head" in
276
+ # "for VAR in <words>" — pure iteration, no side effect. The <words>
277
+ # already had any substitution validated in Step 2.
278
+ for\ [A-Za-z_]*\ in\ *)
279
+ ;;
280
+ while\ *)
281
+ # The while CONDITION is a real command — classify it.
282
+ local wcond="${head#while }"
283
+ if [ "$(is_readonly_bash_command "$wcond" $((depth + 1)))" != "true" ]; then
284
+ echo "false"
285
+ return 0
286
+ fi
287
+ ;;
288
+ *)
289
+ echo "false"
290
+ return 0
291
+ ;;
292
+ esac
293
+
294
+ is_readonly_bash_command "$body" $((depth + 1))
295
+ return 0
296
+ ;;
297
+ esac
298
+
299
+ # --- Step 6: compound command — split on "&&", ";", or a single "|" and
139
300
  # require EVERY segment to be read-only (#1629). An empty segment (e.g. a
140
- # trailing separator) is treated as ambiguous => write, same as any
141
- # segment that fails single-command classification.
301
+ # trailing separator) is ambiguous => write.
142
302
  if [[ "$cmd" == *'&&'* || "$cmd" == *';'* || "$cmd" == *'|'* ]]; then
143
303
  local normalized="$cmd"
144
304
  normalized="${normalized//&&/$'\n'}"
@@ -167,15 +327,67 @@ input=$(cat)
167
327
 
168
328
  # Extract tool info
169
329
  tool_name=$(printf '%s' "$input" | jq -r '.tool_name // "unknown"')
170
- # 300 (not 120): a 120-char cutoff let two genuinely different long Bash
171
- # commands collide on their shared 120-char prefix, causing a false-positive
172
- # same-path/same-tool+target hard-block. 300 chars covers the vast majority
173
- # of real commands without their differing tail being cut off (#1629).
174
- file_path=$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.command // ""' | head -c 300)
330
+ # Bash commands are NOT files. Feeding tool_input.command into file_path made
331
+ # the hard-block message report a command fragment as a "file" and let Check 1
332
+ # ("Same file ... edited") fire on Bash at all (#1641). Keep the command text in
333
+ # a SEPARATE field (target_key) so tool+target repetition detection (Check 3 /
334
+ # Signal 3) still works, while the file-oriented checks see nothing for Bash.
335
+ #
336
+ # 1000 (was 300, originally 120): a short cutoff let two genuinely different
337
+ # long Bash commands collide on their shared prefix, causing a false-positive
338
+ # same-path/same-tool+target hard-block (#1629). Truncation alone can never
339
+ # remove that collision class, so truncate_key() ALSO appends the ORIGINAL
340
+ # length: two values that share the capped prefix but differ in total length
341
+ # no longer produce the same key (M-3).
342
+ TARGET_KEY_CAP=1000
343
+
344
+ truncate_key() {
345
+ local v="$1"
346
+ if [ "${#v}" -gt "$TARGET_KEY_CAP" ]; then
347
+ printf '%s' "${v:0:$TARGET_KEY_CAP}#len=${#v}"
348
+ else
349
+ printf '%s' "$v"
350
+ fi
351
+ }
352
+
353
+ if [ "$tool_name" = "Bash" ]; then
354
+ file_path=""
355
+ # ".tool_input.file_path" is a defensive fallback only: the real Bash tool has
356
+ # no file_path parameter, but a caller (or a test fixture) may still supply one
357
+ # as the target identifier. The command text wins whenever it is present.
358
+ target_key=$(truncate_key "$(printf '%s' "$input" | jq -r '.tool_input.command // .tool_input.file_path // ""')")
359
+ else
360
+ file_path_full=$(printf '%s' "$input" | jq -r '.tool_input.file_path // ""')
361
+ # file_path is display/guard only (basename in the messages); target_key is
362
+ # the value matched against history, so only it carries the length tag.
363
+ file_path=$(printf '%s' "$file_path_full" | head -c "$TARGET_KEY_CAP")
364
+ target_key=$(truncate_key "$file_path_full")
365
+ fi
175
366
  is_error=$(printf '%s' "$input" | jq -r '.tool_output.is_error // false')
176
367
  output_preview=$(printf '%s' "$input" | jq -r '.tool_output.output // ""' | head -c 200)
177
368
  raw_command=$(printf '%s' "$input" | jq -r '.tool_input.command // ""')
178
369
 
370
+ # Distinguish "the same file edited 3 times with DIFFERENT content" (a normal
371
+ # incremental-edit workflow — the #1641 false positive) from "the same edit
372
+ # repeated 3 times" (a genuine stuck loop). Only the latter should hard-block.
373
+ # Alphanumerics-only + length cap: deterministic, platform-independent (no
374
+ # md5sum, absent on macOS) and safe to embed in a grep pattern.
375
+ edit_hash=""
376
+ if [ "$tool_name" = "Edit" ] || [ "$tool_name" = "Write" ]; then
377
+ edit_hash=$(printf '%s' "$input" \
378
+ | jq -r '(.tool_input.old_string // .tool_input.content // .tool_input.new_string // "")
379
+ | gsub("[^A-Za-z0-9]"; "") | .[0:120]')
380
+ fi
381
+
382
+ # History entries are written by jq, so their "path" values are JSON-ENCODED.
383
+ # Matching them requires the SAME encoding, not the raw text: a raw value
384
+ # containing a quote (common in Bash commands) can never match the encoded
385
+ # form. The previous BRE escaping was doubly broken — in POSIX BRE "( ) + ? { |"
386
+ # are literals, so escaping them turned them INTO operators and silently
387
+ # stopped matching. Encode once here and match with grep -F (fixed string).
388
+ target_key_json=$(jq -n --arg v "$target_key" '$v')
389
+ path_match="\"path\":${target_key_json}"
390
+
179
391
  is_readonly="false"
180
392
  if [ "$tool_name" = "Bash" ]; then
181
393
  is_readonly=$(is_readonly_bash_command "$raw_command")
@@ -190,18 +402,22 @@ timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)
190
402
  # Generate error hash for deduplication (first 50 chars of error)
191
403
  error_hash=""
192
404
  if [ "$is_error" = "true" ]; then
193
- error_hash=$(echo "$output_preview" | head -c 50 | md5sum 2>/dev/null | cut -d' ' -f1 || echo "unknown")
405
+ error_hash=$(printf '%s' "$output_preview" | head -c 50 | md5sum 2>/dev/null | cut -d' ' -f1 || printf '%s\n' "unknown")
194
406
  fi
195
407
 
408
+ # "path" holds $target_key — a file path for file tools, the command text for
409
+ # Bash (#1641). Keeping Bash commands in this field preserves Check 3 / Signal 3
410
+ # repetition detection while file_path stays empty for Bash.
196
411
  entry=$(jq -cn \
197
412
  --arg ts "$timestamp" \
198
413
  --arg tool "$tool_name" \
199
- --arg path "$file_path" \
414
+ --arg path "$target_key" \
200
415
  --arg err "$is_error" \
201
416
  --arg hash "$error_hash" \
417
+ --arg ehash "$edit_hash" \
202
418
  --arg preview "$output_preview" \
203
419
  --arg readonly "$is_readonly" \
204
- '{timestamp: $ts, tool: $tool, path: $path, is_error: $err, error_hash: $hash, preview: $preview, readonly: $readonly}')
420
+ '{timestamp: $ts, tool: $tool, path: $path, is_error: $err, error_hash: $hash, edit_hash: $ehash, preview: $preview, readonly: $readonly}')
205
421
 
206
422
  printf '%s\n' "$entry" >> "$HISTORY_FILE"
207
423
 
@@ -237,7 +453,7 @@ recovery=""
237
453
 
238
454
  # Signal 1: Repeated error (same error_hash 3+ times in last 10 entries)
239
455
  if [ "$is_error" = "true" ] && [ -n "$error_hash" ]; then
240
- error_repeat=$(tail -10 "$HISTORY_FILE" | grep -c "\"error_hash\":\"${error_hash}\"" 2>/dev/null || echo "0")
456
+ error_repeat=$(tail -10 "$HISTORY_FILE" | grep -c "\"error_hash\":\"${error_hash}\"" 2>/dev/null || printf '%s\n' "0")
241
457
  if [ "$error_repeat" -ge 3 ]; then
242
458
  stuck_detected=true
243
459
  signal_type="Repeated error"
@@ -251,12 +467,12 @@ fi
251
467
  # Signal 2: Edit loop (same file edited 3+ times in last 8 entries)
252
468
  if [ "$stuck_detected" = false ] && { [ "$tool_name" = "Edit" ] || [ "$tool_name" = "Write" ]; }; then
253
469
  if [ -n "$file_path" ]; then
254
- escaped_path=$(echo "$file_path" | sed 's/[.[\*^$()+?{|]/\\&/g')
255
- edit_repeat=$(tail -8 "$HISTORY_FILE" | grep -c "\"path\":\"${escaped_path}\"" 2>/dev/null || echo "0")
470
+ edit_repeat=$(tail -8 "$HISTORY_FILE" | grep -cF -e "$path_match" 2>/dev/null || true)
471
+ [ -n "$edit_repeat" ] || edit_repeat=0
256
472
  if [ "$edit_repeat" -ge 3 ]; then
257
473
  stuck_detected=true
258
474
  signal_type="Edit loop"
259
- pattern_desc="$(basename "$file_path") edited ${edit_repeat} times in last 8 calls"
475
+ pattern_desc="$(basename -- "$file_path") edited ${edit_repeat} times in last 8 calls"
260
476
  occurrence_count=$edit_repeat
261
477
  threshold=3
262
478
  recovery="Try a different file or approach instead of re-editing"
@@ -271,7 +487,7 @@ fi
271
487
  # calls (git status / gh view / ls / ...) doesn't inflate the "same tool
272
488
  # called N times" advisory.
273
489
  if [ "$stuck_detected" = false ] && [ "$is_readonly" != "true" ]; then
274
- tool_repeat=$(tail -8 "$HISTORY_FILE" | grep -v "\"readonly\":\"true\"" | grep -c "\"tool\":\"${tool_name}\"" 2>/dev/null || echo "0")
490
+ tool_repeat=$(tail -8 "$HISTORY_FILE" | grep -v "\"readonly\":\"true\"" | grep -c "\"tool\":\"${tool_name}\"" 2>/dev/null || printf '%s\n' "0")
275
491
  if [ "$tool_repeat" -ge 5 ]; then
276
492
  stuck_detected=true
277
493
  signal_type="Tool loop"
@@ -302,38 +518,64 @@ hard_block_reason=""
302
518
 
303
519
  if [ -f "$HISTORY_FILE" ]; then
304
520
  last_n=$(tail -"$HARD_BLOCK_THRESHOLD" "$HISTORY_FILE" 2>/dev/null)
305
- last_n_count=$(echo "$last_n" | wc -l | tr -d ' ')
521
+ last_n_count=$(printf '%s\n' "$last_n" | wc -l | tr -d ' ')
306
522
 
307
523
  if [ "$last_n_count" -ge "$HARD_BLOCK_THRESHOLD" ]; then
308
- # Check 1: Same file edited HARD_BLOCK_THRESHOLD+ times consecutively
524
+ # Check 1: Same file edited with the SAME content HARD_BLOCK_THRESHOLD+
525
+ # times consecutively. Narrowed in #1641: three DIFFERENT edits to one file
526
+ # is a normal incremental workflow (observed repeatedly on wiki/rule/doc
527
+ # files), not a stuck loop — only an IDENTICAL repeated edit blocks. Both
528
+ # the path AND the edit_hash must match.
309
529
  # (skip when current call is a read-only Bash command — repeated read-only
310
530
  # polling of the same target is not a stuck-loop signal; see #1625)
531
+ # Bash never reaches here — file_path is empty for Bash (#1641), and
532
+ # edit_hash is empty too; repeated Bash commands are handled by Check 3
533
+ # via target_key instead.
311
534
  if [ "$is_readonly" != "true" ] && [ -n "$file_path" ]; then
312
- escaped_path=$(echo "$file_path" | sed 's/[.[\*^$()+?{|]/\\&/g')
313
- consecutive_file=$(echo "$last_n" | grep -c "\"path\":\"${escaped_path}\"" 2>/dev/null || echo "0")
535
+ consecutive_file=$(printf '%s\n' "$last_n" \
536
+ | grep -F -e "$path_match" \
537
+ | grep -cF -e "\"edit_hash\":\"${edit_hash}\"" 2>/dev/null || true)
538
+ [ -n "$consecutive_file" ] || consecutive_file=0
314
539
  if [ "$consecutive_file" -ge "$HARD_BLOCK_THRESHOLD" ]; then
315
540
  hard_block=true
316
- hard_block_reason="Same file ($(basename "$file_path")) edited ${consecutive_file} consecutive times"
541
+ hard_block_reason="Same file ($(basename -- "$file_path")) received the identical edit ${consecutive_file} consecutive times"
317
542
  fi
318
543
  fi
319
544
 
320
545
  # Check 2: Same error repeated HARD_BLOCK_THRESHOLD+ times consecutively
321
546
  if [ "$hard_block" = false ] && [ "$is_error" = "true" ] && [ -n "$error_hash" ]; then
322
- consecutive_error=$(echo "$last_n" | grep -c "\"error_hash\":\"${error_hash}\"" 2>/dev/null || echo "0")
547
+ consecutive_error=$(printf '%s\n' "$last_n" | grep -c "\"error_hash\":\"${error_hash}\"" 2>/dev/null || printf '%s\n' "0")
323
548
  if [ "$consecutive_error" -ge "$HARD_BLOCK_THRESHOLD" ]; then
324
549
  hard_block=true
325
550
  hard_block_reason="Same error repeated ${consecutive_error} consecutive times"
326
551
  fi
327
552
  fi
328
553
 
329
- # Check 3: Same tool+target combination HARD_BLOCK_THRESHOLD+ times consecutively
554
+ # Check 3: Same tool+target combination HARD_BLOCK_THRESHOLD+ times
555
+ # consecutively. Uses target_key (a file path for file tools, the command
556
+ # text for Bash) so repeated identical Bash commands are still blocked
557
+ # after file_path was emptied for Bash (#1641).
330
558
  # (skip when current call is a read-only Bash command — see Check 1 note)
331
- if [ "$hard_block" = false ] && [ "$is_readonly" != "true" ] && [ -n "$file_path" ]; then
332
- escaped_path=$(echo "$file_path" | sed 's/[.[\*^$()+?{|]/\\&/g')
333
- consecutive_tool_target=$(echo "$last_n" | grep "\"tool\":\"${tool_name}\"" | grep -c "\"path\":\"${escaped_path}\"" 2>/dev/null || echo "0")
559
+ # The edit_hash filter mirrors Check 1's #1641 narrowing: without it this
560
+ # check re-creates the very false positive Check 1 was narrowed to remove
561
+ # (tool=Edit + same path fires at 3 regardless of content). For Bash the
562
+ # filter is a no-op — every Bash history entry carries edit_hash "".
563
+ if [ "$hard_block" = false ] && [ "$is_readonly" != "true" ] && [ -n "$target_key" ]; then
564
+ consecutive_tool_target=$(printf '%s\n' "$last_n" \
565
+ | grep -F -e "\"tool\":\"${tool_name}\"" \
566
+ | grep -F -e "$path_match" \
567
+ | grep -cF -e "\"edit_hash\":\"${edit_hash}\"" 2>/dev/null || true)
568
+ [ -n "$consecutive_tool_target" ] || consecutive_tool_target=0
334
569
  if [ "$consecutive_tool_target" -ge "$HARD_BLOCK_THRESHOLD" ]; then
335
570
  hard_block=true
336
- hard_block_reason="${tool_name} called on $(basename "$file_path") ${consecutive_tool_target} consecutive times"
571
+ # Do NOT call basename() on a Bash target: a command is not a path, and
572
+ # basename() on command text produced the misleading "Same file (pr)"
573
+ # style message reported in #1641.
574
+ if [ "$tool_name" = "Bash" ]; then
575
+ hard_block_reason="Identical Bash command repeated ${consecutive_tool_target} times: $(printf '%s' "$target_key" | head -c 60)"
576
+ else
577
+ hard_block_reason="${tool_name} called on $(basename -- "$target_key") ${consecutive_tool_target} consecutive times"
578
+ fi
337
579
  fi
338
580
  fi
339
581
  fi
@@ -56,6 +56,8 @@
56
56
 
57
57
  > **v2.1.233+**: Linux에서 Bash 도구 명령에 **memory cgroup**을 걸 수 있게 되어(`CLAUDE_CODE_TOOL_MEMORY_LIMIT`, opt-in) 폭주하는 빌드가 세션을 마비시키지 못합니다. 이 변수가 설정된 환경에서는 대용량 빌드·테스트가 **OOM으로 죽을 수 있으므로**, 실패를 코드 결함으로 특성화하기 전에 이 변수 설정 여부를 확인합니다(R020 Read-Before-Characterize). 같은 릴리즈에서 **샌드박스 활성 Linux의 유휴 세션이 CPU 코어 1개를 100% 점유하던 문제**도 수정되었습니다 — 구버전 Linux에서 병렬 배치의 CPU 포화·타임아웃 실패를 "부하 의존"으로 귀속하기 전에 유휴 세션의 상시 점유를 배제해야 했습니다(cross-ref R009 「파일 disjoint ≠ 자원 disjoint」). 이 저장소의 기본 실행 환경은 Darwin이므로 두 항목 모두 **현재 미적용**이며, Linux CI·컨테이너 실행에만 해당합니다.
58
58
 
59
+ > **v2.1.252+/v2.1.257+**: (252) 일부 Mac에서 Bash 명령이 "task output swap refused (tasks dir moved or linked)"로 실패하던 결함 수정 — Darwin이 이 저장소 기본 실행 환경이므로 직접 해당하며, 구버전에서 이 문구의 Bash 실패는 명령 결함이 아니라 플랫폼 tasks 디렉토리 처리 결함이었습니다(R020 Read-Before-Characterize). (257) `timeout`/`setsid`로 셸에서 분리된 background 명령이 task stop·CC 종료 후에도 살아남던 결함 수정, background 명령을 tasks 패널에서 중지하면 이제 Claude에 통지됨, `claude -p --input-format stream-json`에 비-JSONL 입력 시 무한 메모리 증가 대신 즉시 실패. 위 macOS `gtimeout` 노트(#1327)와 결합하면, `gtimeout`으로 감싼 백그라운드 명령이 세션 종료 후 잔존하던 관측은 이 결함의 산물일 수 있습니다.
60
+
59
61
  ### Capability-Aware Tool Scheduling
60
62
 
61
63
  When dispatching parallel tool calls, consider per-tool capabilities to optimize scheduling:
@@ -47,6 +47,7 @@ Model values resolve differently depending on WHERE they are written. Mixing tie
47
47
  | `claude-opus-4-8` | Opus, previous generation; supports xhigh effort |
48
48
  | `claude-opus-5` | Latest Opus (GA); native 1M context, fast mode at $10/$50 per Mtok |
49
49
  | `claude-fable-5` | Mythos-class; tier above Opus (access via CC v2.1.170+) |
50
+ | `claude-fable-5-1` | Mythos-class; Fable 5.1 — v2.1.257부터 기본 Fable 모델, 1M context |
50
51
 
51
52
  Full IDs are valid ONLY in agent frontmatter — the Agent tool's `model:` spawn parameter does NOT accept them (see Tier 3). Writing the full ID directly (not a project-invented shorthand) pins the agent regardless of future CC default changes. This is the recommended way to opt into Sonnet 5 / Opus 5 / Fable 5 explicitly rather than riding CC's Tier-1 default resolution.
52
53
 
@@ -76,8 +77,12 @@ Skill/rule text instructing "spawn with `model: opus`" refers to this tier — a
76
77
 
77
78
  > **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
79
 
80
+ > **v2.1.257+**: `CLAUDE_CODE_SUBAGENT_MODEL_FORCE`가 신설되어, 설정 시 `CLAUDE_CODE_SUBAGENT_MODEL`(또는 메인 모델)을 **모든** 서브에이전트에 강제 적용하며 per-spawn(Tier 3)과 agent-definition(Tier 1/2) model override를 무시합니다. 즉 위 v2.1.251 노트의 "이 env var가 project의 model pin을 더 이상 깨뜨릴 수 없다"는 서술은 **FORCE 미설정 시에 한해** 참으로 좁혀집니다. FORCE가 설정된 환경에서는 frontmatter의 model이 실행 모델의 증거가 아니므로(R020 "attempt ≠ outcome"의 모델 각도, v2.1.223 강등 경고 노트와 같은 계열) 무인 실행 전 `env | grep -c CLAUDE_CODE_SUBAGENT_MODEL_FORCE`처럼 값 노출 없이 **설정 여부만** 확인합니다.
81
+
79
82
  > **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.
80
83
 
84
+ > **v2.1.257+**: Claude Fable 5.1(`claude-fable-5-1`)이 추가되어 **기본 Fable 모델**이 되었습니다 — 1M context, $10/$50 per Mtok(캐시 읽기 $0.25/Mtok). Tier-3 `model: fable` alias는 이제 Fable 5.1로 해석됩니다(단 Claude apps gateway 세션은 게이트웨이가 아직 Fable 5.1을 미지원해 당분간 Fable 5로 유지됩니다 — `/model`에서 명시 선택해야 Fable 5.1 사용 가능). frontmatter에서 확정하려면 Tier-2 full ID `claude-fable-5-1`을 쓰고, 기존 `claude-fable-5` pin은 그대로 Fable 5에 남습니다 — Tier-1 alias 해석 주체는 CC라는 위 원칙(v2.1.219/222 노트와 동일 계열)의 재확인입니다.
85
+
81
86
  <!-- ARCHIVED CC version notes (historical):
82
87
  > **v2.1.173+**: Fable 5 model IDs carrying a `[1m]` suffix are now auto-normalized (the suffix is stripped) because Fable 5 includes 1M context by default. Use `claude-fable-5` / `model: fable` WITHOUT a `[1m]` suffix — appending it is redundant and normalized away. (The `[1m]` suffix remains meaningful for Opus/Sonnet IDs.)
83
88
 
@@ -509,6 +514,8 @@ Key optional fields: `scope`, `context`, `version`, `effort`, `model`, `agent`,
509
514
 
510
515
  > **v2.1.228+**: claude.ai에서 동기화된 스킬이 하드닝되었습니다 — 로컬 커맨드·MCP prompt를 **shadow하지 않고**, description이 sanitize·labeling되며, 로컬 머신에서 그 본문이 `!` 명령을 실행하거나 `@` 파일 참조를 확장하지 **않습니다**. 즉 외부 출처 스킬은 로컬 `.claude/skills/` 스킬과 **동일한 실행 능력을 갖지 않으므로**, 동기화 스킬에 `!`/`@` 동작을 전제한 본문을 작성하면 무음 미실행이 됩니다. 구버전에서는 동기화 스킬이 로컬 커맨드를 가릴 수 있어 같은 이름 호출이 어느 정의로 해소되는지 결정론적이지 않았습니다.
511
516
 
517
+ > **v2.1.257+**: `/add-dir`이 현재 작업 디렉토리 **내부**의 디렉토리를 거부하던 문제가 수정되어, 이제 startup 시 `--add-dir`와 동일하게 그 디렉토리의 skills/commands/agents를 로드합니다. 구버전에서는 세션 중 `/add-dir`로 하위 디렉토리의 스킬 트리를 추가 로드할 수 없었으므로, 서브디렉토리 단위 스킬 확장 워크플로우가 이 버전부터 가능해집니다.
518
+
512
519
  <!-- DETAIL: Skill Optional Fields (full yaml block)
513
520
  ```yaml
514
521
  scope: core # core | harness | package (default: core)
@@ -430,6 +430,8 @@ Cross-reference: R020 ("actual outcome ≠ attempt" — verifying that a command
430
430
 
431
431
  > **v2.1.234+**: `/config`의 "Default teammate model" 설정이 **제거**되어, agent-team teammate는 이제 spawn이 모델을 지정하지 않는 한 **leader의 모델**을 사용합니다. 이전에는 teammate 모델을 전역 설정값으로 지정할 수 있었으므로, 과거 세션의 "teammate가 어떤 모델로 실행됐는지" 서술은 이 변경 이전 버전 기준일 수 있습니다.
432
432
 
433
+ > **v2.1.257+**: 세 건이 함께 수정되었습니다. (a) leader의 mailbox 쓰기가 잠시 잠긴 사이 teammate permission request가 **두 번 응답**되던 결함 수정 — v2.1.224/251 SendMessage·inbox 신뢰성 계열의 연장이며, 구버전에서 승인이 2회 적용된 흔적은 이중 승인 의도의 증거가 아닙니다. (b) tmux/iTerm2 pane의 teammate가 shutdown 확인 후에도 열려 있던 결함 수정 — 위 Lifecycle의 `TeamDelete` 이후 pane 잔존은 더 이상 정상이 아닙니다. (c) `/fork`가 원 대화의 prompt cache를 새 background 세션에서 유지하도록 개선(worktree briefing이 system-prompt 변경 대신 메시지로 도착) — R009 fork 컨텍스트 상속 노트의 비용 각도. 이 저장소는 `TeamCreate` 미등록으로 R018이 dormant이므로 (a)(b)는 기록용, (c)는 fork 사용 시 즉시 해당합니다.
434
+
433
435
  <!-- ARCHIVED CC version note (historical):
434
436
  > **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.
435
437
 
@@ -145,6 +145,8 @@ Cross-reference: R018 (Member Completion Verification), `feedback_release_delega
145
145
  | 리서치 위임이 수집을 끝낸 뒤 일괄 기록 | 첫 2턴 내 골격 Write + 증분 Edit |
146
146
  | "대기 중" 보고를 미완료로 단정 | 훅 피드백 잠식 가능성 포함해 ground-truth로 완료 여부 판정 |
147
147
 
148
+ > **v2.1.257+**: 서브에이전트가 컴퓨터 절전·연결 끊김·서버 오류로 응답이 mid-stream 절단될 때 불완전 응답으로 그대로 종료하던 동작이 **자동 이어감**으로 수정되었습니다. 즉 "판정 없이 종료" 증상의 원인 축 중 **네트워크/서버 절단 축은 v2.1.257부터 소멸**하며, 남는 실재 원인은 `maxTurns` 한도(위 실증)·위임 경계 미분할·에이전트 자체 판단 종료·훅 피드백 잠식(8항)입니다. 따라서 v2.1.257+ 환경에서 mid-step 종료를 관측하면 네트워크 절단을 원인 후보에서 먼저 제외하고 `maxTurns` partial 표시 유무를 확인합니다 — 단 원인 축 하나가 사라졌다고 ground-truth 실측 원칙을 낮추지 않습니다. 같은 릴리즈에서 턴을 백그라운드로 보낼 때(`←`/Ctrl+B) 실행 중이던 도구가 거부된 것으로 처리되던 결함도 수정되어, 구버전 background 세션의 "도구 거부됨" 기록은 실제 거부의 증거가 아닐 수 있습니다.
149
+
148
150
  Cross-reference: R018 (v2.1.246 maxTurns partial-marking 노트), R009 (Member Prompt Size Cap — 프롬프트 토큰 상한과 별개로 턴 수 상한도 위임 크기 설계 변수임을 추가).
149
151
 
150
152
  <!--
@@ -408,6 +410,8 @@ Origin: #1269 ① (R020 self-violation, session 106).
408
410
  | **v2.1.246+**: 헤드리스/원격 세션에서 수신 메시지로 인터럽트된 MCP 도구 호출이 "출력 없이 완료됨"으로 보고됨(v2.1.246 이전) | 실제로는 **인터럽트**됐다 — 정상 완료가 아니었다 | v2.1.246+는 명시적 interrupted 에러로 보고하도록 수정됨; 구버전 세션의 "빈 출력 완료"는 무음 인터럽트였을 수 있음 |
409
411
  | **v2.1.246+**: 실행 중 인터럽트된 셸 명령이 "Ran 1 shell command"로만 표시(잘렸다는 표시 없음, v2.1.246 이전) | 명령이 **완주하지 못했다** | 출력 완결성을 별도로 확인(예상 출력 패턴 대조) 없이 "실행됨"만으로 성공 단정 금지 |
410
412
 
413
+ > **v2.1.252/257/258+**: 실패/중단 관련 보고 무결성 결함 3건이 추가로 수정되었습니다. (252) 매우 큰 실패 출력(디스크 풀 상태의 git 오류 등)을 실은 background task 알림이 대화를 API 요청 크기 한도 밖으로 밀어내던 결함 — R021 v2.1.247 훅 출력 폭주 계열의 background task 각도이며, 구버전에서 "Prompt is too long"으로 세션이 멈춘 것은 작업 자체의 실패가 아니라 실패 **알림의 크기** 때문일 수 있었습니다. (257) `claude -p`가 모델이 armed한 Monitor가 아직 도는 중인데도 최종 결과 약 5초 뒤 종료하던 결함이 수정되어, 이제 감시가 발화하거나 타임아웃될 때까지 대기합니다 — 무인 `-p` 실행에서 Monitor 결과 부재는 이제 "감시 미발화"로 해석하며 "조기 종료"로 오판하지 않습니다. (258) 원격·예약 세션이 재전송된 permission approval을 적용하지 못한 뒤 "user messages must have non-empty content"로 실패하던 결함이 수정되었습니다 — R010 v2.1.234 "background subagent 승인·거부가 드롭될 수 있던 결함" 계열의 원격 세션 각도이며, 구버전 `/schedule`·remote 세션의 이 오류 문구는 프롬프트 자체의 결함이 아니라 승인 채널의 결함이었습니다.
414
+
411
415
  > **v2.1.234+**: print/SDK 모드에서 SIGTERM 수신 시 더 이상 interrupted turn이나 synthetic tool denial을 기록하지 않는다(명령은 여전히 종료되고 프로세스는 exit code 143). 무인 실행(`-p` 모드) 강제 종료 후 트랜스크립트를 완료 판정 근거로 쓸 때, v2.1.234+에서는 SIGTERM에 의한 중단이 트랜스크립트 상에 "interrupted"로 남지 않는다는 점을 전제해야 한다 — 트랜스크립트가 깨끗해 보여도 실제로는 SIGTERM으로 잘렸을 수 있다.
412
416
 
413
417
  **교훈**: 위 Core Rule("actual outcome ≠ attempt")은 방향이 없다 — 도구가 성공을 보고하든 실패를 보고하든, 보고 자체는 ground-truth가 아니다. 실패 보고를 받았다고 곧바로 재시도·롤백에 들어가지 말고, 먼저 실제 산출물 상태를 확인한다.
@@ -428,7 +428,17 @@ The Agent tool defaults to `mode: "acceptEdits"`, which overrides agent frontmat
428
428
  ### Self-Check
429
429
 
430
430
  Before spawning any agent:
431
- 1. Does the Agent tool call include `mode: "bypassPermissions"`? → YES: proceed → NO: add it
431
+ 1. **유효 permission mode 확인** — per-call `mode` 파라미터는 v2.1.212+ 에서 무시되고,
432
+ **프로젝트 scope `permissions.defaultMode` 는 v2.1.257+ 에서도 무시된다(#1644)**.
433
+ 무인 실행 전 실측할 것: `jq -r '.permissions.defaultMode // "unset"' ~/.claude/settings.json`
434
+ (user scope) — 이 값 또는 `--permission-mode` 실행 플래그만이 유효하다.
435
+ bypassPermissions 가 아니면 프롬프트 발생을 전제로 계획한다.
436
+ 하위 호환을 위해 per-call `mode: "bypassPermissions"` 는 계속 포함하되,
437
+ **그 존재를 무인 실행의 증거로 삼지 않는다**(R020 "attempt ≠ outcome").
438
+ 실측(2026-09-03, `claude -p --debug-file`): `[WARN] settings defaultMode "bypassPermissions"
439
+ ignored — only policy/user/flag settings may grant bypass mode (projectSettings and
440
+ localSettings are repo-controllable)` — 무시 동작이 직접 실증되었다. 프로젝트 settings에는
441
+ `permissions._comment_defaultMode` 안내 키가 추가되었다(v1.1.59).
432
442
  2. Is this a new skill that spawns agents? → Add Permission Mode section
433
443
 
434
444
  ### Common Violation
@@ -504,6 +514,8 @@ Before spawning any agent:
504
514
 
505
515
  > **v2.1.234+**: background task 알림(턴 사이에 전달되는 것)이 이제 mid-turn 전달과 동일하게 `<system-reminder>` 태그 안에 담겨 모델에 전달됩니다. 오케스트레이터가 background 에이전트 완료 통지를 받는 경로가 이것이므로, 그 통지는 **시스템 메시지이지 사용자 입력이 아닙니다** — R015 "다른 에이전트의 메시지는 결코 사용자의 승인이 아니다" 원칙과 마찬가지로, background 통지 역시 사용자 승인의 증거로 인용하지 않습니다. 이전에는 턴 사이 알림 형식이 mid-turn과 달라 이 구분이 덜 명확했습니다.
506
516
 
517
+ > **★ v2.1.257+**: 프로젝트 스코프 `.claude/settings.json`/`.claude/settings.local.json`의 `defaultMode: "bypassPermissions"`가 이제 **무시**됩니다(`"auto"`와 동일 취급) — user 또는 managed settings에 설정하거나 `--permission-mode` 플래그로 전달해야 합니다. 이 섹션은 v2.1.212+에서 "통제점은 부모 세션의 permission mode"라고 규정했는데, 그 부모 세션 mode를 프로젝트 settings로는 더 이상 켤 수 없으므로 통제점이 **user/managed settings 또는 `--permission-mode` 플래그**로 한 단계 더 밀려납니다. 이 저장소 실측(2026-09-02): `.claude/settings.json`과 `.claude/settings.local.json` 둘 다 `permissions.defaultMode = "bypassPermissions"`였으나 `~/.claude/settings.json`(user)은 `"auto"`였고, 세션 훅 컨텍스트도 "auto mode is active"를 보고했습니다 — 즉 v2.1.257 이후 "bypass로 무인 실행 중"이라는 전제가 **조용히 깨져 있었습니다**. 무인 루프(`/fsd`) 착수 전에는 user settings의 `permissions.defaultMode`를 조회하거나 `--permission-mode bypassPermissions`를 명시적으로 전달해 유효 모드를 확인합니다 — R002/R006의 이 섹션 canonical 참조는 그대로 유지합니다. 같은 릴리즈에서 agent view(`←`)로 dispatch된 세션이 원본 세션의 permission mode를 강제 상속하던 결함도 수정되어, 대상 디렉토리의 `defaultMode`와 agent의 `permissionMode`가 이제 존중됩니다.
518
+
507
519
  > **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
520
 
509
521
  ## Agent Capability Pre-Check
@@ -77,6 +77,8 @@ Use a `"*"` deny rule in `settings.json` to enforce a deny-by-default posture, t
77
77
 
78
78
  > **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 권고의 연장선.
79
79
 
80
+ > **v2.1.252/257+**: 두 건이 allow 규칙 저장·반영 신뢰성을 보강합니다. (252) `.claude/settings.local.json`이 아직 없는 프로젝트에서 "always allow"를 눌러도 저장되지 않던 결함이 수정되었습니다 — 구버전에서 "always allow를 눌렀는데 다시 묻는다"는 관측은 이 파일 부재가 원인일 수 있었습니다. (257) 세션 시작 후 새로 생성된 `.claude/` 폴더의 settings가 재시작 전까지 반영되지 않던 결함이 수정되었습니다 — R021 「훅 배선 경로」가 서술하는 settings 재생성 흐름에서, 세션 중 생성한 settings 파일이 이제 즉시 로드됩니다.
81
+
80
82
  > **v2.1.221/222+**: 세 건이 Tier-3/4 권한 흐름에 영향을 줍니다.
81
83
  > 1. **(v2.1.221) Bash 도구 권한 검사 우회 수정** — zsh가 `[[ ]]` 정규식 조건문 안에서 숨겨진 명령을 실행할 수 있었고, 해당 명령들은 이제 권한 프롬프트를 발생시킵니다. **이 저장소의 Claude Code Bash 도구 실행 셸이 zsh**이므로(R005 #1540), `[[ ... =~ ... ]]` 안에 명령을 포함하는 형태는 Tier-4 프롬프트 대상이며 무인 흐름의 새 프롬프트 발생원이 될 수 있습니다. Windows의 따옴표 포함 경로 PowerShell 권한 검사도 같은 방향으로 수정되었습니다.
82
84
  > 2. **(v2.1.221) auto mode 병렬 권한 검사 최적화** — 병렬 tool call의 권한 검사가 cache-efficient해지고 캐시된 대화 prefix 재사용으로 비용이 감소했습니다(R009 병렬 배치의 부담 완화). 검사 대기 중 모드를 전환하면 stale 결과를 적용하지 않고 재프롬프트합니다.
@@ -98,9 +100,13 @@ Use a `"*"` deny rule in `settings.json` to enforce a deny-by-default posture, t
98
100
 
99
101
  > **v2.1.247~251 재도입 여부 확인 (실측)**: 위 「일반 교훈」이 언급하는 v2.1.233 롤백 2건(Windows Git Bash의 Cygwin-style symlink 우회, Bash 입력 리다이렉션 `< file`)의 "좁힌 형태 재도입"은 v2.1.247~251 CHANGELOG 범위에서 **확인되지 않았습니다** — "Cygwin"이라는 단어도 `< file` 입력 리다이렉션 언급도 4개 릴리즈 어디에도 없습니다. 대신 v2.1.251에 위 5건의 (c)(d)처럼 **메커니즘이 다른** 별개의 심링크·경로 우회 수정이 새로 등장했습니다 — 같은 "심링크 우회"라는 결과이지만 원인 버그는 다릅니다. 따라서 위 "233에서 롤백됨" 서술은 이 시점까지 **여전히 유효**하며, 재도입이 확인되면 이 노트를 갱신합니다.
100
102
 
103
+ > **v2.1.257+ (재도입 확인)**: v2.1.233에서 롤백됐던 2건 중 **`< file` 입력 리다이렉션 권한검사가 v2.1.257에서 재도입**됐습니다 — Bash `Read()`/`Edit()` deny 규칙이 이제 `< file` 리다이렉트와 `tac`/`egrep` 같은 reader 명령에도 적용되어, 인자나 리다이렉트 대상 중 하나라도 deny에 걸리면 명령이 거부됩니다. 232 원본("인자 표기와 동일하게 검사")보다 넓은 형태입니다. Windows Git Bash의 Cygwin-style symlink 우회 건은 v2.1.257/258 CHANGELOG에도 여전히 언급이 없어 **미재도입 상태로 유지**합니다. 위 「`< file` 회고적 함의(버전 무관 유지)」 문단은 이제 **v2.1.257 미만 버전에 한정**해 읽습니다 — v2.1.257 이후로는 `< file`이 다시 검사 대상입니다.
104
+
105
+ > **v2.1.257+**: 권한검사 보강 5건이 추가로 확인되었습니다. (a) auto mode에서 `permissions.ask` 규칙이 매칭 명령이 복합 명령·서브셸 내부에서 실행될 때 건너뛰어지던 결함이 수정되어 확인 프롬프트 없이 실행되지 않습니다 — 위 「allow ≠ classifier」 계열의 ask 층 보강입니다. (b) zsh가 bash와 다르게 파싱하는 `[[ ]]` 조건문을 auto-approve하던 결함이 추가로 수정되었습니다 — v2.1.221/238에 이은 3번째 보강이며, 이 저장소의 Bash 도구 실행 셸이 zsh이므로 직접 해당합니다. (c) `permissions.blockReadsOutsideWorkingDirectories` 설정이 신설되어, auto mode에서 작업 디렉토리 밖 첫 파일 읽기 전 1회 프롬프트를 표시하고 그런 읽기를 차단하는 옵션을 제공합니다. (d) `allowManagedPermissionRulesOnly`가 활성 상태일 때 첫 settings reload 이후 `--disallowedTools`와 세션 deny 규칙이 탈락하던 결함이 수정되었습니다. (e) 워크트리 격리 세션이 git을 건드리지 않는 Bash 루프·`$VAR` 읽기·`"$(…)"`·heredoc을 "too complex to verify that it stays inside the worktree"로 거부하던 결함이 수정되었습니다 — R009 워크트리 병렬 위임 시 이런 복합 명령의 거부를 더 이상 격리 결함으로 진단하지 않습니다.
106
+
101
107
  > **v2.1.238+**: Bash 도구의 permission 검사가 zsh 전용 조건문(shell conditional) 문법에 대해 추가로 개선되었습니다. 이는 위 v2.1.221 "zsh `[[ ]]` 정규식 조건문 안에서 숨겨진 명령이 권한 검사를 우회"의 **직접 연장선**입니다 — "개선"으로만 기술되어 있어 v2.1.221 수정이 완전 해결이 아니었거나 추가 우회 벡터가 있었음을 시사합니다. 이 저장소의 Bash 도구 실행 셸이 zsh이므로(R005 #1540 실측) 직접 관련됩니다.
102
108
 
103
- > **v2.1.246/248+**: (246) 끝에 매달린 `&&`/`||`가 있는 손상된(malformed) 명령에 대해 Bash 권한검사가 이제 **항상 승인을 요구**합니다 — 구버전에서는 이런 형태가 검사를 우회할 수 있었습니다. (248) `--restricted`(또는 `CLAUDE_CODE_RESTRICTED=1`) 모드가 신설되어 명령/코드 실행 도구와 `WebFetch`를 제거하고(`--tools`에 명시 시 예외), 파일 도구를 작업 디렉토리 내부로 제한하며, `bypassPermissions`를 거부하고, user/project/local settings 파일을 무시합니다. 이 저장소는 기본적으로 `bypassPermissions`를 쓰므로(R010 Universal bypassPermissions) `--restricted`와는 **상호 배타적**입니다 — 이 저장소 워크플로우에는 적용하지 않되, 신규 안전 모드 옵션으로 존재를 기록합니다.
109
+ > **v2.1.246/248+**: (246) 끝에 매달린 `&&`/`||`가 있는 손상된(malformed) 명령에 대해 Bash 권한검사가 이제 **항상 승인을 요구**합니다 — 구버전에서는 이런 형태가 검사를 우회할 수 있었습니다. (248) `--restricted`(또는 `CLAUDE_CODE_RESTRICTED=1`) 모드가 신설되어 명령/코드 실행 도구와 `WebFetch`를 제거하고(`--tools`에 명시 시 예외), 파일 도구를 작업 디렉토리 내부로 제한하며, `bypassPermissions`를 거부하고, user/project/local settings 파일을 무시합니다. 이 저장소는 프로젝트 settings에 `bypassPermissions`를 선언하지만 v2.1.257부터 그 선언은 무시되므로(R010 Universal bypassPermissions의 ★ v2.1.257 노트 — 2026-09-02 실측 유효 모드는 user settings `auto`), `--restricted`와의 상호 배타성은 **user/managed scope에서 bypass를 켠 경우에 한해** 성립합니다 — 이 저장소 워크플로우에는 적용하지 않되, 신규 안전 모드 옵션으로 존재를 기록합니다.
104
110
 
105
111
  ### Todo/Task 도구 기본 제거 (v2.1.233+) — 위 표의 †
106
112
 
@@ -127,3 +133,10 @@ Origin: #1582. Cross-ref: R018(Member TaskUpdate Discipline 대체 규약), R020
127
133
  > 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.
128
134
 
129
135
  > **v2.1.212+**: CC가 Agent(구 Task) tool의 `mode` 파라미터를 deprecated 처리했습니다(이제 무시) — subagent는 부모 세션의 permission mode를 기본 상속합니다. 위 canonical 요약의 default `mode`(`acceptEdits`)가 frontmatter `permissionMode`를 override한다는 서술 및 항상 `mode: "bypassPermissions"`를 넘기라는 요건은 이 버전부터 stale이며(파라미터가 무시됨), 무인 실행의 실질 게이트는 부모 세션의 permission mode입니다. 요건 재조정은 R010 "Universal bypassPermissions"가 canonical — R002는 이 flag만 유지합니다.
136
+
137
+ > **v2.1.257+**: 프로젝트 scope `permissions.defaultMode` 가 무시됩니다(user/managed scope 또는
138
+ > `--permission-mode` 플래그만 유효). 위 v2.1.212 노트가 "무인 실행의 실질 게이트는 부모 세션의
139
+ > permission mode"라고 정정했는데, **그 부모 세션 모드를 프로젝트 settings로 지정하는 경로가 이
140
+ > 버전에서 끊겼습니다** — 이 파일 상단 「Deny Rule Glob Patterns」의 v2.1.214 노트(allow 규칙만
141
+ > `<cwd>`로 좁아진 비대칭)와 같은 계열의 **project-scope 축소** 흐름입니다. Canonical owner 는
142
+ > R010 "Universal bypassPermissions" — 상세와 실측 절차는 그쪽을 참조합니다. Origin: #1644.
@@ -131,6 +131,8 @@ Cross-reference: R010 Subagent Scope-Creep STOP Protocol, R002 (permission tiers
131
131
 
132
132
  > **v2.1.232+**: 시크릿·격리 보호가 확장되었습니다 — GitLab 토큰 계열(`glrt-`/`gloas-`/`glptt-`/`glagent-`/`glimt-`/`glsoat-`/`glcbt-`/`glft-`/`glffct-`) redaction 추가와 routable `glpat-`/`gldt-` 전체 redaction, `glab` CLI config가 `gh`와 동일한 샌드박스·자격증명 경로 보호를 받습니다. 또한 공유 `/tmp`의 cross-session messaging 소켓 디렉토리가 사전에 심어진 symlink나 타 사용자 소유 디렉토리를 **사용 대신 거부**하도록, Linux 파일시스템 샌드박스가 protected-path 우회에 대해 하드닝되었습니다. **구버전에서 GitLab 토큰은 redaction 대상이 아니었으므로 트랜스크립트·에이전트 출력에 원문 노출이 가능했습니다** — 과거 세션 로그를 공유하기 전 이 점을 전제합니다. 위 표의 "자격증명 저장소 덤프 금지"는 플랫폼 redaction과 무관하게 유지합니다(redaction은 최후 방어선이지 1차 방어선이 아님).
133
133
 
134
+ > **v2.1.257+**: 시크릿·격리 보호가 4건 추가로 강화되었습니다. (a) auto mode에 **Containment Escape** 규칙이 신설되어, 클라우드 메타데이터 자격증명 조회·egress 회피·cross-tenant 접근이 환경이 "expected"로 명시하지 않는 한 더 이상 auto-approve되지 않습니다 — 이 섹션의 "자격증명 저장소 덤프 금지" 원칙과 R010 Subagent Scope-Creep STOP Protocol의 플랫폼 측 대응이며, 인프라 위임 서브에이전트가 이 규칙에 걸리면 R010의 trip 계수 대상으로 취급합니다. (b) Remote Control 동의 프롬프트를 Esc 또는 `n`으로 닫은 것이 **동의로 계상**되어 다음 요청이 확인 없이 연결되던 결함이 수정되었습니다 — 위 v2.1.223/234/236/238 승인 다이얼로그 표시 무결성 계열의 다섯 번째 사례로, 단발 결함이 아니라 구조적 계열임을 재확인시킵니다. (c) 자격증명 전송·노출 경계 4건도 함께 수정되었습니다 — Foundry API-key 모드에서 잔여 Anthropic API 키/토큰이 함께 전송되던 결함, 게이트웨이가 Foundry/Vertex/Bedrock에 stray host `Authorization`·프로필 헤더를 보내던 결함, MCP 연결·OAuth 디버그/에러 로그의 URL·헤더 자격증명 미redaction, 샌드박스 `deniedDomains`가 후행 점(`example.com.`)이 붙은 호스트를 차단하지 못하고 "don't ask again"도 무한 재프롬프트되던 결함 — 위 v2.1.246/251 자격증명 전송 경계 노트와 같은 위협 클래스입니다. (d) 플러그인이 선언된 command/agent/skill/hooks 경로가 symlink일 때 이를 따라가 자기 디렉토리 밖 파일을 읽던 결함이 수정되어 이제 에러로 거부되며, Cowork·claude.ai cloud 세션에서 자신의 것이 아닌 artifact를 읽는 동작은 auto mode에서도 항상 먼저 확인을 거치도록 변경되었습니다.
135
+
134
136
  ## Required Before Destructive Operations
135
137
 
136
138
  Verify target, assess impact scope, check recoverability, get user approval.
@@ -27,6 +27,8 @@ Recommended: {action1}, {action2}
27
27
 
28
28
  > **v2.1.246+**: 비대화형 세션(`-p`, SDK, 클라우드 세션)에서 서버 오류·연결 끊김·정체로 중간에 끊긴 응답을 이제 **자동으로 이어서 완료**한다 — 이전에는 오류로 종료됐다. `/fsd` 등 무인 자율 루프의 신뢰성에 직접 영향 — 구버전에서는 네트워크 일시 장애가 자율 루프 전체를 조기 종료시킬 수 있었다. 위 표의 "Retryable" 재시도 전략이 이제 이 경로에서는 플랫폼이 자동 수행하므로, 무인 루프 중단을 관측했을 때 "재시도 로직 부재"로 오진하기 전에 이 자동-이어짐 경계(오류 종류·세션 종류)를 먼저 확인한다.
29
29
 
30
+ > **v2.1.257+**: v2.1.246의 자동 이어감이 **서브에이전트**로 확장되어, 절전·연결 끊김·서버 오류로 mid-stream 절단된 서브에이전트 응답이 불완전 종료 대신 자동으로 이어집니다. 위 Retryable 재시도 전략이 서브에이전트 경로에서도 플랫폼이 수행하므로, 위임 에이전트의 중간 종료를 진단할 때 네트워크 절단 축은 후순위로 두고 R020 「maxTurns 절단 실증」의 turn 한도를 먼저 확인합니다(R020 해당 노트 cross-ref).
31
+
30
32
  ## Validation
31
33
 
32
34
  | When | Checks |
@@ -169,6 +169,22 @@ If any of those underlying skills evolve, FSD automatically benefits — its onl
169
169
 
170
170
  When this skill delegates work via Agent tool calls, ALL Agent tool calls MUST include `mode: "bypassPermissions"` per R010 Universal bypassPermissions.
171
171
 
172
+ ⚠ **그 파라미터는 CC v2.1.212+ 에서 무시되며, 프로젝트 scope `permissions.defaultMode` 역시
173
+ CC v2.1.257+ 에서 무시된다(#1644).** 즉 위 지시를 지켰다는 사실은 **무인 실행의 증거가 아니다**.
174
+ 자율 루프 진입 전에 유효 모드를 실측한다:
175
+
176
+ ```bash
177
+ jq -r '.permissions.defaultMode // "unset"' ~/.claude/settings.json 2>/dev/null || echo unset
178
+ ```
179
+
180
+ `bypassPermissions` 가 아니면 루프 도중 permission 프롬프트로 정지할 수 있으므로,
181
+ `--permission-mode bypassPermissions` 로 재시작하거나 사람이 지켜보는 실행임을 전제한다.
182
+ (파이프라인 쪽 배선은 `pipeline auto-dev` 의 pre-triage Phase 0.5 가 담당한다.)
183
+
184
+ ⚠ **커밋 위임 타임아웃 (#1645)**: 메인 워크트리의 `.husky/pre-commit` 이 전체 테스트
185
+ 스위트(약 165초)를 돌리므로, `git commit` 을 위임할 때는 Bash `timeout: 400000` 을 명시한다.
186
+ 기본값 120000ms 로는 exit 143(SIGTERM)으로 끊긴다. `--no-verify` 우회는 금지(R010).
187
+
172
188
  ## Artifact Output
173
189
 
174
190
  Artifacts from each iteration follow the conventions of the constituent skills:
@@ -54,6 +54,30 @@ steps:
54
54
  "[pre-triage] WARNING: Issue #N references vX.Y.Z (not in git tags) — body may be stale"
55
55
  - Output all warnings as advisory log; do NOT halt
56
56
 
57
+ Phase 0.5 — Measure the EFFECTIVE permission mode (advisory, do NOT halt).
58
+
59
+ Why: CC v2.1.257 stopped honoring `permissions.defaultMode` declared in PROJECT scope
60
+ (`.claude/settings.json` / `.claude/settings.local.json`). Only user/managed scope or an
61
+ explicit `--permission-mode` flag takes effect. R010 「Universal bypassPermissions」 assumes
62
+ the parent session runs unattended under bypassPermissions; if that assumption is silently
63
+ false, this pipeline stalls on a permission prompt mid-run with no diagnostic (#1644).
64
+
65
+ 1. Read the user-scope setting (project scope is NOT authoritative on v2.1.257+):
66
+ user_mode=$(jq -r '.permissions.defaultMode // "unset"' ~/.claude/settings.json 2>/dev/null || echo "unset")
67
+
68
+ 2. The `--permission-mode` launch flag cannot be read from inside the session. Treat it as
69
+ UNKNOWN — never infer that it was passed.
70
+
71
+ 3. Report and, when not bypassPermissions, warn:
72
+ - user_mode == "bypassPermissions":
73
+ report: "[pre-triage] effective permission mode: bypassPermissions (user scope)"
74
+ - otherwise:
75
+ stderr warning: "[pre-triage] WARNING: effective permission mode is <user_mode>; unattended run may prompt. Project-scope permissions.defaultMode is IGNORED on CC v2.1.257+ (#1644). To run unattended, launch with --permission-mode bypassPermissions or set it in user-scope ~/.claude/settings.json."
76
+ - Do NOT halt (advisory only — a prompted run still completes with a human present).
77
+
78
+ 4. Do NOT read, echo, or modify any credential material while checking settings — read the
79
+ single `permissions.defaultMode` field only (R001).
80
+
57
81
  Phase 1 — Ensure required labels exist, then scan issues.
58
82
 
59
83
  1. Create labels if missing (idempotent):
@@ -297,6 +321,14 @@ steps:
297
321
  develop-bound commit auto-closes the issue on push — BEFORE release/tag/publish (observed v1.1.38,
298
322
  07:49:26Z). Close keywords belong ONLY in the release-stage PR body (see release step 3.b) — auto-tag.yml
299
323
  greps the PR body, not commit messages, to decide which issues to close.
324
+ ⚠ Bash timeout (#1645): the main-worktree `.husky/pre-commit` runs typecheck + lint +
325
+ the FULL `bun test --coverage` suite (~165s measured) before the commit lands. The Bash
326
+ tool's default timeout is 120000ms, so a `git commit` delegation with the default budget
327
+ is killed mid-hook with exit 143 (SIGTERM). Delegate `git commit` with an explicit
328
+ `timeout: 400000` (≈6.7 min). Do NOT work around a timeout with `--no-verify` — that
329
+ bypasses the quality gate and is on the standing deny list (R010).
330
+ Note: git worktrees run typecheck only (`.husky/pre-commit` lines 7-12 branch on
331
+ `[ -f .git ]` and exit 0), so the long budget matters most on the MAIN worktree.
300
332
  6. On success: remove in-progress, add verify-ready
301
333
  7. On failure: remove in-progress, add needs-review, comment error summary
302
334
 
@@ -425,6 +457,9 @@ steps:
425
457
  ⚠ do NOT add Closes/Fixes/Resolves keywords to this commit message (#1542) — this commit merges into
426
458
  develop later via the release PR; a close keyword here bypasses the PR-body mechanism auto-tag.yml
427
459
  relies on (see step 3.b below). Use `Refs #N` if a cross-reference is needed.
460
+ ⚠ Bash timeout (#1645): delegate this commit with an explicit `timeout: 400000` — the
461
+ main-worktree pre-commit hook runs the full test suite (~165s) and the Bash default of
462
+ 120000ms kills it with exit 143. `--no-verify` is NOT an acceptable workaround (R010).
428
463
  h. mgr-gitnerd push release/v{NEW} to origin — NEVER push this commit to develop directly. Pushing to
429
464
  develop here reproduces the diff=0 PR failure this reordering fixes (#1542).
430
465
  i. mandatory verification (with existence guard for partial-update safety), run on release/v{NEW}:
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "1.1.57",
3
- "lastUpdated": "2026-07-14T00:00:00.000Z",
2
+ "version": "1.1.59",
3
+ "lastUpdated": "2026-09-03",
4
4
  "omcustomMinClaudeCode": "2.1.121",
5
5
  "omcustomMinClaudeCodeReason": "Sensitive-path direct Write/Edit on .claude/** under bypassPermissions (R010 deprecation, #1101)",
6
6
  "components": [
@@ -54,6 +54,30 @@ steps:
54
54
  "[pre-triage] WARNING: Issue #N references vX.Y.Z (not in git tags) — body may be stale"
55
55
  - Output all warnings as advisory log; do NOT halt
56
56
 
57
+ Phase 0.5 — Measure the EFFECTIVE permission mode (advisory, do NOT halt).
58
+
59
+ Why: CC v2.1.257 stopped honoring `permissions.defaultMode` declared in PROJECT scope
60
+ (`.claude/settings.json` / `.claude/settings.local.json`). Only user/managed scope or an
61
+ explicit `--permission-mode` flag takes effect. R010 「Universal bypassPermissions」 assumes
62
+ the parent session runs unattended under bypassPermissions; if that assumption is silently
63
+ false, this pipeline stalls on a permission prompt mid-run with no diagnostic (#1644).
64
+
65
+ 1. Read the user-scope setting (project scope is NOT authoritative on v2.1.257+):
66
+ user_mode=$(jq -r '.permissions.defaultMode // "unset"' ~/.claude/settings.json 2>/dev/null || echo "unset")
67
+
68
+ 2. The `--permission-mode` launch flag cannot be read from inside the session. Treat it as
69
+ UNKNOWN — never infer that it was passed.
70
+
71
+ 3. Report and, when not bypassPermissions, warn:
72
+ - user_mode == "bypassPermissions":
73
+ report: "[pre-triage] effective permission mode: bypassPermissions (user scope)"
74
+ - otherwise:
75
+ stderr warning: "[pre-triage] WARNING: effective permission mode is <user_mode>; unattended run may prompt. Project-scope permissions.defaultMode is IGNORED on CC v2.1.257+ (#1644). To run unattended, launch with --permission-mode bypassPermissions or set it in user-scope ~/.claude/settings.json."
76
+ - Do NOT halt (advisory only — a prompted run still completes with a human present).
77
+
78
+ 4. Do NOT read, echo, or modify any credential material while checking settings — read the
79
+ single `permissions.defaultMode` field only (R001).
80
+
57
81
  Phase 1 — Ensure required labels exist, then scan issues.
58
82
 
59
83
  1. Create labels if missing (idempotent):
@@ -297,6 +321,14 @@ steps:
297
321
  develop-bound commit auto-closes the issue on push — BEFORE release/tag/publish (observed v1.1.38,
298
322
  07:49:26Z). Close keywords belong ONLY in the release-stage PR body (see release step 3.b) — auto-tag.yml
299
323
  greps the PR body, not commit messages, to decide which issues to close.
324
+ ⚠ Bash timeout (#1645): the main-worktree `.husky/pre-commit` runs typecheck + lint +
325
+ the FULL `bun test --coverage` suite (~165s measured) before the commit lands. The Bash
326
+ tool's default timeout is 120000ms, so a `git commit` delegation with the default budget
327
+ is killed mid-hook with exit 143 (SIGTERM). Delegate `git commit` with an explicit
328
+ `timeout: 400000` (≈6.7 min). Do NOT work around a timeout with `--no-verify` — that
329
+ bypasses the quality gate and is on the standing deny list (R010).
330
+ Note: git worktrees run typecheck only (`.husky/pre-commit` lines 7-12 branch on
331
+ `[ -f .git ]` and exit 0), so the long budget matters most on the MAIN worktree.
300
332
  6. On success: remove in-progress, add verify-ready
301
333
  7. On failure: remove in-progress, add needs-review, comment error summary
302
334
 
@@ -425,6 +457,9 @@ steps:
425
457
  ⚠ do NOT add Closes/Fixes/Resolves keywords to this commit message (#1542) — this commit merges into
426
458
  develop later via the release PR; a close keyword here bypasses the PR-body mechanism auto-tag.yml
427
459
  relies on (see step 3.b below). Use `Refs #N` if a cross-reference is needed.
460
+ ⚠ Bash timeout (#1645): delegate this commit with an explicit `timeout: 400000` — the
461
+ main-worktree pre-commit hook runs the full test suite (~165s) and the Bash default of
462
+ 120000ms kills it with exit 143. `--no-verify` is NOT an acceptable workaround (R010).
428
463
  h. mgr-gitnerd push release/v{NEW} to origin — NEVER push this commit to develop directly. Pushing to
429
464
  develop here reproduces the diff=0 PR failure this reordering fixes (#1542).
430
465
  i. mandatory verification (with existence guard for partial-update safety), run on release/v{NEW}: