oh-my-customcode 1.1.52 → 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 +509 -5
- package/dist/index.js +505 -1
- package/package.json +1 -1
- package/templates/.claude/hooks/hooks.json +24 -34
- 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/.claude/rules/MUST-enforcement-policy.md +8 -2
- package/templates/.claude/settings.json +650 -0
- package/templates/manifest.json +1 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"command": "bash .claude/hooks/scripts/stage-blocker.sh"
|
|
11
11
|
}
|
|
12
12
|
],
|
|
13
|
-
"description": "Block Write/Edit tools during plan/verify/compound stages
|
|
13
|
+
"description": "Block Write/Edit tools during plan/verify/compound stages — only allow in implement stage"
|
|
14
14
|
},
|
|
15
15
|
{
|
|
16
16
|
"matcher": "tool == \"Bash\" && tool_input.command matches \"(npm run dev|pnpm( run)? dev|yarn dev|bun run dev)\"",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"command": "bash .claude/hooks/scripts/content-hash-validator.sh"
|
|
71
71
|
}
|
|
72
72
|
],
|
|
73
|
-
"description": "Validate file content hash before Edit
|
|
73
|
+
"description": "Validate file content hash before Edit — advisory staleness warning"
|
|
74
74
|
},
|
|
75
75
|
{
|
|
76
76
|
"matcher": "tool == \"Write\" || tool == \"Edit\" || tool == \"Bash\"",
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"command": "bash .claude/hooks/scripts/schema-validator.sh"
|
|
81
81
|
}
|
|
82
82
|
],
|
|
83
|
-
"description": "Schema-based tool input validation
|
|
83
|
+
"description": "Schema-based tool input validation — Phase 1 advisory only"
|
|
84
84
|
},
|
|
85
85
|
{
|
|
86
86
|
"matcher": "tool == \"Bash\"",
|
|
@@ -110,14 +110,14 @@
|
|
|
110
110
|
"command": "bash .claude/hooks/scripts/rule-deletion-guard.sh"
|
|
111
111
|
}
|
|
112
112
|
],
|
|
113
|
-
"description": "Block rule file deletion
|
|
113
|
+
"description": "Block rule file deletion — requires individual user confirmation per rule (R001 safety)"
|
|
114
114
|
},
|
|
115
115
|
{
|
|
116
116
|
"matcher": "tool == \"Task\" || tool == \"Agent\"",
|
|
117
117
|
"hooks": [
|
|
118
118
|
{
|
|
119
119
|
"type": "command",
|
|
120
|
-
"command": "#!/bin/bash\ninput=$(cat)\nagent_type=$(echo \"$input\" | jq -r '.tool_input.subagent_type // \"unknown\"')\nmodel=$(echo \"$input\" | jq -r '.tool_input.model // \"inherit\"')\ndesc=$(echo \"$input\" | jq -r '.tool_input.description // \"\"' | head -c 40)\nresume=$(echo \"$input\" | jq -r '.tool_input.resume // empty')\nif [ -n \"$resume\" ]; then\n echo \"
|
|
120
|
+
"command": "#!/bin/bash\ninput=$(cat)\nagent_type=$(echo \"$input\" | jq -r '.tool_input.subagent_type // \"unknown\"')\nmodel=$(echo \"$input\" | jq -r '.tool_input.model // \"inherit\"')\ndesc=$(echo \"$input\" | jq -r '.tool_input.description // \"\"' | head -c 40)\nresume=$(echo \"$input\" | jq -r '.tool_input.resume // empty')\nif [ -n \"$resume\" ]; then\n echo \"─── [Resume] ${agent_type}:${model} | ${desc} ───\" >&2\nelse\n echo \"─── [Spawn] ${agent_type}:${model} | ${desc} ───\" >&2\nfi\necho \"$input\""
|
|
121
121
|
},
|
|
122
122
|
{
|
|
123
123
|
"type": "command",
|
|
@@ -216,7 +216,7 @@
|
|
|
216
216
|
"command": "bash .claude/hooks/scripts/user-prompt-preprocessor.sh"
|
|
217
217
|
}
|
|
218
218
|
],
|
|
219
|
-
"description": "Advisory pre-processing of user input
|
|
219
|
+
"description": "Advisory pre-processing of user input — skill matching hints and intent analysis"
|
|
220
220
|
},
|
|
221
221
|
{
|
|
222
222
|
"matcher": "*",
|
|
@@ -226,7 +226,7 @@
|
|
|
226
226
|
"command": "bash .claude/hooks/scripts/r007-r008-drift-advisor.sh"
|
|
227
227
|
}
|
|
228
228
|
],
|
|
229
|
-
"description": "Proactive R007/R008 drift advisory
|
|
229
|
+
"description": "Proactive R007/R008 drift advisory — checks last assistant turn for identification omission (#1229)"
|
|
230
230
|
},
|
|
231
231
|
{
|
|
232
232
|
"matcher": "*",
|
|
@@ -246,7 +246,7 @@
|
|
|
246
246
|
"command": "bash .claude/hooks/scripts/fail-axis-cause-advisor.sh"
|
|
247
247
|
}
|
|
248
248
|
],
|
|
249
|
-
"description": "Advisory on cause-free progress nudges when this session has logged tool failures
|
|
249
|
+
"description": "Advisory on cause-free progress nudges when this session has logged tool failures — delivers via hookSpecificOutput.additionalContext (#1561)"
|
|
250
250
|
}
|
|
251
251
|
],
|
|
252
252
|
"SubagentStart": [
|
|
@@ -255,7 +255,7 @@
|
|
|
255
255
|
"hooks": [
|
|
256
256
|
{
|
|
257
257
|
"type": "command",
|
|
258
|
-
"command": "#!/bin/bash\ninput=$(cat)\nagent_type=$(echo \"$input\" | jq -r '.agent_type // \"unknown\"')\nmodel=$(echo \"$input\" | jq -r '.model // \"inherit\"')\ndesc=$(echo \"$input\" | jq -r '.description // \"\"' | head -c 40)\necho \"
|
|
258
|
+
"command": "#!/bin/bash\ninput=$(cat)\nagent_type=$(echo \"$input\" | jq -r '.agent_type // \"unknown\"')\nmodel=$(echo \"$input\" | jq -r '.model // \"inherit\"')\ndesc=$(echo \"$input\" | jq -r '.description // \"\"' | head -c 40)\necho \"─── [SubagentStart] ${agent_type}:${model} | ${desc} ───\" >&2\necho \"$input\""
|
|
259
259
|
},
|
|
260
260
|
{
|
|
261
261
|
"type": "command",
|
|
@@ -287,7 +287,7 @@
|
|
|
287
287
|
},
|
|
288
288
|
{
|
|
289
289
|
"type": "prompt",
|
|
290
|
-
"prompt": "A background subagent just completed. If it FAILED, do NOT auto-continue
|
|
290
|
+
"prompt": "A background subagent just completed. If it FAILED, do NOT auto-continue — report the failure and wait for user input."
|
|
291
291
|
}
|
|
292
292
|
],
|
|
293
293
|
"description": "Record agent outcomes + auto-continue workflow + R007/R008 drift advisory on subagent completion (autonomous-loop re-entry, #1545)"
|
|
@@ -314,7 +314,7 @@
|
|
|
314
314
|
"command": "bash .claude/hooks/scripts/file-change-validator.sh"
|
|
315
315
|
}
|
|
316
316
|
],
|
|
317
|
-
"description": "Validate externally modified files
|
|
317
|
+
"description": "Validate externally modified files — advisory staleness warning (v2.1.83+)"
|
|
318
318
|
}
|
|
319
319
|
],
|
|
320
320
|
"Notification": [
|
|
@@ -335,7 +335,7 @@
|
|
|
335
335
|
"hooks": [
|
|
336
336
|
{
|
|
337
337
|
"type": "command",
|
|
338
|
-
"command": "#!/bin/bash\ninput=$(cat)\nteammate=$(echo \"$input\" | jq -r '.teammate_id // \"unknown\"' 2>/dev/null)\necho \"[Hook] TeammateIdle: $teammate
|
|
338
|
+
"command": "#!/bin/bash\ninput=$(cat)\nteammate=$(echo \"$input\" | jq -r '.teammate_id // \"unknown\"' 2>/dev/null)\necho \"[Hook] TeammateIdle: $teammate — available for reassignment\" >&2\necho \"$input\""
|
|
339
339
|
}
|
|
340
340
|
],
|
|
341
341
|
"description": "Track idle Agent Teams members for work reassignment"
|
|
@@ -347,7 +347,7 @@
|
|
|
347
347
|
"hooks": [
|
|
348
348
|
{
|
|
349
349
|
"type": "command",
|
|
350
|
-
"command": "#!/bin/bash\ninput=$(cat)\ntask_id=$(echo \"$input\" | jq -r '.task_id // \"\"' 2>/dev/null)\ndesc=$(echo \"$input\" | jq -r '.description // \"\"' 2>/dev/null | head -c 60)\necho \"[Hook] TaskCreated: $task_id
|
|
350
|
+
"command": "#!/bin/bash\ninput=$(cat)\ntask_id=$(echo \"$input\" | jq -r '.task_id // \"\"' 2>/dev/null)\ndesc=$(echo \"$input\" | jq -r '.description // \"\"' 2>/dev/null | head -c 60)\necho \"[Hook] TaskCreated: $task_id — $desc\" >&2\necho \"$input\""
|
|
351
351
|
}
|
|
352
352
|
],
|
|
353
353
|
"description": "Log task creation events for Agent Teams coordination"
|
|
@@ -359,7 +359,7 @@
|
|
|
359
359
|
"hooks": [
|
|
360
360
|
{
|
|
361
361
|
"type": "command",
|
|
362
|
-
"command": "#!/bin/bash\ninput=$(cat)\ntask_id=$(echo \"$input\" | jq -r '.task_id // \"\"' 2>/dev/null)\nresult=$(echo \"$input\" | jq -r '.result // \"\"' 2>/dev/null | head -c 60)\necho \"[Hook] TaskCompleted: $task_id
|
|
362
|
+
"command": "#!/bin/bash\ninput=$(cat)\ntask_id=$(echo \"$input\" | jq -r '.task_id // \"\"' 2>/dev/null)\nresult=$(echo \"$input\" | jq -r '.result // \"\"' 2>/dev/null | head -c 60)\necho \"[Hook] TaskCompleted: $task_id — $result\" >&2\necho \"$input\""
|
|
363
363
|
}
|
|
364
364
|
],
|
|
365
365
|
"description": "Log task completion events for Agent Teams coordination"
|
|
@@ -374,7 +374,7 @@
|
|
|
374
374
|
"command": "bash .claude/hooks/scripts/task-state-precompact.sh"
|
|
375
375
|
}
|
|
376
376
|
],
|
|
377
|
-
"description": "Serialize active task state before compaction
|
|
377
|
+
"description": "Serialize active task state before compaction — pairs with PostCompact restore (#922)"
|
|
378
378
|
}
|
|
379
379
|
],
|
|
380
380
|
"PostCompact": [
|
|
@@ -383,10 +383,10 @@
|
|
|
383
383
|
"hooks": [
|
|
384
384
|
{
|
|
385
385
|
"type": "prompt",
|
|
386
|
-
"prompt": "Context compacted. RULES ACTIVE.\n\nR007: next response MUST start with
|
|
386
|
+
"prompt": "Context compacted. RULES ACTIVE.\n\nR007: next response MUST start with ┌─ Agent: claude (default) / └─ Task: {task}\nR008: before EVERY tool call → [claude][model] → Tool: Name / Target: path\nR010: write→delegate to subagent; orchestrator read-only (Read/Glob/Grep); git→mgr-gitnerd\nR009: 2+ independent tasks → parallel agents, same message\nR018: 3+ agents OR review cycle → Agent Teams\nR000: user communication in Korean (한국어); code/files in English — NON-NEGOTIABLE\nR001: prohibited actions (no secrets, no system files, no rm -rf); stop on violation → preserve state → report → wait\nR002: tool tiers (Read/Grep=free, Write/Edit=notify, Bash=approval); no writes outside project; no .env/.git/config\n\nAutonomous mode: if /tmp/.claude-autonomous-$PPID exists → simple git (add/commit/push) may run directly; Write/Edit still delegates.\nbypassPermissions does NOT persist across compaction — re-enable if tasks seem blocked.\n\nTask state restore: if /tmp/.claude-task-state-$PPID.json exists, read it to recover active pipeline, dev stage, and autonomous mode state. This was saved by PreCompact hook.\n\nRe-read CLAUDE.md NOW."
|
|
387
387
|
}
|
|
388
388
|
],
|
|
389
|
-
"description": "Reinforce enforced rules after context compaction
|
|
389
|
+
"description": "Reinforce enforced rules after context compaction — prevents rule amnesia (v2.1.76+)"
|
|
390
390
|
}
|
|
391
391
|
],
|
|
392
392
|
"PostToolUse": [
|
|
@@ -473,7 +473,7 @@
|
|
|
473
473
|
"continueOnBlock": true
|
|
474
474
|
}
|
|
475
475
|
],
|
|
476
|
-
"description": "Context budget advisor
|
|
476
|
+
"description": "Context budget advisor — track tool usage patterns and advise ecomode activation"
|
|
477
477
|
},
|
|
478
478
|
{
|
|
479
479
|
"matcher": "tool == \"Edit\" || tool == \"Write\" || tool == \"Bash\" || tool == \"Task\" || tool == \"Agent\" || tool == \"Read\" || tool == \"Glob\" || tool == \"Grep\"",
|
|
@@ -484,7 +484,7 @@
|
|
|
484
484
|
"continueOnBlock": true
|
|
485
485
|
}
|
|
486
486
|
],
|
|
487
|
-
"description": "Proactive R007/R008 drift advisory on tool use
|
|
487
|
+
"description": "Proactive R007/R008 drift advisory on tool use — covers the orchestrator-only stretch before the first subagent spawn, where neither UserPromptSubmit nor SubagentStop fires (#1553). Turn-deduplicated; exit 0 advisory only."
|
|
488
488
|
},
|
|
489
489
|
{
|
|
490
490
|
"matcher": "tool == \"Edit\" || tool == \"Write\" || tool == \"Bash\" || tool == \"Task\" || tool == \"Agent\"",
|
|
@@ -506,7 +506,7 @@
|
|
|
506
506
|
"continueOnBlock": true
|
|
507
507
|
}
|
|
508
508
|
],
|
|
509
|
-
"description": "Advisory cost cap monitoring
|
|
509
|
+
"description": "Advisory cost cap monitoring — warn when session cost approaches configurable limit"
|
|
510
510
|
},
|
|
511
511
|
{
|
|
512
512
|
"matcher": "tool == \"Read\"",
|
|
@@ -516,7 +516,7 @@
|
|
|
516
516
|
"command": "bash .claude/hooks/scripts/content-hash-validator.sh"
|
|
517
517
|
}
|
|
518
518
|
],
|
|
519
|
-
"description": "Store content hashes for Read operations
|
|
519
|
+
"description": "Store content hashes for Read operations — enables Edit staleness detection"
|
|
520
520
|
},
|
|
521
521
|
{
|
|
522
522
|
"matcher": "tool == \"Bash\" || tool == \"Read\" || tool == \"Grep\"",
|
|
@@ -526,7 +526,7 @@
|
|
|
526
526
|
"command": "bash .claude/hooks/scripts/secret-filter.sh"
|
|
527
527
|
}
|
|
528
528
|
],
|
|
529
|
-
"description": "Detect potential secrets in Bash/Read/Grep output
|
|
529
|
+
"description": "Detect potential secrets in Bash/Read/Grep output — advisory warning only"
|
|
530
530
|
},
|
|
531
531
|
{
|
|
532
532
|
"matcher": "tool == \"Edit\" || tool == \"Write\" || tool == \"Bash\" || tool == \"Agent\"",
|
|
@@ -567,7 +567,7 @@
|
|
|
567
567
|
"command": "bash .claude/hooks/scripts/auto-dev-token-tracker.sh"
|
|
568
568
|
}
|
|
569
569
|
],
|
|
570
|
-
"description": "auto-dev token spend tracker
|
|
570
|
+
"description": "auto-dev token spend tracker — records per-phase token estimates during /pipeline auto-dev (Issue #1057, advisory)"
|
|
571
571
|
}
|
|
572
572
|
],
|
|
573
573
|
"Stop": [
|
|
@@ -621,16 +621,6 @@
|
|
|
621
621
|
],
|
|
622
622
|
"description": "Detect reusable skill candidates from session task outcomes (advisory, exit 0)"
|
|
623
623
|
},
|
|
624
|
-
{
|
|
625
|
-
"matcher": "*",
|
|
626
|
-
"hooks": [
|
|
627
|
-
{
|
|
628
|
-
"type": "prompt",
|
|
629
|
-
"prompt": "Session-end memory checkpoint (R011 enforcement). Check if the session-end memory save workflow was completed: 1) sys-memory-keeper was delegated to update MEMORY.md. Note: episodic-memory auto-indexes after session \u2014 no manual action needed. Decision rules (evaluate in order): 1. If session had no explicit session-end signal (quick question, no memory work): APPROVE immediately. 2. If the step was already attempted (success or failure both count): APPROVE. 3. If the step was NOT yet performed: PERFORM IT NOW (maximum 1 attempt): a. Delegate to sys-memory-keeper to update MEMORY.md \u2014 call Agent tool with sys-memory-keeper. b. After completion (success or failure), APPROVE stop. IMPORTANT: Do NOT block indefinitely. After 1 attempt, always APPROVE. IMPORTANT: Do NOT loop \u2014 if you already performed this step in this same stop hook evaluation, APPROVE."
|
|
630
|
-
}
|
|
631
|
-
],
|
|
632
|
-
"description": "Enforce R011 session-end memory saves \u2014 auto-perform if not yet done (episodic-memory auto-indexes)"
|
|
633
|
-
},
|
|
634
624
|
{
|
|
635
625
|
"matcher": "*",
|
|
636
626
|
"hooks": [
|
|
@@ -651,7 +641,7 @@
|
|
|
651
641
|
"command": "bash .claude/hooks/scripts/failure-ledger.sh"
|
|
652
642
|
}
|
|
653
643
|
],
|
|
654
|
-
"description": "FAIL-axis instrumentation
|
|
644
|
+
"description": "FAIL-axis instrumentation — append tool failures to the error ledger (JSONL). Never blocks; feeds fail-axis-cause-advisor.sh (#1561)"
|
|
655
645
|
}
|
|
656
646
|
]
|
|
657
647
|
}
|
|
@@ -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
|
|
|
@@ -21,9 +21,15 @@ oh-my-customcode uses an **advisory-first enforcement model**. Most rules are en
|
|
|
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 |
|
|
23
23
|
|
|
24
|
-
>
|
|
24
|
+
> **훅 배선 경로 (v1.1.53, #1623)**: `.claude/hooks/hooks.json`은 CC가 로드하는 파일이 아니다 — CC 2.1.251 바이너리에 `.claude/hooks` 경로 참조가 0건이고, `settings.json`에 `hooks` 키가 이력상 존재한 적이 없었다. 컴파일레이션 메타포로는 `hooks.json` = **소스**(source), CC가 실제 로드하는 것은 `.claude/settings.json`(+`.local.json`, +`templates/` 미러)의 공식 `hooks` 블록 = **빌드 산출물**이며, 변환은 `src/core/hooks-settings.ts`가 담당한다(matcher 조건 DSL 12건은 스크립트 자체 가드 3건 + stdin-가드 래퍼 9건으로 이관). `omcustom init` 사용자는 `installHooksSettings()`가 최종 settings.local.json에 병합하는 경로를 거친다. **`hooks.json`만 고치고 settings 재생성(빌드)을 누락하면 변경이 발화에 반영되지 않는다** — R022 Wiki Sync의 "페이지 갱신 + 매니페스트 재시딩" 이원 요구와 동형이다. 라이브 실증(2026-08-29 새 세션 프로브): SessionStart hook_success 9건(플러그인 2 + 프로젝트 7), `[claude-md-reinject]` 마커 5히트, UserPromptSubmit·Stop 발화 확인 — **세션 단위 발화의 최초 실측**은 이 릴리즈다.
|
|
25
|
+
|
|
26
|
+
| Anti-pattern | Required |
|
|
27
|
+
|--------------|----------|
|
|
28
|
+
| `hooks.json`만 편집하고 커밋 → settings.json `hooks` 블록 미재생성 | 편집 후 `hooks-settings.ts` 변환기(빌드) 실행 → settings.json(+local+templates) 재생성 확인 후 커밋 |
|
|
29
|
+
|
|
30
|
+
> **Advisory (proactive/retroactive) 발화 결함과 해소 (실측)**: `hookSpecificOutput.additionalContext` **전달 경로 자체는 #1547(v1.1.40)에서 구현**됐으나, 그 앞단 **파서 셀렉터 결함**으로 advisory가 **v1.1.42까지 한 번도 발화하지 못했다** — `jq -r '.role'`로 읽었으나 트랜스크립트 최상위에 `role` 키가 없어(실제는 `.message.role`) `last_assistant`가 항상 비고 즉시 `exit 0`으로 종료됐다. 당시 실측: 트랜스크립트 771개 전수에서 `"additionalContext":` 출현 0건, 라이브 프로브 stdout/stderr 각 0바이트. **proactive(`r007-r008-drift-advisor.sh`)와 retroactive(`session-reflection.sh`, 동일 결함) 두 계층 모두 미발화**였다. **v1.1.43에서 양 계층 파서 복구 + `PostToolUse` 배선을 완료했고, 라이브 프로브로 최초 발화를 확인했다(#1553).** **재검토(#1623)**: 이 서술은 세션 단위 CC 훅 서브시스템 경유 증거가 없다 — 당시에도 CC가 로드하는 `settings.json`에 `hooks` 블록이 부재했으므로, 리서치(git 이력 전수) 판단으로는 스크립트에 synthetic stdin을 직접 주입한 검증이었을 가능성이 가장 유력하다(단정 금지 — "직접 증거 없음"으로 기록). 세션 단위 발화의 최초 실측은 위 「훅 배선 경로 (v1.1.53, #1623)」의 v1.1.53이다. 후속으로 v1.1.44에서 R008 판정을 블록 인접 비교 → 턴 단위 개수 비교로 전환(#1563), v1.1.45에서 Skill 도구 면제를 추가했다(#1569). **v1.1.49에서 역방향 신호**(announce > tool_use — 도구 호출을 예고해 놓고 tool_use 블록 없이 턴을 종료한 방향)**를 추가했다(#1595 #6)** — 기존 판정식 `max(0, tool_use − announce)`는 이 방향을 **구조적으로 0으로 처리**해 원리적으로 탐지 불가였다. 역방향은 **전용 앵커 정규식**(`$an_anchored`, 줄 시작 앵커 있음 — forward의 `$an_tool`에는 적용하지 않는다. forward는 announce를 덜 세면 위반이 **늘어나기** 때문)을 쓰고, **Skill 포함 전체 tool_use가 0건**일 때만 계상한다(Skill 제외 카운트를 쓰면 Skill만 호출한 준수 턴에서 오발화). 기본 off 옵트인(`OMCUSTOM_R008_REVERSE=on`으로 활성)이다 — 482턴 실측에서 순진한 `announce − ntools > 0` 구현은 36턴에 발화해 advisory 총량을 2배로 만들었고(16건은 Skill 제외 아티팩트, 15건은 앵커 없는 정규식의 산문 매칭), 협소화 후 3/3 진양성·오탐 0(앵커 비용은 실제 announce 969줄 중 1줄, 0.1%)이 되었으나 표본이 3건이라 기본 활성은 보류했다. **배선 구조상 예방 효과가 없다는 점도 보류 근거다** — 결함 턴은 tool_use가 0이라 `PostToolUse`·`SubagentStop`이 발화하지 않고, `UserPromptSubmit`은 사용자가 이미 개입한 뒤 발화한다. 정시에 발화하는 유일한 이벤트는 `Stop`이며 거기 걸린 훅은 `session-reflection.sh`다. 역방향은 그래서 **의도적으로 advisor 전용**이며 `session-reflection.sh`에는 복제하지 않았다(같은 결함을 두 번 보고하면서 교정 기회는 여전히 0이 되고, 되돌릴 지점만 두 곳이 된다).
|
|
25
31
|
>
|
|
26
|
-
> 교훈: **배선 확인 ≠ 전달 확인 ≠ 발화 확인** — R020 "actual outcome ≠ attempt"의 훅 도메인 재현 사례.
|
|
32
|
+
> 교훈: **배선 확인 ≠ 전달 확인 ≠ 발화 확인 ≠ 로드 확인** — R020 "actual outcome ≠ attempt"의 훅 도메인 재현 사례. v1.1.53(#1623)이 드러낸 것은 이 셋보다 앞선 **제4층**이다 — 배선 파일(`hooks.json`) 자체가 CC에 **로드되지 않았다면** 배선·전달·발화 확인은 전부 무의미한 층 위에서 이루어진 것이다.
|
|
27
33
|
|
|
28
34
|
<!--
|
|
29
35
|
> **v2.1.163+**: Stop and SubagentStop hooks can return `hookSpecificOutput.additionalContext` (JSON) to feed structured feedback back into Claude's context without triggering a hook error label. This enables advisory-style enforcement via Stop/SubagentStop hooks (e.g., `session-reflection.sh`, omcustom-loop SubagentStop) to pass richer context — replacing plain stderr text — without disrupting the turn continuation behavior that advisory-first enforcement relies on.
|