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
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 (
|
|
19
|
+
# Conservative: any ambiguity (redirection, substitution, "||", or an
|
|
20
20
|
# unrecognized command/subcommand) is treated as NOT read-only (write).
|
|
21
|
-
#
|
|
22
|
-
# (
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
218
|
-
|
|
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"
|
package/templates/manifest.json
CHANGED