oh-my-customcode 1.1.106 → 1.1.107
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/README.md +8 -9
- package/dist/cli/index.js +227 -191
- package/dist/index.js +220 -138
- package/package.json +1 -1
- package/templates/.claude/hooks/hooks.json +0 -10
- package/templates/.claude/hooks/scripts/claude-md-reinject.sh +7 -1
- package/templates/.claude/hooks/scripts/cost-cap-advisor.sh +107 -52
- package/templates/.claude/hooks/scripts/session-env-check.sh +0 -8
- package/templates/.claude/hooks/scripts/session-reflection.sh +60 -15
- package/templates/.claude/hooks/scripts/stuck-detector.sh +110 -14
- package/templates/.claude/rules/MAY-optimization.md +1 -1
- package/templates/.claude/rules/MUST-agent-design.md +1 -1
- package/templates/.claude/rules/SHOULD-memory-integration.md +1 -1
- package/templates/.claude/rules/SHOULD-verification-ladder.md +9 -0
- package/templates/.claude/settings.json +0 -10
- package/templates/.claude/skills/intent-detection/patterns/agent-triggers.yaml +0 -8
- package/templates/.claude/skills/pipeline/workflows/auto-dev.yaml +3 -1
- package/templates/CLAUDE.md +1 -1
- package/templates/CLAUDE.md.en +1 -1
- package/templates/CLAUDE.md.ko +1 -1
- package/templates/README.md +4 -4
- package/templates/guides/external-tools/ecc-absorption-decisions.md +2 -0
- package/templates/guides/multi-provider-exec/README.md +7 -18
- package/templates/manifest.json +3 -3
- package/templates/workflows/auto-dev.yaml +3 -1
- package/templates/.claude/hooks/scripts/rtk-intercept.sh +0 -77
- package/templates/.claude/skills/rtk-exec/SKILL.md +0 -199
- package/templates/.claude/skills/rtk-exec/scripts/rtk-wrapper.cjs +0 -377
|
@@ -1,73 +1,128 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# cost-cap-advisor.sh — Advisory hook for session cost monitoring
|
|
3
|
-
# Trigger: PostToolUse (
|
|
4
|
-
# Purpose: Warn
|
|
5
|
-
# Protocol: stdin JSON -> stdout pass-through
|
|
3
|
+
# Trigger: PostToolUse (Edit/Write/Bash/Task/Agent), with continueOnBlock enabled
|
|
4
|
+
# Purpose: Warn once per threshold for the current numeric cost cap
|
|
5
|
+
# Protocol: stdin JSON -> stdout pass-through; first 100% warning exits 2, otherwise 0
|
|
6
6
|
|
|
7
|
-
input=$(cat)
|
|
7
|
+
input=$(command cat)
|
|
8
8
|
|
|
9
|
-
# Cost bridge file written by statusline.sh
|
|
9
|
+
# Cost bridge file written by statusline.sh; both files belong to the caller's PPID.
|
|
10
10
|
COST_FILE="/tmp/.claude-cost-${PPID}"
|
|
11
11
|
ADVISORY_FILE="/tmp/.claude-cost-advisory-${PPID}"
|
|
12
|
-
|
|
13
|
-
# Configurable cap (default $5.00, override via CLAUDE_COST_CAP env var)
|
|
14
12
|
COST_CAP="${CLAUDE_COST_CAP:-5.00}"
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
if [ ! -f "$COST_FILE" ]; then
|
|
14
|
+
pass_through() {
|
|
18
15
|
printf '%s\n' "$input"
|
|
19
16
|
exit 0
|
|
20
|
-
|
|
17
|
+
}
|
|
21
18
|
|
|
22
|
-
#
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
# Decimal grammar excludes signs, exponents, whitespace and calculator expressions.
|
|
20
|
+
# Normalize strings without floating-point rounding: 005.00 and 5 share identity 5.
|
|
21
|
+
normalize_decimal() {
|
|
22
|
+
printf '%s\n' "$1" | LC_ALL=C awk 'END {
|
|
23
|
+
if (NR != 1) exit 1
|
|
24
|
+
value = $0
|
|
25
|
+
if (value !~ /^([0-9]+([.][0-9]*)?|[.][0-9]+)$/) exit 1
|
|
26
|
+
split(value, parts, "[.]")
|
|
27
|
+
whole = parts[1]
|
|
28
|
+
sub(/^0+/, "", whole)
|
|
29
|
+
if (whole == "") whole = "0"
|
|
30
|
+
fraction = parts[2]
|
|
31
|
+
sub(/0+$/, "", fraction)
|
|
32
|
+
if (fraction == "") printf "%s\n", whole
|
|
33
|
+
else printf "%s.%s\n", whole, fraction
|
|
34
|
+
}' 2>/dev/null
|
|
26
35
|
}
|
|
27
36
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
printf '%s\n' "$input"
|
|
31
|
-
exit 0
|
|
32
|
-
fi
|
|
37
|
+
cap_identity=$(normalize_decimal "$COST_CAP") || pass_through
|
|
38
|
+
[ "$cap_identity" != "0" ] || pass_through
|
|
33
39
|
|
|
34
|
-
#
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
# Missing/unreadable/non-regular bridge data cannot mutate the advisory state.
|
|
41
|
+
[ -f "$COST_FILE" ] && [ -r "$COST_FILE" ] || pass_through
|
|
42
|
+
# TSV: cost_usd, ctx_pct, timestamp, rl_5h_pct, rl_7d_pct, rl_5h_resets, rl_7d_resets
|
|
43
|
+
IFS=$'\t' read -r cost_usd ctx_pct timestamp _rl_5h _rl_7d _rl_5h_resets _rl_7d_resets < "$COST_FILE" 2>/dev/null || pass_through
|
|
44
|
+
cost_identity=$(normalize_decimal "$cost_usd") || pass_through
|
|
37
45
|
|
|
38
|
-
#
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
# Validate timestamps outside shell arithmetic, including oversized numeric input.
|
|
47
|
+
# Preserve the existing policy: skip samples older than 60 seconds.
|
|
48
|
+
now=$(date +%s 2>/dev/null) || pass_through
|
|
49
|
+
printf '%s\t%s\n' "$timestamp" "$now" | LC_ALL=C awk -F '\t' 'END {
|
|
50
|
+
if (NR != 1 || NF != 2) exit 1
|
|
51
|
+
stamp = $1
|
|
52
|
+
now = $2
|
|
53
|
+
if (stamp !~ /^[0-9]+$/ || now !~ /^[0-9]+$/) exit 1
|
|
54
|
+
if (sprintf("%f", stamp + 0) !~ /^[0-9]+[.][0-9]+$/) exit 1
|
|
55
|
+
if (now - stamp > 60) exit 1
|
|
56
|
+
}' 2>/dev/null || pass_through
|
|
57
|
+
|
|
58
|
+
# bc preserves decimal identity/thresholds without a floating-point cap comparison.
|
|
59
|
+
# Remove bc's long-number line wrapping; reject any non-integer result.
|
|
60
|
+
cost_pct=$(printf 'scale=0; (%s * 100) / %s\n' "$cost_identity" "$cap_identity" | bc 2>/dev/null) || pass_through
|
|
61
|
+
cost_pct=$(printf '%s' "$cost_pct" | tr -d '\\\n')
|
|
62
|
+
[[ "$cost_pct" =~ ^[0-9]+$ ]] || pass_through
|
|
63
|
+
|
|
64
|
+
level=0
|
|
65
|
+
# Three or more decimal digits imply >=100; avoid shell integer overflow.
|
|
66
|
+
if [ "${#cost_pct}" -ge 3 ]; then
|
|
67
|
+
level=100
|
|
68
|
+
elif [ "$cost_pct" -ge 90 ]; then
|
|
69
|
+
level=90
|
|
70
|
+
elif [ "$cost_pct" -ge 75 ]; then
|
|
71
|
+
level=75
|
|
72
|
+
elif [ "$cost_pct" -ge 50 ]; then
|
|
73
|
+
level=50
|
|
44
74
|
fi
|
|
45
75
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
76
|
+
last_level=0
|
|
77
|
+
state_matches=false
|
|
78
|
+
# State is one TSV line: normalized cap, high-water level (0/50/75/90/100).
|
|
79
|
+
# A legacy level-only or corrupt state has unknown cap and starts a fresh comparison.
|
|
80
|
+
if [ -e "$ADVISORY_FILE" ] || [ -L "$ADVISORY_FILE" ]; then
|
|
81
|
+
[ -f "$ADVISORY_FILE" ] && [ -r "$ADVISORY_FILE" ] && [ ! -L "$ADVISORY_FILE" ] || pass_through
|
|
82
|
+
state=$(command cat "$ADVISORY_FILE" 2>/dev/null) || pass_through
|
|
83
|
+
IFS=$'\t' read -r state_cap state_level state_extra <<< "$state"
|
|
84
|
+
if [ "$state" = "$state_cap"$'\t'"$state_level" ]; then
|
|
85
|
+
case "$state_level" in
|
|
86
|
+
0|50|75|90|100)
|
|
87
|
+
old_cap_identity=$(normalize_decimal "$state_cap") || old_cap_identity=""
|
|
88
|
+
if [ "$old_cap_identity" = "$cap_identity" ]; then
|
|
89
|
+
last_level="$state_level"
|
|
90
|
+
state_matches=true
|
|
91
|
+
fi
|
|
92
|
+
;;
|
|
93
|
+
esac
|
|
94
|
+
fi
|
|
50
95
|
fi
|
|
51
96
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
echo "90" > "$ADVISORY_FILE"
|
|
63
|
-
elif [ "$cost_pct" -ge 75 ] && [ "$last_level" != "75" ] && [ "$last_level" != "90" ] && [ "$last_level" != "100" ]; then
|
|
64
|
-
echo "[Cost Cap] Session cost \$${cost_usd} at 75% of cap \$${COST_CAP}" >&2
|
|
65
|
-
echo "75" > "$ADVISORY_FILE"
|
|
66
|
-
elif [ "$cost_pct" -ge 50 ] && [ -z "$last_level" ]; then
|
|
67
|
-
echo "[Cost Cap] Session cost \$${cost_usd} at 50% of cap \$${COST_CAP}" >&2
|
|
68
|
-
echo "50" > "$ADVISORY_FILE"
|
|
97
|
+
if [ "$state_matches" = true ] && [ "$level" -le "$last_level" ]; then
|
|
98
|
+
pass_through
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
# Persist cap changes even below 50%, so returning to an earlier cap resets correctly.
|
|
102
|
+
# Publish a complete state before warning; failed state I/O stays benign (rc0).
|
|
103
|
+
advisory_tmp=$(mktemp "${ADVISORY_FILE}.tmp.XXXXXX" 2>/dev/null) || pass_through
|
|
104
|
+
if ! { printf '%s\t%s\n' "$cap_identity" "$level" > "$advisory_tmp" && mv -f "$advisory_tmp" "$ADVISORY_FILE"; } 2>/dev/null; then
|
|
105
|
+
command rm -f "$advisory_tmp" 2>/dev/null
|
|
106
|
+
pass_through
|
|
69
107
|
fi
|
|
70
108
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
109
|
+
case "$level" in
|
|
110
|
+
100)
|
|
111
|
+
echo "[Cost Cap] Session cost \$${cost_usd} has reached cap \$${COST_CAP} (${cost_pct}%)" >&2
|
|
112
|
+
echo "[Cost Cap] Consider wrapping up or increasing CLAUDE_COST_CAP" >&2
|
|
113
|
+
printf '%s\n' "$input"
|
|
114
|
+
exit 2
|
|
115
|
+
;;
|
|
116
|
+
90)
|
|
117
|
+
echo "[Cost Cap] Session cost \$${cost_usd} at 90% of cap \$${COST_CAP}" >&2
|
|
118
|
+
echo "[Cost Cap] Ecomode recommended — consider /compact" >&2
|
|
119
|
+
;;
|
|
120
|
+
75)
|
|
121
|
+
echo "[Cost Cap] Session cost \$${cost_usd} at 75% of cap \$${COST_CAP}" >&2
|
|
122
|
+
;;
|
|
123
|
+
50)
|
|
124
|
+
echo "[Cost Cap] Session cost \$${cost_usd} at 50% of cap \$${COST_CAP}" >&2
|
|
125
|
+
;;
|
|
126
|
+
esac
|
|
127
|
+
|
|
128
|
+
pass_through
|
|
@@ -39,12 +39,6 @@ if command -v gemini >/dev/null 2>&1; then
|
|
|
39
39
|
fi
|
|
40
40
|
fi
|
|
41
41
|
|
|
42
|
-
# Check RTK CLI availability
|
|
43
|
-
RTK_STATUS="unavailable"
|
|
44
|
-
if command -v rtk >/dev/null 2>&1; then
|
|
45
|
-
RTK_STATUS="available"
|
|
46
|
-
fi
|
|
47
|
-
|
|
48
42
|
# Check Agent Teams ENV-VAR INTENT — NOT activation (#1588)
|
|
49
43
|
#
|
|
50
44
|
# R018 Detection (.claude/rules/MUST-agent-teams.md) resolves Agent Teams as ACTIVE only when
|
|
@@ -216,7 +210,6 @@ STATUS_FILE="/tmp/.claude-env-status-${PPID}"
|
|
|
216
210
|
cat > "$STATUS_FILE" << ENVEOF
|
|
217
211
|
codex=${CODEX_STATUS}
|
|
218
212
|
gemini=${GEMINI_STATUS}
|
|
219
|
-
rtk=${RTK_STATUS}
|
|
220
213
|
agent_teams=${AGENT_TEAMS_STATUS}
|
|
221
214
|
git_branch=${CURRENT_BRANCH}
|
|
222
215
|
claude_version=${CLAUDE_VERSION}
|
|
@@ -228,7 +221,6 @@ ENVEOF
|
|
|
228
221
|
# Report to stderr (visible in conversation)
|
|
229
222
|
echo " codex CLI: ${CODEX_STATUS}" >&2
|
|
230
223
|
echo " gemini CLI: ${GEMINI_STATUS}" >&2
|
|
231
|
-
echo " RTK CLI: ${RTK_STATUS}" >&2
|
|
232
224
|
echo " Agent Teams: ${AGENT_TEAMS_STATUS}" >&2
|
|
233
225
|
if [ "$AGENT_TEAMS_STATUS" = "env-set" ]; then
|
|
234
226
|
echo " CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 is set — this is INTENT, not activation." >&2
|
|
@@ -29,22 +29,67 @@ if ! command -v jq >/dev/null 2>&1; then
|
|
|
29
29
|
exit 0
|
|
30
30
|
fi
|
|
31
31
|
|
|
32
|
-
# ──
|
|
33
|
-
|
|
32
|
+
# ── 부모 필드 추출: 한 jq 생산자, 독립 오류 fallback 유지 ──
|
|
33
|
+
# jq -j는 session_id의 원래 raw/pretty 출력을 유지한다. 문자열의 실제 NUL만
|
|
34
|
+
# 제거하여 필드 구분자와 충돌하지 않게 하고, 각 필드의 끝 LF는 기존 command
|
|
35
|
+
# substitution처럼 제거한다. JSON 문자열 안의 escaped NUL은 그대로 유지한다.
|
|
36
|
+
# slurp 후에도 입력별 출력 순서와 오류 전 부분 출력 + fallback을 유지한다.
|
|
37
|
+
# process substitution의 jq 실패를 잃지 않도록 별도 종료 상태 필드를 보낸다.
|
|
38
|
+
parent_fields=()
|
|
39
|
+
while IFS= read -r -d '' parent_field; do
|
|
40
|
+
parent_fields[${#parent_fields[@]}]="$parent_field"
|
|
41
|
+
done < <(
|
|
42
|
+
if printf '%s\n' "$input" | jq -sj '
|
|
43
|
+
. as $inputs
|
|
44
|
+
| [try ($inputs[] | .background_tasks // [] | tojson) catch "[]"] as $bg
|
|
45
|
+
| [try ($inputs[] | .session_crons // [] | tojson) catch "[]"] as $crons
|
|
46
|
+
| [try ($bg[] | fromjson | length) catch 0] as $bg_count
|
|
47
|
+
| [try ($crons[] | fromjson | length) catch 0] as $cron_count
|
|
48
|
+
| [try ($bg[] | fromjson
|
|
49
|
+
| [.[] | select(.status? == "running" or .status? == "pending" or .status? == "in_progress")]
|
|
50
|
+
| length) catch 0] as $dangling
|
|
51
|
+
| ($inputs[] | .session_id // empty
|
|
52
|
+
| if type == "string" then gsub("\u0000"; "") else . end
|
|
53
|
+
| (., "\n")),
|
|
54
|
+
"\u0000",
|
|
55
|
+
($bg | join("\n")), "\u0000",
|
|
56
|
+
($crons | join("\n")), "\u0000",
|
|
57
|
+
($bg_count | map(tostring) | join("\n")), "\u0000",
|
|
58
|
+
($cron_count | map(tostring) | join("\n")), "\u0000",
|
|
59
|
+
($dangling | map(tostring) | join("\n")), "\u0000"
|
|
60
|
+
' 2>/dev/null; then
|
|
61
|
+
parent_status=0
|
|
62
|
+
else
|
|
63
|
+
parent_status=$?
|
|
64
|
+
fi
|
|
65
|
+
# An extra separator terminates partial last fields from a failed producer.
|
|
66
|
+
printf '\0%s\0' "$parent_status"
|
|
67
|
+
)
|
|
68
|
+
parent_status="${parent_fields[${#parent_fields[@]}-1]}"
|
|
69
|
+
if [ "$parent_status" -ne 0 ]; then
|
|
70
|
+
exit "$parent_status"
|
|
71
|
+
fi
|
|
72
|
+
if [ "${#parent_fields[@]}" -ne 8 ] || [ -n "${parent_fields[6]}" ]; then
|
|
73
|
+
exit 1
|
|
74
|
+
fi
|
|
75
|
+
for parent_index in 0 1 2 3 4 5; do
|
|
76
|
+
while [[ "${parent_fields[$parent_index]}" == *$'\n' ]]; do
|
|
77
|
+
parent_fields[$parent_index]="${parent_fields[$parent_index]%$'\n'}"
|
|
78
|
+
done
|
|
79
|
+
done
|
|
80
|
+
session_id="${parent_fields[0]}"
|
|
34
81
|
if [ -z "$session_id" ]; then
|
|
35
82
|
printf '%s\n' "$input"
|
|
36
83
|
exit 0
|
|
37
84
|
fi
|
|
38
85
|
|
|
39
|
-
# ── Phase 2 (#1196): background_tasks / session_crons
|
|
40
|
-
#
|
|
41
|
-
bg_tasks_json
|
|
42
|
-
session_crons_json
|
|
43
|
-
bg_tasks_count
|
|
44
|
-
session_crons_count
|
|
45
|
-
|
|
46
|
-
# 미완료 백그라운드 태스크: running/pending/in_progress 상태 카운트
|
|
47
|
-
bg_dangling_count=$(printf '%s\n' "$bg_tasks_json" | jq '[.[] | select(.status? == "running" or .status? == "pending" or .status? == "in_progress")] | length' 2>/dev/null || echo 0)
|
|
86
|
+
# ── Phase 2 (#1196): background_tasks / session_crons (CC v2.1.145+) ──
|
|
87
|
+
# Missing/null/false fields retain [], and each count retains its own fallback.
|
|
88
|
+
bg_tasks_json="${parent_fields[1]}"
|
|
89
|
+
session_crons_json="${parent_fields[2]}"
|
|
90
|
+
bg_tasks_count="${parent_fields[3]}"
|
|
91
|
+
session_crons_count="${parent_fields[4]}"
|
|
92
|
+
bg_dangling_count="${parent_fields[5]}"
|
|
48
93
|
|
|
49
94
|
# ── 경로 결정 (환경변수 override 지원) ──
|
|
50
95
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
@@ -239,7 +284,8 @@ WORKER_EOF
|
|
|
239
284
|
chmod +x "$WORKER_SCRIPT"
|
|
240
285
|
|
|
241
286
|
# ── 백그라운드로 실행 (Stop hook 시간 예산 <3s 준수) ──
|
|
242
|
-
#
|
|
287
|
+
# 전체 subshell의 표준 입출력을 분리하여 부모의 파이프를 붙들지 않는다.
|
|
288
|
+
# 분석은 비동기이며 완료 후 임시 파일을 제거한다. setsid/session 생존 보장은 없다.
|
|
243
289
|
WORKER_ERR_LOG="/tmp/.claude-reflection-err-${PPID}.log"
|
|
244
290
|
|
|
245
291
|
(
|
|
@@ -250,10 +296,9 @@ WORKER_ERR_LOG="/tmp/.claude-reflection-err-${PPID}.log"
|
|
|
250
296
|
"$SCRIPT_DIR" \
|
|
251
297
|
"$bg_tasks_json" \
|
|
252
298
|
"$session_crons_json" \
|
|
253
|
-
"$bg_dangling_count"
|
|
254
|
-
2>>"$WORKER_ERR_LOG"
|
|
299
|
+
"$bg_dangling_count"
|
|
255
300
|
rm -f "$WORKER_SCRIPT"
|
|
256
|
-
) &
|
|
301
|
+
) </dev/null >/dev/null 2>>"$WORKER_ERR_LOG" &
|
|
257
302
|
disown $! 2>/dev/null || true
|
|
258
303
|
|
|
259
304
|
# ── 즉시 stdin pass-through 후 exit 0 (Stop hook 체인 유지) ──
|
|
@@ -536,8 +536,92 @@ input=$(cat)
|
|
|
536
536
|
# Swallow it instead: exit 0 with no stdout, exactly as if the hook had not
|
|
537
537
|
# run. "jq -e" exits non-zero both for a parse error and for a false/null
|
|
538
538
|
# result, so one guard covers every non-object shape.
|
|
539
|
-
|
|
539
|
+
# Common string fields can share one jq producer. Unusual types and embedded
|
|
540
|
+
# NUL retain the original extraction path: command substitution has platform-
|
|
541
|
+
# specific NUL handling, and independent jq failures are part of the contract.
|
|
542
|
+
extraction_mode=$(printf '%s' "$input" | jq -er '
|
|
543
|
+
if type != "object" then false
|
|
544
|
+
elif any(.. | strings; contains("\u0000")) then "legacy"
|
|
545
|
+
elif ((.tool_name // "unknown" | type) != "string") then "legacy"
|
|
546
|
+
elif ((.tool_input != null) and ((.tool_input | type) != "object")) then "legacy"
|
|
547
|
+
elif ((.tool_output != null) and ((.tool_output | type) != "object")) then "legacy"
|
|
548
|
+
elif ((.tool_input.file_path // "" | type) != "string")
|
|
549
|
+
or ((.tool_input.command // "" | type) != "string")
|
|
550
|
+
or ((.tool_input.old_string // .tool_input.content // .tool_input.new_string // "" | type) != "string")
|
|
551
|
+
then "legacy" else "batched" end
|
|
552
|
+
' 2>/dev/null) || exit 0
|
|
553
|
+
|
|
554
|
+
# NUL-delimited fields are safe only after the guard above excludes embedded
|
|
555
|
+
# NUL. A final producer-status record makes jq failures observable; process
|
|
556
|
+
# substitution alone would discard its exit status. jq -j preserves -r scalar
|
|
557
|
+
# rendering without an added newline; trim actual trailing LF just as $(...) did.
|
|
558
|
+
_read_stuck_fields() {
|
|
559
|
+
local filter="$1" field status
|
|
560
|
+
STUCK_FIELDS=()
|
|
561
|
+
while IFS= read -r -d '' field; do
|
|
562
|
+
while [[ "$field" == *$'\n' ]]; do field="${field%$'\n'}"; done
|
|
563
|
+
STUCK_FIELDS[${#STUCK_FIELDS[@]}]="$field"
|
|
564
|
+
done < <(
|
|
565
|
+
if printf '%s' "$input" | jq -j "$filter"; then
|
|
566
|
+
printf '%s\0' '__OMCUSTOM_JQ_STATUS__0'
|
|
567
|
+
else
|
|
568
|
+
printf '__OMCUSTOM_JQ_STATUS__%s\0' "$?"
|
|
569
|
+
fi
|
|
570
|
+
)
|
|
571
|
+
status="${STUCK_FIELDS[${#STUCK_FIELDS[@]}-1]:-}"
|
|
572
|
+
case "$status" in
|
|
573
|
+
__OMCUSTOM_JQ_STATUS__*) status="${status#__OMCUSTOM_JQ_STATUS__}" ;;
|
|
574
|
+
*) return 1 ;;
|
|
575
|
+
esac
|
|
576
|
+
unset "STUCK_FIELDS[$((${#STUCK_FIELDS[@]} - 1))]"
|
|
577
|
+
return "$status"
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
TARGET_KEY_CAP=1000
|
|
581
|
+
|
|
582
|
+
truncate_key() {
|
|
583
|
+
local v="$1"
|
|
584
|
+
if [ "${#v}" -gt "$TARGET_KEY_CAP" ]; then
|
|
585
|
+
printf '%s' "${v:0:$TARGET_KEY_CAP}#len=${#v}"
|
|
586
|
+
else
|
|
587
|
+
printf '%s' "$v"
|
|
588
|
+
fi
|
|
589
|
+
}
|
|
540
590
|
|
|
591
|
+
# Batched extraction changes no classifier or detector inputs. Preview retains
|
|
592
|
+
# its original byte-cap pipeline, including broken-pipe tolerance.
|
|
593
|
+
if [ "$extraction_mode" = "batched" ]; then
|
|
594
|
+
_read_stuck_fields '(.tool_name // "unknown"), "\u0000",
|
|
595
|
+
(if (.tool_name // "unknown") == "Bash"
|
|
596
|
+
then (.tool_input.command // .tool_input.file_path // "")
|
|
597
|
+
else (.tool_input.file_path // "") end), "\u0000",
|
|
598
|
+
(.tool_output.is_error // false), "\u0000"'
|
|
599
|
+
[ "${#STUCK_FIELDS[@]}" -eq 3 ] || exit 1
|
|
600
|
+
tool_name="${STUCK_FIELDS[0]}"
|
|
601
|
+
if [ "$tool_name" = "Bash" ]; then
|
|
602
|
+
file_path=""
|
|
603
|
+
target_key=$(truncate_key "${STUCK_FIELDS[1]}")
|
|
604
|
+
else
|
|
605
|
+
file_path_full="${STUCK_FIELDS[1]}"
|
|
606
|
+
file_path=$(printf '%s' "$file_path_full" | head -c "$TARGET_KEY_CAP" || true)
|
|
607
|
+
target_key=$(truncate_key "$file_path_full")
|
|
608
|
+
fi
|
|
609
|
+
is_error="${STUCK_FIELDS[2]}"
|
|
610
|
+
output_preview=$(printf '%s' "$input" \
|
|
611
|
+
| jq -r '(.tool_output.output // "" | tostring)[0:200]' \
|
|
612
|
+
| head -c 200 || true)
|
|
613
|
+
_read_stuck_fields '(.tool_input.command // ""), "\u0000",
|
|
614
|
+
(if (.tool_name == "Edit" or .tool_name == "Write")
|
|
615
|
+
then ((.tool_input.old_string // .tool_input.content // .tool_input.new_string // "")
|
|
616
|
+
| gsub("[[:space:]]+"; " ") | ltrimstr(" ") | rtrimstr(" ")
|
|
617
|
+
| (length as $l | if $l <= 120 then "\($l)#\(.)"
|
|
618
|
+
else ((explode | add // 0) as $sum
|
|
619
|
+
| "\($l)#\($sum)#\(.[0:80])#\(.[-40:])") end))
|
|
620
|
+
else "" end), "\u0000"'
|
|
621
|
+
[ "${#STUCK_FIELDS[@]}" -eq 2 ] || exit 1
|
|
622
|
+
raw_command="${STUCK_FIELDS[0]}"
|
|
623
|
+
edit_hash="${STUCK_FIELDS[1]}"
|
|
624
|
+
else
|
|
541
625
|
# Extract tool info
|
|
542
626
|
tool_name=$(printf '%s' "$input" | jq -r '.tool_name // "unknown"')
|
|
543
627
|
# Bash commands are NOT files. Feeding tool_input.command into file_path made
|
|
@@ -552,16 +636,6 @@ tool_name=$(printf '%s' "$input" | jq -r '.tool_name // "unknown"')
|
|
|
552
636
|
# remove that collision class, so truncate_key() ALSO appends the ORIGINAL
|
|
553
637
|
# length: two values that share the capped prefix but differ in total length
|
|
554
638
|
# no longer produce the same key (M-3).
|
|
555
|
-
TARGET_KEY_CAP=1000
|
|
556
|
-
|
|
557
|
-
truncate_key() {
|
|
558
|
-
local v="$1"
|
|
559
|
-
if [ "${#v}" -gt "$TARGET_KEY_CAP" ]; then
|
|
560
|
-
printf '%s' "${v:0:$TARGET_KEY_CAP}#len=${#v}"
|
|
561
|
-
else
|
|
562
|
-
printf '%s' "$v"
|
|
563
|
-
fi
|
|
564
|
-
}
|
|
565
639
|
|
|
566
640
|
if [ "$tool_name" = "Bash" ]; then
|
|
567
641
|
file_path=""
|
|
@@ -627,13 +701,35 @@ if [ "$tool_name" = "Edit" ] || [ "$tool_name" = "Write" ]; then
|
|
|
627
701
|
end)')
|
|
628
702
|
fi
|
|
629
703
|
|
|
704
|
+
fi
|
|
705
|
+
|
|
630
706
|
# History entries are written by jq, so their "path" values are JSON-ENCODED.
|
|
631
707
|
# Matching them requires the SAME encoding, not the raw text: a raw value
|
|
632
708
|
# containing a quote (common in Bash commands) can never match the encoded
|
|
633
709
|
# form. The previous BRE escaping was doubly broken — in POSIX BRE "( ) + ? { |"
|
|
634
710
|
# are literals, so escaping them turned them INTO operators and silently
|
|
635
711
|
# stopped matching. Encode once here and match with grep -F (fixed string).
|
|
636
|
-
|
|
712
|
+
# JSON-encoded literals contain no raw NUL or LF, including legacy fields.
|
|
713
|
+
encoded_matches=()
|
|
714
|
+
while IFS= read -r -d '' encoded_match; do
|
|
715
|
+
encoded_matches[${#encoded_matches[@]}]="$encoded_match"
|
|
716
|
+
done < <(
|
|
717
|
+
if jq -nj --arg path "$target_key" --arg edit "$edit_hash" \
|
|
718
|
+
'($path | tojson), "\u0000", ($edit | tojson), "\u0000"'; then
|
|
719
|
+
printf '%s\0' '__OMCUSTOM_JQ_STATUS__0'
|
|
720
|
+
else
|
|
721
|
+
printf '__OMCUSTOM_JQ_STATUS__%s\0' "$?"
|
|
722
|
+
fi
|
|
723
|
+
)
|
|
724
|
+
encoded_status="${encoded_matches[${#encoded_matches[@]}-1]:-}"
|
|
725
|
+
case "$encoded_status" in
|
|
726
|
+
__OMCUSTOM_JQ_STATUS__0) ;;
|
|
727
|
+
__OMCUSTOM_JQ_STATUS__*) exit "${encoded_status#__OMCUSTOM_JQ_STATUS__}" ;;
|
|
728
|
+
*) exit 1 ;;
|
|
729
|
+
esac
|
|
730
|
+
[ "${#encoded_matches[@]}" -eq 3 ] || exit 1
|
|
731
|
+
target_key_json="${encoded_matches[0]}"
|
|
732
|
+
edit_hash_json="${encoded_matches[1]}"
|
|
637
733
|
path_match="\"path\":${target_key_json}"
|
|
638
734
|
|
|
639
735
|
# #1649 follow-up: edit_hash now preserves punctuation (see the edit_hash
|
|
@@ -642,7 +738,7 @@ path_match="\"path\":${target_key_json}"
|
|
|
642
738
|
# (--arg ehash "$edit_hash" -> jq serializes it), so Check 1 / Check 3 below
|
|
643
739
|
# must match it with the SAME JSON-encoded literal, not the raw string — same
|
|
644
740
|
# fix as path_match above, applied to edit_hash.
|
|
645
|
-
edit_hash_json
|
|
741
|
+
# edit_hash_json was encoded together with target_key_json above.
|
|
646
742
|
edit_hash_match="\"edit_hash\":${edit_hash_json}"
|
|
647
743
|
|
|
648
744
|
is_readonly="false"
|
|
@@ -651,7 +747,7 @@ if [ "$tool_name" = "Bash" ]; then
|
|
|
651
747
|
fi
|
|
652
748
|
|
|
653
749
|
# Session-scoped history
|
|
654
|
-
HISTORY_FILE="
|
|
750
|
+
HISTORY_FILE="${OMCUSTOM_STUCK_HISTORY_FILE:-/tmp/.claude-tool-history-${PPID}}"
|
|
655
751
|
|
|
656
752
|
# Create entry
|
|
657
753
|
timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
@@ -86,7 +86,7 @@ zsh는 미인용 `$var`를 단어분할하지 않습니다(#1683 #5) — 분할
|
|
|
86
86
|
|
|
87
87
|
도구 이름 ≠ 그 프로그램(#1590): 사용 전 `type <tool>`로 실체를 확인합니다 — 이 저장소 Bash의 `grep`은 `.gitignore`를 존중하는 셸 함수이므로 저장소 전수조사는 `git grep`을 표준으로 합니다.
|
|
88
88
|
|
|
89
|
-
Bash `cd`는 세션 cwd로 남습니다(#1778, #1767): 훅 명령은 `CLAUDE_PROJECT_DIR`로 고정되어 지속 `cd`에 영향받지 않지만, 상대 경로를 내부에서 읽는 스크립트·도구와 이후 명령의 상대 경로는 어긋날 수 있습니다 — 절대 경로나 서브셸 `( cd <dir> && … )`만 사용하고 지속 `cd`는 남기지 않습니다.
|
|
89
|
+
Bash `cd`는 세션 cwd로 남습니다(#1778, #1767): 훅 명령은 `CLAUDE_PROJECT_DIR`로 고정되어 지속 `cd`에 영향받지 않지만, 상대 경로를 내부에서 읽는 스크립트·도구와 이후 명령의 상대 경로는 어긋날 수 있습니다 — 절대 경로나 서브셸 `( cd <dir> && … )`만 사용하고 지속 `cd`는 남기지 않습니다. `cd X && ( … )`도 `cd`가 서브셸 밖에 놓인 지속 `cd`이므로 금지합니다(#1816) — 괄호는 `cd` 앞에서 열어야 합니다.
|
|
90
90
|
|
|
91
91
|
<!-- DETAIL: tool name vs program
|
|
92
92
|
> **도구 이름 ≠ 그 프로그램 (#1590)**: 도구를 쓰기 전에 `type <tool>`로 실체를 확인한다. Bash 도구의 `grep`은 `~/.claude/shell-snapshots/snapshot-zsh-*.sh`의 **셸 함수**이며 `ugrep --ignore-files`에 위임한다. 그 결과 `.gitignore`의 리터럴 `CLAUDE.md` 패턴을 존중해, **force-tracked 파일을 재귀 탐색에서 조용히 누락**한다(에러 없이 exit 0). 명시 경로를 준 grep은 정상 동작하므로 **traversal만 영향**을 받는다. 실측(2026-08-15): 동일 패턴·동일 대상에 대해 셸 함수 36 / `command grep` 43 / `git grep` 38 히트 — 셸 함수만 `CLAUDE.md`를 0 히트로 놓쳤다. 진단 함정: `git check-ignore`는 **index-aware**라 tracked 파일에 "not ignored"(exit 1)를 반환한다 — 원인을 보려면 `git check-ignore --no-index`를 써야 한다. 처방: 저장소 전수 조사는 `git grep`을 표준으로 한다(R017 Count Sync cross-ref). Origin: #1590.
|
|
@@ -238,7 +238,7 @@ experimental:
|
|
|
238
238
|
cacheTtl: "5m" # "5m" | "1h" — subagent prompt cache TTL when not otherwise set (v2.1.248+)
|
|
239
239
|
```
|
|
240
240
|
|
|
241
|
-
> **Note**: When `disableSkillShellExecution` is enabled (v2.1.91+), skills that rely on inline shell execution
|
|
241
|
+
> **Note**: When `disableSkillShellExecution` is enabled (v2.1.91+), skills that rely on inline shell execution will have their shell blocks disabled. This is a security hardening option.
|
|
242
242
|
-->
|
|
243
243
|
|
|
244
244
|
<!-- DETAIL: CC Version Compatibility History
|
|
@@ -259,7 +259,7 @@ MEMORY.md supports an optional `## User Model` section (DISTINCT from `## Behavi
|
|
|
259
259
|
- Focus: AI agent orchestration, CLI tooling
|
|
260
260
|
|
|
261
261
|
### Override Decisions [confidence: low]
|
|
262
|
-
- Overrode /scout SKIP verdict → INTEGRATE for RTK (#756)
|
|
262
|
+
- Overrode /scout SKIP verdict → INTEGRATE for RTK (#756) (과거 결정 예시입니다. RTK 프로젝트 통합은 v1.1.107에서 퇴역하며 현재 실행 추천으로 해석하지 마십시오.)
|
|
263
263
|
|
|
264
264
|
### Categories
|
|
265
265
|
|
|
@@ -101,6 +101,15 @@ Fable 5 실행 에이전트/스킬은 지시(instruction) 장문화가 오히려
|
|
|
101
101
|
- [ ] Tier 1에서 잡을 수 있는 문제를 다루는가? (상위 tier 대신 시프트 권고)
|
|
102
102
|
- [ ] Ladder 순서를 문서화했는가? (어떤 검사를 먼저 실행하는지)
|
|
103
103
|
|
|
104
|
+
### 문구 작성·정정
|
|
105
|
+
|
|
106
|
+
새 주석/JSDoc/문서의 문구 작성·정정 시 확인합니다(#1816 H1):
|
|
107
|
+
|
|
108
|
+
- [ ] 새 문구의 사실 절마다 코드 앵커 또는 실제 관측 출력을 대응표에 기록했는가?
|
|
109
|
+
- [ ] 독립 문구 검토로 사실 절과 근거의 대응을 확인했는가?
|
|
110
|
+
|
|
111
|
+
lint·typecheck·테스트 통과만으로 문구의 사실 정확성을 확정하지 않습니다. pipeline auto-dev의 문구 검토 요구와 함께 확인합니다.
|
|
112
|
+
|
|
104
113
|
## Safety-Signal Rule Authoring — Carve-Out Pre-Check (shift-left)
|
|
105
114
|
|
|
106
115
|
<!-- DETAIL: Origin note
|
|
@@ -81,16 +81,6 @@
|
|
|
81
81
|
"matcher": "Write|Edit|Bash",
|
|
82
82
|
"description": "Schema-based tool input validation — Phase 1 advisory only"
|
|
83
83
|
},
|
|
84
|
-
{
|
|
85
|
-
"hooks": [
|
|
86
|
-
{
|
|
87
|
-
"type": "command",
|
|
88
|
-
"command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/rtk-intercept.sh\""
|
|
89
|
-
}
|
|
90
|
-
],
|
|
91
|
-
"matcher": "Bash",
|
|
92
|
-
"description": "RTK auto-intercept — transparently rewrites CLI commands through RTK proxy when available (R013 advisory)"
|
|
93
|
-
},
|
|
94
84
|
{
|
|
95
85
|
"hooks": [
|
|
96
86
|
{
|
|
@@ -552,14 +552,6 @@ agents:
|
|
|
552
552
|
base_confidence: 85
|
|
553
553
|
routing_rule: "MUST use professor-triage skill for codebase-driven issue analysis and automated triage"
|
|
554
554
|
|
|
555
|
-
rtk-exec:
|
|
556
|
-
keywords:
|
|
557
|
-
korean: [rtk, 토큰절감, 출력압축, 토큰최적화]
|
|
558
|
-
english: [rtk, "token savings", "compress output", "token optimization"]
|
|
559
|
-
file_patterns: []
|
|
560
|
-
supported_actions: [optimize, compress, proxy]
|
|
561
|
-
base_confidence: 85
|
|
562
|
-
|
|
563
555
|
# ---------------------------------------------------------------------------
|
|
564
556
|
# Skill Triggers — route user intent to skill invocation
|
|
565
557
|
# ---------------------------------------------------------------------------
|
|
@@ -1446,9 +1446,11 @@ steps:
|
|
|
1446
1446
|
re-running step d then step e, re-staging, and re-running this assertion.
|
|
1447
1447
|
|
|
1448
1448
|
Version decision (semver) — PATCH-PREFERRED policy (post-1.0.0):
|
|
1449
|
+
- The user's explicit version or bump-level judgment takes precedence. Approval to remove a feature or change hooks is not approval for a major or minor version.
|
|
1449
1450
|
- No existing tags → v0.1.0
|
|
1450
1451
|
- Previous tag exists → DEFAULT to patch. When in doubt, patch — do NOT reflexively bump minor for rule/doc/skill edits.
|
|
1451
1452
|
- patch (DEFAULT): rule/doc/skill/config/wiki/workflow changes, bugfixes, hardening, CC-compat notes — the vast majority of this project's changes
|
|
1453
|
+
- Public skill/agent/integration additions and removals follow this repository's PATCH-PREFERRED lifecycle policy, including RTK integration and /rtk-exec retirement. Names, files, or counts appearing or disappearing alone do not justify a major or minor bump.
|
|
1452
1454
|
- minor: ONLY a new user-facing capability that changes HOW the harness is used — a new
|
|
1453
1455
|
workflow axis, a new command surface users must learn, or a contract other components
|
|
1454
1456
|
depend on. NOT "a file was added under .claude/skills/ or .claude/agents/".
|
|
@@ -1460,7 +1462,7 @@ steps:
|
|
|
1460
1462
|
baseline rate of change, NOT a minor signal. If a version bump would follow
|
|
1461
1463
|
mechanically from "a new file exists", it is patch. Ask instead: does a user's
|
|
1462
1464
|
workflow change because of this?
|
|
1463
|
-
- major:
|
|
1465
|
+
- major: actual compatibility breaks in existing CLI flags or public APIs, or an explicit major milestone declaration (e.g., v1.0.0). Skill/agent/integration lifecycle changes alone are excluded; the user's explicit version or bump-level judgment takes precedence.
|
|
1464
1466
|
- Previous tag is ahead of source version (e.g., tag v0.136.1, package.json 0.136.0): use next available skip-version (0.136.2)
|
|
1465
1467
|
|
|
1466
1468
|
2. Release notes via omcustom-release-notes skill
|
package/templates/CLAUDE.md
CHANGED
|
@@ -118,7 +118,7 @@ project/
|
|
|
118
118
|
+-- CLAUDE.md # 진입점
|
|
119
119
|
+-- .claude/
|
|
120
120
|
| +-- agents/ # 서브에이전트 정의 (50 파일)
|
|
121
|
-
| +-- skills/ # 스킬 (
|
|
121
|
+
| +-- skills/ # 스킬 (114 디렉토리)
|
|
122
122
|
| +-- rules/ # 전역 규칙 (R000-R023)
|
|
123
123
|
| +-- hooks/ # 훅 스크립트 (보안, 검증, HUD)
|
|
124
124
|
| +-- contexts/ # 컨텍스트 파일 (ecomode)
|
package/templates/CLAUDE.md.en
CHANGED
|
@@ -130,7 +130,7 @@ project/
|
|
|
130
130
|
+-- CLAUDE.md # Entry point
|
|
131
131
|
+-- .claude/
|
|
132
132
|
| +-- agents/ # Subagent definitions (50 files)
|
|
133
|
-
| +-- skills/ # Skills (
|
|
133
|
+
| +-- skills/ # Skills (114 directories)
|
|
134
134
|
| +-- rules/ # Global rules (R000-R023)
|
|
135
135
|
| +-- hooks/ # Hook scripts (security, validation, HUD)
|
|
136
136
|
| +-- contexts/ # Context files (ecomode)
|
package/templates/CLAUDE.md.ko
CHANGED
|
@@ -130,7 +130,7 @@ project/
|
|
|
130
130
|
+-- CLAUDE.md # 진입점
|
|
131
131
|
+-- .claude/
|
|
132
132
|
| +-- agents/ # 서브에이전트 정의 (50 파일)
|
|
133
|
-
| +-- skills/ # 스킬 (
|
|
133
|
+
| +-- skills/ # 스킬 (114 디렉토리)
|
|
134
134
|
| +-- rules/ # 전역 규칙 (R000-R023)
|
|
135
135
|
| +-- hooks/ # 훅 스크립트 (보안, 검증, HUD)
|
|
136
136
|
| +-- contexts/ # 컨텍스트 파일 (ecomode)
|