sequant 1.10.1 → 1.11.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 (41) hide show
  1. package/README.md +6 -1
  2. package/dist/bin/cli.js +55 -2
  3. package/dist/dashboard/server.d.ts +37 -0
  4. package/dist/dashboard/server.js +968 -0
  5. package/dist/src/commands/dashboard.d.ts +25 -0
  6. package/dist/src/commands/dashboard.js +44 -0
  7. package/dist/src/commands/doctor.d.ts +18 -1
  8. package/dist/src/commands/doctor.js +105 -2
  9. package/dist/src/commands/init.d.ts +1 -0
  10. package/dist/src/commands/init.js +26 -2
  11. package/dist/src/commands/run.d.ts +20 -0
  12. package/dist/src/commands/run.js +151 -3
  13. package/dist/src/commands/state.d.ts +60 -0
  14. package/dist/src/commands/state.js +267 -0
  15. package/dist/src/commands/stats.d.ts +3 -2
  16. package/dist/src/commands/stats.js +246 -38
  17. package/dist/src/commands/status.d.ts +2 -0
  18. package/dist/src/commands/status.js +28 -3
  19. package/dist/src/lib/ac-parser.d.ts +61 -0
  20. package/dist/src/lib/ac-parser.js +156 -0
  21. package/dist/src/lib/fs.d.ts +19 -0
  22. package/dist/src/lib/fs.js +58 -1
  23. package/dist/src/lib/settings.d.ts +7 -0
  24. package/dist/src/lib/settings.js +1 -0
  25. package/dist/src/lib/system.d.ts +19 -0
  26. package/dist/src/lib/system.js +26 -0
  27. package/dist/src/lib/templates.d.ts +34 -1
  28. package/dist/src/lib/templates.js +109 -5
  29. package/dist/src/lib/workflow/metrics-schema.d.ts +153 -0
  30. package/dist/src/lib/workflow/metrics-schema.js +138 -0
  31. package/dist/src/lib/workflow/metrics-writer.d.ts +102 -0
  32. package/dist/src/lib/workflow/metrics-writer.js +189 -0
  33. package/dist/src/lib/workflow/state-manager.d.ts +18 -1
  34. package/dist/src/lib/workflow/state-manager.js +61 -1
  35. package/dist/src/lib/workflow/state-schema.d.ts +152 -1
  36. package/dist/src/lib/workflow/state-schema.js +99 -0
  37. package/dist/src/lib/workflow/state-utils.d.ts +67 -3
  38. package/dist/src/lib/workflow/state-utils.js +289 -8
  39. package/dist/src/lib/workflow/types.d.ts +2 -0
  40. package/dist/src/lib/workflow/types.js +1 -0
  41. package/package.json +5 -1
@@ -0,0 +1,267 @@
1
+ /**
2
+ * sequant state - Manage workflow state for existing worktrees
3
+ *
4
+ * Provides commands to bootstrap, rebuild, and clean up workflow state:
5
+ * - init: Populate state for untracked worktrees
6
+ * - rebuild: Recreate entire state from git worktrees + logs
7
+ * - clean: Remove entries for worktrees that no longer exist
8
+ */
9
+ import chalk from "chalk";
10
+ import { StateManager } from "../lib/workflow/state-manager.js";
11
+ import { rebuildStateFromLogs, cleanupStaleEntries, discoverUntrackedWorktrees, } from "../lib/workflow/state-utils.js";
12
+ import { createIssueState } from "../lib/workflow/state-schema.js";
13
+ /**
14
+ * Initialize state for untracked worktrees
15
+ *
16
+ * Scans for worktrees with issue-* or feature/* patterns,
17
+ * extracts issue numbers, fetches titles from GitHub,
18
+ * and populates state file with reasonable defaults.
19
+ */
20
+ export async function stateInitCommand(options = {}) {
21
+ if (!options.json) {
22
+ console.log(chalk.bold("\nšŸ” Discovering untracked worktrees...\n"));
23
+ }
24
+ const discoverOptions = {
25
+ verbose: options.verbose && !options.json,
26
+ };
27
+ const result = await discoverUntrackedWorktrees(discoverOptions);
28
+ if (options.json) {
29
+ console.log(JSON.stringify(result, null, 2));
30
+ return;
31
+ }
32
+ if (!result.success) {
33
+ console.log(chalk.red(`āœ— Discovery failed: ${result.error}`));
34
+ return;
35
+ }
36
+ if (result.discovered.length === 0) {
37
+ console.log(chalk.green("āœ“ All worktrees are already tracked"));
38
+ console.log(chalk.gray(` Worktrees scanned: ${result.worktreesScanned}`));
39
+ return;
40
+ }
41
+ // Initialize state for discovered worktrees
42
+ const stateManager = new StateManager({ verbose: options.verbose });
43
+ for (const worktree of result.discovered) {
44
+ try {
45
+ // Create issue state with worktree info
46
+ const issueState = createIssueState(worktree.issueNumber, worktree.title, {
47
+ worktree: worktree.worktreePath,
48
+ branch: worktree.branch,
49
+ });
50
+ // Set status based on inferred phase
51
+ if (worktree.inferredPhase) {
52
+ issueState.currentPhase = worktree.inferredPhase;
53
+ issueState.status = "in_progress";
54
+ }
55
+ // Save to state
56
+ const state = await stateManager.getState();
57
+ state.issues[String(worktree.issueNumber)] = issueState;
58
+ await stateManager.saveState(state);
59
+ console.log(chalk.green(`āœ“ Added #${worktree.issueNumber}: ${worktree.title}`));
60
+ console.log(chalk.gray(` Branch: ${worktree.branch}`));
61
+ if (worktree.inferredPhase) {
62
+ console.log(chalk.gray(` Inferred phase: ${worktree.inferredPhase}`));
63
+ }
64
+ console.log("");
65
+ }
66
+ catch (error) {
67
+ console.log(chalk.yellow(`⚠ Failed to add #${worktree.issueNumber}: ${error}`));
68
+ }
69
+ }
70
+ console.log(chalk.bold("\nSummary:"));
71
+ console.log(chalk.gray(` Worktrees scanned: ${result.worktreesScanned}`));
72
+ console.log(chalk.gray(` Already tracked: ${result.alreadyTracked}`));
73
+ console.log(chalk.green(` Newly added: ${result.discovered.length}`));
74
+ if (result.skipped.length > 0) {
75
+ console.log(chalk.yellow(` Skipped: ${result.skipped.length}`));
76
+ for (const skip of result.skipped) {
77
+ console.log(chalk.gray(` - ${skip.path}: ${skip.reason}`));
78
+ }
79
+ }
80
+ }
81
+ /**
82
+ * Rebuild entire state from scratch
83
+ *
84
+ * Combines worktree discovery with log-based state reconstruction.
85
+ * Creates backup of existing state before rebuilding.
86
+ */
87
+ export async function stateRebuildCommand(options = {}) {
88
+ if (!options.json) {
89
+ console.log(chalk.bold("\nšŸ”„ Rebuilding state from scratch...\n"));
90
+ if (!options.force) {
91
+ console.log(chalk.yellow("⚠ This will replace the existing state file. Use --force to proceed.\n"));
92
+ return;
93
+ }
94
+ }
95
+ // Step 1: Rebuild from logs
96
+ if (!options.json) {
97
+ console.log(chalk.gray("Step 1: Rebuilding from run logs..."));
98
+ }
99
+ const logResult = await rebuildStateFromLogs({
100
+ verbose: options.verbose && !options.json,
101
+ });
102
+ if (!logResult.success) {
103
+ if (options.json) {
104
+ console.log(JSON.stringify({ success: false, error: logResult.error }, null, 2));
105
+ }
106
+ else {
107
+ console.log(chalk.red(`āœ— Log rebuild failed: ${logResult.error}`));
108
+ }
109
+ return;
110
+ }
111
+ // Step 2: Discover and add untracked worktrees
112
+ if (!options.json) {
113
+ console.log(chalk.gray("Step 2: Discovering untracked worktrees..."));
114
+ }
115
+ const discoverResult = await discoverUntrackedWorktrees({
116
+ verbose: options.verbose && !options.json,
117
+ });
118
+ if (!discoverResult.success) {
119
+ if (options.json) {
120
+ console.log(JSON.stringify({ success: false, error: discoverResult.error }, null, 2));
121
+ }
122
+ else {
123
+ console.log(chalk.yellow(`⚠ Worktree discovery failed: ${discoverResult.error}`));
124
+ console.log(chalk.gray(" State rebuilt from logs only."));
125
+ }
126
+ }
127
+ else {
128
+ // Add discovered worktrees to state
129
+ const stateManager = new StateManager({ verbose: options.verbose });
130
+ for (const worktree of discoverResult.discovered) {
131
+ try {
132
+ const issueState = createIssueState(worktree.issueNumber, worktree.title, {
133
+ worktree: worktree.worktreePath,
134
+ branch: worktree.branch,
135
+ });
136
+ if (worktree.inferredPhase) {
137
+ issueState.currentPhase = worktree.inferredPhase;
138
+ issueState.status = "in_progress";
139
+ }
140
+ const state = await stateManager.getState();
141
+ // Only add if not already present from logs
142
+ if (!state.issues[String(worktree.issueNumber)]) {
143
+ state.issues[String(worktree.issueNumber)] = issueState;
144
+ await stateManager.saveState(state);
145
+ }
146
+ else {
147
+ // Update worktree info if missing
148
+ const existing = state.issues[String(worktree.issueNumber)];
149
+ if (!existing.worktree) {
150
+ existing.worktree = worktree.worktreePath;
151
+ existing.branch = worktree.branch;
152
+ await stateManager.saveState(state);
153
+ }
154
+ }
155
+ }
156
+ catch {
157
+ // Continue with other worktrees
158
+ }
159
+ }
160
+ }
161
+ // Output results
162
+ if (options.json) {
163
+ console.log(JSON.stringify({
164
+ success: true,
165
+ logsProcessed: logResult.logsProcessed,
166
+ issuesFromLogs: logResult.issuesFound,
167
+ worktreesScanned: discoverResult.worktreesScanned,
168
+ worktreesAdded: discoverResult.discovered.length,
169
+ }, null, 2));
170
+ return;
171
+ }
172
+ console.log(chalk.green("\nāœ“ State rebuilt successfully"));
173
+ console.log(chalk.gray(` Logs processed: ${logResult.logsProcessed}`));
174
+ console.log(chalk.gray(` Issues from logs: ${logResult.issuesFound}`));
175
+ console.log(chalk.gray(` Worktrees scanned: ${discoverResult.worktreesScanned}`));
176
+ console.log(chalk.gray(` Worktrees added: ${discoverResult.discovered.length}`));
177
+ console.log(chalk.gray("\nRun `sequant status --issues` to see the rebuilt state."));
178
+ }
179
+ /**
180
+ * Clean up orphaned state entries
181
+ *
182
+ * Removes entries for worktrees that no longer exist.
183
+ * Detects merged PRs and auto-removes them.
184
+ */
185
+ export async function stateCleanCommand(options = {}) {
186
+ const dryRun = options.dryRun ?? false;
187
+ const removeAll = options.all ?? false;
188
+ if (!options.json) {
189
+ if (dryRun) {
190
+ console.log(chalk.bold("\n🧹 Cleanup preview (dry run)...\n"));
191
+ }
192
+ else if (removeAll) {
193
+ console.log(chalk.bold("\n🧹 Cleaning up all orphaned entries...\n"));
194
+ }
195
+ else {
196
+ console.log(chalk.bold("\n🧹 Cleaning up orphaned entries...\n"));
197
+ }
198
+ }
199
+ const result = await cleanupStaleEntries({
200
+ dryRun,
201
+ maxAgeDays: options.maxAge,
202
+ removeAll,
203
+ verbose: options.verbose && !options.json,
204
+ });
205
+ if (options.json) {
206
+ console.log(JSON.stringify(result, null, 2));
207
+ return;
208
+ }
209
+ if (!result.success) {
210
+ console.log(chalk.red(`āœ— Cleanup failed: ${result.error}`));
211
+ return;
212
+ }
213
+ const orphanedCount = result.orphaned.length;
214
+ const removedCount = result.removed.length;
215
+ const mergedCount = result.merged.length;
216
+ if (orphanedCount === 0 && removedCount === 0 && mergedCount === 0) {
217
+ console.log(chalk.green("āœ“ No orphaned entries found"));
218
+ return;
219
+ }
220
+ if (dryRun) {
221
+ console.log(chalk.yellow("Preview (no changes made):"));
222
+ }
223
+ else {
224
+ console.log(chalk.green("āœ“ Cleanup completed"));
225
+ }
226
+ if (mergedCount > 0) {
227
+ console.log(chalk.green(` Merged PRs (auto-removed): ${result.merged.map((n) => `#${n}`).join(", ")}`));
228
+ }
229
+ if (orphanedCount > 0) {
230
+ const orphanedNotMerged = result.orphaned.filter((n) => !result.merged.includes(n));
231
+ if (orphanedNotMerged.length > 0) {
232
+ console.log(chalk.yellow(` Abandoned (no merge): ${orphanedNotMerged.map((n) => `#${n}`).join(", ")}`));
233
+ }
234
+ }
235
+ if (removedCount > 0) {
236
+ const removedNotMerged = result.removed.filter((n) => !result.merged.includes(n));
237
+ if (removedNotMerged.length > 0) {
238
+ console.log(chalk.gray(` Removed: ${removedNotMerged.map((n) => `#${n}`).join(", ")}`));
239
+ }
240
+ }
241
+ if (dryRun) {
242
+ console.log(chalk.gray("\nRun without --dry-run to apply these changes."));
243
+ if (!removeAll && orphanedCount > 0) {
244
+ console.log(chalk.gray("Use --all to remove both merged and abandoned entries."));
245
+ }
246
+ }
247
+ }
248
+ /**
249
+ * Main state command handler (routes to subcommands)
250
+ */
251
+ export async function stateCommand(subcommand, options = {}) {
252
+ switch (subcommand) {
253
+ case "init":
254
+ return stateInitCommand(options);
255
+ case "rebuild":
256
+ return stateRebuildCommand(options);
257
+ case "clean":
258
+ return stateCleanCommand(options);
259
+ default:
260
+ console.log(chalk.bold("\nšŸ“Š sequant state - Manage workflow state\n"));
261
+ console.log("Available subcommands:");
262
+ console.log(chalk.gray(" init Populate state for untracked worktrees"));
263
+ console.log(chalk.gray(" rebuild Recreate state from logs + worktrees"));
264
+ console.log(chalk.gray(" clean Remove entries for deleted worktrees"));
265
+ console.log(chalk.gray("\nRun `sequant state <subcommand> --help` for more information."));
266
+ }
267
+ }
@@ -1,7 +1,8 @@
1
1
  /**
2
- * sequant stats - Aggregate analysis of workflow run logs
2
+ * sequant stats - Local workflow analytics
3
3
  *
4
- * Provides success/failure rates, phase durations, and common failure patterns.
4
+ * Provides success/failure rates, workflow insights, and aggregate statistics.
5
+ * All data is local - no telemetry is ever sent remotely.
5
6
  */
6
7
  interface StatsOptions {
7
8
  path?: string;
@@ -1,13 +1,15 @@
1
1
  /**
2
- * sequant stats - Aggregate analysis of workflow run logs
2
+ * sequant stats - Local workflow analytics
3
3
  *
4
- * Provides success/failure rates, phase durations, and common failure patterns.
4
+ * Provides success/failure rates, workflow insights, and aggregate statistics.
5
+ * All data is local - no telemetry is ever sent remotely.
5
6
  */
6
7
  import chalk from "chalk";
7
8
  import * as fs from "fs";
8
9
  import * as path from "path";
9
10
  import * as os from "os";
10
11
  import { RunLogSchema, LOG_PATHS, } from "../lib/workflow/run-log-schema.js";
12
+ import { MetricsSchema, METRICS_FILE_PATH, } from "../lib/workflow/metrics-schema.js";
11
13
  /**
12
14
  * Resolve the log directory path
13
15
  */
@@ -201,55 +203,219 @@ function displayStats(stats, logDir) {
201
203
  console.log("");
202
204
  }
203
205
  /**
204
- * Main stats command
206
+ * Load metrics from file
205
207
  */
206
- export async function statsCommand(options) {
207
- const logDir = resolveLogPath(options.path);
208
- // List log files
209
- const logFiles = listLogFiles(logDir);
210
- if (logFiles.length === 0) {
211
- if (options.json) {
212
- console.log(JSON.stringify({ error: "No logs found", runs: [] }));
208
+ function loadMetrics() {
209
+ if (!fs.existsSync(METRICS_FILE_PATH)) {
210
+ return null;
211
+ }
212
+ try {
213
+ const content = fs.readFileSync(METRICS_FILE_PATH, "utf-8");
214
+ const data = JSON.parse(content);
215
+ return MetricsSchema.parse(data);
216
+ }
217
+ catch {
218
+ return null;
219
+ }
220
+ }
221
+ /**
222
+ * Calculate analytics from metrics
223
+ */
224
+ function calculateMetricsAnalytics(metrics) {
225
+ const runs = metrics.runs;
226
+ if (runs.length === 0) {
227
+ return {
228
+ totalRuns: 0,
229
+ successCount: 0,
230
+ partialCount: 0,
231
+ failedCount: 0,
232
+ successRate: 0,
233
+ avgTokensPerRun: 0,
234
+ avgFilesChanged: 0,
235
+ avgLinesAdded: 0,
236
+ avgDuration: 0,
237
+ chainModeSuccessRate: 0,
238
+ singleIssueSuccessRate: 0,
239
+ insights: [],
240
+ };
241
+ }
242
+ const successCount = runs.filter((r) => r.outcome === "success").length;
243
+ const partialCount = runs.filter((r) => r.outcome === "partial").length;
244
+ const failedCount = runs.filter((r) => r.outcome === "failed").length;
245
+ const successRate = (successCount / runs.length) * 100;
246
+ const avgTokensPerRun = runs.reduce((sum, r) => sum + r.metrics.tokensUsed, 0) / runs.length;
247
+ const avgFilesChanged = runs.reduce((sum, r) => sum + r.metrics.filesChanged, 0) / runs.length;
248
+ const avgLinesAdded = runs.reduce((sum, r) => sum + r.metrics.linesAdded, 0) / runs.length;
249
+ const avgDuration = runs.reduce((sum, r) => sum + r.duration, 0) / runs.length;
250
+ // Chain mode vs single issue analysis
251
+ const chainRuns = runs.filter((r) => r.flags.includes("--chain"));
252
+ const singleIssueRuns = runs.filter((r) => r.issues.length === 1);
253
+ const chainModeSuccessRate = chainRuns.length > 0
254
+ ? (chainRuns.filter((r) => r.outcome === "success").length /
255
+ chainRuns.length) *
256
+ 100
257
+ : 0;
258
+ const singleIssueSuccessRate = singleIssueRuns.length > 0
259
+ ? (singleIssueRuns.filter((r) => r.outcome === "success").length /
260
+ singleIssueRuns.length) *
261
+ 100
262
+ : 0;
263
+ // Generate insights
264
+ const insights = generateInsights(runs, successRate, avgFilesChanged, avgLinesAdded, chainModeSuccessRate, singleIssueSuccessRate);
265
+ return {
266
+ totalRuns: runs.length,
267
+ successCount,
268
+ partialCount,
269
+ failedCount,
270
+ successRate,
271
+ avgTokensPerRun,
272
+ avgFilesChanged,
273
+ avgLinesAdded,
274
+ avgDuration,
275
+ chainModeSuccessRate,
276
+ singleIssueSuccessRate,
277
+ insights,
278
+ };
279
+ }
280
+ /**
281
+ * Generate insights from metrics data
282
+ */
283
+ function generateInsights(runs, successRate, avgFilesChanged, avgLinesAdded, chainModeSuccessRate, singleIssueSuccessRate) {
284
+ const insights = [];
285
+ // Success rate insight
286
+ if (successRate >= 80) {
287
+ insights.push(`Strong success rate: ${successRate.toFixed(0)}%`);
288
+ }
289
+ else if (successRate >= 60) {
290
+ insights.push(`Moderate success rate: ${successRate.toFixed(0)}% - consider simpler AC`);
291
+ }
292
+ else if (runs.length >= 5) {
293
+ insights.push(`Low success rate: ${successRate.toFixed(0)}% - review issue complexity`);
294
+ }
295
+ // Optimal file change range (based on common patterns)
296
+ if (avgFilesChanged > 0) {
297
+ if (avgFilesChanged <= 5) {
298
+ insights.push(`Your sweet spot: ${avgFilesChanged.toFixed(1)} files changed avg`);
213
299
  }
214
- else if (options.csv) {
215
- console.log("runId,startTime,duration,issues,passed,failed,phases");
300
+ else if (avgFilesChanged <= 10) {
301
+ insights.push(`Avg files changed: ${avgFilesChanged.toFixed(1)} (moderate scope)`);
216
302
  }
217
303
  else {
218
- console.log(chalk.blue("\nšŸ“Š Sequant Run Statistics\n"));
219
- console.log(chalk.yellow(" No logs found."));
220
- console.log(chalk.gray(" Run `npx sequant run <issues>` to generate logs."));
221
- console.log("");
304
+ insights.push(`High avg files (${avgFilesChanged.toFixed(1)}) - consider smaller issues`);
222
305
  }
223
- return;
224
306
  }
225
- // Parse all logs
226
- const logs = logFiles
227
- .map((filename) => {
228
- const filePath = path.join(logDir, filename);
229
- return parseLogFile(filePath);
230
- })
231
- .filter((log) => log !== null);
232
- if (logs.length === 0) {
233
- if (options.json) {
234
- console.log(JSON.stringify({ error: "No valid logs found", runs: [] }));
307
+ // Lines of code insight
308
+ if (avgLinesAdded > 0) {
309
+ if (avgLinesAdded >= 200 && avgLinesAdded <= 400) {
310
+ insights.push(`Optimal LOC range: ~${avgLinesAdded.toFixed(0)} lines avg`);
235
311
  }
236
- else if (options.csv) {
237
- console.log("runId,startTime,duration,issues,passed,failed,phases");
312
+ else if (avgLinesAdded > 500) {
313
+ insights.push(`Large changes (${avgLinesAdded.toFixed(0)} LOC avg) - consider splitting issues`);
238
314
  }
239
- else {
240
- console.log(chalk.yellow("\n No valid log files found.\n"));
315
+ }
316
+ // Chain mode comparison
317
+ if (chainModeSuccessRate > 0 && singleIssueSuccessRate > 0) {
318
+ const diff = singleIssueSuccessRate - chainModeSuccessRate;
319
+ if (diff > 10) {
320
+ insights.push(`Chain mode: ${chainModeSuccessRate.toFixed(0)}% (vs ${singleIssueSuccessRate.toFixed(0)}% single issue)`);
241
321
  }
242
- return;
243
322
  }
244
- // CSV output
245
- if (options.csv) {
246
- console.log(generateCsv(logs));
247
- return;
323
+ // Multi-issue runs
324
+ const multiIssueRuns = runs.filter((r) => r.issues.length > 1);
325
+ if (multiIssueRuns.length >= 3) {
326
+ const multiIssueSuccess = multiIssueRuns.filter((r) => r.outcome === "success").length;
327
+ const multiSuccessRate = (multiIssueSuccess / multiIssueRuns.length) * 100;
328
+ if (multiSuccessRate < singleIssueSuccessRate - 15) {
329
+ insights.push(`Multi-issue runs less successful (${multiSuccessRate.toFixed(0)}%)`);
330
+ }
248
331
  }
249
- // JSON output
332
+ return insights;
333
+ }
334
+ /**
335
+ * Display local metrics analytics
336
+ */
337
+ function displayMetricsAnalytics(analytics) {
338
+ console.log(chalk.blue("\nšŸ“Š Sequant Analytics (local data only)\n"));
339
+ // Overall summary
340
+ console.log(chalk.gray(` Runs: ${analytics.totalRuns} total`));
341
+ console.log(chalk.green(` Success: ${analytics.successCount} (${analytics.successRate.toFixed(0)}%)`));
342
+ if (analytics.partialCount > 0) {
343
+ console.log(chalk.yellow(` Partial: ${analytics.partialCount}`));
344
+ }
345
+ if (analytics.failedCount > 0) {
346
+ console.log(chalk.red(` Failed: ${analytics.failedCount}`));
347
+ }
348
+ // Averages
349
+ console.log(chalk.blue("\n Averages"));
350
+ console.log(chalk.gray(` ─────────────────────────────────`));
351
+ if (analytics.avgTokensPerRun > 0) {
352
+ console.log(chalk.gray(` Tokens per run: ${analytics.avgTokensPerRun.toLocaleString()}`));
353
+ }
354
+ console.log(chalk.gray(` Files changed: ${analytics.avgFilesChanged.toFixed(1)}`));
355
+ if (analytics.avgLinesAdded > 0) {
356
+ console.log(chalk.gray(` Lines added: ${analytics.avgLinesAdded.toFixed(0)}`));
357
+ }
358
+ console.log(chalk.gray(` Duration: ${formatDuration(analytics.avgDuration)}`));
359
+ // Insights
360
+ if (analytics.insights.length > 0) {
361
+ console.log(chalk.blue("\n Insights"));
362
+ console.log(chalk.gray(` ─────────────────────────────────`));
363
+ for (const insight of analytics.insights) {
364
+ console.log(chalk.gray(` • ${insight}`));
365
+ }
366
+ }
367
+ console.log(chalk.gray("\n Data stored locally in .sequant/metrics.json"));
368
+ console.log("");
369
+ }
370
+ /**
371
+ * Main stats command
372
+ */
373
+ export async function statsCommand(options) {
374
+ // Try to load local metrics first
375
+ const metrics = loadMetrics();
376
+ // If JSON output requested
250
377
  if (options.json) {
378
+ if (metrics && metrics.runs.length > 0) {
379
+ const analytics = calculateMetricsAnalytics(metrics);
380
+ const output = {
381
+ source: "metrics",
382
+ totalRuns: analytics.totalRuns,
383
+ successCount: analytics.successCount,
384
+ partialCount: analytics.partialCount,
385
+ failedCount: analytics.failedCount,
386
+ successRate: analytics.successRate,
387
+ avgTokensPerRun: analytics.avgTokensPerRun,
388
+ avgFilesChanged: analytics.avgFilesChanged,
389
+ avgLinesAdded: analytics.avgLinesAdded,
390
+ avgDuration: analytics.avgDuration,
391
+ chainModeSuccessRate: analytics.chainModeSuccessRate,
392
+ singleIssueSuccessRate: analytics.singleIssueSuccessRate,
393
+ insights: analytics.insights,
394
+ runs: metrics.runs,
395
+ };
396
+ console.log(JSON.stringify(output, null, 2));
397
+ return;
398
+ }
399
+ // Fall back to run logs for JSON
400
+ const logDir = resolveLogPath(options.path);
401
+ const logFiles = listLogFiles(logDir);
402
+ if (logFiles.length === 0) {
403
+ console.log(JSON.stringify({ error: "No data found", runs: [] }));
404
+ return;
405
+ }
406
+ const logs = logFiles
407
+ .map((filename) => {
408
+ const filePath = path.join(logDir, filename);
409
+ return parseLogFile(filePath);
410
+ })
411
+ .filter((log) => log !== null);
412
+ if (logs.length === 0) {
413
+ console.log(JSON.stringify({ error: "No valid logs found", runs: [] }));
414
+ return;
415
+ }
251
416
  const stats = calculateStats(logs);
252
417
  const output = {
418
+ source: "logs",
253
419
  totalRuns: stats.totalRuns,
254
420
  totalIssues: stats.totalIssues,
255
421
  passed: stats.passed,
@@ -264,7 +430,49 @@ export async function statsCommand(options) {
264
430
  console.log(JSON.stringify(output, null, 2));
265
431
  return;
266
432
  }
267
- // Human-readable output
433
+ // CSV output - use run logs
434
+ if (options.csv) {
435
+ const logDir = resolveLogPath(options.path);
436
+ const logFiles = listLogFiles(logDir);
437
+ if (logFiles.length === 0) {
438
+ console.log("runId,startTime,duration,issues,passed,failed,phases");
439
+ return;
440
+ }
441
+ const logs = logFiles
442
+ .map((filename) => {
443
+ const filePath = path.join(logDir, filename);
444
+ return parseLogFile(filePath);
445
+ })
446
+ .filter((log) => log !== null);
447
+ console.log(generateCsv(logs));
448
+ return;
449
+ }
450
+ // Human-readable output - prefer metrics, fall back to logs
451
+ if (metrics && metrics.runs.length > 0) {
452
+ const analytics = calculateMetricsAnalytics(metrics);
453
+ displayMetricsAnalytics(analytics);
454
+ return;
455
+ }
456
+ // Fall back to run logs display
457
+ const logDir = resolveLogPath(options.path);
458
+ const logFiles = listLogFiles(logDir);
459
+ if (logFiles.length === 0) {
460
+ console.log(chalk.blue("\nšŸ“Š Sequant Analytics (local data only)\n"));
461
+ console.log(chalk.yellow(" No data found."));
462
+ console.log(chalk.gray(" Run `npx sequant run <issues>` to collect metrics."));
463
+ console.log("");
464
+ return;
465
+ }
466
+ const logs = logFiles
467
+ .map((filename) => {
468
+ const filePath = path.join(logDir, filename);
469
+ return parseLogFile(filePath);
470
+ })
471
+ .filter((log) => log !== null);
472
+ if (logs.length === 0) {
473
+ console.log(chalk.yellow("\n No valid log files found.\n"));
474
+ return;
475
+ }
268
476
  const stats = calculateStats(logs);
269
477
  displayStats(stats, logDir);
270
478
  }
@@ -16,5 +16,7 @@ export interface StatusCommandOptions {
16
16
  dryRun?: boolean;
17
17
  /** Remove entries older than this many days (used with --cleanup) */
18
18
  maxAge?: number;
19
+ /** Remove all orphaned entries (both merged and abandoned) in one step */
20
+ all?: boolean;
19
21
  }
20
22
  export declare function statusCommand(options?: StatusCommandOptions): Promise<void>;