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.
Files changed (56) hide show
  1. package/README.md +688 -154
  2. package/dist/commands/feedback.d.ts.map +1 -1
  3. package/dist/commands/feedback.js +21 -2
  4. package/dist/commands/feedback.js.map +1 -1
  5. package/dist/commands/git/commit.d.ts.map +1 -1
  6. package/dist/commands/git/commit.js +1 -4
  7. package/dist/commands/git/commit.js.map +1 -1
  8. package/dist/commands/improve.d.ts +3 -0
  9. package/dist/commands/improve.d.ts.map +1 -0
  10. package/dist/commands/improve.js +286 -0
  11. package/dist/commands/improve.js.map +1 -0
  12. package/dist/commands/init.js +3 -2
  13. package/dist/commands/init.js.map +1 -1
  14. package/dist/commands/log.d.ts.map +1 -1
  15. package/dist/commands/log.js +202 -2
  16. package/dist/commands/log.js.map +1 -1
  17. package/dist/commands/metrics.d.ts.map +1 -1
  18. package/dist/commands/metrics.js +404 -99
  19. package/dist/commands/metrics.js.map +1 -1
  20. package/dist/commands/retro.d.ts.map +1 -1
  21. package/dist/commands/retro.js +454 -54
  22. package/dist/commands/retro.js.map +1 -1
  23. package/dist/commands/session.d.ts +3 -0
  24. package/dist/commands/session.d.ts.map +1 -0
  25. package/dist/commands/session.js +346 -0
  26. package/dist/commands/session.js.map +1 -0
  27. package/dist/index.js +4 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/lib/template.d.ts.map +1 -1
  30. package/dist/lib/template.js +66 -1
  31. package/dist/lib/template.js.map +1 -1
  32. package/dist/types/feedback.d.ts +52 -1
  33. package/dist/types/feedback.d.ts.map +1 -1
  34. package/dist/types/feedback.js +0 -3
  35. package/dist/types/feedback.js.map +1 -1
  36. package/dist/types/project.d.ts +1 -1
  37. package/dist/types/project.d.ts.map +1 -1
  38. package/dist/types/project.js +3 -1
  39. package/dist/types/project.js.map +1 -1
  40. package/package.json +1 -1
  41. package/templates/common/claude/agents/tsq-dba.md +21 -0
  42. package/templates/common/claude/agents/tsq-designer.md +19 -0
  43. package/templates/common/claude/agents/tsq-developer.md +59 -0
  44. package/templates/common/claude/agents/tsq-planner.md +101 -1
  45. package/templates/common/claude/agents/tsq-prompter.md +20 -0
  46. package/templates/common/claude/agents/tsq-qa.md +38 -4
  47. package/templates/common/claude/agents/tsq-retro.md +25 -0
  48. package/templates/common/claude/agents/tsq-security.md +31 -0
  49. package/templates/common/claude/hooks/auto-metrics.sh +165 -0
  50. package/templates/common/claude/hooks/auto-worklog.sh +245 -0
  51. package/templates/common/claude/hooks/event-logger.sh +208 -0
  52. package/templates/common/claude/settings.json +86 -0
  53. package/templates/common/config.template.yaml +2 -1
  54. package/templates/common/timsquad/process/phase-checklist.yaml +174 -0
  55. package/templates/common/timsquad/process/state-machine.xml +12 -0
  56. package/templates/common/timsquad/process/workflow-base.xml +124 -0
@@ -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 log statistics
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 ssotStats = await collectSSOTStats(ssotDir);
70
- // Create metrics data
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
- const workEntries = (content.match(/## \d{2}:\d{2}:\d{2} \[work\]/g) || []).length;
121
- const errorEntries = (content.match(/## \d{2}:\d{2}:\d{2} \[error\]/g) || []).length;
122
- const decisionEntries = (content.match(/## \d{2}:\d{2}:\d{2} \[decision\]/g) || []).length;
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
- total: 0,
136
- byLevel: {},
137
- };
138
- if (!await exists(logsDir)) {
139
- return stats;
140
- }
141
- const files = await listFiles('*-feedback.md', logsDir);
142
- const startDateStr = startDate.toISOString().split('T')[0];
143
- for (const file of files) {
144
- // Parse filename: YYYY-MM-DD-feedback.md
145
- const match = file.match(/^(\d{4}-\d{2}-\d{2})-feedback\.md$/);
146
- if (!match)
147
- continue;
148
- const [, dateStr] = match;
149
- // Check date range
150
- if (dateStr < startDateStr)
151
- continue;
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
- catch {
164
- // Ignore read errors
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
- // Ignore read errors
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
- async function showMetricsSummary() {
206
- const projectRoot = await findProjectRoot();
207
- if (!projectRoot) {
208
- throw new Error('Not a TimSquad project');
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
- printHeader('Metrics Summary');
211
- // Load latest metrics
212
- const metricsDir = path.join(projectRoot, '.timsquad', 'retrospective', 'metrics');
213
- if (!await exists(metricsDir)) {
214
- console.log(colors.dim('No metrics collected yet'));
215
- console.log(colors.dim('\nRun: tsq metrics collect'));
216
- return;
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
- const files = await listFiles('metrics-*.json', metricsDir);
219
- files.sort().reverse();
220
- if (files.length === 0) {
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
- console.log(colors.subheader('\n📊 Log Statistics'));
234
- printKeyValue('Total log files', String(metrics.logs.total));
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('\nBy Agent:'));
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(` ${agent.padEnd(12)} ${colors.highlight(String(count))}`);
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('\nBy Type:'));
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(` ${type.padEnd(12)} ${colors.highlight(String(count))}`);
371
+ console.log(` ${type.padEnd(12)} ${colors.highlight(String(count))}`);
249
372
  });
250
373
  }
251
- console.log(colors.subheader('\n📝 Feedback Statistics'));
252
- printKeyValue('Total feedback', String(metrics.feedback.total));
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
- console.log(colors.dim('\nBy Level:'));
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 label = level === '1' ? '구현 수정' : level === '2' ? '설계 수정' : '기획 수정';
258
- console.log(` Level ${level} (${label.padEnd(6)}) ${colors.highlight(String(count))}`);
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
- console.log(colors.subheader('\n📄 SSOT Status'));
262
- printKeyValue('Documents', String(metrics.ssot.documentsCount));
263
- printKeyValue('Filled', String(metrics.ssot.filledCount));
264
- printKeyValue('Completion rate', `${metrics.ssot.completionRate}%`);
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
- async function exportMetrics(outputPath) {
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
- // Load all metrics
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