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
package/dist/commands/metrics.js
CHANGED
|
@@ -7,11 +7,11 @@ import { getDateString, getTimestamp } from '../utils/date.js';
|
|
|
7
7
|
export function registerMetricsCommand(program) {
|
|
8
8
|
const metricsCmd = program
|
|
9
9
|
.command('metrics')
|
|
10
|
-
.description('Collect and view project metrics');
|
|
10
|
+
.description('Collect and view project quality metrics');
|
|
11
11
|
// tsq metrics collect
|
|
12
12
|
metricsCmd
|
|
13
13
|
.command('collect')
|
|
14
|
-
.description('Collect metrics from logs')
|
|
14
|
+
.description('Collect metrics from logs and session events')
|
|
15
15
|
.option('-d, --days <days>', 'Days to analyze', '7')
|
|
16
16
|
.action(async (options) => {
|
|
17
17
|
try {
|
|
@@ -25,7 +25,7 @@ export function registerMetricsCommand(program) {
|
|
|
25
25
|
// tsq metrics summary
|
|
26
26
|
metricsCmd
|
|
27
27
|
.command('summary')
|
|
28
|
-
.description('Show metrics summary')
|
|
28
|
+
.description('Show latest metrics summary with explanations')
|
|
29
29
|
.action(async () => {
|
|
30
30
|
try {
|
|
31
31
|
await showMetricsSummary();
|
|
@@ -35,6 +35,20 @@ export function registerMetricsCommand(program) {
|
|
|
35
35
|
process.exit(1);
|
|
36
36
|
}
|
|
37
37
|
});
|
|
38
|
+
// tsq metrics trend
|
|
39
|
+
metricsCmd
|
|
40
|
+
.command('trend')
|
|
41
|
+
.description('Compare metrics across collection periods')
|
|
42
|
+
.option('-n <count>', 'Number of periods to compare', '5')
|
|
43
|
+
.action(async (options) => {
|
|
44
|
+
try {
|
|
45
|
+
await showTrend(parseInt(options.n, 10));
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
printError(error.message);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
38
52
|
// tsq metrics export
|
|
39
53
|
metricsCmd
|
|
40
54
|
.command('export')
|
|
@@ -50,6 +64,9 @@ export function registerMetricsCommand(program) {
|
|
|
50
64
|
}
|
|
51
65
|
});
|
|
52
66
|
}
|
|
67
|
+
// ============================================================
|
|
68
|
+
// Collect
|
|
69
|
+
// ============================================================
|
|
53
70
|
async function collectMetrics(days) {
|
|
54
71
|
const projectRoot = await findProjectRoot();
|
|
55
72
|
if (!projectRoot) {
|
|
@@ -59,15 +76,16 @@ async function collectMetrics(days) {
|
|
|
59
76
|
printKeyValue('Period', `Last ${days} days`);
|
|
60
77
|
const now = new Date();
|
|
61
78
|
const startDate = new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
|
|
62
|
-
// Collect
|
|
79
|
+
// Collect all data sources in parallel
|
|
63
80
|
const logsDir = path.join(projectRoot, '.timsquad', 'logs');
|
|
64
|
-
const logStats = await collectLogStats(logsDir, startDate);
|
|
65
|
-
// Collect feedback statistics
|
|
66
|
-
const feedbackStats = await collectFeedbackStats(logsDir, startDate);
|
|
67
|
-
// Collect SSOT statistics
|
|
68
81
|
const ssotDir = path.join(projectRoot, '.timsquad', 'ssot');
|
|
69
|
-
const
|
|
70
|
-
|
|
82
|
+
const sessionsDir = path.join(projectRoot, '.timsquad', 'logs', 'sessions');
|
|
83
|
+
const [logStats, feedbackStats, ssotStats, sessionStats] = await Promise.all([
|
|
84
|
+
collectLogStats(logsDir, startDate),
|
|
85
|
+
collectFeedbackStats(logsDir, startDate),
|
|
86
|
+
collectSSOTStats(ssotDir),
|
|
87
|
+
collectSessionStats(sessionsDir, startDate),
|
|
88
|
+
]);
|
|
71
89
|
const metrics = {
|
|
72
90
|
collectedAt: getTimestamp(),
|
|
73
91
|
period: {
|
|
@@ -77,6 +95,7 @@ async function collectMetrics(days) {
|
|
|
77
95
|
logs: logStats,
|
|
78
96
|
feedback: feedbackStats,
|
|
79
97
|
ssot: ssotStats,
|
|
98
|
+
sessions: sessionStats,
|
|
80
99
|
};
|
|
81
100
|
// Save metrics
|
|
82
101
|
const metricsDir = path.join(projectRoot, '.timsquad', 'retrospective', 'metrics');
|
|
@@ -85,41 +104,41 @@ async function collectMetrics(days) {
|
|
|
85
104
|
await fs.writeJson(metricsFile, metrics, { spaces: 2 });
|
|
86
105
|
printSuccess('Metrics collected');
|
|
87
106
|
console.log(colors.path(`\nSaved to: ${metricsFile}`));
|
|
88
|
-
// Show summary
|
|
89
107
|
console.log('');
|
|
90
108
|
await displayMetrics(metrics);
|
|
91
109
|
}
|
|
110
|
+
// ============================================================
|
|
111
|
+
// Log Statistics
|
|
112
|
+
// ============================================================
|
|
92
113
|
async function collectLogStats(logsDir, startDate) {
|
|
93
114
|
const stats = {
|
|
94
115
|
total: 0,
|
|
95
116
|
byAgent: {},
|
|
96
117
|
byType: {},
|
|
118
|
+
decisionRatio: 0,
|
|
119
|
+
errorRate: 0,
|
|
97
120
|
};
|
|
98
|
-
if (!await exists(logsDir))
|
|
121
|
+
if (!await exists(logsDir))
|
|
99
122
|
return stats;
|
|
100
|
-
}
|
|
101
123
|
const files = await listFiles('*.md', logsDir);
|
|
102
124
|
const startDateStr = startDate.toISOString().split('T')[0];
|
|
103
125
|
for (const file of files) {
|
|
104
|
-
// Skip templates
|
|
105
126
|
if (file.startsWith('_'))
|
|
106
127
|
continue;
|
|
107
|
-
// Parse filename: YYYY-MM-DD-agent.md
|
|
108
128
|
const match = file.match(/^(\d{4}-\d{2}-\d{2})-([a-z]+)\.md$/);
|
|
109
129
|
if (!match)
|
|
110
130
|
continue;
|
|
111
131
|
const [, dateStr, agent] = match;
|
|
112
|
-
// Check date range
|
|
113
132
|
if (dateStr < startDateStr)
|
|
114
133
|
continue;
|
|
115
134
|
stats.total++;
|
|
116
135
|
stats.byAgent[agent] = (stats.byAgent[agent] || 0) + 1;
|
|
117
|
-
// Count entries in the file
|
|
118
136
|
try {
|
|
119
137
|
const content = await readFile(path.join(logsDir, file));
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
const
|
|
138
|
+
// log.sh 형식: ## [HH:MM:SS] type
|
|
139
|
+
const workEntries = (content.match(/## \[[\d:]+\] work/g) || []).length;
|
|
140
|
+
const errorEntries = (content.match(/## \[[\d:]+\] error/g) || []).length;
|
|
141
|
+
const decisionEntries = (content.match(/## \[[\d:]+\] decision/g) || []).length;
|
|
123
142
|
stats.byType['work'] = (stats.byType['work'] || 0) + workEntries;
|
|
124
143
|
stats.byType['error'] = (stats.byType['error'] || 0) + errorEntries;
|
|
125
144
|
stats.byType['decision'] = (stats.byType['decision'] || 0) + decisionEntries;
|
|
@@ -128,63 +147,84 @@ async function collectLogStats(logsDir, startDate) {
|
|
|
128
147
|
// Ignore read errors
|
|
129
148
|
}
|
|
130
149
|
}
|
|
150
|
+
// 파생 지표 계산
|
|
151
|
+
const totalEntries = Object.values(stats.byType).reduce((a, b) => a + b, 0);
|
|
152
|
+
if (totalEntries > 0) {
|
|
153
|
+
stats.decisionRatio = Math.round(((stats.byType['decision'] || 0) / totalEntries) * 100);
|
|
154
|
+
stats.errorRate = Math.round(((stats.byType['error'] || 0) / totalEntries) * 100);
|
|
155
|
+
}
|
|
131
156
|
return stats;
|
|
132
157
|
}
|
|
158
|
+
// ============================================================
|
|
159
|
+
// Feedback Statistics
|
|
160
|
+
// ============================================================
|
|
133
161
|
async function collectFeedbackStats(logsDir, startDate) {
|
|
134
|
-
const stats = {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
// Count feedback entries
|
|
153
|
-
try {
|
|
154
|
-
const content = await readFile(path.join(logsDir, file));
|
|
155
|
-
const level1 = (content.match(/\[Level 1\]/g) || []).length;
|
|
156
|
-
const level2 = (content.match(/\[Level 2\]/g) || []).length;
|
|
157
|
-
const level3 = (content.match(/\[Level 3\]/g) || []).length;
|
|
158
|
-
stats.byLevel['1'] = (stats.byLevel['1'] || 0) + level1;
|
|
159
|
-
stats.byLevel['2'] = (stats.byLevel['2'] || 0) + level2;
|
|
160
|
-
stats.byLevel['3'] = (stats.byLevel['3'] || 0) + level3;
|
|
161
|
-
stats.total += level1 + level2 + level3;
|
|
162
|
+
const stats = { total: 0, byLevel: {} };
|
|
163
|
+
// Structured JSON feedback (우선)
|
|
164
|
+
const projectRoot = path.dirname(logsDir.replace(/\/.timsquad\/logs$/, ''));
|
|
165
|
+
const structuredDir = path.join(projectRoot, '.timsquad', 'feedback');
|
|
166
|
+
if (await exists(structuredDir)) {
|
|
167
|
+
const jsonFiles = await listFiles('FB-*.json', structuredDir);
|
|
168
|
+
for (const file of jsonFiles) {
|
|
169
|
+
try {
|
|
170
|
+
const data = await fs.readJson(path.join(structuredDir, file));
|
|
171
|
+
if (data.level) {
|
|
172
|
+
const level = String(data.level);
|
|
173
|
+
stats.byLevel[level] = (stats.byLevel[level] || 0) + 1;
|
|
174
|
+
stats.total++;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// skip
|
|
179
|
+
}
|
|
162
180
|
}
|
|
163
|
-
|
|
164
|
-
|
|
181
|
+
}
|
|
182
|
+
// Fallback: markdown feedback logs
|
|
183
|
+
if (stats.total === 0 && await exists(logsDir)) {
|
|
184
|
+
const startDateStr = startDate.toISOString().split('T')[0];
|
|
185
|
+
const files = await listFiles('*-feedback.md', logsDir);
|
|
186
|
+
for (const file of files) {
|
|
187
|
+
const match = file.match(/^(\d{4}-\d{2}-\d{2})-feedback\.md$/);
|
|
188
|
+
if (!match)
|
|
189
|
+
continue;
|
|
190
|
+
const [, dateStr] = match;
|
|
191
|
+
if (dateStr < startDateStr)
|
|
192
|
+
continue;
|
|
193
|
+
try {
|
|
194
|
+
const content = await readFile(path.join(logsDir, file));
|
|
195
|
+
const level1 = (content.match(/\[Level 1\]/g) || []).length;
|
|
196
|
+
const level2 = (content.match(/\[Level 2\]/g) || []).length;
|
|
197
|
+
const level3 = (content.match(/\[Level 3\]/g) || []).length;
|
|
198
|
+
stats.byLevel['1'] = (stats.byLevel['1'] || 0) + level1;
|
|
199
|
+
stats.byLevel['2'] = (stats.byLevel['2'] || 0) + level2;
|
|
200
|
+
stats.byLevel['3'] = (stats.byLevel['3'] || 0) + level3;
|
|
201
|
+
stats.total += level1 + level2 + level3;
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
// skip
|
|
205
|
+
}
|
|
165
206
|
}
|
|
166
207
|
}
|
|
167
208
|
return stats;
|
|
168
209
|
}
|
|
210
|
+
// ============================================================
|
|
211
|
+
// SSOT Statistics
|
|
212
|
+
// ============================================================
|
|
169
213
|
async function collectSSOTStats(ssotDir) {
|
|
170
214
|
const stats = {
|
|
171
215
|
documentsCount: 0,
|
|
172
216
|
filledCount: 0,
|
|
173
217
|
completionRate: 0,
|
|
174
218
|
};
|
|
175
|
-
if (!await exists(ssotDir))
|
|
219
|
+
if (!await exists(ssotDir))
|
|
176
220
|
return stats;
|
|
177
|
-
}
|
|
178
221
|
const files = await listFiles('*.md', ssotDir);
|
|
179
222
|
for (const file of files) {
|
|
180
|
-
// Skip templates
|
|
181
223
|
if (file.includes('template'))
|
|
182
224
|
continue;
|
|
183
225
|
stats.documentsCount++;
|
|
184
|
-
// Check if document has content
|
|
185
226
|
try {
|
|
186
227
|
const content = await readFile(path.join(ssotDir, file));
|
|
187
|
-
// Consider filled if more than 200 characters of non-template content
|
|
188
228
|
const cleanContent = content
|
|
189
229
|
.replace(/^#.*$/gm, '')
|
|
190
230
|
.replace(/^\s*$/gm, '')
|
|
@@ -194,7 +234,7 @@ async function collectSSOTStats(ssotDir) {
|
|
|
194
234
|
}
|
|
195
235
|
}
|
|
196
236
|
catch {
|
|
197
|
-
//
|
|
237
|
+
// skip
|
|
198
238
|
}
|
|
199
239
|
}
|
|
200
240
|
stats.completionRate = stats.documentsCount > 0
|
|
@@ -202,73 +242,331 @@ async function collectSSOTStats(ssotDir) {
|
|
|
202
242
|
: 0;
|
|
203
243
|
return stats;
|
|
204
244
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
245
|
+
// ============================================================
|
|
246
|
+
// Session Statistics (세션 이벤트 기반 품질 지표)
|
|
247
|
+
// ============================================================
|
|
248
|
+
async function collectSessionStats(sessionsDir, startDate) {
|
|
249
|
+
const stats = {
|
|
250
|
+
totalSessions: 0,
|
|
251
|
+
totalEvents: 0,
|
|
252
|
+
totalToolUses: 0,
|
|
253
|
+
totalFailures: 0,
|
|
254
|
+
toolEfficiency: 0,
|
|
255
|
+
subagentCount: 0,
|
|
256
|
+
tokens: {
|
|
257
|
+
totalInput: 0,
|
|
258
|
+
totalOutput: 0,
|
|
259
|
+
totalCacheCreate: 0,
|
|
260
|
+
totalCacheRead: 0,
|
|
261
|
+
cacheHitRate: 0,
|
|
262
|
+
avgOutputPerTurn: 0,
|
|
263
|
+
maxOutputPerTurn: 0,
|
|
264
|
+
},
|
|
265
|
+
toolBreakdown: {},
|
|
266
|
+
cliAdoption: {
|
|
267
|
+
totalBashCommands: 0,
|
|
268
|
+
tsqCommands: 0,
|
|
269
|
+
adoptionRate: 0,
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
if (!await exists(sessionsDir))
|
|
273
|
+
return stats;
|
|
274
|
+
const files = await listFiles('*.jsonl', sessionsDir);
|
|
275
|
+
const startDateStr = startDate.toISOString().split('T')[0];
|
|
276
|
+
const allTurnOutputs = [];
|
|
277
|
+
for (const file of files) {
|
|
278
|
+
// 날짜 필터
|
|
279
|
+
const dateStr = file.split('-').slice(0, 3).join('-');
|
|
280
|
+
if (dateStr < startDateStr)
|
|
281
|
+
continue;
|
|
282
|
+
stats.totalSessions++;
|
|
283
|
+
try {
|
|
284
|
+
const content = await readFile(path.join(sessionsDir, file));
|
|
285
|
+
const events = [];
|
|
286
|
+
for (const line of content.split('\n')) {
|
|
287
|
+
const trimmed = line.trim();
|
|
288
|
+
if (!trimmed)
|
|
289
|
+
continue;
|
|
290
|
+
try {
|
|
291
|
+
events.push(JSON.parse(trimmed));
|
|
292
|
+
}
|
|
293
|
+
catch { /* skip */ }
|
|
294
|
+
}
|
|
295
|
+
stats.totalEvents += events.length;
|
|
296
|
+
for (const ev of events) {
|
|
297
|
+
// 도구 사용 통계
|
|
298
|
+
if (ev.event === 'PostToolUse') {
|
|
299
|
+
stats.totalToolUses++;
|
|
300
|
+
if (ev.tool) {
|
|
301
|
+
stats.toolBreakdown[ev.tool] = (stats.toolBreakdown[ev.tool] || 0) + 1;
|
|
302
|
+
}
|
|
303
|
+
// CLI 채택률: Bash 명령 중 tsq 사용 비율
|
|
304
|
+
if (ev.tool === 'Bash') {
|
|
305
|
+
stats.cliAdoption.totalBashCommands++;
|
|
306
|
+
const cmd = ev.detail?.command || '';
|
|
307
|
+
if (cmd.match(/^(tsq|npx tsq)\s/)) {
|
|
308
|
+
stats.cliAdoption.tsqCommands++;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (ev.event === 'PostToolUseFailure')
|
|
313
|
+
stats.totalFailures++;
|
|
314
|
+
if (ev.event === 'SubagentStart')
|
|
315
|
+
stats.subagentCount++;
|
|
316
|
+
// 토큰 (Stop 이벤트에서 턴별 수집)
|
|
317
|
+
if (ev.event === 'Stop' && ev.usage) {
|
|
318
|
+
allTurnOutputs.push(ev.usage.output || 0);
|
|
319
|
+
}
|
|
320
|
+
// 토큰 (SessionEnd에서 세션 합산)
|
|
321
|
+
if (ev.event === 'SessionEnd' && ev.total_usage) {
|
|
322
|
+
stats.tokens.totalInput += ev.total_usage.total_input || 0;
|
|
323
|
+
stats.tokens.totalOutput += ev.total_usage.total_output || 0;
|
|
324
|
+
stats.tokens.totalCacheCreate += ev.total_usage.total_cache_create || 0;
|
|
325
|
+
stats.tokens.totalCacheRead += ev.total_usage.total_cache_read || 0;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
// skip unreadable files
|
|
331
|
+
}
|
|
209
332
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
333
|
+
// 파생 지표 계산
|
|
334
|
+
const totalToolAttempts = stats.totalToolUses + stats.totalFailures;
|
|
335
|
+
stats.toolEfficiency = totalToolAttempts > 0
|
|
336
|
+
? Math.round((stats.totalToolUses / totalToolAttempts) * 100) : 0;
|
|
337
|
+
const allInput = stats.tokens.totalInput + stats.tokens.totalCacheCreate + stats.tokens.totalCacheRead;
|
|
338
|
+
stats.tokens.cacheHitRate = allInput > 0
|
|
339
|
+
? Math.round((stats.tokens.totalCacheRead / allInput) * 100) : 0;
|
|
340
|
+
if (allTurnOutputs.length > 0) {
|
|
341
|
+
stats.tokens.avgOutputPerTurn = Math.round(allTurnOutputs.reduce((a, b) => a + b, 0) / allTurnOutputs.length);
|
|
342
|
+
stats.tokens.maxOutputPerTurn = Math.max(...allTurnOutputs);
|
|
217
343
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
console.log(colors.dim('No metrics collected yet'));
|
|
222
|
-
console.log(colors.dim('\nRun: tsq metrics collect'));
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
// Load latest metrics
|
|
226
|
-
const latestFile = path.join(metricsDir, files[0]);
|
|
227
|
-
const metrics = await fs.readJson(latestFile);
|
|
228
|
-
await displayMetrics(metrics);
|
|
344
|
+
stats.cliAdoption.adoptionRate = stats.cliAdoption.totalBashCommands > 0
|
|
345
|
+
? Math.round((stats.cliAdoption.tsqCommands / stats.cliAdoption.totalBashCommands) * 100) : 0;
|
|
346
|
+
return stats;
|
|
229
347
|
}
|
|
348
|
+
// ============================================================
|
|
349
|
+
// Display
|
|
350
|
+
// ============================================================
|
|
230
351
|
async function displayMetrics(metrics) {
|
|
231
352
|
printKeyValue('Collected at', metrics.collectedAt);
|
|
232
353
|
printKeyValue('Period', `${metrics.period.start} ~ ${metrics.period.end}`);
|
|
233
|
-
|
|
234
|
-
|
|
354
|
+
// ── 프로세스 지표 ──
|
|
355
|
+
console.log(colors.subheader('\n Process Metrics'));
|
|
356
|
+
console.log(colors.dim(' 에이전트 작업 기록 현황. 프로세스 준수도를 나타냄\n'));
|
|
357
|
+
printKeyValue(' Log files', String(metrics.logs.total));
|
|
235
358
|
if (Object.keys(metrics.logs.byAgent).length > 0) {
|
|
236
|
-
console.log(colors.dim('
|
|
359
|
+
console.log(colors.dim(' By Agent:'));
|
|
237
360
|
Object.entries(metrics.logs.byAgent)
|
|
238
361
|
.sort((a, b) => b[1] - a[1])
|
|
239
362
|
.forEach(([agent, count]) => {
|
|
240
|
-
console.log(`
|
|
363
|
+
console.log(` ${agent.padEnd(12)} ${colors.highlight(String(count))}`);
|
|
241
364
|
});
|
|
242
365
|
}
|
|
243
366
|
if (Object.keys(metrics.logs.byType).length > 0) {
|
|
244
|
-
console.log(colors.dim('
|
|
367
|
+
console.log(colors.dim(' By Type:'));
|
|
245
368
|
Object.entries(metrics.logs.byType)
|
|
246
369
|
.sort((a, b) => b[1] - a[1])
|
|
247
370
|
.forEach(([type, count]) => {
|
|
248
|
-
console.log(`
|
|
371
|
+
console.log(` ${type.padEnd(12)} ${colors.highlight(String(count))}`);
|
|
249
372
|
});
|
|
250
373
|
}
|
|
251
|
-
|
|
252
|
-
|
|
374
|
+
printKeyValue(' Decision Ratio', `${metrics.logs.decisionRatio}%`);
|
|
375
|
+
console.log(colors.dim(' 의사결정 기록 비율. 높을수록 결정 추적 가능'));
|
|
376
|
+
printKeyValue(' Error Rate', `${metrics.logs.errorRate}%`);
|
|
377
|
+
console.log(colors.dim(' 에러 로그 비율. 낮을수록 안정적'));
|
|
378
|
+
// ── 피드백 지표 ──
|
|
379
|
+
console.log(colors.subheader('\n Feedback Metrics'));
|
|
380
|
+
console.log(colors.dim(' 피드백 레벨별 분포. Level 3이 잦으면 요구사항 정의 개선 필요\n'));
|
|
381
|
+
printKeyValue(' Total feedback', String(metrics.feedback.total));
|
|
253
382
|
if (Object.keys(metrics.feedback.byLevel).length > 0) {
|
|
254
|
-
|
|
383
|
+
const levelDesc = {
|
|
384
|
+
'1': '구현 수정 - 경미한 코드 수정 (정상적 개발 과정)',
|
|
385
|
+
'2': '설계 수정 - SSOT 변경 필요 (빈번하면 초기 설계 검토)',
|
|
386
|
+
'3': '기획 수정 - 요구사항 재검토 (잦으면 기획 프로세스 개선)',
|
|
387
|
+
};
|
|
255
388
|
['1', '2', '3'].forEach(level => {
|
|
256
389
|
const count = metrics.feedback.byLevel[level] || 0;
|
|
257
|
-
const
|
|
258
|
-
console.log(`
|
|
390
|
+
const bar = count > 0 ? ' ' + '█'.repeat(Math.min(count, 20)) : '';
|
|
391
|
+
console.log(` Level ${level} ${colors.highlight(String(count).padStart(3))}${colors.dim(bar)}`);
|
|
392
|
+
console.log(colors.dim(` ${levelDesc[level]}`));
|
|
259
393
|
});
|
|
260
394
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
printKeyValue('
|
|
395
|
+
// ── SSOT 지표 ──
|
|
396
|
+
console.log(colors.subheader('\n SSOT Health'));
|
|
397
|
+
console.log(colors.dim(' 문서 기반 개발 성숙도. 100%에 가까울수록 SSOT 운영 양호\n'));
|
|
398
|
+
printKeyValue(' Documents', `${metrics.ssot.filledCount}/${metrics.ssot.documentsCount}`);
|
|
399
|
+
printKeyValue(' Completion', `${metrics.ssot.completionRate}%`);
|
|
400
|
+
if (metrics.ssot.completionRate < 50) {
|
|
401
|
+
console.log(colors.dim(' SSOT 완성률이 낮음. 문서 작성을 우선 진행하세요'));
|
|
402
|
+
}
|
|
403
|
+
// ── 세션 지표 ──
|
|
404
|
+
const s = metrics.sessions;
|
|
405
|
+
console.log(colors.subheader('\n Session & Token Metrics'));
|
|
406
|
+
console.log(colors.dim(' Claude Code 세션 활동. 토큰 효율과 에이전트 정확도 추적\n'));
|
|
407
|
+
printKeyValue(' Sessions', String(s.totalSessions));
|
|
408
|
+
printKeyValue(' Total events', String(s.totalEvents));
|
|
409
|
+
printKeyValue(' Tool uses', String(s.totalToolUses));
|
|
410
|
+
printKeyValue(' Failures', String(s.totalFailures));
|
|
411
|
+
printKeyValue(' Tool Efficiency', `${s.toolEfficiency}%`);
|
|
412
|
+
console.log(colors.dim(' 도구 성공률. 95%+ 정상, 90% 미만이면 에이전트 프롬프트 점검'));
|
|
413
|
+
if (s.subagentCount > 0) {
|
|
414
|
+
printKeyValue(' Subagents', String(s.subagentCount));
|
|
415
|
+
}
|
|
416
|
+
// 토큰
|
|
417
|
+
if (s.tokens.totalOutput > 0) {
|
|
418
|
+
console.log('');
|
|
419
|
+
console.log(colors.dim(' Token Usage:'));
|
|
420
|
+
printKeyValue(' Input', formatTokens(s.tokens.totalInput));
|
|
421
|
+
printKeyValue(' Output', formatTokens(s.tokens.totalOutput));
|
|
422
|
+
printKeyValue(' Cache Create', formatTokens(s.tokens.totalCacheCreate));
|
|
423
|
+
printKeyValue(' Cache Read', formatTokens(s.tokens.totalCacheRead));
|
|
424
|
+
printKeyValue(' Cache Hit Rate', `${s.tokens.cacheHitRate}%`);
|
|
425
|
+
if (s.tokens.cacheHitRate >= 80) {
|
|
426
|
+
console.log(colors.dim(' 우수 - 프롬프트 구조 안정, 캐시 효율 높음'));
|
|
427
|
+
}
|
|
428
|
+
else if (s.tokens.cacheHitRate >= 60) {
|
|
429
|
+
console.log(colors.dim(' 보통 - 일부 프롬프트 변경으로 캐시 미스 발생'));
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
console.log(colors.dim(' 주의 - 프롬프트 구조 불안정, 캐시 효율 낮음. CLAUDE.md 검토 필요'));
|
|
433
|
+
}
|
|
434
|
+
printKeyValue(' Avg Output/Turn', formatTokens(s.tokens.avgOutputPerTurn));
|
|
435
|
+
console.log(colors.dim(' 턴당 평균 출력 토큰. 비정상적으로 높으면 응답이 불필요하게 장황'));
|
|
436
|
+
printKeyValue(' Max Output/Turn', formatTokens(s.tokens.maxOutputPerTurn));
|
|
437
|
+
console.log(colors.dim(' 최대 출력 토큰. 이상치 턴 감지용'));
|
|
438
|
+
}
|
|
439
|
+
// 도구 분포
|
|
440
|
+
if (Object.keys(s.toolBreakdown).length > 0) {
|
|
441
|
+
console.log('');
|
|
442
|
+
console.log(colors.dim(' Tool Breakdown:'));
|
|
443
|
+
const sorted = Object.entries(s.toolBreakdown).sort(([, a], [, b]) => b - a);
|
|
444
|
+
for (const [tool, count] of sorted.slice(0, 8)) {
|
|
445
|
+
const bar = '█'.repeat(Math.min(Math.round(count / Math.max(1, sorted[0][1]) * 20), 20));
|
|
446
|
+
console.log(` ${colors.primary(tool.padEnd(12))} ${colors.dim(bar)} ${count}`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
// CLI 채택률
|
|
450
|
+
if (s.cliAdoption.totalBashCommands > 0) {
|
|
451
|
+
console.log('');
|
|
452
|
+
printKeyValue(' CLI Adoption', `${s.cliAdoption.adoptionRate}% (${s.cliAdoption.tsqCommands}/${s.cliAdoption.totalBashCommands} Bash commands)`);
|
|
453
|
+
console.log(colors.dim(' Bash 명령 중 tsq CLI 사용 비율. 높을수록 프레임워크 정착도 높음'));
|
|
454
|
+
}
|
|
265
455
|
}
|
|
266
|
-
|
|
456
|
+
// ============================================================
|
|
457
|
+
// Trend (시계열 비교)
|
|
458
|
+
// ============================================================
|
|
459
|
+
async function showTrend(count) {
|
|
267
460
|
const projectRoot = await findProjectRoot();
|
|
268
|
-
if (!projectRoot)
|
|
461
|
+
if (!projectRoot)
|
|
269
462
|
throw new Error('Not a TimSquad project');
|
|
463
|
+
const metricsDir = path.join(projectRoot, '.timsquad', 'retrospective', 'metrics');
|
|
464
|
+
if (!await exists(metricsDir)) {
|
|
465
|
+
console.log(colors.dim('No metrics collected yet. Run: tsq metrics collect'));
|
|
466
|
+
return;
|
|
270
467
|
}
|
|
271
|
-
|
|
468
|
+
const files = await listFiles('metrics-*.json', metricsDir);
|
|
469
|
+
files.sort().reverse();
|
|
470
|
+
if (files.length < 2) {
|
|
471
|
+
console.log(colors.dim('Need at least 2 collection periods for trend analysis.'));
|
|
472
|
+
console.log(colors.dim('Run: tsq metrics collect --days 7'));
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
printHeader('Metrics Trend');
|
|
476
|
+
console.log(colors.dim(' 핵심 지표의 시계열 변화. 우측이 최신.\n'));
|
|
477
|
+
const periods = [];
|
|
478
|
+
for (const file of files.slice(0, count).reverse()) {
|
|
479
|
+
const data = await fs.readJson(path.join(metricsDir, file));
|
|
480
|
+
periods.push(data);
|
|
481
|
+
}
|
|
482
|
+
// 헤더
|
|
483
|
+
const dates = periods.map(p => p.period.end.slice(5)); // MM-DD
|
|
484
|
+
console.log(` ${'Metric'.padEnd(22)} ${dates.map(d => d.padStart(8)).join('')}`);
|
|
485
|
+
console.log(colors.dim(` ${'─'.repeat(22 + dates.length * 8)}`));
|
|
486
|
+
// SSOT Completion
|
|
487
|
+
const ssotValues = periods.map(p => `${p.ssot.completionRate}%`);
|
|
488
|
+
console.log(` ${'SSOT Completion'.padEnd(22)} ${ssotValues.map(v => v.padStart(8)).join('')}`);
|
|
489
|
+
// Feedback Total
|
|
490
|
+
const fbValues = periods.map(p => String(p.feedback.total));
|
|
491
|
+
console.log(` ${'Feedback Count'.padEnd(22)} ${fbValues.map(v => v.padStart(8)).join('')}`);
|
|
492
|
+
// Decision Ratio
|
|
493
|
+
const drValues = periods.map(p => `${p.logs.decisionRatio || 0}%`);
|
|
494
|
+
console.log(` ${'Decision Ratio'.padEnd(22)} ${drValues.map(v => v.padStart(8)).join('')}`);
|
|
495
|
+
// Error Rate
|
|
496
|
+
const erValues = periods.map(p => `${p.logs.errorRate || 0}%`);
|
|
497
|
+
console.log(` ${'Error Rate'.padEnd(22)} ${erValues.map(v => v.padStart(8)).join('')}`);
|
|
498
|
+
// Session metrics (if available)
|
|
499
|
+
if (periods.some(p => p.sessions?.totalSessions > 0)) {
|
|
500
|
+
console.log('');
|
|
501
|
+
const teValues = periods.map(p => `${p.sessions?.toolEfficiency || 0}%`);
|
|
502
|
+
console.log(` ${'Tool Efficiency'.padEnd(22)} ${teValues.map(v => v.padStart(8)).join('')}`);
|
|
503
|
+
const chValues = periods.map(p => `${p.sessions?.tokens.cacheHitRate || 0}%`);
|
|
504
|
+
console.log(` ${'Cache Hit Rate'.padEnd(22)} ${chValues.map(v => v.padStart(8)).join('')}`);
|
|
505
|
+
const aoValues = periods.map(p => formatTokens(p.sessions?.tokens.avgOutputPerTurn || 0));
|
|
506
|
+
console.log(` ${'Avg Output/Turn'.padEnd(22)} ${aoValues.map(v => v.padStart(8)).join('')}`);
|
|
507
|
+
const caValues = periods.map(p => `${p.sessions?.cliAdoption.adoptionRate || 0}%`);
|
|
508
|
+
console.log(` ${'CLI Adoption'.padEnd(22)} ${caValues.map(v => v.padStart(8)).join('')}`);
|
|
509
|
+
}
|
|
510
|
+
// 변화 분석
|
|
511
|
+
if (periods.length >= 2) {
|
|
512
|
+
const prev = periods[periods.length - 2];
|
|
513
|
+
const curr = periods[periods.length - 1];
|
|
514
|
+
console.log(colors.subheader('\n Changes (latest vs previous):'));
|
|
515
|
+
const changes = [];
|
|
516
|
+
const ssotDelta = curr.ssot.completionRate - prev.ssot.completionRate;
|
|
517
|
+
if (ssotDelta !== 0) {
|
|
518
|
+
changes.push(` SSOT Completion: ${ssotDelta > 0 ? colors.success(`+${ssotDelta}%`) : colors.error(`${ssotDelta}%`)}`);
|
|
519
|
+
}
|
|
520
|
+
if (curr.sessions && prev.sessions) {
|
|
521
|
+
const cacheDelta = curr.sessions.tokens.cacheHitRate - prev.sessions.tokens.cacheHitRate;
|
|
522
|
+
if (cacheDelta !== 0) {
|
|
523
|
+
changes.push(` Cache Hit Rate: ${cacheDelta > 0 ? colors.success(`+${cacheDelta}%`) : colors.error(`${cacheDelta}%`)}`);
|
|
524
|
+
}
|
|
525
|
+
const effDelta = curr.sessions.toolEfficiency - prev.sessions.toolEfficiency;
|
|
526
|
+
if (effDelta !== 0) {
|
|
527
|
+
changes.push(` Tool Efficiency: ${effDelta > 0 ? colors.success(`+${effDelta}%`) : colors.error(`${effDelta}%`)}`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (changes.length > 0) {
|
|
531
|
+
changes.forEach(c => console.log(c));
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
console.log(colors.dim(' No significant changes detected.'));
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
// ============================================================
|
|
539
|
+
// Summary
|
|
540
|
+
// ============================================================
|
|
541
|
+
async function showMetricsSummary() {
|
|
542
|
+
const projectRoot = await findProjectRoot();
|
|
543
|
+
if (!projectRoot)
|
|
544
|
+
throw new Error('Not a TimSquad project');
|
|
545
|
+
printHeader('Metrics Summary');
|
|
546
|
+
const metricsDir = path.join(projectRoot, '.timsquad', 'retrospective', 'metrics');
|
|
547
|
+
if (!await exists(metricsDir)) {
|
|
548
|
+
console.log(colors.dim('No metrics collected yet'));
|
|
549
|
+
console.log(colors.dim('\nRun: tsq metrics collect'));
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
const files = await listFiles('metrics-*.json', metricsDir);
|
|
553
|
+
files.sort().reverse();
|
|
554
|
+
if (files.length === 0) {
|
|
555
|
+
console.log(colors.dim('No metrics collected yet'));
|
|
556
|
+
console.log(colors.dim('\nRun: tsq metrics collect'));
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
const latestFile = path.join(metricsDir, files[0]);
|
|
560
|
+
const metrics = await fs.readJson(latestFile);
|
|
561
|
+
await displayMetrics(metrics);
|
|
562
|
+
}
|
|
563
|
+
// ============================================================
|
|
564
|
+
// Export
|
|
565
|
+
// ============================================================
|
|
566
|
+
async function exportMetrics(outputPath) {
|
|
567
|
+
const projectRoot = await findProjectRoot();
|
|
568
|
+
if (!projectRoot)
|
|
569
|
+
throw new Error('Not a TimSquad project');
|
|
272
570
|
const metricsDir = path.join(projectRoot, '.timsquad', 'retrospective', 'metrics');
|
|
273
571
|
if (!await exists(metricsDir)) {
|
|
274
572
|
throw new Error('No metrics collected yet. Run: tsq metrics collect');
|
|
@@ -277,15 +575,12 @@ async function exportMetrics(outputPath) {
|
|
|
277
575
|
if (files.length === 0) {
|
|
278
576
|
throw new Error('No metrics collected yet. Run: tsq metrics collect');
|
|
279
577
|
}
|
|
280
|
-
// Load all metrics files
|
|
281
578
|
const allMetrics = [];
|
|
282
579
|
for (const file of files.sort()) {
|
|
283
580
|
const data = await fs.readJson(path.join(metricsDir, file));
|
|
284
581
|
allMetrics.push(data);
|
|
285
582
|
}
|
|
286
|
-
// Determine output path
|
|
287
583
|
const output = outputPath || path.join(projectRoot, `timsquad-metrics-export-${getDateString()}.json`);
|
|
288
|
-
// Export
|
|
289
584
|
const exportData = {
|
|
290
585
|
exportedAt: getTimestamp(),
|
|
291
586
|
projectRoot,
|
|
@@ -296,4 +591,14 @@ async function exportMetrics(outputPath) {
|
|
|
296
591
|
printSuccess('Metrics exported');
|
|
297
592
|
console.log(colors.path(`\n${output}`));
|
|
298
593
|
}
|
|
594
|
+
// ============================================================
|
|
595
|
+
// Helpers
|
|
596
|
+
// ============================================================
|
|
597
|
+
function formatTokens(tokens) {
|
|
598
|
+
if (tokens < 1000)
|
|
599
|
+
return String(tokens);
|
|
600
|
+
if (tokens < 1000000)
|
|
601
|
+
return `${(tokens / 1000).toFixed(1)}K`;
|
|
602
|
+
return `${(tokens / 1000000).toFixed(2)}M`;
|
|
603
|
+
}
|
|
299
604
|
//# sourceMappingURL=metrics.js.map
|