timsquad 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +688 -154
- package/dist/commands/feedback.d.ts.map +1 -1
- package/dist/commands/feedback.js +21 -2
- package/dist/commands/feedback.js.map +1 -1
- package/dist/commands/git/commit.d.ts.map +1 -1
- package/dist/commands/git/commit.js +1 -4
- package/dist/commands/git/commit.js.map +1 -1
- package/dist/commands/improve.d.ts +3 -0
- package/dist/commands/improve.d.ts.map +1 -0
- package/dist/commands/improve.js +286 -0
- package/dist/commands/improve.js.map +1 -0
- package/dist/commands/init.js +3 -2
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/log.d.ts.map +1 -1
- package/dist/commands/log.js +202 -2
- package/dist/commands/log.js.map +1 -1
- package/dist/commands/metrics.d.ts.map +1 -1
- package/dist/commands/metrics.js +404 -99
- package/dist/commands/metrics.js.map +1 -1
- package/dist/commands/retro.d.ts.map +1 -1
- package/dist/commands/retro.js +454 -54
- package/dist/commands/retro.js.map +1 -1
- package/dist/commands/session.d.ts +3 -0
- package/dist/commands/session.d.ts.map +1 -0
- package/dist/commands/session.js +346 -0
- package/dist/commands/session.js.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/template.d.ts.map +1 -1
- package/dist/lib/template.js +66 -1
- package/dist/lib/template.js.map +1 -1
- package/dist/types/feedback.d.ts +52 -1
- package/dist/types/feedback.d.ts.map +1 -1
- package/dist/types/feedback.js +0 -3
- package/dist/types/feedback.js.map +1 -1
- package/dist/types/project.d.ts +1 -1
- package/dist/types/project.d.ts.map +1 -1
- package/dist/types/project.js +3 -1
- package/dist/types/project.js.map +1 -1
- package/package.json +1 -1
- package/templates/common/claude/agents/tsq-dba.md +21 -0
- package/templates/common/claude/agents/tsq-designer.md +19 -0
- package/templates/common/claude/agents/tsq-developer.md +59 -0
- package/templates/common/claude/agents/tsq-planner.md +101 -1
- package/templates/common/claude/agents/tsq-prompter.md +20 -0
- package/templates/common/claude/agents/tsq-qa.md +38 -4
- package/templates/common/claude/agents/tsq-retro.md +25 -0
- package/templates/common/claude/agents/tsq-security.md +31 -0
- package/templates/common/claude/hooks/auto-metrics.sh +165 -0
- package/templates/common/claude/hooks/auto-worklog.sh +245 -0
- package/templates/common/claude/hooks/event-logger.sh +208 -0
- package/templates/common/claude/settings.json +86 -0
- package/templates/common/config.template.yaml +2 -1
- package/templates/common/timsquad/process/phase-checklist.yaml +174 -0
- package/templates/common/timsquad/process/state-machine.xml +12 -0
- package/templates/common/timsquad/process/workflow-base.xml +124 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# TimSquad Auto Metrics Collector
|
|
3
|
+
# SessionEnd 이벤트 시 호출되어 세션 JSONL을 분석하고
|
|
4
|
+
# .timsquad/retrospective/metrics/latest.json 에 누적 메트릭 갱신
|
|
5
|
+
#
|
|
6
|
+
# 토큰 비용: 0 (순수 파일 I/O)
|
|
7
|
+
# 사용법: auto-metrics.sh <jsonl_file> <session_id_short>
|
|
8
|
+
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
JSONL_FILE="${1:-}"
|
|
12
|
+
SESSION_SHORT="${2:-unknown}"
|
|
13
|
+
|
|
14
|
+
if [ -z "$JSONL_FILE" ] || [ ! -f "$JSONL_FILE" ]; then
|
|
15
|
+
exit 0
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
19
|
+
METRICS_DIR="$PROJECT_DIR/.timsquad/retrospective/metrics"
|
|
20
|
+
mkdir -p "$METRICS_DIR"
|
|
21
|
+
|
|
22
|
+
LATEST_FILE="$METRICS_DIR/latest.json"
|
|
23
|
+
TODAY=$(date +"%Y-%m-%d")
|
|
24
|
+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
25
|
+
|
|
26
|
+
# ============================================================
|
|
27
|
+
# JSONL에서 세션 통계 추출
|
|
28
|
+
# ============================================================
|
|
29
|
+
|
|
30
|
+
TOTAL_EVENTS=$(wc -l < "$JSONL_FILE" | tr -d ' ')
|
|
31
|
+
|
|
32
|
+
if [ "$TOTAL_EVENTS" -lt 3 ]; then
|
|
33
|
+
exit 0
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# 도구 사용
|
|
37
|
+
TOOL_USES=$(jq -r 'select(.event == "PostToolUse")' "$JSONL_FILE" 2>/dev/null | wc -l | tr -d ' ')
|
|
38
|
+
TOOL_FAILURES=$(jq -r 'select(.event == "PostToolUseFailure")' "$JSONL_FILE" 2>/dev/null | wc -l | tr -d ' ')
|
|
39
|
+
SUBAGENTS=$(jq -r 'select(.event == "SubagentStart")' "$JSONL_FILE" 2>/dev/null | wc -l | tr -d ' ')
|
|
40
|
+
|
|
41
|
+
# 토큰 (SessionEnd에서 추출)
|
|
42
|
+
TOKEN_DATA=$(jq -c 'select(.event == "SessionEnd") | .total_usage // {}' "$JSONL_FILE" 2>/dev/null | head -1)
|
|
43
|
+
if [ -z "$TOKEN_DATA" ] || [ "$TOKEN_DATA" = "{}" ]; then
|
|
44
|
+
TOKEN_INPUT=0
|
|
45
|
+
TOKEN_OUTPUT=0
|
|
46
|
+
TOKEN_CACHE_CREATE=0
|
|
47
|
+
TOKEN_CACHE_READ=0
|
|
48
|
+
else
|
|
49
|
+
TOKEN_INPUT=$(echo "$TOKEN_DATA" | jq -r '.total_input // 0')
|
|
50
|
+
TOKEN_OUTPUT=$(echo "$TOKEN_DATA" | jq -r '.total_output // 0')
|
|
51
|
+
TOKEN_CACHE_CREATE=$(echo "$TOKEN_DATA" | jq -r '.total_cache_create // 0')
|
|
52
|
+
TOKEN_CACHE_READ=$(echo "$TOKEN_DATA" | jq -r '.total_cache_read // 0')
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
# CLI 채택률
|
|
56
|
+
TOTAL_BASH=$(jq -r 'select(.event == "PostToolUse" and .tool == "Bash") | .detail.command // ""' "$JSONL_FILE" 2>/dev/null | wc -l | tr -d ' ')
|
|
57
|
+
TSQ_BASH=$(jq -r 'select(.event == "PostToolUse" and .tool == "Bash") | .detail.command // ""' "$JSONL_FILE" 2>/dev/null | grep -c '^\(tsq\|npx tsq\)' || echo "0")
|
|
58
|
+
|
|
59
|
+
# ============================================================
|
|
60
|
+
# latest.json 읽기 또는 초기화
|
|
61
|
+
# ============================================================
|
|
62
|
+
|
|
63
|
+
if [ -f "$LATEST_FILE" ]; then
|
|
64
|
+
EXISTING=$(cat "$LATEST_FILE")
|
|
65
|
+
else
|
|
66
|
+
EXISTING='{}'
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# 기존 값 추출 (없으면 0)
|
|
70
|
+
PREV_SESSIONS=$(echo "$EXISTING" | jq -r '.totalSessions // 0')
|
|
71
|
+
PREV_EVENTS=$(echo "$EXISTING" | jq -r '.totalEvents // 0')
|
|
72
|
+
PREV_TOOL_USES=$(echo "$EXISTING" | jq -r '.totalToolUses // 0')
|
|
73
|
+
PREV_FAILURES=$(echo "$EXISTING" | jq -r '.totalFailures // 0')
|
|
74
|
+
PREV_SUBAGENTS=$(echo "$EXISTING" | jq -r '.subagentCount // 0')
|
|
75
|
+
PREV_INPUT=$(echo "$EXISTING" | jq -r '.tokens.totalInput // 0')
|
|
76
|
+
PREV_OUTPUT=$(echo "$EXISTING" | jq -r '.tokens.totalOutput // 0')
|
|
77
|
+
PREV_CACHE_CREATE=$(echo "$EXISTING" | jq -r '.tokens.totalCacheCreate // 0')
|
|
78
|
+
PREV_CACHE_READ=$(echo "$EXISTING" | jq -r '.tokens.totalCacheRead // 0')
|
|
79
|
+
PREV_BASH=$(echo "$EXISTING" | jq -r '.cliAdoption.totalBashCommands // 0')
|
|
80
|
+
PREV_TSQ=$(echo "$EXISTING" | jq -r '.cliAdoption.tsqCommands // 0')
|
|
81
|
+
|
|
82
|
+
# ============================================================
|
|
83
|
+
# 누적 갱신
|
|
84
|
+
# ============================================================
|
|
85
|
+
|
|
86
|
+
NEW_SESSIONS=$((PREV_SESSIONS + 1))
|
|
87
|
+
NEW_EVENTS=$((PREV_EVENTS + TOTAL_EVENTS))
|
|
88
|
+
NEW_TOOL_USES=$((PREV_TOOL_USES + TOOL_USES))
|
|
89
|
+
NEW_FAILURES=$((PREV_FAILURES + TOOL_FAILURES))
|
|
90
|
+
NEW_SUBAGENTS=$((PREV_SUBAGENTS + SUBAGENTS))
|
|
91
|
+
NEW_INPUT=$((PREV_INPUT + TOKEN_INPUT))
|
|
92
|
+
NEW_OUTPUT=$((PREV_OUTPUT + TOKEN_OUTPUT))
|
|
93
|
+
NEW_CACHE_CREATE=$((PREV_CACHE_CREATE + TOKEN_CACHE_CREATE))
|
|
94
|
+
NEW_CACHE_READ=$((PREV_CACHE_READ + TOKEN_CACHE_READ))
|
|
95
|
+
NEW_BASH=$((PREV_BASH + TOTAL_BASH))
|
|
96
|
+
NEW_TSQ=$((PREV_TSQ + TSQ_BASH))
|
|
97
|
+
|
|
98
|
+
# 파생 지표
|
|
99
|
+
TOTAL_ATTEMPTS=$((NEW_TOOL_USES + NEW_FAILURES))
|
|
100
|
+
if [ "$TOTAL_ATTEMPTS" -gt 0 ]; then
|
|
101
|
+
TOOL_EFFICIENCY=$((NEW_TOOL_USES * 100 / TOTAL_ATTEMPTS))
|
|
102
|
+
else
|
|
103
|
+
TOOL_EFFICIENCY=0
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
ALL_INPUT=$((NEW_INPUT + NEW_CACHE_CREATE + NEW_CACHE_READ))
|
|
107
|
+
if [ "$ALL_INPUT" -gt 0 ]; then
|
|
108
|
+
CACHE_HIT_RATE=$((NEW_CACHE_READ * 100 / ALL_INPUT))
|
|
109
|
+
else
|
|
110
|
+
CACHE_HIT_RATE=0
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
if [ "$NEW_BASH" -gt 0 ]; then
|
|
114
|
+
CLI_ADOPTION=$((NEW_TSQ * 100 / NEW_BASH))
|
|
115
|
+
else
|
|
116
|
+
CLI_ADOPTION=0
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
# ============================================================
|
|
120
|
+
# latest.json 갱신
|
|
121
|
+
# ============================================================
|
|
122
|
+
|
|
123
|
+
jq -n -c \
|
|
124
|
+
--arg ts "$TIMESTAMP" \
|
|
125
|
+
--arg date "$TODAY" \
|
|
126
|
+
--arg last_session "$SESSION_SHORT" \
|
|
127
|
+
--argjson sessions "$NEW_SESSIONS" \
|
|
128
|
+
--argjson events "$NEW_EVENTS" \
|
|
129
|
+
--argjson tool_uses "$NEW_TOOL_USES" \
|
|
130
|
+
--argjson failures "$NEW_FAILURES" \
|
|
131
|
+
--argjson efficiency "$TOOL_EFFICIENCY" \
|
|
132
|
+
--argjson subagents "$NEW_SUBAGENTS" \
|
|
133
|
+
--argjson input "$NEW_INPUT" \
|
|
134
|
+
--argjson output "$NEW_OUTPUT" \
|
|
135
|
+
--argjson cache_create "$NEW_CACHE_CREATE" \
|
|
136
|
+
--argjson cache_read "$NEW_CACHE_READ" \
|
|
137
|
+
--argjson cache_rate "$CACHE_HIT_RATE" \
|
|
138
|
+
--argjson bash_total "$NEW_BASH" \
|
|
139
|
+
--argjson tsq_total "$NEW_TSQ" \
|
|
140
|
+
--argjson cli_rate "$CLI_ADOPTION" \
|
|
141
|
+
'{
|
|
142
|
+
updatedAt: $ts,
|
|
143
|
+
lastDate: $date,
|
|
144
|
+
lastSession: $last_session,
|
|
145
|
+
totalSessions: $sessions,
|
|
146
|
+
totalEvents: $events,
|
|
147
|
+
totalToolUses: $tool_uses,
|
|
148
|
+
totalFailures: $failures,
|
|
149
|
+
toolEfficiency: $efficiency,
|
|
150
|
+
subagentCount: $subagents,
|
|
151
|
+
tokens: {
|
|
152
|
+
totalInput: $input,
|
|
153
|
+
totalOutput: $output,
|
|
154
|
+
totalCacheCreate: $cache_create,
|
|
155
|
+
totalCacheRead: $cache_read,
|
|
156
|
+
cacheHitRate: $cache_rate
|
|
157
|
+
},
|
|
158
|
+
cliAdoption: {
|
|
159
|
+
totalBashCommands: $bash_total,
|
|
160
|
+
tsqCommands: $tsq_total,
|
|
161
|
+
adoptionRate: $cli_rate
|
|
162
|
+
}
|
|
163
|
+
}' | jq '.' > "$LATEST_FILE"
|
|
164
|
+
|
|
165
|
+
exit 0
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# TimSquad Auto Work Log Generator
|
|
3
|
+
# SessionEnd 이벤트 시 호출되어 세션 JSONL을 분석하고
|
|
4
|
+
# .timsquad/logs/{date}-session.md 작업 로그를 자동 생성
|
|
5
|
+
#
|
|
6
|
+
# 사용법: auto-worklog.sh <jsonl_file> <session_id_short>
|
|
7
|
+
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
JSONL_FILE="${1:-}"
|
|
11
|
+
SESSION_SHORT="${2:-unknown}"
|
|
12
|
+
|
|
13
|
+
if [ -z "$JSONL_FILE" ] || [ ! -f "$JSONL_FILE" ]; then
|
|
14
|
+
exit 0
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
18
|
+
LOGS_DIR="$PROJECT_DIR/.timsquad/logs"
|
|
19
|
+
mkdir -p "$LOGS_DIR"
|
|
20
|
+
|
|
21
|
+
TODAY=$(date +"%Y-%m-%d")
|
|
22
|
+
LOG_FILE="$LOGS_DIR/${TODAY}-session.md"
|
|
23
|
+
TIMESTAMP=$(date +"%H:%M:%S")
|
|
24
|
+
|
|
25
|
+
# ============================================================
|
|
26
|
+
# JSONL에서 통계 추출
|
|
27
|
+
# ============================================================
|
|
28
|
+
|
|
29
|
+
TOTAL_EVENTS=$(wc -l < "$JSONL_FILE" | tr -d ' ')
|
|
30
|
+
|
|
31
|
+
# 최소 이벤트 수 체크 (너무 짧은 세션은 스킵)
|
|
32
|
+
if [ "$TOTAL_EVENTS" -lt 3 ]; then
|
|
33
|
+
exit 0
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# 도구 사용 횟수
|
|
37
|
+
TOOL_EVENTS=$(jq -r 'select(.event == "PostToolUse") | .tool' "$JSONL_FILE" 2>/dev/null | wc -l | tr -d ' ')
|
|
38
|
+
FAILURES=$(jq -r 'select(.event == "PostToolUseFailure")' "$JSONL_FILE" 2>/dev/null | wc -l | tr -d ' ')
|
|
39
|
+
|
|
40
|
+
# 서브에이전트 사용
|
|
41
|
+
SUBAGENT_STARTS=$(jq -r 'select(.event == "SubagentStart") | .detail.subagent_type' "$JSONL_FILE" 2>/dev/null)
|
|
42
|
+
SUBAGENT_COUNT=$(echo "$SUBAGENT_STARTS" | grep -c '.' 2>/dev/null || echo "0")
|
|
43
|
+
|
|
44
|
+
# 도구별 횟수 (상위 5개)
|
|
45
|
+
TOOL_BREAKDOWN=$(jq -r 'select(.event == "PostToolUse") | .tool' "$JSONL_FILE" 2>/dev/null \
|
|
46
|
+
| sort | uniq -c | sort -rn | head -5 \
|
|
47
|
+
| awk '{printf "%s(%d) ", $2, $1}')
|
|
48
|
+
|
|
49
|
+
# 수정된 파일 목록
|
|
50
|
+
FILES_MODIFIED=$(jq -r 'select(.event == "PostToolUse" and (.tool == "Write" or .tool == "Edit")) | .detail.file_path // empty' "$JSONL_FILE" 2>/dev/null \
|
|
51
|
+
| sort -u)
|
|
52
|
+
|
|
53
|
+
# 읽은 파일 목록 (상위 10개)
|
|
54
|
+
FILES_READ=$(jq -r 'select(.event == "PostToolUse" and .tool == "Read") | .detail.file_path // empty' "$JSONL_FILE" 2>/dev/null \
|
|
55
|
+
| sort -u | head -10)
|
|
56
|
+
|
|
57
|
+
# Bash 커맨드 (상위 5개)
|
|
58
|
+
BASH_COMMANDS=$(jq -r 'select(.event == "PostToolUse" and .tool == "Bash") | .detail.command // empty' "$JSONL_FILE" 2>/dev/null \
|
|
59
|
+
| head -5)
|
|
60
|
+
|
|
61
|
+
# 서브에이전트 상세
|
|
62
|
+
SUBAGENT_DETAILS=$(jq -r 'select(.event == "SubagentStart") | "\(.detail.subagent_type): \(.detail.description // "")"' "$JSONL_FILE" 2>/dev/null)
|
|
63
|
+
|
|
64
|
+
# 타임라인
|
|
65
|
+
FIRST_TS=$(jq -r '.timestamp' "$JSONL_FILE" 2>/dev/null | head -1)
|
|
66
|
+
LAST_TS=$(jq -r '.timestamp' "$JSONL_FILE" 2>/dev/null | tail -1)
|
|
67
|
+
|
|
68
|
+
FIRST_TIME=$(echo "$FIRST_TS" | sed 's/.*T//;s/Z//')
|
|
69
|
+
LAST_TIME=$(echo "$LAST_TS" | sed 's/.*T//;s/Z//')
|
|
70
|
+
|
|
71
|
+
# ============================================================
|
|
72
|
+
# 작업 로그 생성
|
|
73
|
+
# ============================================================
|
|
74
|
+
|
|
75
|
+
# 파일이 없으면 헤더 생성
|
|
76
|
+
if [ ! -f "$LOG_FILE" ]; then
|
|
77
|
+
cat > "$LOG_FILE" << EOF
|
|
78
|
+
# session 작업 로그 - ${TODAY}
|
|
79
|
+
|
|
80
|
+
> TimSquad Auto Work Log - Claude Code 세션 이벤트에서 자동 생성
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
EOF
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
# 로그 항목 추가
|
|
88
|
+
{
|
|
89
|
+
echo ""
|
|
90
|
+
echo "## [$TIMESTAMP] work"
|
|
91
|
+
echo ""
|
|
92
|
+
echo "**Session:** \`$SESSION_SHORT\` ($FIRST_TIME ~ $LAST_TIME)"
|
|
93
|
+
echo ""
|
|
94
|
+
echo "### 세션 통계"
|
|
95
|
+
echo "- 총 이벤트: $TOTAL_EVENTS"
|
|
96
|
+
echo "- 도구 사용: $TOOL_EVENTS회"
|
|
97
|
+
if [ "$FAILURES" -gt 0 ]; then
|
|
98
|
+
echo "- 실패: ${FAILURES}회"
|
|
99
|
+
fi
|
|
100
|
+
if [ "$SUBAGENT_COUNT" -gt 0 ]; then
|
|
101
|
+
echo "- 서브에이전트: ${SUBAGENT_COUNT}회"
|
|
102
|
+
fi
|
|
103
|
+
echo "- 도구 분포: $TOOL_BREAKDOWN"
|
|
104
|
+
echo ""
|
|
105
|
+
|
|
106
|
+
# 수정 파일
|
|
107
|
+
if [ -n "$FILES_MODIFIED" ]; then
|
|
108
|
+
echo "### 수정된 파일"
|
|
109
|
+
echo "$FILES_MODIFIED" | while read -r f; do
|
|
110
|
+
[ -n "$f" ] && echo "- \`$f\`"
|
|
111
|
+
done
|
|
112
|
+
echo ""
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
# 서브에이전트 활동
|
|
116
|
+
if [ -n "$SUBAGENT_DETAILS" ] && [ "$SUBAGENT_COUNT" -gt 0 ]; then
|
|
117
|
+
echo "### 서브에이전트 활동"
|
|
118
|
+
echo "$SUBAGENT_DETAILS" | while read -r line; do
|
|
119
|
+
[ -n "$line" ] && echo "- $line"
|
|
120
|
+
done
|
|
121
|
+
echo ""
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
# 주요 Bash 명령
|
|
125
|
+
if [ -n "$BASH_COMMANDS" ]; then
|
|
126
|
+
echo "### 주요 명령"
|
|
127
|
+
echo '```'
|
|
128
|
+
echo "$BASH_COMMANDS"
|
|
129
|
+
echo '```'
|
|
130
|
+
echo ""
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
# 토큰 사용량 (SessionEnd 이벤트에서 추출)
|
|
134
|
+
SESSION_END_USAGE=$(jq -c 'select(.event == "SessionEnd") | .total_usage // empty' "$JSONL_FILE" 2>/dev/null | head -1)
|
|
135
|
+
|
|
136
|
+
if [ -n "$SESSION_END_USAGE" ] && [ "$SESSION_END_USAGE" != "{}" ]; then
|
|
137
|
+
TOTAL_INPUT=$(echo "$SESSION_END_USAGE" | jq -r '.total_input // 0')
|
|
138
|
+
TOTAL_OUTPUT=$(echo "$SESSION_END_USAGE" | jq -r '.total_output // 0')
|
|
139
|
+
TOTAL_CACHE_CREATE=$(echo "$SESSION_END_USAGE" | jq -r '.total_cache_create // 0')
|
|
140
|
+
TOTAL_CACHE_READ=$(echo "$SESSION_END_USAGE" | jq -r '.total_cache_read // 0')
|
|
141
|
+
TOTAL_TURNS=$(echo "$SESSION_END_USAGE" | jq -r '.turns // 0')
|
|
142
|
+
|
|
143
|
+
# cache hit rate 계산
|
|
144
|
+
TOTAL_ALL_INPUT=$((TOTAL_INPUT + TOTAL_CACHE_CREATE + TOTAL_CACHE_READ))
|
|
145
|
+
if [ "$TOTAL_ALL_INPUT" -gt 0 ]; then
|
|
146
|
+
CACHE_HIT_RATE=$((TOTAL_CACHE_READ * 100 / TOTAL_ALL_INPUT))
|
|
147
|
+
else
|
|
148
|
+
CACHE_HIT_RATE=0
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
echo "### 토큰 사용량"
|
|
152
|
+
echo ""
|
|
153
|
+
echo "| 항목 | 토큰 | 의미 |"
|
|
154
|
+
echo "|------|------|------|"
|
|
155
|
+
echo "| Input | $(printf "%'d" "$TOTAL_INPUT") | 새로 처리된 입력 (캐시 미스) |"
|
|
156
|
+
echo "| Output | $(printf "%'d" "$TOTAL_OUTPUT") | 생성된 출력 (비용 주요 부분) |"
|
|
157
|
+
echo "| Cache Create | $(printf "%'d" "$TOTAL_CACHE_CREATE") | 새로 캐시 저장 (첫 턴에 높음) |"
|
|
158
|
+
echo "| Cache Read | $(printf "%'d" "$TOTAL_CACHE_READ") | 캐시 재사용 (높을수록 효율적) |"
|
|
159
|
+
echo "| **Cache Hit Rate** | **${CACHE_HIT_RATE}%** | **80%+ 우수 / 60-80% 보통 / <60% 주의** |"
|
|
160
|
+
echo "| Turns | $TOTAL_TURNS | API 호출 횟수 |"
|
|
161
|
+
echo ""
|
|
162
|
+
fi
|
|
163
|
+
|
|
164
|
+
} >> "$LOG_FILE"
|
|
165
|
+
|
|
166
|
+
# ============================================================
|
|
167
|
+
# 품질 경고 (threshold 기반 자동 감지)
|
|
168
|
+
# 토큰 비용: 0 - 순수 조건 체크
|
|
169
|
+
# ============================================================
|
|
170
|
+
|
|
171
|
+
ALERT_FILE="$LOGS_DIR/${TODAY}-alerts.md"
|
|
172
|
+
ALERTS_FOUND=0
|
|
173
|
+
|
|
174
|
+
# Alert 1: 도구 실패율 > 10%
|
|
175
|
+
TOTAL_ATTEMPTS=$((TOOL_EVENTS + FAILURES))
|
|
176
|
+
if [ "$TOTAL_ATTEMPTS" -gt 5 ]; then
|
|
177
|
+
FAIL_RATE=$((FAILURES * 100 / TOTAL_ATTEMPTS))
|
|
178
|
+
if [ "$FAIL_RATE" -gt 10 ]; then
|
|
179
|
+
if [ "$ALERTS_FOUND" -eq 0 ] && [ ! -f "$ALERT_FILE" ]; then
|
|
180
|
+
echo "# Quality Alerts - ${TODAY}" > "$ALERT_FILE"
|
|
181
|
+
echo "" >> "$ALERT_FILE"
|
|
182
|
+
echo "> TimSquad 자동 품질 경고 - threshold 초과 시 자동 생성" >> "$ALERT_FILE"
|
|
183
|
+
echo "" >> "$ALERT_FILE"
|
|
184
|
+
fi
|
|
185
|
+
echo "- **[$TIMESTAMP]** Tool Failure Rate ${FAIL_RATE}% (> 10%) - 세션 \`$SESSION_SHORT\`" >> "$ALERT_FILE"
|
|
186
|
+
echo " - 도구 실패 ${FAILURES}/${TOTAL_ATTEMPTS}회. 에이전트 프롬프트나 권한 설정 점검" >> "$ALERT_FILE"
|
|
187
|
+
ALERTS_FOUND=1
|
|
188
|
+
fi
|
|
189
|
+
fi
|
|
190
|
+
|
|
191
|
+
# Alert 2: Cache Hit Rate < 60% (토큰 데이터가 있을 때만)
|
|
192
|
+
if [ -n "$SESSION_END_USAGE" ] && [ "$SESSION_END_USAGE" != "{}" ]; then
|
|
193
|
+
if [ "$TOTAL_ALL_INPUT" -gt 0 ] && [ "$CACHE_HIT_RATE" -lt 60 ]; then
|
|
194
|
+
if [ "$ALERTS_FOUND" -eq 0 ] && [ ! -f "$ALERT_FILE" ]; then
|
|
195
|
+
echo "# Quality Alerts - ${TODAY}" > "$ALERT_FILE"
|
|
196
|
+
echo "" >> "$ALERT_FILE"
|
|
197
|
+
echo "> TimSquad 자동 품질 경고 - threshold 초과 시 자동 생성" >> "$ALERT_FILE"
|
|
198
|
+
echo "" >> "$ALERT_FILE"
|
|
199
|
+
fi
|
|
200
|
+
echo "- **[$TIMESTAMP]** Cache Hit Rate ${CACHE_HIT_RATE}% (< 60%) - 세션 \`$SESSION_SHORT\`" >> "$ALERT_FILE"
|
|
201
|
+
echo " - 프롬프트 구조 불안정. CLAUDE.md 또는 에이전트 프롬프트 검토" >> "$ALERT_FILE"
|
|
202
|
+
ALERTS_FOUND=1
|
|
203
|
+
fi
|
|
204
|
+
fi
|
|
205
|
+
|
|
206
|
+
# ============================================================
|
|
207
|
+
# 자동 피드백 생성 (실패 패턴 감지)
|
|
208
|
+
# 도구 실패 3회 이상 → 자동으로 FB-XXXX.json 생성
|
|
209
|
+
# 토큰 비용: 0
|
|
210
|
+
# ============================================================
|
|
211
|
+
|
|
212
|
+
if [ "$FAILURES" -ge 3 ]; then
|
|
213
|
+
FEEDBACK_DIR="$PROJECT_DIR/.timsquad/feedback"
|
|
214
|
+
mkdir -p "$FEEDBACK_DIR"
|
|
215
|
+
|
|
216
|
+
# 다음 피드백 번호 결정
|
|
217
|
+
EXISTING_COUNT=$(ls "$FEEDBACK_DIR"/FB-*.json 2>/dev/null | wc -l | tr -d ' ')
|
|
218
|
+
NEXT_NUM=$((EXISTING_COUNT + 1))
|
|
219
|
+
FB_ID=$(printf "FB-%04d" "$NEXT_NUM")
|
|
220
|
+
FB_FILE="$FEEDBACK_DIR/${FB_ID}.json"
|
|
221
|
+
|
|
222
|
+
# 실패한 도구 목록 추출
|
|
223
|
+
FAILED_TOOLS=$(jq -r 'select(.event == "PostToolUseFailure") | .tool // "unknown"' "$JSONL_FILE" 2>/dev/null \
|
|
224
|
+
| sort | uniq -c | sort -rn | head -3 \
|
|
225
|
+
| awk '{printf "%s(%d) ", $2, $1}')
|
|
226
|
+
|
|
227
|
+
jq -n \
|
|
228
|
+
--arg id "$FB_ID" \
|
|
229
|
+
--arg ts "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
|
|
230
|
+
--arg msg "세션 ${SESSION_SHORT}에서 도구 실패 ${FAILURES}회 감지. 실패 도구: ${FAILED_TOOLS}" \
|
|
231
|
+
--arg trigger "tool_failure" \
|
|
232
|
+
--arg session "$SESSION_SHORT" \
|
|
233
|
+
'{
|
|
234
|
+
id: $id,
|
|
235
|
+
timestamp: $ts,
|
|
236
|
+
type: "auto-feedback",
|
|
237
|
+
level: 1,
|
|
238
|
+
trigger: $trigger,
|
|
239
|
+
message: $msg,
|
|
240
|
+
routeTo: "developer",
|
|
241
|
+
tags: ["auto-detected", "tool_failure", $session]
|
|
242
|
+
}' > "$FB_FILE"
|
|
243
|
+
fi
|
|
244
|
+
|
|
245
|
+
exit 0
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# TimSquad Event Logger - Claude Code Hook
|
|
3
|
+
# PostToolUse, Stop, SessionStart, SessionEnd 이벤트를 자동 기록
|
|
4
|
+
#
|
|
5
|
+
# 저장 위치: .timsquad/logs/sessions/{date}-{session_id_short}.jsonl
|
|
6
|
+
# 형식: JSON Lines (한 줄 = 하나의 이벤트)
|
|
7
|
+
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
# 프로젝트 루트 결정
|
|
11
|
+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
12
|
+
SESSIONS_DIR="$PROJECT_DIR/.timsquad/logs/sessions"
|
|
13
|
+
mkdir -p "$SESSIONS_DIR"
|
|
14
|
+
|
|
15
|
+
# stdin에서 JSON 읽기
|
|
16
|
+
INPUT=$(cat)
|
|
17
|
+
|
|
18
|
+
# 기본 필드 추출
|
|
19
|
+
EVENT=$(echo "$INPUT" | jq -r '.hook_event_name // "unknown"')
|
|
20
|
+
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"')
|
|
21
|
+
SESSION_SHORT="${SESSION_ID:0:8}"
|
|
22
|
+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
23
|
+
TODAY=$(date +"%Y-%m-%d")
|
|
24
|
+
|
|
25
|
+
# 로그 파일 경로
|
|
26
|
+
LOG_FILE="$SESSIONS_DIR/${TODAY}-${SESSION_SHORT}.jsonl"
|
|
27
|
+
|
|
28
|
+
# 이벤트별 데이터 추출
|
|
29
|
+
case "$EVENT" in
|
|
30
|
+
PostToolUse|PostToolUseFailure)
|
|
31
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // "unknown"')
|
|
32
|
+
|
|
33
|
+
# tool_input에서 핵심 정보만 추출 (전체 content 제외 - 용량 절약)
|
|
34
|
+
case "$TOOL_NAME" in
|
|
35
|
+
Write|Edit)
|
|
36
|
+
TOOL_SUMMARY=$(echo "$INPUT" | jq -c '{
|
|
37
|
+
file_path: .tool_input.file_path,
|
|
38
|
+
action: "'"$TOOL_NAME"'"
|
|
39
|
+
}')
|
|
40
|
+
;;
|
|
41
|
+
Read)
|
|
42
|
+
TOOL_SUMMARY=$(echo "$INPUT" | jq -c '{
|
|
43
|
+
file_path: .tool_input.file_path,
|
|
44
|
+
action: "Read"
|
|
45
|
+
}')
|
|
46
|
+
;;
|
|
47
|
+
Bash)
|
|
48
|
+
# 커맨드만 추출 (출력 제외)
|
|
49
|
+
TOOL_SUMMARY=$(echo "$INPUT" | jq -c '{
|
|
50
|
+
command: (.tool_input.command // "" | .[0:200]),
|
|
51
|
+
action: "Bash"
|
|
52
|
+
}')
|
|
53
|
+
;;
|
|
54
|
+
Glob|Grep)
|
|
55
|
+
TOOL_SUMMARY=$(echo "$INPUT" | jq -c '{
|
|
56
|
+
pattern: (.tool_input.pattern // ""),
|
|
57
|
+
path: (.tool_input.path // ""),
|
|
58
|
+
action: "'"$TOOL_NAME"'"
|
|
59
|
+
}')
|
|
60
|
+
;;
|
|
61
|
+
Task)
|
|
62
|
+
TOOL_SUMMARY=$(echo "$INPUT" | jq -c '{
|
|
63
|
+
description: (.tool_input.description // ""),
|
|
64
|
+
subagent_type: (.tool_input.subagent_type // ""),
|
|
65
|
+
action: "Task"
|
|
66
|
+
}')
|
|
67
|
+
;;
|
|
68
|
+
*)
|
|
69
|
+
TOOL_SUMMARY=$(echo "$INPUT" | jq -c '{action: "'"$TOOL_NAME"'"}')
|
|
70
|
+
;;
|
|
71
|
+
esac
|
|
72
|
+
|
|
73
|
+
# 성공 여부
|
|
74
|
+
if [ "$EVENT" = "PostToolUseFailure" ]; then
|
|
75
|
+
STATUS="failure"
|
|
76
|
+
else
|
|
77
|
+
STATUS="success"
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
LOG_ENTRY=$(jq -n -c \
|
|
81
|
+
--arg ts "$TIMESTAMP" \
|
|
82
|
+
--arg ev "$EVENT" \
|
|
83
|
+
--arg sid "$SESSION_SHORT" \
|
|
84
|
+
--arg tool "$TOOL_NAME" \
|
|
85
|
+
--arg status "$STATUS" \
|
|
86
|
+
--argjson summary "$TOOL_SUMMARY" \
|
|
87
|
+
'{timestamp: $ts, event: $ev, session: $sid, tool: $tool, status: $status, detail: $summary}')
|
|
88
|
+
;;
|
|
89
|
+
|
|
90
|
+
Stop)
|
|
91
|
+
# transcript에서 마지막 assistant 턴의 토큰 사용량 추출
|
|
92
|
+
TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // ""')
|
|
93
|
+
USAGE='{}'
|
|
94
|
+
|
|
95
|
+
if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
|
|
96
|
+
# 마지막 assistant 메시지의 usage 추출
|
|
97
|
+
LAST_USAGE=$(tac "$TRANSCRIPT_PATH" | jq -c 'select(.type == "assistant" and .message.usage) | .message.usage' 2>/dev/null | head -1)
|
|
98
|
+
|
|
99
|
+
if [ -n "$LAST_USAGE" ] && [ "$LAST_USAGE" != "null" ]; then
|
|
100
|
+
USAGE=$(echo "$LAST_USAGE" | jq -c '{
|
|
101
|
+
input: (.input_tokens // 0),
|
|
102
|
+
output: (.output_tokens // 0),
|
|
103
|
+
cache_create: (.cache_creation_input_tokens // 0),
|
|
104
|
+
cache_read: (.cache_read_input_tokens // 0)
|
|
105
|
+
}')
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
# 누적 토큰 합산
|
|
109
|
+
CUMULATIVE=$(jq -s -c '{
|
|
110
|
+
total_input: ([.[].message.usage.input_tokens // 0] | add),
|
|
111
|
+
total_output: ([.[].message.usage.output_tokens // 0] | add),
|
|
112
|
+
total_cache_create: ([.[].message.usage.cache_creation_input_tokens // 0] | add),
|
|
113
|
+
total_cache_read: ([.[].message.usage.cache_read_input_tokens // 0] | add),
|
|
114
|
+
turns: length
|
|
115
|
+
}' <(jq -c 'select(.type == "assistant" and .message.usage)' "$TRANSCRIPT_PATH" 2>/dev/null) 2>/dev/null || echo '{}')
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
LOG_ENTRY=$(jq -n -c \
|
|
119
|
+
--arg ts "$TIMESTAMP" \
|
|
120
|
+
--arg ev "Stop" \
|
|
121
|
+
--arg sid "$SESSION_SHORT" \
|
|
122
|
+
--argjson usage "${USAGE:-'{}'}" \
|
|
123
|
+
--argjson cumulative "${CUMULATIVE:-'{}'}" \
|
|
124
|
+
'{timestamp: $ts, event: $ev, session: $sid, usage: $usage, cumulative: $cumulative}')
|
|
125
|
+
;;
|
|
126
|
+
|
|
127
|
+
SessionStart)
|
|
128
|
+
SOURCE=$(echo "$INPUT" | jq -r '.source // "unknown"')
|
|
129
|
+
LOG_ENTRY=$(jq -n -c \
|
|
130
|
+
--arg ts "$TIMESTAMP" \
|
|
131
|
+
--arg ev "SessionStart" \
|
|
132
|
+
--arg sid "$SESSION_SHORT" \
|
|
133
|
+
--arg src "$SOURCE" \
|
|
134
|
+
--arg cwd "$(echo "$INPUT" | jq -r '.cwd // ""')" \
|
|
135
|
+
'{timestamp: $ts, event: $ev, session: $sid, source: $src, cwd: $cwd}')
|
|
136
|
+
;;
|
|
137
|
+
|
|
138
|
+
SessionEnd)
|
|
139
|
+
# transcript에서 최종 토큰 합산
|
|
140
|
+
TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // ""')
|
|
141
|
+
TOTAL_USAGE='{}'
|
|
142
|
+
|
|
143
|
+
if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
|
|
144
|
+
TOTAL_USAGE=$(jq -s -c '{
|
|
145
|
+
total_input: ([.[].message.usage.input_tokens // 0] | add),
|
|
146
|
+
total_output: ([.[].message.usage.output_tokens // 0] | add),
|
|
147
|
+
total_cache_create: ([.[].message.usage.cache_creation_input_tokens // 0] | add),
|
|
148
|
+
total_cache_read: ([.[].message.usage.cache_read_input_tokens // 0] | add),
|
|
149
|
+
turns: length
|
|
150
|
+
}' <(jq -c 'select(.type == "assistant" and .message.usage)' "$TRANSCRIPT_PATH" 2>/dev/null) 2>/dev/null || echo '{}')
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
LOG_ENTRY=$(jq -n -c \
|
|
154
|
+
--arg ts "$TIMESTAMP" \
|
|
155
|
+
--arg ev "SessionEnd" \
|
|
156
|
+
--arg sid "$SESSION_SHORT" \
|
|
157
|
+
--argjson total_usage "${TOTAL_USAGE:-'{}'}" \
|
|
158
|
+
'{timestamp: $ts, event: $ev, session: $sid, total_usage: $total_usage}')
|
|
159
|
+
|
|
160
|
+
# SessionEnd 시 자동화 스크립트 체인 실행
|
|
161
|
+
# 1. 작업 로그 자동 생성
|
|
162
|
+
AUTO_WORKLOG="$PROJECT_DIR/.claude/hooks/auto-worklog.sh"
|
|
163
|
+
if [ -f "$AUTO_WORKLOG" ]; then
|
|
164
|
+
bash "$AUTO_WORKLOG" "$LOG_FILE" "$SESSION_SHORT" &
|
|
165
|
+
fi
|
|
166
|
+
|
|
167
|
+
# 2. 메트릭 자동 수집 (누적 갱신, 토큰 비용 0)
|
|
168
|
+
AUTO_METRICS="$PROJECT_DIR/.claude/hooks/auto-metrics.sh"
|
|
169
|
+
if [ -f "$AUTO_METRICS" ]; then
|
|
170
|
+
bash "$AUTO_METRICS" "$LOG_FILE" "$SESSION_SHORT" &
|
|
171
|
+
fi
|
|
172
|
+
;;
|
|
173
|
+
|
|
174
|
+
SubagentStart)
|
|
175
|
+
AGENT_TYPE=$(echo "$INPUT" | jq -r '.subagent_type // "unknown"')
|
|
176
|
+
AGENT_DESC=$(echo "$INPUT" | jq -r '.description // ""' | head -c 200)
|
|
177
|
+
LOG_ENTRY=$(jq -n -c \
|
|
178
|
+
--arg ts "$TIMESTAMP" \
|
|
179
|
+
--arg ev "SubagentStart" \
|
|
180
|
+
--arg sid "$SESSION_SHORT" \
|
|
181
|
+
--arg atype "$AGENT_TYPE" \
|
|
182
|
+
--arg desc "$AGENT_DESC" \
|
|
183
|
+
'{timestamp: $ts, event: $ev, session: $sid, detail: {subagent_type: $atype, description: $desc}}')
|
|
184
|
+
;;
|
|
185
|
+
|
|
186
|
+
SubagentStop)
|
|
187
|
+
AGENT_TYPE=$(echo "$INPUT" | jq -r '.subagent_type // "unknown"')
|
|
188
|
+
LOG_ENTRY=$(jq -n -c \
|
|
189
|
+
--arg ts "$TIMESTAMP" \
|
|
190
|
+
--arg ev "SubagentStop" \
|
|
191
|
+
--arg sid "$SESSION_SHORT" \
|
|
192
|
+
--arg atype "$AGENT_TYPE" \
|
|
193
|
+
'{timestamp: $ts, event: $ev, session: $sid, detail: {subagent_type: $atype}}')
|
|
194
|
+
;;
|
|
195
|
+
|
|
196
|
+
*)
|
|
197
|
+
LOG_ENTRY=$(jq -n -c \
|
|
198
|
+
--arg ts "$TIMESTAMP" \
|
|
199
|
+
--arg ev "$EVENT" \
|
|
200
|
+
--arg sid "$SESSION_SHORT" \
|
|
201
|
+
'{timestamp: $ts, event: $ev, session: $sid}')
|
|
202
|
+
;;
|
|
203
|
+
esac
|
|
204
|
+
|
|
205
|
+
# JSONL 파일에 append
|
|
206
|
+
echo "$LOG_ENTRY" >> "$LOG_FILE"
|
|
207
|
+
|
|
208
|
+
exit 0
|