oh-my-customcode 1.1.42 → 1.1.44
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/hooks.json +33 -0
- package/templates/.claude/hooks/scripts/fail-axis-cause-advisor.sh +102 -0
- package/templates/.claude/hooks/scripts/failure-ledger.sh +75 -0
- package/templates/.claude/hooks/scripts/r007-r008-drift-advisor.sh +177 -93
- package/templates/.claude/hooks/scripts/session-reflection.sh +87 -70
- package/templates/.claude/rules/MAY-optimization.md +4 -2
- package/templates/.claude/rules/MUST-agent-design.md +15 -3
- package/templates/.claude/rules/MUST-agent-identification.md +13 -0
- package/templates/.claude/rules/MUST-agent-teams.md +8 -0
- package/templates/.claude/rules/MUST-completion-verification.md +13 -0
- package/templates/.claude/rules/MUST-continuous-improvement.md +12 -1
- package/templates/.claude/rules/MUST-enforcement-policy.md +9 -3
- package/templates/.claude/rules/MUST-orchestrator-coordination.md +35 -0
- package/templates/.claude/rules/MUST-parallel-execution.md +2 -0
- package/templates/.claude/rules/MUST-permissions.md +8 -1
- package/templates/.claude/rules/MUST-safety.md +5 -1
- package/templates/.claude/rules/SHOULD-ecomode.md +2 -0
- package/templates/.claude/rules/SHOULD-hud-statusline.md +1 -1
- package/templates/.claude/rules/SHOULD-memory-integration.md +10 -0
- package/templates/.claude/rules/SHOULD-verification-ladder.md +12 -0
- package/templates/.claude/skills/pipeline/workflows/auto-dev.yaml +56 -0
- package/templates/manifest.json +1 -1
- package/templates/workflows/auto-dev.yaml +56 -0
package/dist/cli/index.js
CHANGED
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -227,6 +227,16 @@
|
|
|
227
227
|
}
|
|
228
228
|
],
|
|
229
229
|
"description": "Inject session auto-fix findings into first user prompt (#838)"
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
"matcher": "*",
|
|
233
|
+
"hooks": [
|
|
234
|
+
{
|
|
235
|
+
"type": "command",
|
|
236
|
+
"command": "bash .claude/hooks/scripts/fail-axis-cause-advisor.sh"
|
|
237
|
+
}
|
|
238
|
+
],
|
|
239
|
+
"description": "Advisory on cause-free progress nudges when this session has logged tool failures \u2014 delivers via hookSpecificOutput.additionalContext (#1561)"
|
|
230
240
|
}
|
|
231
241
|
],
|
|
232
242
|
"SubagentStart": [
|
|
@@ -455,6 +465,17 @@
|
|
|
455
465
|
],
|
|
456
466
|
"description": "Context budget advisor \u2014 track tool usage patterns and advise ecomode activation"
|
|
457
467
|
},
|
|
468
|
+
{
|
|
469
|
+
"matcher": "tool == \"Edit\" || tool == \"Write\" || tool == \"Bash\" || tool == \"Task\" || tool == \"Agent\" || tool == \"Read\" || tool == \"Glob\" || tool == \"Grep\"",
|
|
470
|
+
"hooks": [
|
|
471
|
+
{
|
|
472
|
+
"type": "command",
|
|
473
|
+
"command": "bash .claude/hooks/scripts/r007-r008-drift-advisor.sh",
|
|
474
|
+
"continueOnBlock": true
|
|
475
|
+
}
|
|
476
|
+
],
|
|
477
|
+
"description": "Proactive R007/R008 drift advisory on tool use \u2014 covers the orchestrator-only stretch before the first subagent spawn, where neither UserPromptSubmit nor SubagentStop fires (#1553). Turn-deduplicated; exit 0 advisory only."
|
|
478
|
+
},
|
|
458
479
|
{
|
|
459
480
|
"matcher": "tool == \"Edit\" || tool == \"Write\" || tool == \"Bash\" || tool == \"Task\" || tool == \"Agent\"",
|
|
460
481
|
"hooks": [
|
|
@@ -610,6 +631,18 @@
|
|
|
610
631
|
],
|
|
611
632
|
"description": "Print auto-dev token spend summary on session end (Issue #1057, advisory)"
|
|
612
633
|
}
|
|
634
|
+
],
|
|
635
|
+
"PostToolUseFailure": [
|
|
636
|
+
{
|
|
637
|
+
"matcher": "*",
|
|
638
|
+
"hooks": [
|
|
639
|
+
{
|
|
640
|
+
"type": "command",
|
|
641
|
+
"command": "bash .claude/hooks/scripts/failure-ledger.sh"
|
|
642
|
+
}
|
|
643
|
+
],
|
|
644
|
+
"description": "FAIL-axis instrumentation \u2014 append tool failures to the error ledger (JSONL). Never blocks; feeds fail-axis-cause-advisor.sh (#1561)"
|
|
645
|
+
}
|
|
613
646
|
]
|
|
614
647
|
}
|
|
615
648
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# fail-axis-cause-advisor.sh — UserPromptSubmit: 원인 없는 재촉 발화 감지 (FAIL 축)
|
|
3
|
+
#
|
|
4
|
+
# 배경:
|
|
5
|
+
# 8주 세션 실측에서 사용자 발화 216턴 중 원인 분석 표현이 0건(error_cause_ratio = 0.000)이었다.
|
|
6
|
+
# 반면 "계속해"(14) / "ㄱㄱ"(5) / "계속 진행해"(5) 등 원인 진술 없는 진행 지시가 24건,
|
|
7
|
+
# 전체 발화의 11%를 차지했다. 도구 실패가 세션당 1.72건 발생하는데도 원인을 묻지 않고
|
|
8
|
+
# 재촉으로 통과시키는 패턴이다.
|
|
9
|
+
#
|
|
10
|
+
# 역할:
|
|
11
|
+
# (1) 짧은 진행 지시이고 (2) 원인 언급이 없으며 (3) 이 세션에 기록된 도구 실패가 있을 때,
|
|
12
|
+
# Claude에게 "진행 전에 사용자에게 원인 가설 한 줄을 되물어라"는 advisory를 전달한다.
|
|
13
|
+
#
|
|
14
|
+
# 왜 사용자가 아니라 Claude에게 전달하는가:
|
|
15
|
+
# hookSpecificOutput.additionalContext는 모델 컨텍스트로만 들어간다(사용자에게 표시되지 않음).
|
|
16
|
+
# 따라서 Claude가 사용자에게 되묻게 만들고, 사용자의 답변이 대화 로그에 사용자 발화로
|
|
17
|
+
# 남게 하는 우회 경로를 택한다. 이렇게 해야 실제 진단 행동과 계측 지표가 함께 개선된다.
|
|
18
|
+
#
|
|
19
|
+
# 왜 차단하지 않는가:
|
|
20
|
+
# decision:"block"을 쓰면 프롬프트 자체가 거부되어 자율 루프(/fsd)가 멈춘다.
|
|
21
|
+
# R021 advisory-first 원칙에 따라 절대 차단하지 않고 exit 0을 유지한다.
|
|
22
|
+
#
|
|
23
|
+
# 의존:
|
|
24
|
+
# failure-ledger.sh(PostToolUseFailure)가 기록한 원장을 발동 조건으로 읽는다.
|
|
25
|
+
# 원장이 없으면 조용히 통과한다 — 훅 단독으로도 안전하게 동작한다.
|
|
26
|
+
#
|
|
27
|
+
# 환경변수 override:
|
|
28
|
+
# OMCUSTOM_FAIL_ADVISOR=off — advisory 완전 비활성화
|
|
29
|
+
# OMCUSTOM_ERROR_LEDGER=<path> — 원장 경로 override
|
|
30
|
+
|
|
31
|
+
set -euo pipefail
|
|
32
|
+
|
|
33
|
+
input=$(cat)
|
|
34
|
+
|
|
35
|
+
# ── Opt-out 체크 ──
|
|
36
|
+
if [ "${OMCUSTOM_FAIL_ADVISOR:-}" = "off" ]; then
|
|
37
|
+
exit 0
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
41
|
+
exit 0
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
prompt=$(printf '%s' "$input" | jq -r '.prompt // empty' 2>/dev/null) || exit 0
|
|
45
|
+
session=$(printf '%s' "$input" | jq -r '.session_id // empty' 2>/dev/null) || exit 0
|
|
46
|
+
|
|
47
|
+
if [ -z "$prompt" ] || [ -z "$session" ]; then
|
|
48
|
+
exit 0
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# ── 조건 1: 짧은 발화만 대상 (긴 발화는 이미 맥락을 담고 있다고 본다) ──
|
|
52
|
+
# 실측 p75가 27자이므로 40자를 상한으로 둔다.
|
|
53
|
+
if [ "${#prompt}" -gt 40 ]; then
|
|
54
|
+
exit 0
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
# ── 조건 2: 진행/재촉 패턴인가 ──
|
|
58
|
+
if ! printf '%s' "$prompt" \
|
|
59
|
+
| grep -qiE '(계속|이어서|진행해|재개|다음|ㄱㄱ|고고|가자|continue|keep going|go on|resume|proceed|next)'; then
|
|
60
|
+
exit 0
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
# ── 조건 3: 이미 원인/이유를 언급했다면 개입하지 않는다 (오탐 방지) ──
|
|
64
|
+
if printf '%s' "$prompt" \
|
|
65
|
+
| grep -qiE '(원인|이유|왜|때문|에러|오류|error|fail|because|cause)'; then
|
|
66
|
+
exit 0
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# ── 조건 4: 이 세션에 기록된 도구 실패가 있는가 ──
|
|
70
|
+
LEDGER="${OMCUSTOM_ERROR_LEDGER:-${HOME}/.claude/error-ledger.jsonl}"
|
|
71
|
+
if [ ! -f "$LEDGER" ]; then
|
|
72
|
+
exit 0
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
# 원장 꼬리만 스캔한다 (전체 파일 스캔 회피).
|
|
76
|
+
# interrupt == true 는 사용자가 직접 중단시킨 것이므로 진단 대상 실패가 아니다 — 제외한다.
|
|
77
|
+
# (제외하지 않으면 사용자가 스스로 끊은 도구까지 "원인을 대라"고 되묻는 오탐이 된다.)
|
|
78
|
+
fail_count=$(tail -n 300 "$LEDGER" 2>/dev/null \
|
|
79
|
+
| jq -r --arg s "$session" 'select(.session == $s and .interrupt != true) | .tool' 2>/dev/null \
|
|
80
|
+
| wc -l | tr -d ' ') || fail_count=0
|
|
81
|
+
|
|
82
|
+
if [ -z "$fail_count" ] || [ "$fail_count" -eq 0 ] 2>/dev/null; then
|
|
83
|
+
exit 0
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
# 최근 실패 도구 요약 (최대 3종)
|
|
87
|
+
fail_tools=$(tail -n 300 "$LEDGER" 2>/dev/null \
|
|
88
|
+
| jq -r --arg s "$session" 'select(.session == $s and .interrupt != true) | .tool' 2>/dev/null \
|
|
89
|
+
| sort | uniq -c | sort -rn | head -3 \
|
|
90
|
+
| awk '{printf "%s(%s) ", $2, $1}') || fail_tools=""
|
|
91
|
+
|
|
92
|
+
advisory_text=$(printf '[FAIL Advisory] 이 세션에 도구 실패 %s건이 기록되어 있습니다 (%s). 방금 입력은 원인 언급이 없는 진행 지시입니다. 곧바로 재시도하지 말고, 먼저 직전 실패의 원인 가설을 한 줄로 제시한 뒤 사용자에게 "이 진단이 맞는지 / 다른 원인이 짚이는지" 짧게 한 번만 확인하십시오. 사용자가 답하면 그대로 진행합니다. 이 확인은 한 턴을 넘기지 마십시오.' \
|
|
93
|
+
"$fail_count" "${fail_tools:-unknown}")
|
|
94
|
+
|
|
95
|
+
# 사람이 보는 감사 추적용 (exit 0에서 stderr는 모델에 전달되지 않음)
|
|
96
|
+
printf '%s\n' "$advisory_text" >&2
|
|
97
|
+
|
|
98
|
+
# 실제 전달 경로: additionalContext. decision 필드는 절대 포함하지 않는다.
|
|
99
|
+
jq -cn --arg ctx "$advisory_text" \
|
|
100
|
+
'{hookSpecificOutput: {hookEventName: "UserPromptSubmit", additionalContext: $ctx}}'
|
|
101
|
+
|
|
102
|
+
exit 0
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# failure-ledger.sh — PostToolUseFailure 에러 원장 기록 (FAIL 축 계측)
|
|
3
|
+
#
|
|
4
|
+
# 배경:
|
|
5
|
+
# hooks.json은 성공 경로(PreToolUse 12 / PostToolUse 16 / Stop 7)에는 촘촘히 배선되어
|
|
6
|
+
# 있으나 실패 경로에는 훅이 하나도 없었다. 그 결과 도구 실패가 세션당 평균 1.72건
|
|
7
|
+
# 발생함에도 아무 데이터도 남지 않아, R023 검증 래더의 상위 tier(에러 패턴 인식,
|
|
8
|
+
# post-mortem)를 측정할 근거 자체가 없는 상태였다.
|
|
9
|
+
#
|
|
10
|
+
# 역할:
|
|
11
|
+
# 도구 호출 실패 시 한 줄 JSONL을 원장에 append 한다. 이 원장은
|
|
12
|
+
# (1) recovery 성공률 산출, (2) 반복 실패 도구/명령 식별,
|
|
13
|
+
# (3) fail-axis-cause-advisor.sh(UserPromptSubmit)의 발동 조건으로 쓰인다.
|
|
14
|
+
#
|
|
15
|
+
# 설계 원칙:
|
|
16
|
+
# - 순수 append-only. 기존 파일을 읽거나 수정하지 않는다.
|
|
17
|
+
# - 네트워크 호출 없음. 외부 명령은 jq/date만 사용.
|
|
18
|
+
# - 어떤 실패에도 exit 0 — 원장 기록 실패가 본 작업을 막아서는 안 된다 (R021 advisory-first).
|
|
19
|
+
# - 명령/에러 문자열은 절단하여 기록한다 (원장 비대화 방지).
|
|
20
|
+
#
|
|
21
|
+
# 환경변수 override:
|
|
22
|
+
# OMCUSTOM_FAILURE_LEDGER=off — 기록 완전 비활성화
|
|
23
|
+
# OMCUSTOM_ERROR_LEDGER=<path> — 원장 경로 override (기본: ~/.claude/error-ledger.jsonl)
|
|
24
|
+
|
|
25
|
+
set -euo pipefail
|
|
26
|
+
|
|
27
|
+
input=$(cat)
|
|
28
|
+
|
|
29
|
+
# ── Opt-out 체크 ──
|
|
30
|
+
if [ "${OMCUSTOM_FAILURE_LEDGER:-}" = "off" ]; then
|
|
31
|
+
exit 0
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# ── jq 의존성 체크 (없으면 조용히 통과) ──
|
|
35
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
LEDGER="${OMCUSTOM_ERROR_LEDGER:-${HOME}/.claude/error-ledger.jsonl}"
|
|
40
|
+
|
|
41
|
+
if ! mkdir -p "$(dirname "$LEDGER")" 2>/dev/null; then
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
ts=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo "")
|
|
46
|
+
|
|
47
|
+
# ── 한 줄 JSONL append ──
|
|
48
|
+
# 에러 필드 위치 (공식 문서 실측, code.claude.com/docs/en/hooks "PostToolUseFailure input"):
|
|
49
|
+
# PostToolUseFailure는 PostToolUse와 달리 tool_response를 보내지 않는다. 에러는
|
|
50
|
+
# **최상위 `error`** 문자열로 오고, 부수적으로 `is_interrupt` / `duration_ms`가 따라온다.
|
|
51
|
+
# 초판이 `.tool_response.error`를 읽어 err가 항상 빈 문자열이 되던 결함을 교정한 것이다
|
|
52
|
+
# (r007-r008-drift-advisor.sh의 `.role` vs `.message.role`과 동일 계열).
|
|
53
|
+
# .tool_error / .tool_response.* fallback은 스키마 변화에 대한 방어로만 남긴다 —
|
|
54
|
+
# tool_response가 문자열인 경우 인덱싱 에러로 레코드가 통째로 유실되므로 type 검사로 감싼다.
|
|
55
|
+
#
|
|
56
|
+
# 단일 라인(<1KB) append 이므로 O_APPEND 원자성에 기대어 병렬 에이전트 환경에서도 안전.
|
|
57
|
+
printf '%s' "$input" \
|
|
58
|
+
| jq -c --arg ts "$ts" --arg cwd "$PWD" '
|
|
59
|
+
{
|
|
60
|
+
ts: $ts,
|
|
61
|
+
session: (.session_id // ""),
|
|
62
|
+
cwd: $cwd,
|
|
63
|
+
tool: (.tool_name // "unknown"),
|
|
64
|
+
target: ((.tool_input.command // .tool_input.file_path // "") | tostring | .[0:160]),
|
|
65
|
+
interrupt: (.is_interrupt == true),
|
|
66
|
+
err: ((.error
|
|
67
|
+
// .tool_error
|
|
68
|
+
// (if (.tool_response | type) == "object"
|
|
69
|
+
then (.tool_response.error // .tool_response.stderr)
|
|
70
|
+
else .tool_response end)
|
|
71
|
+
// "")
|
|
72
|
+
| tostring | gsub("\\s+"; " ") | .[0:320])
|
|
73
|
+
}' >> "$LEDGER" 2>/dev/null || true
|
|
74
|
+
|
|
75
|
+
exit 0
|
|
@@ -1,47 +1,111 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# r007-r008-drift-advisor.sh — PROACTIVE R007/R008 drift advisory (#1229, #1545, #1547)
|
|
2
|
+
# r007-r008-drift-advisor.sh — PROACTIVE R007/R008 drift advisory (#1229, #1545, #1547, #1553)
|
|
3
3
|
#
|
|
4
|
-
# Wired to
|
|
4
|
+
# Wired to THREE trigger points:
|
|
5
5
|
# 1. UserPromptSubmit — fires before Claude responds to a user-typed prompt (#1229).
|
|
6
6
|
# 2. SubagentStop — fires when a background subagent (Agent tool) completes, covering
|
|
7
7
|
# autonomous-loop re-entry (e.g. /fsd) where the orchestrator resumes WITHOUT a
|
|
8
8
|
# UserPromptSubmit event. Prior to #1545, autonomous-loop re-entry had zero R007/R008
|
|
9
9
|
# advisory coverage since UserPromptSubmit never fires in that path.
|
|
10
|
+
# 3. PostToolUse — fires after every tool call, covering the remaining structural gap
|
|
11
|
+
# (#1553): an orchestrator-only stretch BEFORE the first subagent spawn satisfies
|
|
12
|
+
# neither UserPromptSubmit (no user input) nor SubagentStop (no subagent yet).
|
|
13
|
+
# Measured 2026-08-05: 7/8 responses in such a stretch had missing R007 headers with
|
|
14
|
+
# zero advisory fires.
|
|
10
15
|
#
|
|
11
|
-
# Inspects the LAST completed assistant
|
|
12
|
-
# compliance
|
|
13
|
-
#
|
|
16
|
+
# Inspects the LAST completed assistant TURN in the session transcript for R007/R008
|
|
17
|
+
# compliance. If the turn drifted (missing identification header / tool prefix), delivers
|
|
18
|
+
# an advisory so the next response self-corrects.
|
|
14
19
|
#
|
|
15
20
|
# This is the PROACTIVE complement to the retroactive session-reflection.sh (Stop hook, #1190).
|
|
16
|
-
# Detection patterns are
|
|
21
|
+
# Detection patterns are shared with session-reflection.sh.
|
|
17
22
|
#
|
|
18
23
|
# Advisory-only: ALWAYS exits 0, NEVER blocks.
|
|
19
|
-
# Performance: parses ONLY the last assistant turn (not the whole transcript).
|
|
20
|
-
# Input-schema note: session_id/transcript_path are COMMON fields present on both
|
|
21
|
-
# UserPromptSubmit and SubagentStop hook payloads, so the detection logic below is
|
|
22
|
-
# event-agnostic and required no functional changes for the SubagentStop wiring.
|
|
23
24
|
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
25
|
+
# ── Transcript schema (MEASURED 2026-08-05, #1553) ────────────────────────────────────
|
|
26
|
+
# Claude Code JSONL lines do NOT carry a TOP-LEVEL `role`/`content`. The measured top-level
|
|
27
|
+
# key set is:
|
|
28
|
+
# attributionSkill, cwd, effort, entrypoint, gitBranch, isSidechain, message, parentUuid,
|
|
29
|
+
# requestId, sessionId, session_id, timestamp, type, userType, uuid, version
|
|
30
|
+
# The role lives at `.message.role` and the content blocks at `.message.content`.
|
|
31
|
+
# The previous implementation selected `.role` / `.content`, which ALWAYS evaluated to empty
|
|
32
|
+
# — so this advisor exited before ever reaching its detection logic and had NEVER fired
|
|
33
|
+
# (verified: 0 occurrences of `"additionalContext":` across 771 transcripts; a live probe
|
|
34
|
+
# produced 0 bytes on both stdout and stderr).
|
|
35
|
+
#
|
|
36
|
+
# Additional measured facts that shape this implementation:
|
|
37
|
+
# * ONE content block per JSONL line (867 assistant lines across 3 sessions, 0 multi-block).
|
|
38
|
+
# A single assistant TURN therefore spans MULTIPLE consecutive lines. Treating "the last
|
|
39
|
+
# assistant line" as "the last turn" makes the R008 adjacency test (`i > 0`) permanently
|
|
40
|
+
# false — every tool call would be reported as a violation. Turn reconstruction is a
|
|
41
|
+
# PRECONDITION for the PostToolUse wiring, not an optimization.
|
|
42
|
+
# * `isSidechain: true` marks subagent turns. They MUST be excluded or a subagent's turn
|
|
43
|
+
# is misattributed to the orchestrator. (Note: jq's `//` treats `false` as empty, so the
|
|
44
|
+
# filter is written as `(.isSidechain // false) != true`.)
|
|
45
|
+
# * User lines are NOT all turn boundaries — tool results arrive as `.message.role == "user"`
|
|
46
|
+
# with `tool_result` content blocks. Only a genuine prompt (string content, or an array
|
|
47
|
+
# with no tool_result block) ends a turn.
|
|
48
|
+
# * `thinking` blocks are interleaved with text/tool_use and never carry an R008 prefix;
|
|
49
|
+
# they are filtered out before analysis.
|
|
50
|
+
#
|
|
51
|
+
# ── R008 verdict: TURN-LEVEL COUNTING, not block adjacency (#1563 찐빠 #1) ─────────────
|
|
52
|
+
# R008 (`.claude/rules/MUST-tool-identification.md`) says, verbatim:
|
|
53
|
+
# "For parallel calls: list ALL identifications BEFORE the tool calls."
|
|
54
|
+
# The rule therefore requires the announce lines of a parallel BATCH to be grouped ahead of
|
|
55
|
+
# the batch — it does NOT require a text block wedged immediately before every single
|
|
56
|
+
# tool_use. The previous implementation tested block ADJACENCY (`$blocks[i-1]` is a text
|
|
57
|
+
# block matching the prefix), so in a compliant parallel batch `[text, tool_use, tool_use]`
|
|
58
|
+
# every tool_use after the first had a `tool_use` predecessor and was counted as a violation
|
|
59
|
+
# — R009 MANDATES those batches, so the advisor fired against rule-compliant behavior
|
|
60
|
+
# (measured: 212 bytes on a compliant live turn, 348 on a synthetic fixture; both must be 0).
|
|
61
|
+
#
|
|
62
|
+
# The verdict is now a per-turn count comparison:
|
|
63
|
+
# violations = max(0, tool_use_blocks − announce_lines)
|
|
64
|
+
#
|
|
65
|
+
# Announce lines counted (over ALL text blocks of the turn, split into lines):
|
|
66
|
+
# * `[agent][model] → Tool: X` — the Core Rule form; ONE per tool call.
|
|
67
|
+
# * `→ Target:` is NOT counted. It is the COMPANION line of `→ Tool:` (Core Rule prints the
|
|
68
|
+
# pair), so counting it would score 2 per tool and silently mask real omissions.
|
|
69
|
+
# * Spawn notation from R008 §"Parallel Spawn Prefix Rule", which documents parallel Agent
|
|
70
|
+
# calls as a `[agent][model] → Spawning:` header followed by one indented
|
|
71
|
+
# `[N] subagent_type:model → description` line per agent — with NO `→ Tool: Agent` line.
|
|
72
|
+
# The per-agent unit is the numbered line, so those are counted when present; the bare
|
|
73
|
+
# `→ Spawning:` header counts only when no numbered line exists (single-agent spawn, which
|
|
74
|
+
# R008 explicitly exempts from the `[N]` prefix). Excluding this notation would recreate
|
|
75
|
+
# exactly the false positive this fix removes (N Agent tool_use blocks, 0 `Tool:` lines).
|
|
76
|
+
#
|
|
77
|
+
# R007 detection (first line of the turn's first text block) is unchanged.
|
|
78
|
+
#
|
|
79
|
+
# ── Performance ───────────────────────────────────────────────────────────────────────
|
|
80
|
+
# The previous implementation forked jq once PER LINE inside a `while read` loop (measured
|
|
81
|
+
# 2.80s and 8.64s on real transcripts). PostToolUse fires on EVERY tool call, so that cost
|
|
82
|
+
# would stall the session. This version reads a bounded `tail -n 200` window and forks jq
|
|
83
|
+
# exactly ONCE for the whole analysis.
|
|
84
|
+
#
|
|
85
|
+
# ── Dedup ─────────────────────────────────────────────────────────────────────────────
|
|
86
|
+
# PostToolUse fires repeatedly within a single turn. The turn's first assistant line `uuid`
|
|
87
|
+
# is recorded in a per-session marker file; the same turn never produces a second advisory.
|
|
88
|
+
# The marker is derived purely from transcript content (no `date`, no randomness) so the
|
|
89
|
+
# behavior is idempotent and testable.
|
|
90
|
+
#
|
|
91
|
+
# ── Delivery mechanism (#1547 fix) ────────────────────────────────────────────────────
|
|
92
|
+
# Per the official Claude Code hook spec, stderr on exit 0 is NEVER fed into the model's
|
|
93
|
+
# context for ANY hook event (it is only visible in transcript debug mode, i.e. to a human).
|
|
94
|
+
# The confirmed non-blocking delivery path is `hookSpecificOutput.additionalContext` in
|
|
95
|
+
# JSON stdout with exit 0:
|
|
31
96
|
# {"hookSpecificOutput": {"hookEventName": "<event>", "additionalContext": "<text>"}}
|
|
97
|
+
# `hookEventName` MUST echo the ACTUAL firing event — a wrong value invalidates the output,
|
|
98
|
+
# so a missing `hook_event_name` field is a hard `exit 0` (no default is guessed).
|
|
32
99
|
# This is NOT the same as `"decision": "block"` — that would force Stop/SubagentStop to
|
|
33
|
-
# block
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
# any JSON output and treat stderr as a blocking error instead (see Common JSON Fields /
|
|
38
|
-
# Exit Code Behavior in the official hook reference).
|
|
39
|
-
# The stderr line is kept for human-visible audit trail (harmless on exit 0) but is no
|
|
40
|
-
# longer the delivery mechanism.
|
|
100
|
+
# block, exactly the behavior R021 (advisory-first enforcement) forbids here. Exit code MUST
|
|
101
|
+
# stay 0 — exit 2 causes Claude Code to discard JSON output and treat stderr as a blocking
|
|
102
|
+
# error instead.
|
|
103
|
+
# The stderr line is kept purely as a human-visible audit trail.
|
|
41
104
|
#
|
|
42
105
|
# 환경변수 override (테스트/디버깅용):
|
|
43
106
|
# OMCUSTOM_R007_ADVISOR=off — advisory 완전 비활성화 (pass-through)
|
|
44
|
-
# OMCUSTOM_TRANSCRIPT_BASE — transcript 디렉토리 경로 override
|
|
107
|
+
# OMCUSTOM_TRANSCRIPT_BASE — transcript 디렉토리 경로 override (설정 시 최우선)
|
|
108
|
+
# OMCUSTOM_R007_MARKER_DIR — dedup 마커 디렉토리 override (기본 ${TMPDIR:-/tmp})
|
|
45
109
|
|
|
46
110
|
set -euo pipefail
|
|
47
111
|
|
|
@@ -58,98 +122,118 @@ if ! command -v jq >/dev/null 2>&1; then
|
|
|
58
122
|
exit 0
|
|
59
123
|
fi
|
|
60
124
|
|
|
61
|
-
# ──
|
|
62
|
-
|
|
125
|
+
# ── 입력 필드 추출 (jq 1회 fork) ──
|
|
126
|
+
meta=$(printf '%s' "$input" | jq -r '[(.session_id // ""), (.hook_event_name // ""), (.transcript_path // "")] | @tsv' 2>/dev/null) || exit 0
|
|
127
|
+
session_id=$(printf '%s' "$meta" | cut -f1)
|
|
128
|
+
hook_event_name=$(printf '%s' "$meta" | cut -f2)
|
|
129
|
+
transcript_in=$(printf '%s' "$meta" | cut -f3)
|
|
130
|
+
|
|
63
131
|
if [ -z "$session_id" ]; then
|
|
64
132
|
exit 0
|
|
65
133
|
fi
|
|
66
134
|
|
|
67
|
-
#
|
|
68
|
-
|
|
135
|
+
# hook_event_name이 없으면 hookSpecificOutput.hookEventName을 정확히 채울 수 없다.
|
|
136
|
+
# 잘못된 기본값은 출력을 무효화하므로 추측하지 않고 즉시 종료한다 (fallback 제거).
|
|
69
137
|
if [ -z "$hook_event_name" ]; then
|
|
70
|
-
|
|
138
|
+
exit 0
|
|
71
139
|
fi
|
|
72
140
|
|
|
73
|
-
# ── 경로 결정
|
|
74
|
-
|
|
75
|
-
|
|
141
|
+
# ── transcript 경로 결정 ──
|
|
142
|
+
# 우선순위: 테스트 override > 훅 페이로드의 transcript_path > 기본 경로
|
|
143
|
+
if [ -n "${OMCUSTOM_TRANSCRIPT_BASE:-}" ]; then
|
|
144
|
+
TRANSCRIPT_PATH="${OMCUSTOM_TRANSCRIPT_BASE}/${session_id}.jsonl"
|
|
145
|
+
elif [ -n "$transcript_in" ]; then
|
|
146
|
+
TRANSCRIPT_PATH="$transcript_in"
|
|
147
|
+
else
|
|
148
|
+
TRANSCRIPT_PATH="${HOME}/.claude/projects/-Users-sangyi-workspace-projects-oh-my-customcode/${session_id}.jsonl"
|
|
149
|
+
fi
|
|
76
150
|
|
|
77
151
|
if [ ! -f "$TRANSCRIPT_PATH" ]; then
|
|
78
152
|
exit 0
|
|
79
153
|
fi
|
|
80
154
|
|
|
81
|
-
# ── 마지막 assistant
|
|
82
|
-
#
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
155
|
+
# ── 마지막 assistant 턴 재구성 + R007/R008 판정 (jq 1회 fork) ──
|
|
156
|
+
# 출력: "<turn-uuid>\t<r007-count>\t<r008-count>" (턴이 없으면 무출력)
|
|
157
|
+
JQ_LAST_TURN='
|
|
158
|
+
split("\n")
|
|
159
|
+
| map(select(length > 0) | (fromjson? // empty))
|
|
160
|
+
| map(select((.isSidechain // false) != true))
|
|
161
|
+
| . as $L
|
|
162
|
+
| ($L | length) as $n
|
|
163
|
+
| [ range(0; $n) | select($L[.].message.role? == "assistant") ] as $ai
|
|
164
|
+
| if ($ai | length) == 0 then empty
|
|
165
|
+
else
|
|
166
|
+
($ai[-1]) as $last
|
|
167
|
+
| [ range(0; $last + 1)
|
|
168
|
+
| select( ($L[.].message.role? == "user")
|
|
169
|
+
and ( (($L[.].message.content | type) == "string")
|
|
170
|
+
or (([ $L[.].message.content[]? | select(.type? == "tool_result") ] | length) == 0) ) ) ] as $bi
|
|
171
|
+
| (if ($bi | length) > 0 then $bi[-1] else -1 end) as $b
|
|
172
|
+
| [ range($b + 1; $last + 1) | $L[.] | select(.message.role? == "assistant") ] as $turn
|
|
173
|
+
| [ $turn[] | .message.content[]? | select(.type? != "thinking") ] as $blocks
|
|
174
|
+
| ($turn[0].uuid? // "") as $tuuid
|
|
175
|
+
| ([ $blocks[] | select(.type? == "text") ][0].text? // "") as $ftext
|
|
176
|
+
| (($ftext | split("\n") | .[0]) // "") as $fline
|
|
177
|
+
| (if ($ftext | length) == 0 then 0
|
|
178
|
+
elif ($fline | test("^┌─ Agent:")) or ($fline | test("^\\[.+\\]")) then 0
|
|
179
|
+
else 1 end) as $r007
|
|
180
|
+
| ([ $blocks[] | select(.type? == "text") | (.text? // "") ] | join("\n") | split("\n")) as $lines
|
|
181
|
+
| ([ $lines[] | select(test("\\[.+\\]\\[.+\\] ?(→|->|—>) ?Tool:")) ] | length) as $an_tool
|
|
182
|
+
| ([ $lines[] | select(test("^[[:space:]]*\\[[0-9]+\\][[:space:]].*(→|->|—>)")) ] | length) as $an_spawn_item
|
|
183
|
+
| ([ $lines[] | select(test("\\[.+\\]\\[.+\\] ?(→|->|—>) ?Spawning:")) ] | length) as $an_spawn_hdr
|
|
184
|
+
| ($an_tool + (if $an_spawn_item > 0 then $an_spawn_item else $an_spawn_hdr end)) as $announce
|
|
185
|
+
| ([ $blocks[] | select(.type? == "tool_use") ] | length) as $ntools
|
|
186
|
+
| (if $ntools > $announce then $ntools - $announce else 0 end) as $r008
|
|
187
|
+
| [$tuuid, ($r007 | tostring), ($r008 | tostring)] | @tsv
|
|
188
|
+
end
|
|
189
|
+
'
|
|
190
|
+
|
|
191
|
+
result=$(tail -n 200 "$TRANSCRIPT_PATH" 2>/dev/null | jq -Rsr "$JQ_LAST_TURN" 2>/dev/null) || result=""
|
|
192
|
+
|
|
193
|
+
if [ -z "$result" ]; then
|
|
93
194
|
exit 0
|
|
94
195
|
fi
|
|
95
196
|
|
|
96
|
-
|
|
97
|
-
|
|
197
|
+
turn_uuid=$(printf '%s' "$result" | cut -f1)
|
|
198
|
+
r007_violations=$(printf '%s' "$result" | cut -f2)
|
|
199
|
+
r008_violations=$(printf '%s' "$result" | cut -f3)
|
|
98
200
|
|
|
99
|
-
r007_violations
|
|
100
|
-
r008_violations
|
|
201
|
+
: "${r007_violations:=0}"
|
|
202
|
+
: "${r008_violations:=0}"
|
|
101
203
|
|
|
102
|
-
# ──
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
first_line=$(printf '%s' "$first_text" | head -1)
|
|
106
|
-
# R007 패턴: '┌─ Agent:' 또는 '[anything]' 단축 형태
|
|
107
|
-
if ! printf '%s' "$first_line" | grep -qE '(^┌─ Agent:|^\[.+\])'; then
|
|
108
|
-
r007_violations=$((r007_violations + 1))
|
|
109
|
-
fi
|
|
204
|
+
# ── 위반이 없으면 아무것도 출력하지 않는다 (오탐 방지) ──
|
|
205
|
+
if [ "$r007_violations" -eq 0 ] && [ "$r008_violations" -eq 0 ]; then
|
|
206
|
+
exit 0
|
|
110
207
|
fi
|
|
111
208
|
|
|
112
|
-
# ──
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if [ "$prev_type" = "text" ]; then
|
|
123
|
-
prev_text=$(echo "$content_raw" | jq -r ".[$(( i - 1 ))].text // empty" 2>/dev/null) || true
|
|
124
|
-
# R008 패턴: '[agent-name][model] → Tool:' 또는 '→ Target:'
|
|
125
|
-
if printf '%s' "$prev_text" | grep -qE '\[.+\]\[.+\] ?(→|->|—>) ?(Tool|Target):'; then
|
|
126
|
-
has_prefix=true
|
|
127
|
-
fi
|
|
128
|
-
fi
|
|
129
|
-
fi
|
|
130
|
-
if [ "$has_prefix" = "false" ]; then
|
|
131
|
-
r008_violations=$((r008_violations + 1))
|
|
132
|
-
fi
|
|
209
|
+
# ── dedup: 같은 턴에 대해 두 번 발화하지 않는다 ──
|
|
210
|
+
# 마커 키는 transcript 내용(턴의 첫 assistant uuid)에서만 파생 — date/랜덤 사용 금지.
|
|
211
|
+
MARKER_DIR="${OMCUSTOM_R007_MARKER_DIR:-${TMPDIR:-/tmp}}"
|
|
212
|
+
marker_key=$(printf '%s' "$session_id" | tr -c 'A-Za-z0-9._-' '_')
|
|
213
|
+
MARKER_FILE="${MARKER_DIR}/.omcustom-r007-advisor-${marker_key}"
|
|
214
|
+
|
|
215
|
+
if [ -n "$turn_uuid" ] && [ -f "$MARKER_FILE" ]; then
|
|
216
|
+
prev_uuid=$(cat "$MARKER_FILE" 2>/dev/null || printf '')
|
|
217
|
+
if [ "$prev_uuid" = "$turn_uuid" ]; then
|
|
218
|
+
exit 0
|
|
133
219
|
fi
|
|
220
|
+
fi
|
|
134
221
|
|
|
135
|
-
|
|
136
|
-
|
|
222
|
+
advisory_text=$(printf '[R007/R008 Advisory] 직전 응답에서 식별 누락 감지 (R007 헤더=%s, R008 접두사=%s). 이번 응답은 ┌─ Agent: 헤더로 시작하고, 모든 도구 호출에 [agent][model] → Tool: 접두사를 포함하십시오.' \
|
|
223
|
+
"$r007_violations" "$r008_violations")
|
|
137
224
|
|
|
138
|
-
#
|
|
139
|
-
|
|
140
|
-
advisory_text=$(printf '[R007/R008 Advisory] 직전 응답에서 식별 누락 감지 (R007 헤더=%d, R008 접두사=%d). 이번 응답은 ┌─ Agent: 헤더로 시작하고, 모든 도구 호출에 [agent][model] → Tool: 접두사를 포함하십시오.' \
|
|
141
|
-
"$r007_violations" "$r008_violations")
|
|
225
|
+
# 사람이 보는 감사 추적용 (exit 0에서는 모델에 전달되지 않음 — #1547 참고)
|
|
226
|
+
printf '%s\n' "$advisory_text" >&2
|
|
142
227
|
|
|
143
|
-
|
|
144
|
-
|
|
228
|
+
# #1547 fix: hookSpecificOutput.additionalContext로 모델 컨텍스트에 실제 전달.
|
|
229
|
+
# decision 필드는 절대 포함하지 않는다 — "block"을 쓰면 Stop/SubagentStop 정지를
|
|
230
|
+
# 강제로 막아버려 advisory-only 원칙(R021)을 위반하게 된다. exit code는 반드시 0.
|
|
231
|
+
jq -cn --arg event "$hook_event_name" --arg ctx "$advisory_text" \
|
|
232
|
+
'{hookSpecificOutput: {hookEventName: $event, additionalContext: $ctx}}'
|
|
145
233
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
# 강제로 막아버려 advisory-only 원칙(R021)을 위반하게 된다. exit code는 반드시 0.
|
|
149
|
-
jq -cn --arg event "$hook_event_name" --arg ctx "$advisory_text" \
|
|
150
|
-
'{hookSpecificOutput: {hookEventName: $event, additionalContext: $ctx}}'
|
|
234
|
+
if [ -n "$turn_uuid" ]; then
|
|
235
|
+
printf '%s' "$turn_uuid" > "$MARKER_FILE" 2>/dev/null || true
|
|
151
236
|
fi
|
|
152
237
|
|
|
153
|
-
# ── 위반이 없으면 아무것도 출력하지 않는다 (오탐 방지) ──
|
|
154
238
|
# ── 항상 exit 0 (advisory는 절대 차단 금지) ──
|
|
155
239
|
exit 0
|