oh-my-customcode 1.1.53 → 1.1.54
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 +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/templates/.claude/hooks/scripts/fail-axis-cause-advisor.sh +38 -3
- package/templates/.claude/hooks/scripts/r007-r008-drift-advisor.sh +20 -2
- package/templates/.claude/hooks/scripts/stuck-detector.sh +117 -6
- package/templates/.claude/rules/MAY-optimization.md +2 -0
- package/templates/.claude/rules/MUST-completion-verification.md +2 -0
- package/templates/manifest.json +1 -1
package/dist/cli/index.js
CHANGED
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -24,9 +24,19 @@
|
|
|
24
24
|
# failure-ledger.sh(PostToolUseFailure)가 기록한 원장을 발동 조건으로 읽는다.
|
|
25
25
|
# 원장이 없으면 조용히 통과한다 — 훅 단독으로도 안전하게 동작한다.
|
|
26
26
|
#
|
|
27
|
+
# 해소(resolve) 경로 (#1625 찐빠 #4):
|
|
28
|
+
# failure-ledger.sh는 append-only라 이 advisor에는 원래 해소 경로가 없었다. 그 결과 세션
|
|
29
|
+
# 내에서 이미 원인 확정·해소된 실패가 원장에 그대로 남아, 이후 무관한 짧은 진행 지시마다
|
|
30
|
+
# 반복 발화했다(실측). 완전한 해소 트래킹 대신, "신규 실패가 늘지 않았으면 침묵"하는
|
|
31
|
+
# 실용적 dedup을 둔다 — 세션당 마커 파일에 마지막 발화 시점의 fail_count를 기록하고,
|
|
32
|
+
# 다음 발동 시 fail_count가 그 값보다 늘지 않았으면 조용히 통과한다. 신규 실패가 쌓이면
|
|
33
|
+
# 다시 발화한다. r007-r008-drift-advisor.sh의 turn-uuid 마커 패턴과 동형 — 마커 값이
|
|
34
|
+
# "턴 uuid" 대신 "fail_count"일 뿐이다.
|
|
35
|
+
#
|
|
27
36
|
# 환경변수 override:
|
|
28
|
-
# OMCUSTOM_FAIL_ADVISOR=off
|
|
29
|
-
# OMCUSTOM_ERROR_LEDGER=<path>
|
|
37
|
+
# OMCUSTOM_FAIL_ADVISOR=off — advisory 완전 비활성화
|
|
38
|
+
# OMCUSTOM_ERROR_LEDGER=<path> — 원장 경로 override
|
|
39
|
+
# OMCUSTOM_FAIL_MARKER_DIR=<dir> — dedup 마커 디렉토리 override (기본 ${TMPDIR:-/tmp})
|
|
30
40
|
|
|
31
41
|
set -euo pipefail
|
|
32
42
|
|
|
@@ -55,8 +65,12 @@ if [ "${#prompt}" -gt 40 ]; then
|
|
|
55
65
|
fi
|
|
56
66
|
|
|
57
67
|
# ── 조건 2: 진행/재촉 패턴인가 ──
|
|
68
|
+
# "다음"은 v1.1.53까지 단독 매치였으나 "다음엔"/"다음 세션"/"다음 릴리즈"/"다음번" 같은
|
|
69
|
+
# 스코프 지시(미래 작업 언급)까지 오분류했다 — 예: "다음엔 Pre,PostToolUse 찐빠도 해결한다"는
|
|
70
|
+
# 원인 진술이 필요 없는 계획 서술이지 원인 없는 진행 재촉이 아니다. 즉시-진행 의미가 뚜렷한
|
|
71
|
+
# 구체적 구(다음 단계/다음으로/다음 진행)로 좁힌다 (#1625 찐빠 #4).
|
|
58
72
|
if ! printf '%s' "$prompt" \
|
|
59
|
-
| grep -qiE '(
|
|
73
|
+
| grep -qiE '(계속|이어서|진행해|재개|다음 단계|다음으로|다음 진행|ㄱㄱ|고고|가자|continue|keep going|go on|resume|proceed|next)'; then
|
|
60
74
|
exit 0
|
|
61
75
|
fi
|
|
62
76
|
|
|
@@ -83,6 +97,24 @@ if [ -z "$fail_count" ] || [ "$fail_count" -eq 0 ] 2>/dev/null; then
|
|
|
83
97
|
exit 0
|
|
84
98
|
fi
|
|
85
99
|
|
|
100
|
+
# ── 조건 5: 해소(resolve) dedup — 마지막 발화 이후 신규 실패가 없으면 침묵 ──
|
|
101
|
+
# 마커에는 fail_count(정수)를 저장한다. fail_count가 마커 값보다 늘지 않았다면 이미
|
|
102
|
+
# 발화했던 동일 실패 집합에 대한 재촉이므로 침묵한다. 신규 실패가 append되면 fail_count가
|
|
103
|
+
# 늘어나 다시 발화한다 (append-only 원장을 그대로 두고 발화 측에서만 dedup).
|
|
104
|
+
MARKER_DIR="${OMCUSTOM_FAIL_MARKER_DIR:-${TMPDIR:-/tmp}}"
|
|
105
|
+
marker_key=$(printf '%s' "$session" | tr -c 'A-Za-z0-9._-' '_')
|
|
106
|
+
MARKER_FILE="${MARKER_DIR}/.omcustom-fail-advisor-${marker_key}"
|
|
107
|
+
|
|
108
|
+
if [ -f "$MARKER_FILE" ]; then
|
|
109
|
+
prev_count=$(cat "$MARKER_FILE" 2>/dev/null || printf '0')
|
|
110
|
+
case "$prev_count" in
|
|
111
|
+
''|*[!0-9]*) prev_count=0 ;;
|
|
112
|
+
esac
|
|
113
|
+
if [ "$fail_count" -le "$prev_count" ] 2>/dev/null; then
|
|
114
|
+
exit 0
|
|
115
|
+
fi
|
|
116
|
+
fi
|
|
117
|
+
|
|
86
118
|
# 최근 실패 도구 요약 (최대 3종)
|
|
87
119
|
fail_tools=$(tail -n 300 "$LEDGER" 2>/dev/null \
|
|
88
120
|
| jq -r --arg s "$session" 'select(.session == $s and .interrupt != true) | .tool' 2>/dev/null \
|
|
@@ -99,4 +131,7 @@ printf '%s\n' "$advisory_text" >&2
|
|
|
99
131
|
jq -cn --arg ctx "$advisory_text" \
|
|
100
132
|
'{hookSpecificOutput: {hookEventName: "UserPromptSubmit", additionalContext: $ctx}}'
|
|
101
133
|
|
|
134
|
+
# 발화 후 마커 갱신 — 다음 발동 시 이 fail_count까지는 재발화하지 않는다.
|
|
135
|
+
printf '%s' "$fail_count" > "$MARKER_FILE" 2>/dev/null || true
|
|
136
|
+
|
|
102
137
|
exit 0
|
|
@@ -182,6 +182,17 @@ split("\n")
|
|
|
182
182
|
| if ($ai | length) == 0 then empty
|
|
183
183
|
else
|
|
184
184
|
($ai[-1]) as $last
|
|
185
|
+
# (#1625 찐빠 #3 조사 메모) `<task-notification>` / `Stop hook feedback:`를 role=user
|
|
186
|
+
# string content 경계 판정에서 제외하는 방안을 시도했으나, 실측(200ee7fe /fsd 자율 루프
|
|
187
|
+
# 세션, tail-200 윈도우 시뮬레이션)에서 **역효과가 확정**되어 반려한다. 이 마커들은
|
|
188
|
+
# UserPromptSubmit이 발생하지 않는 자율 루프 재진입에서 사실상 유일한 턴 경계 신호다
|
|
189
|
+
# (파일 상단 header 주석 참조) — 제외하면 $bi가 통째로 비어 $b=-1로 폴백해, 서로
|
|
190
|
+
# 독립적이고 각각 컴플라이언트한 후속 응답들이 하나의 거대 turn으로 계속 병합되고
|
|
191
|
+
# r008 드리프트가 6→10→9로 오히려 커졌다(제외 적용 전에는 각 응답이 새 turn으로
|
|
192
|
+
# 올바르게 분리되어 0/0이었음). 이 조사에서 확정된 실제 원인은 별도 메커니즘이다:
|
|
193
|
+
# tail -n 200 윈도우 안에 경계가 전혀 없을 때 $b=-1로 폴백해 슬라이딩 윈도우 전체를
|
|
194
|
+
# 하나의 turn으로 병합하는 구조 — 본 수정의 승인 범위(0-tool-use 가드 1건) 밖이므로
|
|
195
|
+
# 별도 이슈로 보고한다(오케스트레이터 완료 보고 참조).
|
|
185
196
|
| [ range(0; $last + 1)
|
|
186
197
|
| select( ($L[.].message.role? == "user")
|
|
187
198
|
and ( (($L[.].message.content | type) == "string")
|
|
@@ -201,7 +212,14 @@ split("\n")
|
|
|
201
212
|
| ([ $lines[] | select(test("\\[.+\\]\\[.+\\] ?(→|->|—>) ?Spawning:")) ] | length) as $an_spawn_hdr
|
|
202
213
|
| ($an_tool + (if $an_spawn_item > 0 then $an_spawn_item else $an_spawn_hdr end)) as $announce
|
|
203
214
|
| ([ $blocks[] | select((.type? == "tool_use") and ((.name? // "") != "Skill")) ] | length) as $ntools
|
|
204
|
-
|
|
215
|
+
# 전체 tool_use(Skill 포함)가 0건인 턴은 forward 판정을 무조건 0으로 고정한다(#1625
|
|
216
|
+
# 찐빠 #3, 이슈 제안 그대로). $ntools(Skill 제외) 기반 산식은 정상 입력에서는 이미
|
|
217
|
+
# 0을 내지만, 명시 가드로 불변식을 코드에 못박아 향후 산식 변경이 이 보장을 조용히
|
|
218
|
+
# 깨뜨리지 않게 한다. $nall_tools는 아래 REVERSE 신호와 공유하는 단일 바인딩이다.
|
|
219
|
+
| ($blocks | map(select(.type? == "tool_use")) | length) as $nall_tools
|
|
220
|
+
| (if $nall_tools == 0 then 0
|
|
221
|
+
elif $ntools > $announce then $ntools - $announce
|
|
222
|
+
else 0 end) as $r008
|
|
205
223
|
# REVERSE signal (#1595 제안 #6): announced a tool call, then ended the turn without
|
|
206
224
|
# emitting ANY tool_use block (R009 Self-Check #6). Uses its OWN ANCHORED counter — do NOT
|
|
207
225
|
# reuse $announce. MEASURED over 482 orchestrator turns:
|
|
@@ -216,7 +234,7 @@ split("\n")
|
|
|
216
234
|
# the $ntools denominator above; it is a separate variable, not a weakening of the Skill
|
|
217
235
|
# exemption (#1569), which still governs the forward verdict.
|
|
218
236
|
| ([ $lines[] | select(test("^\\[[^\\]]+\\]\\[[^\\]]+\\] ?(→|->|—>) ?Tool:")) ] | length) as $an_anchored
|
|
219
|
-
|
|
237
|
+
# $nall_tools는 위 forward r008 가드에서 이미 바인딩됨 — 재바인딩하지 않고 재사용한다.
|
|
220
238
|
| (if $nall_tools == 0 and $an_anchored > 0 then $an_anchored else 0 end) as $r008rev
|
|
221
239
|
| [$tuuid, ($r007 | tostring), ($r008 | tostring), ($r008rev | tostring)] | @tsv
|
|
222
240
|
end
|
|
@@ -15,13 +15,121 @@ command -v jq >/dev/null 2>&1 || exit 0
|
|
|
15
15
|
# Hard block threshold: consecutive identical operations before blocking
|
|
16
16
|
HARD_BLOCK_THRESHOLD=${CLAUDE_STUCK_THRESHOLD:-3}
|
|
17
17
|
|
|
18
|
+
# Determine if a Bash command is read-only (metadata/query only, no side effects).
|
|
19
|
+
# Conservative: any ambiguity (chaining, redirection, substitution, or an
|
|
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() {
|
|
27
|
+
local cmd="$1"
|
|
28
|
+
cmd="$(printf '%s' "$cmd" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
|
|
29
|
+
if [ -z "$cmd" ]; then
|
|
30
|
+
echo "false"
|
|
31
|
+
return 0
|
|
32
|
+
fi
|
|
33
|
+
|
|
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
|
+
local parts=()
|
|
43
|
+
read -ra parts <<< "$cmd"
|
|
44
|
+
local w1="${parts[0]:-}"
|
|
45
|
+
local w2="${parts[1]:-}"
|
|
46
|
+
local w3="${parts[2]:-}"
|
|
47
|
+
|
|
48
|
+
case "$w1" in
|
|
49
|
+
ls|cat|head|tail|grep|rg|wc|jq|md5|md5sum|type|command|which|echo|printf|pwd|date)
|
|
50
|
+
echo "true"
|
|
51
|
+
return 0
|
|
52
|
+
;;
|
|
53
|
+
find)
|
|
54
|
+
case "$cmd" in
|
|
55
|
+
*-delete*|*-exec*|*-fprintf*)
|
|
56
|
+
echo "false"
|
|
57
|
+
;;
|
|
58
|
+
*)
|
|
59
|
+
echo "true"
|
|
60
|
+
;;
|
|
61
|
+
esac
|
|
62
|
+
return 0
|
|
63
|
+
;;
|
|
64
|
+
git)
|
|
65
|
+
case "$w2" in
|
|
66
|
+
status|log|diff|show|rev-parse|ls-files)
|
|
67
|
+
echo "true"
|
|
68
|
+
;;
|
|
69
|
+
tag|branch)
|
|
70
|
+
# Only a bare query (no positional args, flags only) counts as read-only.
|
|
71
|
+
# e.g. "git branch -a" / "git tag --sort=..." => read; "git branch foo"
|
|
72
|
+
# or "git branch -D foo" => write (has a non-flag token).
|
|
73
|
+
local ok="true"
|
|
74
|
+
local i
|
|
75
|
+
for ((i = 2; i < ${#parts[@]}; i++)); do
|
|
76
|
+
case "${parts[$i]}" in
|
|
77
|
+
-*) ;;
|
|
78
|
+
*) ok="false" ;;
|
|
79
|
+
esac
|
|
80
|
+
done
|
|
81
|
+
echo "$ok"
|
|
82
|
+
;;
|
|
83
|
+
*)
|
|
84
|
+
# fetch and all other subcommands (checkout/merge/rebase/push/...) => write
|
|
85
|
+
echo "false"
|
|
86
|
+
;;
|
|
87
|
+
esac
|
|
88
|
+
return 0
|
|
89
|
+
;;
|
|
90
|
+
gh)
|
|
91
|
+
if [ "$w2" = "api" ]; then
|
|
92
|
+
# GET-style only; any field/method-mutation flag => write
|
|
93
|
+
case "$cmd" in
|
|
94
|
+
*' -f '*|*' -F '*|*'--raw-field'*|*'--input'*|*' -X '*|*'--method'*)
|
|
95
|
+
echo "false"
|
|
96
|
+
;;
|
|
97
|
+
*)
|
|
98
|
+
echo "true"
|
|
99
|
+
;;
|
|
100
|
+
esac
|
|
101
|
+
else
|
|
102
|
+
case "$w3" in
|
|
103
|
+
view|list)
|
|
104
|
+
echo "true"
|
|
105
|
+
;;
|
|
106
|
+
*)
|
|
107
|
+
echo "false"
|
|
108
|
+
;;
|
|
109
|
+
esac
|
|
110
|
+
fi
|
|
111
|
+
return 0
|
|
112
|
+
;;
|
|
113
|
+
*)
|
|
114
|
+
echo "false"
|
|
115
|
+
return 0
|
|
116
|
+
;;
|
|
117
|
+
esac
|
|
118
|
+
}
|
|
119
|
+
|
|
18
120
|
input=$(cat)
|
|
19
121
|
|
|
20
122
|
# Extract tool info
|
|
21
|
-
tool_name=$(
|
|
22
|
-
file_path=$(
|
|
23
|
-
is_error=$(
|
|
24
|
-
output_preview=$(
|
|
123
|
+
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)
|
|
125
|
+
is_error=$(printf '%s' "$input" | jq -r '.tool_output.is_error // false')
|
|
126
|
+
output_preview=$(printf '%s' "$input" | jq -r '.tool_output.output // ""' | head -c 200)
|
|
127
|
+
raw_command=$(printf '%s' "$input" | jq -r '.tool_input.command // ""')
|
|
128
|
+
|
|
129
|
+
is_readonly="false"
|
|
130
|
+
if [ "$tool_name" = "Bash" ]; then
|
|
131
|
+
is_readonly=$(is_readonly_bash_command "$raw_command")
|
|
132
|
+
fi
|
|
25
133
|
|
|
26
134
|
# Session-scoped history
|
|
27
135
|
HISTORY_FILE="/tmp/.claude-tool-history-${PPID}"
|
|
@@ -142,7 +250,9 @@ if [ -f "$HISTORY_FILE" ]; then
|
|
|
142
250
|
|
|
143
251
|
if [ "$last_n_count" -ge "$HARD_BLOCK_THRESHOLD" ]; then
|
|
144
252
|
# Check 1: Same file edited HARD_BLOCK_THRESHOLD+ times consecutively
|
|
145
|
-
|
|
253
|
+
# (skip when current call is a read-only Bash command — repeated read-only
|
|
254
|
+
# polling of the same target is not a stuck-loop signal; see #1625)
|
|
255
|
+
if [ "$is_readonly" != "true" ] && [ -n "$file_path" ]; then
|
|
146
256
|
escaped_path=$(echo "$file_path" | sed 's/[.[\*^$()+?{|]/\\&/g')
|
|
147
257
|
consecutive_file=$(echo "$last_n" | grep -c "\"path\":\"${escaped_path}\"" 2>/dev/null || echo "0")
|
|
148
258
|
if [ "$consecutive_file" -ge "$HARD_BLOCK_THRESHOLD" ]; then
|
|
@@ -161,7 +271,8 @@ if [ -f "$HISTORY_FILE" ]; then
|
|
|
161
271
|
fi
|
|
162
272
|
|
|
163
273
|
# Check 3: Same tool+target combination HARD_BLOCK_THRESHOLD+ times consecutively
|
|
164
|
-
|
|
274
|
+
# (skip when current call is a read-only Bash command — see Check 1 note)
|
|
275
|
+
if [ "$hard_block" = false ] && [ "$is_readonly" != "true" ] && [ -n "$file_path" ]; then
|
|
165
276
|
escaped_path=$(echo "$file_path" | sed 's/[.[\*^$()+?{|]/\\&/g')
|
|
166
277
|
consecutive_tool_target=$(echo "$last_n" | grep "\"tool\":\"${tool_name}\"" | grep -c "\"path\":\"${escaped_path}\"" 2>/dev/null || echo "0")
|
|
167
278
|
if [ "$consecutive_tool_target" -ge "$HARD_BLOCK_THRESHOLD" ]; then
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
|
|
19
19
|
> **Sandbox/container tool gaps (#1401 찐빠 #4)**: `curl`, `wget`, `nc` 등 공통 CLI 도구는 샌드박스·컨테이너 환경에서 미설치일 수 있다. HTTP 요청에는 `WebFetch` 도구를 우선 사용하고, CLI 도구 사용 전 `command -v <tool>` 으로 가용성을 사전 확인한다.
|
|
20
20
|
|
|
21
|
+
> **zsh 내장 `echo`는 이스케이프를 확장한다 (#1625)**: zsh(이 저장소 Bash 도구 실행 셸)의 내장 `echo`는 `\n` 등 백슬래시 이스케이프를 기본 확장하므로, JSON 문자열을 파이프에 실을 때 `echo "$var"`를 쓰면 valid JSON을 스스로 깨뜨려 하류 파서 오진을 유발한다(v1.1.53 세션 훅 오진의 실제 원인). JSON/구조화 문자열 전달은 `printf '%s' "$var"`를 표준으로 한다.
|
|
22
|
+
|
|
21
23
|
> **로컬 실행 옵션 제시 전 자원 가용성 선확인 (#1455 #2)**: 로컬 실행에 의존하는 검증 옵션(로컬 스모크 테스트, 로컬 스크립트 실행 등)을 사용자에게 제시하기 **전에**, 그 실행에 필요한 로컬 자원(env 키, CLI 도구, 인증 상태)의 가용성을 먼저 확인한다. **저장소 secret 존재 ≠ 로컬 셸 env 존재** — `gh secret list`로 저장소 secret을 확인해도 로컬 셸에 해당 env가 있으리라 단정하지 말 것. 자원 부재 시 옵션에 전제조건을 명시하거나 옵션에서 제외하여, 사용자가 실행 불가한 옵션을 선택했다가 되돌리는 왕복(AskUserQuestion 재질문)을 방지한다. Cross-ref: R020(사전 검증). Origin: #1455 #2 (Session 127 회고 찐빠 #2) — 사용자가 "로컬 스모크 테스트 먼저"를 선택했으나 로컬 셸에 ANTHROPIC_API_KEY 부재로 실행 불가 → "스킵, 바로 커밋" 재선택, AskUserQuestion 왕복 1회 발생.
|
|
22
24
|
|
|
23
25
|
> **Shell output parsing — use Python, not read/grep (#1401 찐빠 #3)**: adb bounds rect, 좌표쌍, JSON 분할 등 구조화된 출력 파싱은 `read`+`grep -o` 파이프라인 대신 Python (`python3 -c "..."`) 을 사용한다. `read`+`grep -o` 조합은 공백 차이에 취약해 헛값을 산출한다. SSH 원격 `bash -c` 인자에 소괄호 포함 금지 — `ssh host "cmd; cmd2"` 형식 사용.
|
|
@@ -137,11 +137,13 @@ Cross-reference: R018 (Member Completion Verification), `feedback_release_delega
|
|
|
137
137
|
5. **완화책**: 미러 동기화처럼 **후행 필수 작업은 마지막에 몰지 말고 파일 단위로 즉시 수행**한다 — 절단은 항상 마지막 작업을 자르므로, 마지막에 몰린 작업은 절단 시 전량 유실된다.
|
|
138
138
|
6. **정량 기준 정밀화 — 산술 단위는 파일이 아니라 편집 항목 (Origin: #1621 #2a, v1.1.51 세션)**: 위 4항의 "파일 4~5개 상한"은 신설 당일 재절단을 막지 못했다 — R017 그룹은 담당 6파일로 상한 인접이었으나, 실제 초과 원인은 파일 수가 아니라 **조항 2개 신설**이었다. 파일당 편집 항목이 1개라는 암묵 가정이 깨지면 파일 수 기준 산술이 무의미해진다. 위임 크기 산술의 단위는 **편집 항목 수**(신설 조항 1개, 버전 노트 1개, 사본 배선 1개 등)로 바꾼다: **항목당 약 2턴(Edit + 위치 탐색) + 파일당 고정비(Read 1 + 미러 Edit 1 + diff 1 = 3턴)**로 계산한다. 같은 세션에서 4파일·항목 소수 위임들은 완주해 대조를 이룬다.
|
|
139
139
|
7. **리서치형 위임의 산출물 우선 기록 (Origin: #1621 #2b, v1.1.51 세션)**: 파일 편집형뿐 아니라 리서치형(수집 중심) 위임도 절단에 취약하다 — v1.1.51 세션에서 훅 이벤트 감사 위임이 22회 도구 호출(WebFetch/Read)을 전부 수집에 쓰고 **아티팩트를 1회도 Write하지 않은 채 절단**되어, 수집한 산출물 전량이 에이전트 컨텍스트에만 존재하고 파일에는 남지 않았다(재개 지시 "산출물 우선 기록"으로 복구). 리서치형 위임의 표준 문안에는 **"첫 2턴 안에 아티팩트 골격을 Write하고 수집 즉시 증분 Edit하라 — 수집 완료 후 일괄 기록 금지"**를 포함한다. 절단은 항상 마지막 작업을 자르므로, 기록을 마지막에 몰면 절단 시 산출물이 전량 유실된다 — 위 5항(파일 편집형의 "미러 즉시 동기화")의 리서치형 대응이다.
|
|
140
|
+
8. **훅 피드백 잠식 (Origin: #1625 #5)**: settings 훅은 서브에이전트 세션에도 발화하므로, 세션 종료성 훅(Stop 계열)의 반복 피드백이 서브에이전트의 마지막 턴들을 소모·오염시켜 최종 보고가 "대기 중" 류로 끝날 수 있다 — mid-step 종료·"실제가 보고보다 앞섬"의 신규 원인 축. v1.1.53 세션에서 커밋 에이전트 2건의 "대기 중" 보고가 실측 결과 모두 완전 완료였다. 절단·대기 보고를 받으면 훅 피드백 잠식 가능성도 원인 후보에 포함하고 ground-truth로 판정한다.
|
|
140
141
|
|
|
141
142
|
| Anti-pattern | Required |
|
|
142
143
|
|--------------|----------|
|
|
143
144
|
| 파일 수만 세어 위임 크기 판정 → 조항 다수 파일에서 절단 | 편집 항목 수 × 2턴 + 파일 고정비(3턴)로 산술 |
|
|
144
145
|
| 리서치 위임이 수집을 끝낸 뒤 일괄 기록 | 첫 2턴 내 골격 Write + 증분 Edit |
|
|
146
|
+
| "대기 중" 보고를 미완료로 단정 | 훅 피드백 잠식 가능성 포함해 ground-truth로 완료 여부 판정 |
|
|
145
147
|
|
|
146
148
|
Cross-reference: R018 (v2.1.246 maxTurns partial-marking 노트), R009 (Member Prompt Size Cap — 프롬프트 토큰 상한과 별개로 턴 수 상한도 위임 크기 설계 변수임을 추가).
|
|
147
149
|
|
package/templates/manifest.json
CHANGED