oh-my-customcode 1.1.61 → 1.1.62
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/agent-start-recorder.sh +4 -0
- package/templates/.claude/hooks/scripts/agent-teams-advisor.sh +6 -2
- package/templates/.claude/hooks/scripts/audit-log.sh +19 -2
- package/templates/.claude/hooks/scripts/content-hash-validator.sh +6 -2
- package/templates/.claude/hooks/scripts/model-escalation-advisor.sh +6 -2
- package/templates/.claude/hooks/scripts/playwright-compress.sh +25 -2
- package/templates/.claude/hooks/scripts/r007-r008-drift-advisor.sh +72 -2
- package/templates/.claude/hooks/scripts/schema-validator.sh +14 -9
- package/templates/.claude/hooks/scripts/secret-filter.sh +46 -4
- package/templates/.claude/hooks/scripts/session-reflection.sh +5 -1
- package/templates/.claude/hooks/scripts/stuck-detector.sh +43 -11
- package/templates/.claude/hooks/scripts/task-outcome-recorder.sh +16 -9
- package/templates/.claude/rules/MUST-enforcement-policy.md +1 -1
- package/templates/.claude/rules/MUST-tool-identification.md +1 -1
- package/templates/manifest.json +1 -1
package/dist/cli/index.js
CHANGED
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -10,6 +10,10 @@ command -v jq >/dev/null 2>&1 || { cat; exit 0; }
|
|
|
10
10
|
|
|
11
11
|
input=$(cat)
|
|
12
12
|
|
|
13
|
+
# Guard: non-object stdin (non-JSON / JSON array / empty) — swallow and exit 0.
|
|
14
|
+
# Hooks must never crash (R021); jq parse errors would otherwise abort under `set -e`. (#1650)
|
|
15
|
+
printf '%s' "$input" | jq -e 'type=="object"' >/dev/null 2>&1 || exit 0
|
|
16
|
+
|
|
13
17
|
agent_type=$(printf '%s\n' "$input" | jq -r '.agent_type // "unknown"')
|
|
14
18
|
model=$(printf '%s\n' "$input" | jq -r '.model // "inherit"')
|
|
15
19
|
description=$(printf '%s\n' "$input" | jq -r '.description // ""' | head -c 80)
|
|
@@ -11,6 +11,10 @@ command -v jq >/dev/null 2>&1 || exit 0
|
|
|
11
11
|
|
|
12
12
|
input=$(cat)
|
|
13
13
|
|
|
14
|
+
# Guard: non-object stdin (non-JSON / JSON array / empty) — swallow and exit 0.
|
|
15
|
+
# Hooks must never crash (R021); jq parse errors would otherwise abort under `set -e`. (#1650)
|
|
16
|
+
printf '%s' "$input" | jq -e 'type=="object"' >/dev/null 2>&1 || exit 0
|
|
17
|
+
|
|
14
18
|
# Skip unless Agent Teams is CONFIRMED active.
|
|
15
19
|
# session-env-check.sh writes one of: disabled | env-set | enabled (#1588).
|
|
16
20
|
# `env-set` = the env var is set but TeamCreate presence is UNVERIFIED — R018 is dormant,
|
|
@@ -43,8 +47,8 @@ if [ -f "$RELEASE_PLAN" ]; then
|
|
|
43
47
|
fi
|
|
44
48
|
|
|
45
49
|
# Extract task info from input
|
|
46
|
-
agent_type=$(printf '%s\n' "$input" | jq -r '.tool_input.subagent_type // "unknown"')
|
|
47
|
-
prompt_preview=$(printf '%s\n' "$input" | jq -r '.tool_input.description // ""' | head -c 60)
|
|
50
|
+
agent_type=$(printf '%s\n' "$input" | jq -r '.tool_input.subagent_type? // "unknown"')
|
|
51
|
+
prompt_preview=$(printf '%s\n' "$input" | jq -r '.tool_input.description? // ""' | head -c 60)
|
|
48
52
|
|
|
49
53
|
# Session-scoped counter using parent PID as session identifier
|
|
50
54
|
COUNTER_FILE="/tmp/.claude-task-count-${PPID}"
|
|
@@ -13,12 +13,29 @@ command -v jq >/dev/null 2>&1 || exit 0
|
|
|
13
13
|
|
|
14
14
|
input=$(cat)
|
|
15
15
|
|
|
16
|
+
# Guard: non-object stdin (non-JSON / JSON array / empty) — swallow and exit 0.
|
|
17
|
+
# Hooks must never crash (R021); jq parse errors would otherwise abort under `set -e`. (#1650)
|
|
18
|
+
printf '%s' "$input" | jq -e 'type=="object"' >/dev/null 2>&1 || exit 0
|
|
19
|
+
|
|
16
20
|
# Extract fields from hook input
|
|
17
21
|
tool_name=$(printf '%s\n' "$input" | jq -r '.tool_name // "unknown"')
|
|
18
|
-
file_path=$(printf '%s\n' "$input" | jq -r '.tool_input.file_path // .tool_input.command // ""' | head -c 200)
|
|
22
|
+
file_path=$(printf '%s\n' "$input" | jq -r '.tool_input.file_path? // .tool_input.command? // ""' | head -c 200)
|
|
19
23
|
agent_type=$(printf '%s\n' "$input" | jq -r '.agent_type // "unknown"')
|
|
20
24
|
model=$(printf '%s\n' "$input" | jq -r '.model // "unknown"')
|
|
21
|
-
|
|
25
|
+
# Outcome signal. PostToolUse carries the result under `tool_response`, not
|
|
26
|
+
# `tool_output` (measured 2026-09-03: 1764/1764 PostToolUse payloads had
|
|
27
|
+
# `tool_response`, 0/1764 had `tool_output`), so the old `.tool_output.is_error`
|
|
28
|
+
# read was always absent and every entry was logged as outcome=success.
|
|
29
|
+
# Measured failure signals: `.tool_response.is_error` (generic) and
|
|
30
|
+
# `.tool_response.interrupted` (present on every Bash response).
|
|
31
|
+
# jq's `//` treats both null and false as absent, so this chain is an OR:
|
|
32
|
+
# the result is true iff at least one signal is true.
|
|
33
|
+
is_error=$(printf '%s\n' "$input" | jq -r '
|
|
34
|
+
(.tool_response?.is_error?
|
|
35
|
+
// .tool_response?.interrupted?
|
|
36
|
+
// .tool_output?.is_error?
|
|
37
|
+
// false) | tostring
|
|
38
|
+
' 2>/dev/null) || is_error="false"
|
|
22
39
|
timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
23
40
|
|
|
24
41
|
# Determine outcome
|
|
@@ -12,6 +12,10 @@ command -v jq >/dev/null 2>&1 || exit 0
|
|
|
12
12
|
|
|
13
13
|
input=$(cat)
|
|
14
14
|
|
|
15
|
+
# Guard: non-object stdin (non-JSON / JSON array / empty) — swallow and exit 0.
|
|
16
|
+
# Hooks must never crash (R021); jq parse errors would otherwise abort under `set -e`. (#1650)
|
|
17
|
+
printf '%s' "$input" | jq -e 'type=="object"' >/dev/null 2>&1 || exit 0
|
|
18
|
+
|
|
15
19
|
# Hash store (PPID-scoped, session-only)
|
|
16
20
|
HASH_STORE="/tmp/.claude-content-hashes-${PPID}"
|
|
17
21
|
|
|
@@ -20,7 +24,7 @@ tool_name=$(printf '%s\n' "$input" | jq -r '.tool_name // "unknown"')
|
|
|
20
24
|
case "$tool_name" in
|
|
21
25
|
"Read")
|
|
22
26
|
# Store content hash for the file that was just read
|
|
23
|
-
file_path=$(printf '%s\n' "$input" | jq -r '.tool_input.file_path // ""')
|
|
27
|
+
file_path=$(printf '%s\n' "$input" | jq -r '.tool_input.file_path? // ""')
|
|
24
28
|
|
|
25
29
|
if [ -n "$file_path" ] && [ -f "$file_path" ]; then
|
|
26
30
|
content_hash=$(md5 -q "$file_path" 2>/dev/null || md5sum "$file_path" 2>/dev/null | cut -d' ' -f1 || echo "unknown")
|
|
@@ -43,7 +47,7 @@ case "$tool_name" in
|
|
|
43
47
|
|
|
44
48
|
"Edit")
|
|
45
49
|
# Validate that file hasn't changed since last Read
|
|
46
|
-
file_path=$(printf '%s\n' "$input" | jq -r '.tool_input.file_path // ""')
|
|
50
|
+
file_path=$(printf '%s\n' "$input" | jq -r '.tool_input.file_path? // ""')
|
|
47
51
|
|
|
48
52
|
if [ -n "$file_path" ] && [ -f "$HASH_STORE" ] && [ -f "$file_path" ]; then
|
|
49
53
|
stored_hash=$(grep "\"path\":\"${file_path}\"" "$HASH_STORE" 2>/dev/null | tail -1 | jq -r '.hash // ""' 2>/dev/null || echo "")
|
|
@@ -11,9 +11,13 @@ command -v jq >/dev/null 2>&1 || exit 0
|
|
|
11
11
|
|
|
12
12
|
input=$(cat)
|
|
13
13
|
|
|
14
|
+
# Guard: non-object stdin (non-JSON / JSON array / empty) — swallow and exit 0.
|
|
15
|
+
# Hooks must never crash (R021); jq parse errors would otherwise abort under `set -e`. (#1650)
|
|
16
|
+
printf '%s' "$input" | jq -e 'type=="object"' >/dev/null 2>&1 || exit 0
|
|
17
|
+
|
|
14
18
|
# Extract current task info
|
|
15
|
-
agent_type=$(printf '%s\n' "$input" | jq -r '.tool_input.subagent_type // "unknown"')
|
|
16
|
-
current_model=$(printf '%s\n' "$input" | jq -r '.tool_input.model // "inherit"')
|
|
19
|
+
agent_type=$(printf '%s\n' "$input" | jq -r '.tool_input.subagent_type? // "unknown"')
|
|
20
|
+
current_model=$(printf '%s\n' "$input" | jq -r '.tool_input.model? // "inherit"')
|
|
17
21
|
|
|
18
22
|
# Session-scoped outcome log
|
|
19
23
|
OUTCOME_FILE="/tmp/.claude-task-outcomes-${PPID}"
|
|
@@ -7,7 +7,30 @@
|
|
|
7
7
|
set -euo pipefail
|
|
8
8
|
|
|
9
9
|
input=$(cat)
|
|
10
|
-
|
|
10
|
+
|
|
11
|
+
# Guard: non-object stdin (non-JSON / JSON array / empty) — swallow and exit 0.
|
|
12
|
+
# Hooks must never crash (R021); jq parse errors would otherwise abort under `set -e`. (#1650)
|
|
13
|
+
printf '%s' "$input" | jq -e 'type=="object"' >/dev/null 2>&1 || exit 0
|
|
14
|
+
# PostToolUse carries the tool result under `tool_response`, not `tool_output`
|
|
15
|
+
# (measured 2026-09-03: 1764/1764 PostToolUse payloads had `tool_response`,
|
|
16
|
+
# 0/1764 had `tool_output`), so the previous `.tool_output` read always yielded
|
|
17
|
+
# "" and this hook never compressed anything. MCP responses arrive either as a
|
|
18
|
+
# bare string or as `{content: [{type:"text", text:...}]}`; `.tool_output` is
|
|
19
|
+
# kept as a fallback for events still using the older shape.
|
|
20
|
+
tool_output=$(printf '%s\n' "$input" | jq -r '
|
|
21
|
+
[
|
|
22
|
+
(.tool_response? | if type == "string" then . else empty end),
|
|
23
|
+
(.tool_response?.content? | strings),
|
|
24
|
+
(.tool_response?.content? | if type == "array"
|
|
25
|
+
then map(.text? | strings) | join("\n")
|
|
26
|
+
else empty end),
|
|
27
|
+
(.tool_response?.stdout? | strings),
|
|
28
|
+
(.tool_response?.file?.content? | strings),
|
|
29
|
+
(.tool_output? | if type == "string" then . else empty end),
|
|
30
|
+
(.tool_output?.output? | strings)
|
|
31
|
+
]
|
|
32
|
+
| map(select(. != "")) | join("\n")
|
|
33
|
+
' 2>/dev/null) || tool_output=""
|
|
11
34
|
|
|
12
35
|
# Skip if output is small (< 3000 chars)
|
|
13
36
|
output_len=${#tool_output}
|
|
@@ -47,4 +70,4 @@ fi
|
|
|
47
70
|
compressed_len=${#summary}
|
|
48
71
|
savings=$(( (output_len - compressed_len) * 100 / output_len ))
|
|
49
72
|
printf '%s\n' "$input" | jq --arg summary "$summary" --arg savings "${savings}% reduced (${output_len}→${compressed_len} chars)" \
|
|
50
|
-
'.tool_output = $summary | .["updatedMCPToolOutput"] = $summary'
|
|
73
|
+
'.tool_response = $summary | .tool_output = $summary | .["updatedMCPToolOutput"] = $summary'
|
|
@@ -22,6 +22,11 @@
|
|
|
22
22
|
#
|
|
23
23
|
# Advisory-only: ALWAYS exits 0, NEVER blocks.
|
|
24
24
|
#
|
|
25
|
+
# Scope gate (#1650 D): the advisor judges the ORCHESTRATOR's turns only. When the hook fires
|
|
26
|
+
# from INSIDE a subagent session (the payload carries `agent_id`) it exits immediately — see
|
|
27
|
+
# the measured rationale at the gate itself. SubagentStart/SubagentStop are exempt because
|
|
28
|
+
# `agent_id` is a required TARGET identifier there, not an in-subagent marker.
|
|
29
|
+
#
|
|
25
30
|
# ── Transcript schema (MEASURED 2026-08-05, #1553) ────────────────────────────────────
|
|
26
31
|
# Claude Code JSONL lines do NOT carry a TOP-LEVEL `role`/`content`. The measured top-level
|
|
27
32
|
# key set is:
|
|
@@ -134,15 +139,58 @@ if ! command -v jq >/dev/null 2>&1; then
|
|
|
134
139
|
fi
|
|
135
140
|
|
|
136
141
|
# ── 입력 필드 추출 (jq 1회 fork) ──
|
|
137
|
-
|
|
142
|
+
# stdin 가드: 비-JSON이면 jq가 실패해 `|| exit 0`으로 무음 종료하고, 유효 JSON이지만 객체가
|
|
143
|
+
# 아니면(배열/문자열/숫자) `select(type == "object")`가 걸러 빈 출력 → session_id 공백 → exit 0.
|
|
144
|
+
# 두 경로 모두 추가 fork 없이 기존 1회 fork 안에서 처리한다(PostToolUse는 매 도구 호출마다 발화).
|
|
145
|
+
meta=$(printf '%s' "$input" | jq -r 'select(type == "object") | [(.session_id // ""), (.hook_event_name // ""), (.transcript_path // ""), (.agent_id // "")] | @tsv' 2>/dev/null) || exit 0
|
|
138
146
|
session_id=$(printf '%s' "$meta" | cut -f1)
|
|
139
147
|
hook_event_name=$(printf '%s' "$meta" | cut -f2)
|
|
140
148
|
transcript_in=$(printf '%s' "$meta" | cut -f3)
|
|
149
|
+
agent_id=$(printf '%s' "$meta" | cut -f4)
|
|
141
150
|
|
|
142
151
|
if [ -z "$session_id" ]; then
|
|
143
152
|
exit 0
|
|
144
153
|
fi
|
|
145
154
|
|
|
155
|
+
# ── 서브에이전트 세션이면 즉시 종료 (#1650 D) ────────────────────────────────────────────
|
|
156
|
+
# settings 훅은 서브에이전트 세션에서도 발화한다(서브에이전트의 매 도구 호출마다 PostToolUse).
|
|
157
|
+
# 그 발화가 남기는 advisory 피드백이 서브에이전트의 마지막 턴들을 잠식해 완료 보고를 대체하는
|
|
158
|
+
# 현상이 관측됐다(R020 8항 — 훅 피드백 잠식).
|
|
159
|
+
#
|
|
160
|
+
# 판별 신호 실측 (CC 2.1.259 바이너리의 훅 입력 zod 스키마 원문):
|
|
161
|
+
# agent_id: optional — "Subagent identifier. Present only when the hook fires from within a
|
|
162
|
+
# subagent (e.g., a tool called by an AgentTool worker). Absent for the main thread, even in
|
|
163
|
+
# --agent sessions. Use this field (not agent_type) to distinguish subagent calls from
|
|
164
|
+
# main-thread calls."
|
|
165
|
+
# 그리고 공통 페이로드 빌더가 모든 훅 이벤트에 `agent_id: o?.agentId`를 실어 보낸다.
|
|
166
|
+
# 즉 플랫폼이 명시적으로 지정한 판별 필드다. `agent_type`은 쓰지 않는다 — 같은 스키마가
|
|
167
|
+
# "--agent로 시작한 세션의 메인 스레드에도(agent_id 없이) 존재한다"고 못박고 있다.
|
|
168
|
+
#
|
|
169
|
+
# 다른 두 후보가 왜 작동하지 않는지(실측):
|
|
170
|
+
# * 트랜스크립트의 isSidechain / agentId 레코드 필드 — 서브에이전트 전용 파일
|
|
171
|
+
# agent-<hex>.jsonl의 전 레코드에 존재하지만(실측 65/65, 3/3), 공통 빌더는
|
|
172
|
+
# `transcript_path: Lp(e.id)`로 **세션 id**에서 경로를 만들고 서브에이전트는 부모의
|
|
173
|
+
# session id를 공유한다(실측: agent-*.jsonl의 sessionId == 부모 UUID). 따라서 서브에이전트
|
|
174
|
+
# 안에서 발화한 훅이 읽는 파일은 **부모의 메인 트랜스크립트**이고, 그 파일에는 isSidechain
|
|
175
|
+
# true도 agentId도 없다. 이것이 바로 잠식 경로다.
|
|
176
|
+
# * transcript_path의 `agent-*.jsonl` 경로 패턴 — 같은 이유로 그 경로가 오지 않는다
|
|
177
|
+
# (`agent_transcript_path`는 SubagentStop 페이로드에만 추가되는 별도 필드다).
|
|
178
|
+
#
|
|
179
|
+
# SubagentStart/SubagentStop은 예외다: 두 이벤트의 스키마는 agent_id를 **필수**로 두고 그 값은
|
|
180
|
+
# "대상 서브에이전트"의 식별자다(`Ae().and({hook_event_name:"SubagentStop", agent_id, ...})`).
|
|
181
|
+
# 즉 이 두 이벤트에서 agent_id는 "서브에이전트 안에서 발화했다"는 표지가 아니므로, 여기서
|
|
182
|
+
# 게이트를 걸면 #1545 자율 루프 커버리지가 통째로 사라진다.
|
|
183
|
+
case "$hook_event_name" in
|
|
184
|
+
SubagentStart|SubagentStop)
|
|
185
|
+
: # agent_id는 대상 식별자 — 판별 신호로 쓰지 않는다
|
|
186
|
+
;;
|
|
187
|
+
*)
|
|
188
|
+
if [ -n "$agent_id" ]; then
|
|
189
|
+
exit 0
|
|
190
|
+
fi
|
|
191
|
+
;;
|
|
192
|
+
esac
|
|
193
|
+
|
|
146
194
|
# hook_event_name이 없으면 hookSpecificOutput.hookEventName을 정확히 채울 수 없다.
|
|
147
195
|
# 잘못된 기본값은 출력을 무효화하므로 추측하지 않고 즉시 종료한다 (fallback 제거).
|
|
148
196
|
if [ -z "$hook_event_name" ]; then
|
|
@@ -221,9 +269,31 @@ split("\n")
|
|
|
221
269
|
else 1 end) as $r007
|
|
222
270
|
| ([ $blocks[] | select(.type? == "text") | (.text? // "") ] | join("\n") | split("\n")) as $lines
|
|
223
271
|
| ([ $lines[] | select(test("\\[.+\\]\\[.+\\] ?(→|->|—>) ?Tool:")) ] | length) as $an_tool
|
|
272
|
+
# Core Rule 접두사 중 Agent 스폰을 가리키는 것만 따로 센다 — 아래 스폰 표기 중복 차감용.
|
|
273
|
+
| ([ $lines[] | select(test("\\[.+\\]\\[.+\\] ?(→|->|—>) ?Tool: ?Agent")) ] | length) as $an_tool_agent
|
|
224
274
|
| ([ $lines[] | select(test("^[[:space:]]*\\[[0-9]+\\][[:space:]].*(→|->|—>)")) ] | length) as $an_spawn_item
|
|
275
|
+
# 단일 스폰 표기 (#1650 E): R008 §"Agent Tool Format"이 규정하는 `subagent_type:model → desc`
|
|
276
|
+
# 라인. 같은 절의 Parallel Spawn Prefix Rule은 **단일 스폰을 `[N]` 접두사에서 명시적으로
|
|
277
|
+
# 면제**하므로, 번호 없는 이 형태가 단일 스폰의 정규 표기다. $an_spawn_item은 번호 형태만
|
|
278
|
+
# 세므로 이 라인은 announce에 0을 기여했고, Agent tool_use가 그대로 위반으로 계상됐다.
|
|
279
|
+
# 줄 시작 앵커(선행 공백만 허용)는 필수다 — `- mgr-x:sonnet → ...` 같은 리스트 산문이나
|
|
280
|
+
# 문장 중간 인용이 announce로 계상되면 forward 판정(max(0, ntools - announce))에서 실제
|
|
281
|
+
# 누락을 가려버린다.
|
|
282
|
+
| ([ $lines[] | select(test("^[[:space:]]*[a-z][a-z0-9-]*:(haiku|sonnet|opus|fable|inherit)[[:space:]]*(→|->|—>)")) ] | length) as $an_spawn_single
|
|
225
283
|
| ([ $lines[] | select(test("\\[.+\\]\\[.+\\] ?(→|->|—>) ?Spawning:")) ] | length) as $an_spawn_hdr
|
|
226
|
-
|
|
284
|
+
# 스폰 표기의 단위는 세분화된 것 우선: 번호 라인 > 단일 스폰 라인 > Spawning 헤더.
|
|
285
|
+
| (if $an_spawn_item > 0 then $an_spawn_item
|
|
286
|
+
elif $an_spawn_single > 0 then $an_spawn_single
|
|
287
|
+
else $an_spawn_hdr end) as $an_spawn
|
|
288
|
+
# 중복 계수 차감 (#1650 E): 같은 턴에 Core Rule `→ Tool: Agent` 접두사가 함께 있으면 스폰
|
|
289
|
+
# 표기 라인은 **같은 호출의 동반 라인**이다 — `→ Target:`이 `→ Tool:`의 동반 라인이라 세지
|
|
290
|
+
# 않는 것과 동일한 논리다. 더해버리면 한 호출이 2로 계상돼, 같은 턴의 다른 진짜 누락을
|
|
291
|
+
# 가린다(forward는 max(0, ntools - announce)라 과대 announce는 조용한 마스킹이 된다).
|
|
292
|
+
# 그래서 스폰 기여분에서 `→ Tool: Agent` 라인 수를 빼되 0에서 바닥을 친다 — 빼기만 하므로
|
|
293
|
+
# announce가 늘지 않아 신규 오탐은 생기지 않는다. 대신 구식의 과대 announce(접두사 +
|
|
294
|
+
# 항목 이중 계수)가 가리던 진짜 누락 — 예: 접두사 1 + [1] 항목 1 + tools=(Agent, Read)에서
|
|
295
|
+
# 미announce Read — 은 새로 발화한다(의도된 동작, v1.1.62 적대적 리뷰 재현 C1/C2).
|
|
296
|
+
| ($an_tool + (if $an_spawn > $an_tool_agent then $an_spawn - $an_tool_agent else 0 end)) as $announce
|
|
227
297
|
| ([ $blocks[] | select((.type? == "tool_use") and ((.name? // "") != "Skill")) ] | length) as $ntools
|
|
228
298
|
# 전체 tool_use(Skill 포함)가 0건인 턴은 forward 판정을 무조건 0으로 고정한다(#1625
|
|
229
299
|
# 찐빠 #3, 이슈 제안 그대로). $ntools(Skill 제외) 기반 산식은 정상 입력에서는 이미
|
|
@@ -12,9 +12,14 @@ command -v jq >/dev/null 2>&1 || exit 0
|
|
|
12
12
|
|
|
13
13
|
input=$(cat)
|
|
14
14
|
|
|
15
|
-
#
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
# Non-object stdin guard (#1650 B): a hook must never crash on garbage stdin.
|
|
16
|
+
# jq's parse error propagates through `set -euo pipefail` as rc=5 without this.
|
|
17
|
+
printf '%s' "$input" | jq -e 'type=="object"' >/dev/null 2>&1 || exit 0
|
|
18
|
+
|
|
19
|
+
# Extract tool info — `?` suppresses errors when tool_input arrives as a scalar
|
|
20
|
+
tool_name=$(printf '%s\n' "$input" | jq -r '.tool_name? // "unknown"')
|
|
21
|
+
tool_input=$(printf '%s\n' "$input" | jq -c 'if (.tool_input? | type) == "object" then .tool_input else {} end' 2>/dev/null) || tool_input='{}'
|
|
22
|
+
[ -n "$tool_input" ] || tool_input='{}'
|
|
18
23
|
|
|
19
24
|
SCHEMA_FILE=".claude/schemas/tool-inputs.json"
|
|
20
25
|
|
|
@@ -28,8 +33,8 @@ warnings=()
|
|
|
28
33
|
|
|
29
34
|
case "$tool_name" in
|
|
30
35
|
"Write")
|
|
31
|
-
file_path=$(printf '%s\n' "$tool_input" | jq -r '.file_path // ""')
|
|
32
|
-
content=$(printf '%s\n' "$tool_input" | jq -r '.content // ""')
|
|
36
|
+
file_path=$(printf '%s\n' "$tool_input" | jq -r '.file_path? // ""')
|
|
37
|
+
content=$(printf '%s\n' "$tool_input" | jq -r '.content? // ""')
|
|
33
38
|
|
|
34
39
|
if [ -z "$file_path" ]; then
|
|
35
40
|
warnings+=("[Schema] Write: file_path is empty or missing")
|
|
@@ -40,9 +45,9 @@ case "$tool_name" in
|
|
|
40
45
|
;;
|
|
41
46
|
|
|
42
47
|
"Edit")
|
|
43
|
-
file_path=$(printf '%s\n' "$tool_input" | jq -r '.file_path // ""')
|
|
44
|
-
old_string=$(printf '%s\n' "$tool_input" | jq -r '.old_string // ""')
|
|
45
|
-
new_string=$(printf '%s\n' "$tool_input" | jq -r '.new_string // ""')
|
|
48
|
+
file_path=$(printf '%s\n' "$tool_input" | jq -r '.file_path? // ""')
|
|
49
|
+
old_string=$(printf '%s\n' "$tool_input" | jq -r '.old_string? // ""')
|
|
50
|
+
new_string=$(printf '%s\n' "$tool_input" | jq -r '.new_string? // ""')
|
|
46
51
|
|
|
47
52
|
if [ -z "$file_path" ]; then
|
|
48
53
|
warnings+=("[Schema] Edit: file_path is empty or missing")
|
|
@@ -56,7 +61,7 @@ case "$tool_name" in
|
|
|
56
61
|
;;
|
|
57
62
|
|
|
58
63
|
"Bash")
|
|
59
|
-
command=$(printf '%s\n' "$tool_input" | jq -r '.command // ""')
|
|
64
|
+
command=$(printf '%s\n' "$tool_input" | jq -r '.command? // ""')
|
|
60
65
|
|
|
61
66
|
if [ -z "$command" ]; then
|
|
62
67
|
warnings+=("[Schema] Bash: command is empty")
|
|
@@ -12,9 +12,51 @@ command -v jq >/dev/null 2>&1 || exit 0
|
|
|
12
12
|
|
|
13
13
|
input=$(cat)
|
|
14
14
|
|
|
15
|
-
#
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
# Non-object stdin guard (#1650 B) — rejects ONLY non-object stdin, which carries
|
|
16
|
+
# no scannable payload anyway; a well-formed object always reaches the scan below.
|
|
17
|
+
printf '%s' "$input" | jq -e 'type=="object"' >/dev/null 2>&1 || exit 0
|
|
18
|
+
|
|
19
|
+
tool_name=$(printf '%s\n' "$input" | jq -r '.tool_name? // "unknown"')
|
|
20
|
+
|
|
21
|
+
# Collect every text-bearing field of the payload into one scan buffer.
|
|
22
|
+
#
|
|
23
|
+
# PostToolUse carries the result under `tool_response`, NOT `tool_output`
|
|
24
|
+
# (measured 2026-09-03: 1764 PostToolUse payloads echoed into this project's
|
|
25
|
+
# session transcripts had `tool_response` 1764/1764 and `tool_output` 0/1764;
|
|
26
|
+
# the CC 2.1.259 embedded hook reference documents `"tool_response": {...}
|
|
27
|
+
# // PostToolUse only`). Reading `.tool_output.output` therefore always yielded
|
|
28
|
+
# "" and the scan below never ran — every AWS key, private key and PAT passed
|
|
29
|
+
# through unflagged.
|
|
30
|
+
#
|
|
31
|
+
# Measured text-bearing fields, by tool:
|
|
32
|
+
# Bash .tool_response.stdout / .stderr
|
|
33
|
+
# Read .tool_response.file.content
|
|
34
|
+
# Write .tool_response.content
|
|
35
|
+
# Agent .tool_response.prompt
|
|
36
|
+
# MCP .tool_response (string) or .tool_response.content[].text
|
|
37
|
+
# `.tool_output.output` and a string-shaped `.tool_output` are kept as
|
|
38
|
+
# fallbacks for events that still use the older shape (e.g. SubagentStop).
|
|
39
|
+
#
|
|
40
|
+
# `?` suppresses jq path errors when a field is a scalar rather than an object;
|
|
41
|
+
# every branch is optional, so an unexpected shape degrades to "" instead of
|
|
42
|
+
# aborting the hook under `set -euo pipefail`.
|
|
43
|
+
output=$(printf '%s\n' "$input" | jq -r '
|
|
44
|
+
[
|
|
45
|
+
(.tool_response? | if type == "string" then . else empty end),
|
|
46
|
+
(.tool_response?.stdout? | strings),
|
|
47
|
+
(.tool_response?.stderr? | strings),
|
|
48
|
+
(.tool_response?.content? | strings),
|
|
49
|
+
(.tool_response?.content? | if type == "array"
|
|
50
|
+
then map(.text? | strings) | join("\n")
|
|
51
|
+
else empty end),
|
|
52
|
+
(.tool_response?.file?.content? | strings),
|
|
53
|
+
(.tool_response?.prompt? | strings),
|
|
54
|
+
(.tool_response?.output? | strings),
|
|
55
|
+
(.tool_output? | if type == "string" then . else empty end),
|
|
56
|
+
(.tool_output?.output? | strings)
|
|
57
|
+
]
|
|
58
|
+
| map(select(. != "")) | join("\n")
|
|
59
|
+
' 2>/dev/null) || output=""
|
|
18
60
|
|
|
19
61
|
# Skip if no output
|
|
20
62
|
if [ -z "$output" ] || [ "$output" = "null" ]; then
|
|
@@ -44,7 +86,7 @@ if echo "$output" | grep -qE 'ghp_[a-zA-Z0-9]{36}'; then
|
|
|
44
86
|
fi
|
|
45
87
|
|
|
46
88
|
# Private Key
|
|
47
|
-
if echo "$output" | grep -qE '-----BEGIN.*PRIVATE KEY-----'; then
|
|
89
|
+
if echo "$output" | grep -qE -- '-----BEGIN.*PRIVATE KEY-----'; then
|
|
48
90
|
echo "[Security] Potential private key detected in ${tool_name} output" >&2
|
|
49
91
|
detected=true
|
|
50
92
|
fi
|
|
@@ -13,6 +13,10 @@ set -euo pipefail
|
|
|
13
13
|
# ── Stop hook 프로토콜: stdin을 먼저 읽음 ──
|
|
14
14
|
input=$(cat)
|
|
15
15
|
|
|
16
|
+
# ── 비객체 stdin 가드 (#1650 B) — 없으면 jq 파싱 오류가 pipefail로 rc=5 전파.
|
|
17
|
+
# worker의 `.message.role` 트랜스크립트 파서 경로는 그대로 유지된다.
|
|
18
|
+
printf '%s' "$input" | jq -e 'type=="object"' >/dev/null 2>&1 || exit 0
|
|
19
|
+
|
|
16
20
|
# ── Opt-out 체크 ──
|
|
17
21
|
if [ "${OMCUSTOM_SESSION_REFLECTION:-}" = "off" ]; then
|
|
18
22
|
printf '%s\n' "$input"
|
|
@@ -40,7 +44,7 @@ bg_tasks_count=$(printf '%s\n' "$bg_tasks_json" | jq 'length' 2>/dev/null || ech
|
|
|
40
44
|
session_crons_count=$(printf '%s\n' "$session_crons_json" | jq 'length' 2>/dev/null || echo 0)
|
|
41
45
|
|
|
42
46
|
# 미완료 백그라운드 태스크: running/pending/in_progress 상태 카운트
|
|
43
|
-
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)
|
|
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)
|
|
44
48
|
|
|
45
49
|
# ── 경로 결정 (환경변수 override 지원) ──
|
|
46
50
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
@@ -204,17 +204,34 @@ _strip_heredoc_bodies() {
|
|
|
204
204
|
local text="$1"
|
|
205
205
|
local out="" line tok delim="" in_body=0
|
|
206
206
|
local before_op dq sq
|
|
207
|
+
# A literal single quote cannot be written inside the bracket expression of
|
|
208
|
+
# the quote-counting expansion below, so hold one in a variable.
|
|
209
|
+
local sq_char="'"
|
|
207
210
|
while IFS= read -r line; do
|
|
208
211
|
if [ "$in_body" -eq 1 ]; then
|
|
209
212
|
# "<<-" allows leading tabs before the terminator; accept leading
|
|
210
|
-
# whitespace for either form.
|
|
211
|
-
|
|
213
|
+
# whitespace for either form. The trim is pure parameter expansion: as
|
|
214
|
+
# "printf | sed" it cost TWO forks on every BODY line, which is the
|
|
215
|
+
# dominant cost of a long heredoc (measured: a 1000-line body took
|
|
216
|
+
# 1.8 s of hook latency, #1650 A).
|
|
217
|
+
tok="${line#"${line%%[![:space:]]*}"}"
|
|
218
|
+
tok="${tok%"${tok##*[![:space:]]}"}"
|
|
212
219
|
if [ "$tok" = "$delim" ]; then
|
|
213
220
|
in_body=0
|
|
214
221
|
fi
|
|
215
222
|
continue
|
|
216
223
|
fi
|
|
217
224
|
out="${out}${line}"$'\n'
|
|
225
|
+
# A line with no "<<" can open no heredoc, so neither the forgery guard nor
|
|
226
|
+
# the delimiter scan below has anything to find on it. Skipping them keeps
|
|
227
|
+
# the extraction "sed" off every ordinary line, which used to run
|
|
228
|
+
# unconditionally — two more forks per line (#1650 A). "delim" must still be
|
|
229
|
+
# cleared here, or a delimiter captured on an EARLIER line would re-open a
|
|
230
|
+
# body on this one.
|
|
231
|
+
case "$line" in
|
|
232
|
+
*'<<'*) ;;
|
|
233
|
+
*) delim=""; continue ;;
|
|
234
|
+
esac
|
|
218
235
|
# A DELIMITER FORGERY guard: the extraction below has no idea whether the
|
|
219
236
|
# "<<" it finds is shell syntax or literal text inside a quoted string, so
|
|
220
237
|
# `echo "a << EOF"` followed by `rm -rf x` and a line reading `EOF` used to
|
|
@@ -226,14 +243,15 @@ _strip_heredoc_bodies() {
|
|
|
226
243
|
# as a write. "${line%<<*}" strips the shortest matching suffix, i.e. it
|
|
227
244
|
# yields the text before the LAST "<<" — the same occurrence the greedy
|
|
228
245
|
# extraction regex below settles on.
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
246
|
+
# The quote counts are parameter expansions rather than
|
|
247
|
+
# "printf | tr | wc | tr" (four more forks per opener line): "${v//[^X]/}"
|
|
248
|
+
# deletes everything that is not X, so the remaining LENGTH is the count.
|
|
249
|
+
before_op="${line%<<*}"
|
|
250
|
+
dq="${before_op//[^\"]/}"
|
|
251
|
+
sq="${before_op//[^$sq_char]/}"
|
|
252
|
+
if [ $(( ${#dq} % 2 )) -eq 1 ] || [ $(( ${#sq} % 2 )) -eq 1 ]; then
|
|
253
|
+
printf '%s' "${out}__QUOTED_HEREDOC_OPENER__"$'\n'
|
|
254
|
+
return 0
|
|
237
255
|
fi
|
|
238
256
|
delim="$(printf '%s' "$line" \
|
|
239
257
|
| sed -n -E "s/^.*[^<]<<-?[[:space:]]*[\"']?([A-Za-z_][A-Za-z0-9_]*)[\"']?.*\$/\\1/p")"
|
|
@@ -453,7 +471,12 @@ is_readonly_bash_command() {
|
|
|
453
471
|
normalized="${normalized//|/$'\n'}"
|
|
454
472
|
local seg
|
|
455
473
|
while IFS= read -r seg; do
|
|
456
|
-
|
|
474
|
+
# Pure parameter expansion, not "printf | sed": each segment is a single
|
|
475
|
+
# line, so the two forms trim identically, but the sed form spent two
|
|
476
|
+
# forks on EVERY segment of a multi-line command (measured: 300
|
|
477
|
+
# newline-separated segments took 0.8 s of hook latency, #1650 A).
|
|
478
|
+
seg="${seg#"${seg%%[![:space:]]*}"}"
|
|
479
|
+
seg="${seg%"${seg##*[![:space:]]}"}"
|
|
457
480
|
if [ -z "$seg" ]; then
|
|
458
481
|
echo "false"
|
|
459
482
|
return 0
|
|
@@ -472,6 +495,15 @@ is_readonly_bash_command() {
|
|
|
472
495
|
|
|
473
496
|
input=$(cat)
|
|
474
497
|
|
|
498
|
+
# A hook must never crash the tool call it observes. stdin that is not a JSON
|
|
499
|
+
# OBJECT — empty, a bare scalar, a JSON array, truncated, or not JSON at all —
|
|
500
|
+
# carries no tool record, and the very first "jq -r" below died on it; with
|
|
501
|
+
# "set -euo pipefail" that took the WHOLE hook down with rc=5 (#1650 B).
|
|
502
|
+
# Swallow it instead: exit 0 with no stdout, exactly as if the hook had not
|
|
503
|
+
# run. "jq -e" exits non-zero both for a parse error and for a false/null
|
|
504
|
+
# result, so one guard covers every non-object shape.
|
|
505
|
+
printf '%s' "$input" | jq -e 'type == "object"' >/dev/null 2>&1 || exit 0
|
|
506
|
+
|
|
475
507
|
# Extract tool info
|
|
476
508
|
tool_name=$(printf '%s' "$input" | jq -r '.tool_name // "unknown"')
|
|
477
509
|
# Bash commands are NOT files. Feeding tool_input.command into file_path made
|
|
@@ -11,28 +11,35 @@ command -v jq >/dev/null 2>&1 || exit 0
|
|
|
11
11
|
|
|
12
12
|
input=$(cat)
|
|
13
13
|
|
|
14
|
-
#
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
# Non-object stdin guard (#1650 B): a hook must never crash on garbage stdin.
|
|
15
|
+
# jq's parse error propagates through `set -euo pipefail` as rc=5 without this.
|
|
16
|
+
printf '%s' "$input" | jq -e 'type=="object"' >/dev/null 2>&1 || exit 0
|
|
17
|
+
|
|
18
|
+
# Extract task info — support both PostToolUse (tool_input.*) and SubagentStop (top-level) shapes.
|
|
19
|
+
# `?` suppresses "Cannot index string with ..." when tool_input arrives as a scalar.
|
|
20
|
+
agent_type=$(printf '%s\n' "$input" | jq -r '.tool_input.subagent_type? // .agent_type? // "unknown"')
|
|
21
|
+
model=$(printf '%s\n' "$input" | jq -r '.tool_input.model? // .model? // "inherit"')
|
|
22
|
+
description=$(printf '%s\n' "$input" | jq -r '.tool_input.description? // .description? // ""' | head -c 80)
|
|
18
23
|
|
|
19
24
|
# Extract skill name from description or prompt
|
|
20
25
|
skill_name=""
|
|
26
|
+
# `|| true`: a no-match grep exits 1 and `set -euo pipefail` would abort the hook
|
|
27
|
+
# (measured rc=1 on empty stdin AND on a well-formed object with no skill name).
|
|
21
28
|
if echo "$description" | grep -qiE '(skill:|routing|→.*skill)'; then
|
|
22
|
-
skill_name=$(echo "$description" | grep -oiE '[a-z]+-[a-z]+(-[a-z]+)*-?(routing|skill|practices|detection|decomposition|orchestration|pipeline|guards|cycle|plan|review|refactor|publish|version|audit|exec|analyze|bundle|report|setup|watch|lists|status|help|save|recall)' | head -1)
|
|
29
|
+
skill_name=$(echo "$description" | grep -oiE '[a-z]+-[a-z]+(-[a-z]+)*-?(routing|skill|practices|detection|decomposition|orchestration|pipeline|guards|cycle|plan|review|refactor|publish|version|audit|exec|analyze|bundle|report|setup|watch|lists|status|help|save|recall)' | head -1 || true)
|
|
23
30
|
fi
|
|
24
31
|
# Fallback: check prompt field for "Skill: {name}" pattern
|
|
25
32
|
if [ -z "$skill_name" ]; then
|
|
26
|
-
prompt=$(printf '%s\n' "$input" | jq -r '.tool_input.prompt // ""' | head -c 500)
|
|
27
|
-
skill_name=$(echo "$prompt" | grep -oiE 'Skill:\s*[a-z]+-[a-z]+(-[a-z]+)*' | sed 's/[Ss]kill:\s*//' | head -1)
|
|
33
|
+
prompt=$(printf '%s\n' "$input" | jq -r '.tool_input.prompt? // ""' | head -c 500)
|
|
34
|
+
skill_name=$(echo "$prompt" | grep -oiE 'Skill:\s*[a-z]+-[a-z]+(-[a-z]+)*' | sed 's/[Ss]kill:\s*//' | head -1 || true)
|
|
28
35
|
fi
|
|
29
36
|
|
|
30
37
|
# Determine outcome
|
|
31
|
-
is_error=$(printf '%s\n' "$input" | jq -r '.tool_output.is_error // false')
|
|
38
|
+
is_error=$(printf '%s\n' "$input" | jq -r '.tool_output.is_error? // false')
|
|
32
39
|
|
|
33
40
|
if [ "$is_error" = "true" ]; then
|
|
34
41
|
outcome="failure"
|
|
35
|
-
error_summary=$(printf '%s\n' "$input" | jq -r '.tool_output.output // ""' | head -c 200)
|
|
42
|
+
error_summary=$(printf '%s\n' "$input" | jq -r '.tool_output.output? // ""' | head -c 200)
|
|
36
43
|
else
|
|
37
44
|
outcome="success"
|
|
38
45
|
error_summary=""
|
|
@@ -16,7 +16,7 @@ oh-my-customcode uses an **advisory-first enforcement model**. Most rules are en
|
|
|
16
16
|
| Soft Block | Stop hook prompt | R011 session-end saves | Auto-performs then approves |
|
|
17
17
|
| Conversation Block | PostToolUse hook + `continueOnBlock` (CC v2.1.139+), exit 2 | stuck-detector, context-budget-advisor, cost-cap-advisor | Feeds rejection reason into conversation; Claude continues with awareness |
|
|
18
18
|
| Advisory | PostToolUse hooks | R007, R008, R009, R010, R018 | Warns via stderr, never blocks |
|
|
19
|
-
| Advisory (proactive) | UserPromptSubmit + SubagentStop + PostToolUse hooks | R007, R008 (`r007-r008-drift-advisor.sh` — #1229 UserPromptSubmit, #1545 SubagentStop, #1553 PostToolUse) | Reads last assistant turn; emits advisory if header/prefix absent. SubagentStop wiring (#1545) closes the no-user-input autonomous-loop gap (`/fsd`); PostToolUse (#1553) covers the orchestrator-only stretch before the first subagent spawn. Complements retroactive Stop-hook (`session-reflection.sh`, #1190). **v1.1.43부터 실제 발화 — 아래 각주 참조.** v1.1.49부터 역방향(announce > tool_use) 신호 포함, 기본 off 옵트인 (#1595 #6). |
|
|
19
|
+
| Advisory (proactive) | UserPromptSubmit + SubagentStop + PostToolUse hooks | R007, R008 (`r007-r008-drift-advisor.sh` — #1229 UserPromptSubmit, #1545 SubagentStop, #1553 PostToolUse) | Reads last assistant turn; emits advisory if header/prefix absent. SubagentStop wiring (#1545) closes the no-user-input autonomous-loop gap (`/fsd`); PostToolUse (#1553) covers the orchestrator-only stretch before the first subagent spawn. Complements retroactive Stop-hook (`session-reflection.sh`, #1190). **v1.1.43부터 실제 발화 — 아래 각주 참조.** v1.1.49부터 역방향(announce > tool_use) 신호 포함, 기본 off 옵트인 (#1595 #6). **v1.1.62(#1650 D)부터 서브에이전트 세션에서는 침묵**: hook stdin의 `agent_id`(CC 스키마상 서브에이전트 내부 발화에만 존재)가 있으면 exit 0 — 단 `SubagentStart`/`SubagentStop`은 `agent_id`가 대상 식별자라 예외(판정 수행). 중첩 서브에이전트 안의 SubagentStop은 억제 못 함(R010 정책상 도달 불가 경로). 완료 보고가 advisory 응답으로 대체되던 훅 피드백 잠식(R020 8항, #1652 #3)의 advisory 계열 차단. |
|
|
20
20
|
| Advisory (telemetry) | PostToolUseFailure hook | — (계측 전용, 규칙 강제 없음) | `failure-ledger.sh` (#1561, v1.1.44) — 도구 실패를 JSONL 원장에 append. stdout/stderr 무출력이라 모델에 도달하지 않으며 절대 차단하지 않음 |
|
|
21
21
|
| Advisory (proactive) | UserPromptSubmit hook | R020 (원인 진단) | `fail-axis-cause-advisor.sh` (#1561, v1.1.44) — 원장에 실패 기록이 있는데 원인 진술 없는 재촉 프롬프트가 오면 `hookSpecificOutput.additionalContext`로 "원인 가설 되묻기" advisory 전달. 원장 부재 시 조용히 통과 |
|
|
22
22
|
| Prompt-based | CLAUDE.md + rules/ + `SessionStart` 재주입(matcher `*`; PostCompact 이벤트 배선은 유지되나 효과 미보장[^postcompact]) | All MUST rules | Behavioral guidance in context |
|
|
@@ -78,7 +78,7 @@ Agent(description: "[2] Python code review", subagent_type: "lang-python-expert"
|
|
|
78
78
|
|
|
79
79
|
Single agent spawns do NOT use the `[N]` prefix.
|
|
80
80
|
|
|
81
|
-
**단일 스폰도 Core Rule 접두사 필수 (Origin: #1652 #3-3)**: 단일 Agent 스폰은 `[N]` 항목 형식 대신 Core Rule 형식의 접두사 라인(에이전트·모델 대괄호 + 화살표 + Tool 표기 + `Agent`)을 호출 직전에 출력한다. advisor(`.claude/hooks/scripts/r007-r008-drift-advisor.sh`)가 announce로 계수하는 것은
|
|
81
|
+
**단일 스폰도 Core Rule 접두사 필수 (Origin: #1652 #3-3, advisor 정합 #1650 E)**: 단일 Agent 스폰은 `[N]` 항목 형식 대신 Core Rule 형식의 접두사 라인(에이전트·모델 대괄호 + 화살표 + Tool 표기 + `Agent`)을 호출 직전에 출력한다. advisor(`.claude/hooks/scripts/r007-r008-drift-advisor.sh`)가 announce로 계수하는 것은 4종 — Tool 표기 라인, 대괄호 번호가 붙은 spawn 항목, **대괄호 번호 없이 에이전트타입:모델 다음에 화살표와 설명이 이어지는 단독 라인**(#1650 E에서 추가), Spawning 헤더 라인 — 이며 스폰 표기 3종은 **번호 항목 > 단독 라인 > Spawning 헤더** 순으로 하나만 채택된다. 단독 라인만 쓴 응답이 R008 누락으로 오계상되던 결함은 v1.1.62에서 해소됐다(v1.1.60 세션 실측 → v1.1.61 적대적 리뷰 재현 → #1650 E 적용). **다만 규칙이 요구하는 형식은 여전히 Core Rule 접두사 라인이다** — 단독 라인 계수는 오탐 제거이지 표기 승격이 아니다. 접두사 라인과 단독 라인이 함께 있으면 advisor는 단독 라인을 **동반 라인으로 보아 차감**하므로(Target 라인과 동일 취급) 병기해도 이중 계수되지 않는다. **단독 라인은 반드시 줄 시작(선행 공백만 허용)이어야 한다** — 리스트 마커가 앞에 붙으면 계수되지 않는다. advisor 판정식을 바꿀 때 이 문단을 같은 커밋에서 갱신한다(R016 Rule Wiring Check).
|
|
82
82
|
|
|
83
83
|
```
|
|
84
84
|
[claude][opus] → Tool: Agent
|
package/templates/manifest.json
CHANGED