crewx 0.8.1 → 0.8.2-rc.2

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 (125) hide show
  1. package/README.md +268 -268
  2. package/bin/cli-commands.js +34 -0
  3. package/bin/crewx-lib.js +213 -108
  4. package/bin/crewx-ui.js +83 -83
  5. package/bin/crewx.js +219 -147
  6. package/bin/launcher-flags.js +29 -0
  7. package/bin/package.json +1 -1
  8. package/dist/assets/MarketPage-DptjaFpT.js +36 -0
  9. package/dist/assets/{PromptTab-DVKc7hJY.js → PromptTab-DZha2_v1.js} +1 -1
  10. package/dist/assets/{_baseUniq-wjlVo2E6.js → _baseUniq-jd6NubI3.js} +1 -1
  11. package/dist/assets/{arc-BfPgRtzW.js → arc-C2te3-8P.js} +1 -1
  12. package/dist/assets/{architectureDiagram-Q4EWVU46-ewcueFAG.js → architectureDiagram-Q4EWVU46-CbIQua02.js} +1 -1
  13. package/dist/assets/{blockDiagram-DXYQGD6D-TxlbbvKn.js → blockDiagram-DXYQGD6D-Cg7qkpSM.js} +1 -1
  14. package/dist/assets/{c4Diagram-AHTNJAMY-C1lT_bl_.js → c4Diagram-AHTNJAMY-BkffDY1F.js} +1 -1
  15. package/dist/assets/channel-beae0DeI.js +1 -0
  16. package/dist/assets/chatgpt-logo-dark.svg +15 -15
  17. package/dist/assets/chatgpt-logo.svg +15 -15
  18. package/dist/assets/{chunk-4BX2VUAB-C41j2mCL.js → chunk-4BX2VUAB-BxHe9wPE.js} +1 -1
  19. package/dist/assets/{chunk-4TB4RGXK-HNNsUbz0.js → chunk-4TB4RGXK--f1tN90O.js} +1 -1
  20. package/dist/assets/{chunk-55IACEB6-qtCgO0r2.js → chunk-55IACEB6-B5QXfPXQ.js} +1 -1
  21. package/dist/assets/{chunk-EDXVE4YY-BSnDYtsd.js → chunk-EDXVE4YY-DqKhblOg.js} +1 -1
  22. package/dist/assets/{chunk-FMBD7UC4-DyHRLQqX.js → chunk-FMBD7UC4-DZ1w_G02.js} +1 -1
  23. package/dist/assets/{chunk-OYMX7WX6-CCjfi6WS.js → chunk-OYMX7WX6-BqAgQpv8.js} +1 -1
  24. package/dist/assets/{chunk-QZHKN3VN-COLty8kd.js → chunk-QZHKN3VN-DPqnGqVi.js} +1 -1
  25. package/dist/assets/{chunk-YZCP3GAM-CHUUnGeN.js → chunk-YZCP3GAM-x-ZYSQLd.js} +1 -1
  26. package/dist/assets/classDiagram-6PBFFD2Q-BquWrs1y.js +1 -0
  27. package/dist/assets/classDiagram-v2-HSJHXN6E-BquWrs1y.js +1 -0
  28. package/dist/assets/clone-C9wSPtDN.js +1 -0
  29. package/dist/assets/{cose-bilkent-S5V4N54A-CSip-V2g.js → cose-bilkent-S5V4N54A-4VCNRP-E.js} +1 -1
  30. package/dist/assets/{dagre-KV5264BT-DkdpnWhv.js → dagre-KV5264BT-DucVi1QS.js} +1 -1
  31. package/dist/assets/{diagram-5BDNPKRD-PH4qc6PV.js → diagram-5BDNPKRD-ChdRA8bE.js} +1 -1
  32. package/dist/assets/{diagram-G4DWMVQ6-Cg5xZcjx.js → diagram-G4DWMVQ6-B1-97yHr.js} +1 -1
  33. package/dist/assets/{diagram-MMDJMWI5-soKmeTCW.js → diagram-MMDJMWI5-CQs3cO7G.js} +1 -1
  34. package/dist/assets/{diagram-TYMM5635-Daq5Mihu.js → diagram-TYMM5635-CuNCxDfO.js} +1 -1
  35. package/dist/assets/{erDiagram-SMLLAGMA-kr2OtY0Y.js → erDiagram-SMLLAGMA-DdR8v8g-.js} +1 -1
  36. package/dist/assets/{flowDiagram-DWJPFMVM-DQZCb8gm.js → flowDiagram-DWJPFMVM-Dt02upId.js} +1 -1
  37. package/dist/assets/{ganttDiagram-T4ZO3ILL-BHkn485T.js → ganttDiagram-T4ZO3ILL-cG_k9VOa.js} +1 -1
  38. package/dist/assets/{gitGraphDiagram-UUTBAWPF-FaCyYFmC.js → gitGraphDiagram-UUTBAWPF-Dz1DjhKq.js} +1 -1
  39. package/dist/assets/{graph-BVJlrP6V.js → graph-CF2NtM33.js} +1 -1
  40. package/dist/assets/{infoDiagram-42DDH7IO-DJOWkKdM.js → infoDiagram-42DDH7IO-tQWKrYM6.js} +1 -1
  41. package/dist/assets/{ishikawaDiagram-UXIWVN3A-VfpvNaIf.js → ishikawaDiagram-UXIWVN3A-CLuUMkF0.js} +1 -1
  42. package/dist/assets/{journeyDiagram-VCZTEJTY-CPzsak-v.js → journeyDiagram-VCZTEJTY-a6JenLCk.js} +1 -1
  43. package/dist/assets/{kanban-definition-6JOO6SKY-DFqLDBU0.js → kanban-definition-6JOO6SKY-rqOxTzYb.js} +1 -1
  44. package/dist/assets/{layout-CCSbNPHm.js → layout-Dvic1Hpy.js} +1 -1
  45. package/dist/assets/{linear-C4T7PCKE.js → linear-CfMV1S6a.js} +1 -1
  46. package/dist/assets/main-05K4ggqd.css +10 -0
  47. package/dist/assets/main-CCM1gtr8.js +1165 -0
  48. package/dist/assets/{min-CGQNEYGh.js → min-GpF3DZux.js} +1 -1
  49. package/dist/assets/{mindmap-definition-QFDTVHPH-AuU1EqwS.js → mindmap-definition-QFDTVHPH-Cg80z0Jx.js} +1 -1
  50. package/dist/assets/{pieDiagram-DEJITSTG-CopkCZwp.js → pieDiagram-DEJITSTG-BrtK7lAq.js} +1 -1
  51. package/dist/assets/{quadrantDiagram-34T5L4WZ-lMKrSv_t.js → quadrantDiagram-34T5L4WZ-BL2txAAS.js} +1 -1
  52. package/dist/assets/{requirementDiagram-MS252O5E-dWUpHOFb.js → requirementDiagram-MS252O5E-Co3wpBnu.js} +1 -1
  53. package/dist/assets/{sankeyDiagram-XADWPNL6-C8UQx9Bb.js → sankeyDiagram-XADWPNL6-B4KJXdQ4.js} +1 -1
  54. package/dist/assets/{sequenceDiagram-FGHM5R23-CUVNIItJ.js → sequenceDiagram-FGHM5R23-xs5OuzvV.js} +1 -1
  55. package/dist/assets/{stateDiagram-FHFEXIEX-Ct0GamGl.js → stateDiagram-FHFEXIEX-bbEP20JD.js} +1 -1
  56. package/dist/assets/stateDiagram-v2-QKLJ7IA2-XYh9U1A7.js +1 -0
  57. package/dist/assets/{timeline-definition-GMOUNBTQ-ul8Po7f7.js → timeline-definition-GMOUNBTQ-CTc2wVwC.js} +1 -1
  58. package/dist/assets/{vennDiagram-DHZGUBPP-B4AOWQnP.js → vennDiagram-DHZGUBPP-oJpXott7.js} +1 -1
  59. package/dist/assets/{wardley-RL74JXVD-Dr7Wp3AJ.js → wardley-RL74JXVD-aTmOXkKh.js} +1 -1
  60. package/dist/assets/{wardleyDiagram-NUSXRM2D-Ck70faXX.js → wardleyDiagram-NUSXRM2D-C83SOkig.js} +1 -1
  61. package/dist/assets/{xychartDiagram-5P7HB3ND-Bsy5-cNt.js → xychartDiagram-5P7HB3ND-BScws0tF.js} +1 -1
  62. package/dist/index.html +13 -13
  63. package/dist-electron/main.js +153 -116
  64. package/dist-electron/overlay.js +102 -65
  65. package/dist-electron/package.json +1 -0
  66. package/dist-electron/preload.js +8 -8
  67. package/dist-server/bootstrap/crewx-server.js +19 -10
  68. package/dist-server/domain/agent/agent.service.js +12 -0
  69. package/dist-server/domain/agent/dto/update-agent.dto.js +20 -1
  70. package/dist-server/domain/mcp/crewx-tool.factory.js +44 -113
  71. package/dist-server/domain/mcp/mcp.module.js +2 -0
  72. package/dist-server/domain/mcp/mcp.service.js +37 -12
  73. package/dist-server/domain/message/message.service.js +21 -13
  74. package/dist-server/domain/skill/skill.service.js +63 -43
  75. package/dist-server/domain/task/task.module.js +2 -0
  76. package/dist-server/domain/task/task.service.js +17 -10
  77. package/dist-server/domain/thread/dto/update-thread.dto.js +23 -0
  78. package/dist-server/domain/thread/thread.controller.js +16 -0
  79. package/dist-server/domain/thread/thread.service.js +9 -0
  80. package/dist-server/main.js +1 -1
  81. package/dist-server/modules/crewx.module.js +16 -1
  82. package/dist-server/repository/box.repository.js +20 -20
  83. package/dist-server/repository/project.repository.js +13 -13
  84. package/dist-server/repository/request-log.repository.js +10 -10
  85. package/dist-server/repository/task.repository.js +72 -72
  86. package/dist-server/repository/thread.repository.js +78 -58
  87. package/package.json +6 -6
  88. package/packages/cli/dist/bootstrap/crewx-cli.js +12 -0
  89. package/packages/cli/dist/commands/agent.js +23 -23
  90. package/packages/cli/dist/commands/init.js +19 -19
  91. package/packages/cli/dist/commands/parse-common-flags.d.ts +19 -3
  92. package/packages/cli/dist/commands/parse-common-flags.js +46 -6
  93. package/packages/cli/dist/commands/registry.d.ts +13 -0
  94. package/packages/cli/dist/commands/registry.js +29 -0
  95. package/packages/cli/dist/commands/task-db.js +7 -7
  96. package/packages/cli/dist/examples/deny-secrets-plugin.d.ts +22 -0
  97. package/packages/cli/dist/examples/deny-secrets-plugin.js +40 -0
  98. package/packages/cli/dist/main.js +134 -68
  99. package/packages/cli/dist/plugins/examples/echo-hook.d.ts +24 -0
  100. package/packages/cli/dist/plugins/examples/echo-hook.js +60 -0
  101. package/packages/cli/dist/plugins/examples/verify-echo-hook.d.ts +8 -0
  102. package/packages/cli/dist/plugins/examples/verify-echo-hook.js +47 -0
  103. package/packages/cli/dist/plugins/sqlite-tracing.d.ts +11 -0
  104. package/packages/cli/dist/plugins/sqlite-tracing.js +19 -0
  105. package/packages/cli/dist/schema/tasks.d.ts +7 -0
  106. package/packages/cli/dist/schema/tasks.js +48 -0
  107. package/packages/cli/package.json +52 -52
  108. package/scripts/analyze-task-logs.mjs +569 -0
  109. package/scripts/build-manual.mjs +266 -266
  110. package/scripts/emit-dist-server-package-json.mjs +7 -7
  111. package/scripts/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  112. package/scripts/postinstall.mjs +44 -44
  113. package/scripts/smoke-tarball.mjs +285 -285
  114. package/scripts/snapshot-msg-list.sh +52 -52
  115. package/server.js +167 -164
  116. package/dist/assets/MarketPage-Dwsg6K-B.js +0 -31
  117. package/dist/assets/channel-BP4PNMmz.js +0 -1
  118. package/dist/assets/classDiagram-6PBFFD2Q-Upr3UAcM.js +0 -1
  119. package/dist/assets/classDiagram-v2-HSJHXN6E-Upr3UAcM.js +0 -1
  120. package/dist/assets/clone-B8BP7ReZ.js +0 -1
  121. package/dist/assets/main-CELBpK6r.js +0 -1166
  122. package/dist/assets/main-CmP-VosD.css +0 -10
  123. package/dist/assets/stateDiagram-v2-QKLJ7IA2-CFBLQDEx.js +0 -1
  124. package/dist-server/domain/task/dto/project-usage.dto.js +0 -38
  125. package/dist-server/domain/thread/dto/send-message.dto.js +0 -10
@@ -0,0 +1,569 @@
1
+ #!/usr/bin/env node
2
+ // PM 조사 도구 — tasks.logs 컬럼 통계 분석기 (임시/재활용 가능)
3
+ // 제품 코드 아님. scripts/ 하위 조사용.
4
+ //
5
+ // 사용 예:
6
+ // node scripts/analyze-task-logs.mjs
7
+ // node scripts/analyze-task-logs.mjs --preset failure-analysis
8
+ // node scripts/analyze-task-logs.mjs --since 2026-04-01 --until 2026-04-24
9
+ // node scripts/analyze-task-logs.mjs --filter status=failed --group-by event.type
10
+ // node scripts/analyze-task-logs.mjs --preset failure-analysis --sample 20 --output md,json,console
11
+
12
+ import Database from 'better-sqlite3';
13
+ import { writeFileSync, mkdirSync, existsSync } from 'node:fs';
14
+ import { join, dirname, resolve } from 'node:path';
15
+ import { fileURLToPath } from 'node:url';
16
+ import { homedir } from 'node:os';
17
+
18
+ const __dirname = dirname(fileURLToPath(import.meta.url));
19
+ const REPO_ROOT = resolve(__dirname, '..');
20
+
21
+ // ─── CLI 파싱 ───────────────────────────────────────────────────────────────
22
+ function parseArgs(argv) {
23
+ const args = {
24
+ db: join(homedir(), '.crewx/crewx.db'),
25
+ since: null, // ISO date or YYYY-MM-DD; default: 14d ago
26
+ until: null, // default: now
27
+ filter: [], // array of "col=val" or raw SQL WHERE fragment
28
+ groupBy: [],
29
+ sample: 10,
30
+ preset: null,
31
+ output: ['console'],
32
+ outputDir: join(REPO_ROOT, 'docs/qa-reports'),
33
+ };
34
+ for (let i = 2; i < argv.length; i++) {
35
+ const a = argv[i];
36
+ const next = () => argv[++i];
37
+ switch (a) {
38
+ case '--db': args.db = next(); break;
39
+ case '--since': args.since = next(); break;
40
+ case '--until': args.until = next(); break;
41
+ case '--filter': args.filter.push(next()); break;
42
+ case '--group-by': args.groupBy.push(next()); break;
43
+ case '--sample': args.sample = parseInt(next(), 10); break;
44
+ case '--preset': args.preset = next(); break;
45
+ case '--output': args.output = next().split(',').map(s => s.trim()); break;
46
+ case '--output-dir': args.outputDir = next(); break;
47
+ case '-h':
48
+ case '--help': printHelp(); process.exit(0);
49
+ default: console.error(`Unknown arg: ${a}`); process.exit(1);
50
+ }
51
+ }
52
+ // defaults
53
+ if (!args.since) {
54
+ const d = new Date(); d.setDate(d.getDate() - 14);
55
+ args.since = d.toISOString();
56
+ }
57
+ if (!args.until) args.until = new Date().toISOString();
58
+ return args;
59
+ }
60
+
61
+ function printHelp() {
62
+ console.log(`
63
+ PM Task Logs Analyzer
64
+
65
+ Usage:
66
+ node scripts/analyze-task-logs.mjs [options]
67
+
68
+ Options:
69
+ --db <path> SQLite DB path (default: ~/.crewx/crewx.db)
70
+ --since <date> Include tasks started at/after date (default: 14 days ago)
71
+ --until <date> Include tasks started at/before date (default: now)
72
+ --filter <expr> WHERE fragment (repeatable). e.g. status=failed, agent_id=core_sqa
73
+ --group-by <key> Grouping key (repeatable). Supported: event.type, event.level, provider, agent_id
74
+ --sample <n> Drill-down sample count per bucket (default: 10)
75
+ --preset <name> Preset bundle: failure-analysis | event-catalog
76
+ --output <csv> Comma list of: console, json, md (default: console)
77
+ --output-dir <path> Output directory for md/json (default: docs/qa-reports)
78
+ -h, --help Print this help
79
+
80
+ Presets:
81
+ failure-analysis — 실패 태스크 이벤트 패턴 + 카탈로그 v2 매칭률 + drill-down
82
+ event-catalog — provider별 관찰된 이벤트 타입 전수 목록 (중립 분석)
83
+ `);
84
+ }
85
+
86
+ // ─── Provider 감지 (stdout 이벤트 구조 기반 — ground truth) ────────────────────
87
+ // 외부 파일 의존성 없음. DB 자체만 보고 판단.
88
+ function detectProvider(stdoutLines) {
89
+ const types = new Set();
90
+ for (const line of stdoutLines) {
91
+ try {
92
+ const p = JSON.parse(line);
93
+ if (p.type) types.add(p.type);
94
+ } catch {}
95
+ }
96
+ // Codex: turn.*/thread.*/item.* 전용
97
+ if (types.has('turn.started') || types.has('turn.failed') || types.has('thread.started')) return 'codex';
98
+ // Copilot: assistant.* 델타 + tool.execution_*
99
+ if (types.has('assistant.message_delta') || types.has('tool.execution_complete') || types.has('tool.execution_start')) return 'copilot';
100
+ // OpenCode: step_start + step_finish (part 객체 포함)
101
+ if (types.has('step_start') && types.has('step_finish')) return 'opencode';
102
+ // Claude Code: system + result (둘 다 있는 경우)
103
+ if (types.has('system') && types.has('result')) return 'claude';
104
+ // Gemini native: init + message (type 필드만 보고)
105
+ if (types.has('init') && types.has('message')) return 'gemini';
106
+ // Claude 단독 (짧게 실패) — assistant + result 조합
107
+ if (types.has('result') && types.has('assistant')) return 'claude';
108
+ return null; // 감지 불가 (로그 없거나 너무 짧음)
109
+ }
110
+
111
+ // ─── 카탈로그 v2 기반 extractFailure (설계서 참조) ─────────────────────────────
112
+ // tier: primary(확정) / secondary(noise 허용) / tertiary(rate limit 등 약한 신호)
113
+ function extractClaudeLikeFailure(p) {
114
+ if (p.type === 'result' && p.is_error === true) {
115
+ return { message: String(p.result || p.error || 'Task failed'), tier: 'primary' };
116
+ }
117
+ if (p.type === 'rate_limit_event') {
118
+ const info = p.rate_limit_info;
119
+ if (info?.status === 'rejected') return { message: 'Rate limit rejected', tier: 'tertiary' };
120
+ }
121
+ return null;
122
+ }
123
+ const FAILURE_EXTRACTORS = {
124
+ codex(p) {
125
+ if (p.type === 'turn.failed' && p.error?.message) {
126
+ return { message: String(p.error.message), tier: 'primary' };
127
+ }
128
+ if (p.type === 'error' && p.message) {
129
+ return { message: String(p.message), tier: 'secondary' };
130
+ }
131
+ return null;
132
+ },
133
+ claude: extractClaudeLikeFailure,
134
+ copilot: extractClaudeLikeFailure,
135
+ gemini(p) {
136
+ if (p.type === 'error' && p.message) return { message: String(p.message), tier: 'primary' };
137
+ return null;
138
+ },
139
+ opencode(p) {
140
+ if (p.type === 'step_finish' && p.part?.reason && p.part.reason !== 'stop') {
141
+ return { message: `OpenCode step stopped: ${p.part.reason}`, tier: 'primary' };
142
+ }
143
+ return null;
144
+ },
145
+ };
146
+
147
+ function pickFailureMessage(stdoutLines, provider) {
148
+ const extractor = FAILURE_EXTRACTORS[provider];
149
+ if (!extractor) return { message: null, tier: null };
150
+ let primary = null, lastSecondary = null, lastTertiary = null;
151
+ for (const line of stdoutLines) {
152
+ let p;
153
+ try { p = JSON.parse(line); } catch { continue; }
154
+ const sig = extractor(p);
155
+ if (!sig) continue;
156
+ if (sig.tier === 'primary' && primary == null) primary = sig.message;
157
+ else if (sig.tier === 'secondary') lastSecondary = sig.message;
158
+ else if (sig.tier === 'tertiary') lastTertiary = sig.message;
159
+ }
160
+ if (primary != null) return { message: primary, tier: 'primary' };
161
+ if (lastSecondary != null) return { message: lastSecondary, tier: 'secondary' };
162
+ if (lastTertiary != null) return { message: lastTertiary, tier: 'tertiary' };
163
+ return { message: null, tier: null };
164
+ }
165
+
166
+ // ─── 기본 통계 집계 ──────────────────────────────────────────────────────────
167
+ function analyze({ rows, sampleCount, groupBy }) {
168
+ const stats = {
169
+ totalTasks: rows.length,
170
+ statusDist: {},
171
+ providerDist: {},
172
+ eventLevelTotals: { stdout: 0, stderr: 0, info: 0, other: 0 },
173
+ eventTypesByProvider: {}, // provider → type → count
174
+ eventCardinality: {}, // provider → Set<type>
175
+ tasksPerProvider: {}, // provider → count
176
+ tasksWithStdoutJson: {}, // provider → count
177
+ tasksWithStderr: {}, // provider → count
178
+ jsonParseStats: { ok: 0, fail: 0 },
179
+ eventsPerTaskDist: [], // numeric array → mean/median/max
180
+ stderrFirstLineDist: {}, // provider → first-stderr-line → count
181
+ durationsByStatus: {}, // status → duration_ms[]
182
+ // failure-specific
183
+ failureMatch: { // provider → {primary, secondary, tertiary, none, total}
184
+ },
185
+ unmatchedFailureSamples: {}, // provider → sample[]
186
+ extractedMessages: {}, // provider → {message: count}
187
+ groupBy: {}, // custom group-by
188
+ };
189
+
190
+ for (const row of rows) {
191
+ // status dist
192
+ stats.statusDist[row.status] = (stats.statusDist[row.status] || 0) + 1;
193
+
194
+ // duration
195
+ if (row.duration_ms != null) {
196
+ (stats.durationsByStatus[row.status] ||= []).push(row.duration_ms);
197
+ }
198
+
199
+ // logs
200
+ let logs = [];
201
+ if (row.logs) {
202
+ try { logs = JSON.parse(row.logs); if (!Array.isArray(logs)) logs = []; } catch {}
203
+ }
204
+
205
+ const stdoutLines = [];
206
+ let hasStderr = false;
207
+ let firstStderr = null;
208
+ for (const entry of logs) {
209
+ const level = entry.level || 'other';
210
+ stats.eventLevelTotals[level] = (stats.eventLevelTotals[level] || 0) + 1;
211
+ if (level === 'stdout') {
212
+ stdoutLines.push(entry.message);
213
+ } else if (level === 'stderr') {
214
+ hasStderr = true;
215
+ if (firstStderr == null) firstStderr = (entry.message || '').slice(0, 120);
216
+ }
217
+ }
218
+ // provider 감지 — stdout 이벤트 구조로만 (DB ground truth)
219
+ const provider = detectProvider(stdoutLines) || 'unknown';
220
+ stats.providerDist[provider] = (stats.providerDist[provider] || 0) + 1;
221
+ stats.tasksPerProvider[provider] = (stats.tasksPerProvider[provider] || 0) + 1;
222
+
223
+ if (stdoutLines.length > 0) {
224
+ stats.tasksWithStdoutJson[provider] = (stats.tasksWithStdoutJson[provider] || 0) + 1;
225
+ }
226
+ if (hasStderr) {
227
+ stats.tasksWithStderr[provider] = (stats.tasksWithStderr[provider] || 0) + 1;
228
+ if (firstStderr) {
229
+ (stats.stderrFirstLineDist[provider] ||= {});
230
+ stats.stderrFirstLineDist[provider][firstStderr] = (stats.stderrFirstLineDist[provider][firstStderr] || 0) + 1;
231
+ }
232
+ }
233
+ stats.eventsPerTaskDist.push(logs.length);
234
+
235
+ // event type集계
236
+ (stats.eventTypesByProvider[provider] ||= {});
237
+ (stats.eventCardinality[provider] ||= new Set());
238
+ for (const line of stdoutLines) {
239
+ let parsed;
240
+ try { parsed = JSON.parse(line); stats.jsonParseStats.ok++; } catch { stats.jsonParseStats.fail++; continue; }
241
+ const t = parsed.type || '_no_type';
242
+ stats.eventTypesByProvider[provider][t] = (stats.eventTypesByProvider[provider][t] || 0) + 1;
243
+ stats.eventCardinality[provider].add(t);
244
+ }
245
+
246
+ // failure-specific (status=failed)
247
+ if (row.status === 'failed') {
248
+ const errMsg = normalizeError(row.error);
249
+ const isUserCancelled = errMsg && /중지|cancel|abort/i.test(errMsg);
250
+ (stats.failureMatch[provider] ||= { primary: 0, secondary: 0, tertiary: 0, none: 0, userCancelled: 0, total: 0 });
251
+ const m = stats.failureMatch[provider];
252
+ m.total++;
253
+ if (isUserCancelled) { m.userCancelled++; continue; }
254
+
255
+ const { message, tier } = pickFailureMessage(stdoutLines, provider);
256
+ if (tier) {
257
+ m[tier]++;
258
+ // record unique extracted messages
259
+ (stats.extractedMessages[provider] ||= {});
260
+ const keyMsg = (message || '').slice(0, 200);
261
+ stats.extractedMessages[provider][keyMsg] = (stats.extractedMessages[provider][keyMsg] || 0) + 1;
262
+ } else {
263
+ m.none++;
264
+ // drill-down: capture sample
265
+ (stats.unmatchedFailureSamples[provider] ||= []);
266
+ if (stats.unmatchedFailureSamples[provider].length < sampleCount) {
267
+ const lastEvents = stdoutLines.slice(-5);
268
+ stats.unmatchedFailureSamples[provider].push({
269
+ id: row.id,
270
+ agent: row.agent_id,
271
+ model: row.model,
272
+ errorField: errMsg ? errMsg.slice(0, 200) : null,
273
+ lastStdoutEvents: lastEvents.map(l => l.slice(0, 300)),
274
+ stdoutEventCount: stdoutLines.length,
275
+ firstStderrLine: firstStderr,
276
+ });
277
+ }
278
+ }
279
+ }
280
+
281
+ // custom group-by
282
+ for (const key of groupBy) {
283
+ const bucket = resolveGroupKey(key, { row, provider, logs, stdoutLines });
284
+ if (bucket == null) continue;
285
+ (stats.groupBy[key] ||= {});
286
+ stats.groupBy[key][bucket] = (stats.groupBy[key][bucket] || 0) + 1;
287
+ }
288
+ }
289
+
290
+ // Convert Set to count
291
+ for (const p of Object.keys(stats.eventCardinality)) {
292
+ stats.eventCardinality[p] = stats.eventCardinality[p].size;
293
+ }
294
+
295
+ // Summaries
296
+ stats.eventsPerTaskStats = numericSummary(stats.eventsPerTaskDist);
297
+ delete stats.eventsPerTaskDist;
298
+ for (const [status, arr] of Object.entries(stats.durationsByStatus)) {
299
+ stats.durationsByStatus[status] = numericSummary(arr);
300
+ }
301
+
302
+ return stats;
303
+ }
304
+
305
+ function normalizeError(errField) {
306
+ if (!errField) return null;
307
+ try { const p = JSON.parse(errField); return p.message || errField; } catch { return errField; }
308
+ }
309
+
310
+ function resolveGroupKey(key, ctx) {
311
+ if (key === 'agent_id') return ctx.row.agent_id;
312
+ if (key === 'provider') return ctx.provider;
313
+ if (key === 'status') return ctx.row.status;
314
+ if (key === 'model') return ctx.row.model;
315
+ if (key === 'event.type') {
316
+ // counts per unique event type — returns array... flatten: return null, handled elsewhere
317
+ // For simplicity here, pick first event type
318
+ for (const line of ctx.stdoutLines) {
319
+ try { const p = JSON.parse(line); if (p.type) return p.type; } catch {}
320
+ }
321
+ return null;
322
+ }
323
+ if (key === 'event.level') {
324
+ const levels = new Set(ctx.logs.map(l => l.level || 'other'));
325
+ return [...levels].sort().join('+');
326
+ }
327
+ return null;
328
+ }
329
+
330
+ function numericSummary(arr) {
331
+ if (!arr.length) return { count: 0, mean: 0, median: 0, max: 0, min: 0 };
332
+ const sorted = [...arr].sort((a, b) => a - b);
333
+ const sum = sorted.reduce((s, v) => s + v, 0);
334
+ return {
335
+ count: sorted.length,
336
+ mean: Math.round(sum / sorted.length),
337
+ median: sorted[Math.floor(sorted.length / 2)],
338
+ max: sorted[sorted.length - 1],
339
+ min: sorted[0],
340
+ };
341
+ }
342
+
343
+ // ─── WHERE 절 조립 ────────────────────────────────────────────────────────────
344
+ function buildWhere(filters, since, until) {
345
+ const clauses = [];
346
+ const params = {};
347
+ // time filter
348
+ if (since) { clauses.push('started_at >= @since'); params.since = since; }
349
+ if (until) { clauses.push('started_at <= @until'); params.until = until; }
350
+ // filters
351
+ for (let i = 0; i < filters.length; i++) {
352
+ const f = filters[i];
353
+ const m = f.match(/^(\w+)\s*=\s*(.+)$/);
354
+ if (m) {
355
+ const col = m[1]; const val = m[2].replace(/^['"]|['"]$/g, '');
356
+ clauses.push(`${col} = @f${i}`); params[`f${i}`] = val;
357
+ } else {
358
+ clauses.push(`(${f})`);
359
+ }
360
+ }
361
+ return { where: clauses.length ? 'WHERE ' + clauses.join(' AND ') : '', params };
362
+ }
363
+
364
+ // ─── 출력 포맷터 ─────────────────────────────────────────────────────────────
365
+ function renderConsole(stats, args) {
366
+ const out = [];
367
+ out.push(`\n━━━ Task Logs Analysis ━━━`);
368
+ out.push(`기간: ${args.since} ~ ${args.until}`);
369
+ out.push(`필터: ${args.filter.length ? args.filter.join(' | ') : '(없음)'}`);
370
+ out.push(`총 태스크: ${stats.totalTasks}`);
371
+ out.push('');
372
+ out.push(`[상태 분포]`);
373
+ for (const [k, v] of Object.entries(stats.statusDist).sort((a,b) => b[1]-a[1])) {
374
+ out.push(` ${k.padEnd(12)} ${v}`);
375
+ }
376
+ out.push('');
377
+ out.push(`[Provider 분포]`);
378
+ for (const [k, v] of Object.entries(stats.providerDist).sort((a,b) => b[1]-a[1])) {
379
+ out.push(` ${k.padEnd(12)} ${v}`);
380
+ }
381
+ out.push('');
382
+ out.push(`[이벤트 level 총량]`);
383
+ for (const [k, v] of Object.entries(stats.eventLevelTotals)) out.push(` ${k.padEnd(8)} ${v}`);
384
+ out.push('');
385
+ out.push(`[태스크당 이벤트 수] mean=${stats.eventsPerTaskStats.mean} median=${stats.eventsPerTaskStats.median} max=${stats.eventsPerTaskStats.max}`);
386
+ out.push(`[stdout JSON 파싱] ok=${stats.jsonParseStats.ok} fail=${stats.jsonParseStats.fail}`);
387
+
388
+ out.push(`\n[Provider × 이벤트 타입 카디널리티]`);
389
+ for (const [p, n] of Object.entries(stats.eventCardinality)) {
390
+ out.push(` ${p.padEnd(12)} ${n} unique types`);
391
+ }
392
+
393
+ if (Object.keys(stats.failureMatch).length) {
394
+ out.push(`\n[실패 태스크 — 카탈로그 v2 매칭률]`);
395
+ out.push(` Provider total primary secondary tertiary none userCancelled improve%`);
396
+ for (const [p, m] of Object.entries(stats.failureMatch).sort((a,b) => b[1].total-a[1].total)) {
397
+ const improvable = m.total - m.userCancelled;
398
+ const matched = m.primary + m.secondary + m.tertiary;
399
+ const pct = improvable > 0 ? ((matched / improvable) * 100).toFixed(0) : 0;
400
+ out.push(` ${p.padEnd(12)} ${String(m.total).padStart(5)} ${String(m.primary).padStart(7)} ${String(m.secondary).padStart(9)} ${String(m.tertiary).padStart(8)} ${String(m.none).padStart(4)} ${String(m.userCancelled).padStart(13)} ${pct}%`);
401
+ }
402
+ }
403
+
404
+ if (args.preset === 'failure-analysis' || args.preset === 'event-catalog') {
405
+ out.push(`\n[Provider × Event Type 빈도 (Top 15)]`);
406
+ for (const [p, types] of Object.entries(stats.eventTypesByProvider)) {
407
+ out.push(` --- ${p} ---`);
408
+ const sorted = Object.entries(types).sort((a,b) => b[1]-a[1]).slice(0, 15);
409
+ for (const [t, c] of sorted) out.push(` ${t.padEnd(35)} ${c}`);
410
+ }
411
+ }
412
+
413
+ if (args.preset === 'failure-analysis' && Object.keys(stats.extractedMessages).length) {
414
+ out.push(`\n[추출된 메시지 unique 빈도 (Top 10 per provider)]`);
415
+ for (const [p, msgs] of Object.entries(stats.extractedMessages)) {
416
+ out.push(` --- ${p} ---`);
417
+ const sorted = Object.entries(msgs).sort((a,b) => b[1]-a[1]).slice(0, 10);
418
+ for (const [m, c] of sorted) out.push(` [${String(c).padStart(3)}] ${m.slice(0, 140)}`);
419
+ }
420
+ }
421
+
422
+ if (args.preset === 'failure-analysis' && Object.keys(stats.unmatchedFailureSamples).length) {
423
+ out.push(`\n[비매칭 실패 샘플 drill-down (max ${args.sample} per provider)]`);
424
+ for (const [p, samples] of Object.entries(stats.unmatchedFailureSamples)) {
425
+ out.push(` --- ${p} (${samples.length} samples) ---`);
426
+ for (const s of samples.slice(0, 5)) {
427
+ out.push(` [${s.id}] agent=${s.agent} model=${s.model || '-'} events=${s.stdoutEventCount}`);
428
+ out.push(` errorField: ${s.errorField || '-'}`);
429
+ out.push(` firstStderr: ${s.firstStderrLine || '-'}`);
430
+ const last = s.lastStdoutEvents[s.lastStdoutEvents.length - 1];
431
+ if (last) out.push(` lastEvent: ${last.slice(0, 160)}`);
432
+ }
433
+ }
434
+ }
435
+
436
+ return out.join('\n');
437
+ }
438
+
439
+ function renderMarkdown(stats, args) {
440
+ const lines = [];
441
+ lines.push(`# Task Logs Analysis — ${args.preset || 'custom'}`);
442
+ lines.push('');
443
+ lines.push(`- **기간**: ${args.since} ~ ${args.until}`);
444
+ lines.push(`- **필터**: ${args.filter.length ? args.filter.join(', ') : '(없음)'}`);
445
+ lines.push(`- **총 태스크**: ${stats.totalTasks}`);
446
+ lines.push(`- **생성**: ${new Date().toISOString()}`);
447
+ lines.push('');
448
+ lines.push(`## 상태 분포`);
449
+ lines.push('| status | count |');
450
+ lines.push('|---|---|');
451
+ for (const [k, v] of Object.entries(stats.statusDist).sort((a,b) => b[1]-a[1])) {
452
+ lines.push(`| ${k} | ${v} |`);
453
+ }
454
+ lines.push('');
455
+ lines.push(`## Provider 분포`);
456
+ lines.push('| provider | count |');
457
+ lines.push('|---|---|');
458
+ for (const [k, v] of Object.entries(stats.providerDist).sort((a,b) => b[1]-a[1])) {
459
+ lines.push(`| ${k} | ${v} |`);
460
+ }
461
+ lines.push('');
462
+ if (Object.keys(stats.failureMatch).length) {
463
+ lines.push(`## 실패 매칭률 (카탈로그 v2 기준)`);
464
+ lines.push('| provider | total | primary | secondary | tertiary | none | userCancelled | improve% |');
465
+ lines.push('|---|---|---|---|---|---|---|---|');
466
+ for (const [p, m] of Object.entries(stats.failureMatch).sort((a,b) => b[1].total-a[1].total)) {
467
+ const improvable = m.total - m.userCancelled;
468
+ const matched = m.primary + m.secondary + m.tertiary;
469
+ const pct = improvable > 0 ? ((matched / improvable) * 100).toFixed(0) : 0;
470
+ lines.push(`| ${p} | ${m.total} | ${m.primary} | ${m.secondary} | ${m.tertiary} | ${m.none} | ${m.userCancelled} | ${pct}% |`);
471
+ }
472
+ lines.push('');
473
+ }
474
+ lines.push(`## Provider × Event Type 빈도 (Top 15)`);
475
+ for (const [p, types] of Object.entries(stats.eventTypesByProvider)) {
476
+ lines.push(`### ${p}`);
477
+ lines.push('| event.type | count |');
478
+ lines.push('|---|---|');
479
+ const sorted = Object.entries(types).sort((a,b) => b[1]-a[1]).slice(0, 15);
480
+ for (const [t, c] of sorted) lines.push(`| \`${t}\` | ${c} |`);
481
+ lines.push('');
482
+ }
483
+
484
+ if (Object.keys(stats.extractedMessages).length) {
485
+ lines.push(`## 추출된 메시지 unique 빈도`);
486
+ for (const [p, msgs] of Object.entries(stats.extractedMessages)) {
487
+ lines.push(`### ${p}`);
488
+ lines.push('| count | message |');
489
+ lines.push('|---|---|');
490
+ const sorted = Object.entries(msgs).sort((a,b) => b[1]-a[1]).slice(0, 15);
491
+ for (const [m, c] of sorted) lines.push(`| ${c} | \`${m.replace(/\|/g, '\\|').slice(0, 180)}\` |`);
492
+ lines.push('');
493
+ }
494
+ }
495
+
496
+ if (Object.keys(stats.unmatchedFailureSamples).length) {
497
+ lines.push(`## 비매칭 실패 샘플 drill-down (${args.sample}개 per provider)`);
498
+ for (const [p, samples] of Object.entries(stats.unmatchedFailureSamples)) {
499
+ lines.push(`### ${p} (${samples.length} samples)`);
500
+ for (const s of samples) {
501
+ lines.push(`- **${s.id}** agent=\`${s.agent}\` model=\`${s.model || '-'}\` events=${s.stdoutEventCount}`);
502
+ lines.push(` - errorField: \`${(s.errorField || '-').replace(/`/g, "'")}\``);
503
+ lines.push(` - firstStderr: \`${(s.firstStderrLine || '-').replace(/`/g, "'")}\``);
504
+ if (s.lastStdoutEvents.length) {
505
+ lines.push(` - lastEvent: \`${s.lastStdoutEvents[s.lastStdoutEvents.length-1].slice(0, 200).replace(/`/g, "'")}\``);
506
+ }
507
+ }
508
+ lines.push('');
509
+ }
510
+ }
511
+
512
+ if (Object.keys(stats.stderrFirstLineDist).length) {
513
+ lines.push(`## stderr 첫 줄 분포 (provider별 Top 8)`);
514
+ for (const [p, dist] of Object.entries(stats.stderrFirstLineDist)) {
515
+ lines.push(`### ${p}`);
516
+ lines.push('| count | stderr first line |');
517
+ lines.push('|---|---|');
518
+ const sorted = Object.entries(dist).sort((a,b) => b[1]-a[1]).slice(0, 8);
519
+ for (const [l, c] of sorted) lines.push(`| ${c} | \`${l.replace(/\|/g, '\\|').replace(/`/g, "'").slice(0, 160)}\` |`);
520
+ lines.push('');
521
+ }
522
+ }
523
+
524
+ return lines.join('\n');
525
+ }
526
+
527
+ // ─── 메인 ────────────────────────────────────────────────────────────────────
528
+ function main() {
529
+ const args = parseArgs(process.argv);
530
+
531
+ // 프리셋 자동 필터/그룹 세팅
532
+ if (args.preset === 'failure-analysis' && !args.filter.some(f => f.startsWith('status='))) {
533
+ args.filter.push('status=failed');
534
+ }
535
+
536
+ const db = new Database(args.db, { readonly: true });
537
+ const { where, params } = buildWhere(args.filter, args.since, args.until);
538
+ const sql = `
539
+ SELECT id, agent_id, status, error, logs, started_at, completed_at, duration_ms, model
540
+ FROM tasks
541
+ ${where}
542
+ `;
543
+ const rows = db.prepare(sql).all(params);
544
+
545
+ const stats = analyze({ rows, sampleCount: args.sample, groupBy: args.groupBy });
546
+ stats._meta = { since: args.since, until: args.until, preset: args.preset, filter: args.filter };
547
+
548
+ // 출력
549
+ if (args.output.includes('console')) {
550
+ console.log(renderConsole(stats, args));
551
+ }
552
+ if (args.output.includes('md') || args.output.includes('json')) {
553
+ if (!existsSync(args.outputDir)) mkdirSync(args.outputDir, { recursive: true });
554
+ const date = new Date().toISOString().slice(0, 10);
555
+ const base = args.preset ? `task-logs-${args.preset}-${date}` : `task-logs-${date}`;
556
+ if (args.output.includes('md')) {
557
+ const p = join(args.outputDir, `${base}.md`);
558
+ writeFileSync(p, renderMarkdown(stats, args), 'utf8');
559
+ console.log(`\n📝 MD 리포트: ${p}`);
560
+ }
561
+ if (args.output.includes('json')) {
562
+ const p = join(args.outputDir, `${base}.json`);
563
+ writeFileSync(p, JSON.stringify(stats, null, 2), 'utf8');
564
+ console.log(`📊 JSON dump: ${p}`);
565
+ }
566
+ }
567
+ }
568
+
569
+ main();