oh-my-customcode 1.1.54 → 1.1.55

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.54",
246
+ version: "1.1.55",
247
247
  description: "Batteries-included agent harness for Claude Code",
248
248
  type: "module",
249
249
  bin: {
package/dist/index.js CHANGED
@@ -2535,7 +2535,7 @@ var package_default = {
2535
2535
  workspaces: [
2536
2536
  "packages/*"
2537
2537
  ],
2538
- version: "1.1.54",
2538
+ version: "1.1.55",
2539
2539
  description: "Batteries-included agent harness for Claude Code",
2540
2540
  type: "module",
2541
2541
  bin: {
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "workspaces": [
4
4
  "packages/*"
5
5
  ],
6
- "version": "1.1.54",
6
+ "version": "1.1.55",
7
7
  "description": "Batteries-included agent harness for Claude Code",
8
8
  "type": "module",
9
9
  "bin": {
@@ -192,13 +192,26 @@ split("\n")
192
192
  # 올바르게 분리되어 0/0이었음). 이 조사에서 확정된 실제 원인은 별도 메커니즘이다:
193
193
  # tail -n 200 윈도우 안에 경계가 전혀 없을 때 $b=-1로 폴백해 슬라이딩 윈도우 전체를
194
194
  # 하나의 turn으로 병합하는 구조 — 본 수정의 승인 범위(0-tool-use 가드 1건) 밖이므로
195
- # 별도 이슈로 보고한다(오케스트레이터 완료 보고 참조).
195
+ # 별도 이슈로 보고한다(오케스트레이터 완료 보고 참조). → #1628로 등록·수정 완료:
196
+ # 아래 $b 바인딩 직후의 "(#1628)" 주석 및 $wlc 포화 가드 참조.
196
197
  | [ range(0; $last + 1)
197
198
  | select( ($L[.].message.role? == "user")
198
199
  and ( (($L[.].message.content | type) == "string")
199
200
  or (([ $L[.].message.content[]? | select(.type? == "tool_result") ] | length) == 0) ) ) ] as $bi
200
201
  | (if ($bi | length) > 0 then $bi[-1] else -1 end) as $b
201
- | [ range($b + 1; $last + 1) | $L[.] | select(.message.role? == "assistant") ] as $turn
202
+ # (#1628) 경계가 전혀 없는 경우($bi 비어있음)를 두 케이스로 나눈다:
203
+ # (a) 윈도우가 포화($wlc >= 200 — tail -n 200이 실제로 200줄을 반환) — 윈도우 밖에
204
+ # 진짜 경계가 있었을 수 있다. 이 경우 $b=-1로 윈도우 전체를 하나의 turn으로
205
+ # 병합하면, 서로 독립적이고 각각 컴플라이언트한 여러 응답이 합쳐져 announce/
206
+ # tool_use 카운트가 어긋나 R008 위양성이 5~10건대로 요동친다(#1628 실측). 판정
207
+ # 자체를 스킵한다(empty — 아래에서 result가 비어 exit 0으로 이어진다).
208
+ # (b) 윈도우가 포화되지 않음($wlc < 200 — tail이 transcript 전체를 다 읽었다는 뜻) —
209
+ # 세션이 정말로 짧아 첫 턴부터 지금까지 경계가 없는 것이므로, 기존 $b=-1 폴백을
210
+ # 그대로 유지한다(#1625 찐빠 #3이 확정한 자율 루프 경계 대체 신호와 동일 로직).
211
+ | if ($bi | length) == 0 and $wlc >= 200 then empty
212
+ else
213
+ (
214
+ [ range($b + 1; $last + 1) | $L[.] | select(.message.role? == "assistant") ] as $turn
202
215
  | [ $turn[] | .message.content[]? | select(.type? != "thinking") ] as $blocks
203
216
  | ($turn[0].uuid? // "") as $tuuid
204
217
  | ([ $blocks[] | select(.type? == "text") ][0].text? // "") as $ftext
@@ -237,10 +250,20 @@ split("\n")
237
250
  # $nall_tools는 위 forward r008 가드에서 이미 바인딩됨 — 재바인딩하지 않고 재사용한다.
238
251
  | (if $nall_tools == 0 and $an_anchored > 0 then $an_anchored else 0 end) as $r008rev
239
252
  | [$tuuid, ($r007 | tostring), ($r008 | tostring), ($r008rev | tostring)] | @tsv
253
+ )
254
+ end
240
255
  end
241
256
  '
242
257
 
243
- result=$(tail -n 200 "$TRANSCRIPT_PATH" 2>/dev/null | jq -Rsr "$JQ_LAST_TURN" 2>/dev/null) || result=""
258
+ # ── 윈도우 포화 여부 판별 (#1628) ──
259
+ # tail -n 200이 실제로 200줄을 반환했는지(포화 — 윈도우 밖에 잘려나간 내용이 있을 수 있음)를
260
+ # jq 호출 전에 bash에서 측정해 $wlc로 전달한다. awk는 트레일링 개행 유무와 무관하게 정확한
261
+ # 레코드 수를 센다(wc -l은 트레일링 개행이 없는 마지막 줄을 놓칠 수 있음).
262
+ window_content=$(tail -n 200 "$TRANSCRIPT_PATH" 2>/dev/null) || window_content=""
263
+ window_line_count=$(printf '%s' "$window_content" | awk 'END{print NR}')
264
+ : "${window_line_count:=0}"
265
+
266
+ result=$(printf '%s' "$window_content" | jq -Rsr --argjson wlc "$window_line_count" "$JQ_LAST_TURN" 2>/dev/null) || result=""
244
267
 
245
268
  if [ -z "$result" ]; then
246
269
  exit 0
@@ -16,29 +16,28 @@ command -v jq >/dev/null 2>&1 || exit 0
16
16
  HARD_BLOCK_THRESHOLD=${CLAUDE_STUCK_THRESHOLD:-3}
17
17
 
18
18
  # Determine if a Bash command is read-only (metadata/query only, no side effects).
19
- # Conservative: any ambiguity (chaining, redirection, substitution, or an
19
+ # Conservative: any ambiguity (redirection, substitution, "||", or an
20
20
  # unrecognized command/subcommand) is treated as NOT read-only (write).
21
- # Used ONLY to exclude read-only Bash polling from Hard Block Checks 1 and 3
22
- # (same-path / same-tool+target consecutive-repeat blocking) below — Signal 1
23
- # (repeated-error advisory) and Hard Block Check 2 (same error hash) are
24
- # intentionally unaffected: repeated errors are a genuine stuck signal
25
- # regardless of read/write (issue #1625).
26
- is_readonly_bash_command() {
21
+ # Compound commands chained with "&&", ";", or a single "|" are split into
22
+ # segments (#1629) — the whole command is read-only ONLY when EVERY segment
23
+ # is read-only; one write segment (or one unparseable/empty segment) makes
24
+ # the whole command a write.
25
+ # Used to exclude read-only Bash polling from Hard Block Checks 1 and 3
26
+ # (same-path / same-tool+target consecutive-repeat blocking) AND the Signal 3
27
+ # tool-spam advisory below — Signal 1 (repeated-error advisory) and Hard
28
+ # Block Check 2 (same error hash) are intentionally unaffected: repeated
29
+ # errors are a genuine stuck signal regardless of read/write (issue #1625).
30
+
31
+ # Classify a SINGLE (non-compound) command segment as read-only. Callers
32
+ # (is_readonly_bash_command) are responsible for stripping ambiguous
33
+ # constructs and splitting compound commands before invoking this directly.
34
+ is_readonly_single_command() {
27
35
  local cmd="$1"
28
- cmd="$(printf '%s' "$cmd" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
29
36
  if [ -z "$cmd" ]; then
30
37
  echo "false"
31
38
  return 0
32
39
  fi
33
40
 
34
- # Any chaining/redirection/substitution metachar => ambiguous, treat as write
35
- case "$cmd" in
36
- *'>'*|*'|'*|*'&&'*|*';'*|*'$('*|*'`'*|*'<('*)
37
- echo "false"
38
- return 0
39
- ;;
40
- esac
41
-
42
41
  local parts=()
43
42
  read -ra parts <<< "$cmd"
44
43
  local w1="${parts[0]:-}"
@@ -117,11 +116,62 @@ is_readonly_bash_command() {
117
116
  esac
118
117
  }
119
118
 
119
+ is_readonly_bash_command() {
120
+ local cmd="$1"
121
+ cmd="$(printf '%s' "$cmd" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
122
+ if [ -z "$cmd" ]; then
123
+ echo "false"
124
+ return 0
125
+ fi
126
+
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.
131
+ case "$cmd" in
132
+ *'>'*|*'||'*|*'$('*|*'`'*|*'<('*)
133
+ echo "false"
134
+ return 0
135
+ ;;
136
+ esac
137
+
138
+ # Compound command: split on "&&", ";", or a single "|" into segments and
139
+ # 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.
142
+ if [[ "$cmd" == *'&&'* || "$cmd" == *';'* || "$cmd" == *'|'* ]]; then
143
+ local normalized="$cmd"
144
+ normalized="${normalized//&&/$'\n'}"
145
+ normalized="${normalized//;/$'\n'}"
146
+ normalized="${normalized//|/$'\n'}"
147
+ local seg
148
+ while IFS= read -r seg; do
149
+ seg="$(printf '%s' "$seg" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
150
+ if [ -z "$seg" ]; then
151
+ echo "false"
152
+ return 0
153
+ fi
154
+ if [ "$(is_readonly_single_command "$seg")" != "true" ]; then
155
+ echo "false"
156
+ return 0
157
+ fi
158
+ done <<< "$normalized"
159
+ echo "true"
160
+ return 0
161
+ fi
162
+
163
+ is_readonly_single_command "$cmd"
164
+ }
165
+
120
166
  input=$(cat)
121
167
 
122
168
  # Extract tool info
123
169
  tool_name=$(printf '%s' "$input" | jq -r '.tool_name // "unknown"')
124
- file_path=$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.command // ""' | head -c 120)
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)
125
175
  is_error=$(printf '%s' "$input" | jq -r '.tool_output.is_error // false')
126
176
  output_preview=$(printf '%s' "$input" | jq -r '.tool_output.output // ""' | head -c 200)
127
177
  raw_command=$(printf '%s' "$input" | jq -r '.tool_input.command // ""')
@@ -150,7 +200,8 @@ entry=$(jq -cn \
150
200
  --arg err "$is_error" \
151
201
  --arg hash "$error_hash" \
152
202
  --arg preview "$output_preview" \
153
- '{timestamp: $ts, tool: $tool, path: $path, is_error: $err, error_hash: $hash, preview: $preview}')
203
+ --arg readonly "$is_readonly" \
204
+ '{timestamp: $ts, tool: $tool, path: $path, is_error: $err, error_hash: $hash, preview: $preview, readonly: $readonly}')
154
205
 
155
206
  echo "$entry" >> "$HISTORY_FILE"
156
207
 
@@ -214,8 +265,13 @@ if [ "$stuck_detected" = false ] && { [ "$tool_name" = "Edit" ] || [ "$tool_name
214
265
  fi
215
266
 
216
267
  # Signal 3: Tool spam (same tool 5+ times in last 8 entries)
217
- if [ "$stuck_detected" = false ]; then
218
- tool_repeat=$(tail -8 "$HISTORY_FILE" | grep -c "\"tool\":\"${tool_name}\"" 2>/dev/null || echo "0")
268
+ # Read-only Bash polling is excluded from this count (issue #1629): skip
269
+ # entirely when the CURRENT call is read-only, and exclude prior read-only
270
+ # entries from the historical count so a mix of harmless read-only Bash
271
+ # calls (git status / gh view / ls / ...) doesn't inflate the "same tool
272
+ # called N times" advisory.
273
+ 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")
219
275
  if [ "$tool_repeat" -ge 5 ]; then
220
276
  stuck_detected=true
221
277
  signal_type="Tool loop"
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.1.54",
2
+ "version": "1.1.55",
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)",