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.
- package/README.md +6 -1
- package/dist/bin/cli.js +55 -2
- package/dist/dashboard/server.d.ts +37 -0
- package/dist/dashboard/server.js +968 -0
- package/dist/src/commands/dashboard.d.ts +25 -0
- package/dist/src/commands/dashboard.js +44 -0
- package/dist/src/commands/doctor.d.ts +18 -1
- package/dist/src/commands/doctor.js +105 -2
- package/dist/src/commands/init.d.ts +1 -0
- package/dist/src/commands/init.js +26 -2
- package/dist/src/commands/run.d.ts +20 -0
- package/dist/src/commands/run.js +151 -3
- package/dist/src/commands/state.d.ts +60 -0
- package/dist/src/commands/state.js +267 -0
- package/dist/src/commands/stats.d.ts +3 -2
- package/dist/src/commands/stats.js +246 -38
- package/dist/src/commands/status.d.ts +2 -0
- package/dist/src/commands/status.js +28 -3
- package/dist/src/lib/ac-parser.d.ts +61 -0
- package/dist/src/lib/ac-parser.js +156 -0
- package/dist/src/lib/fs.d.ts +19 -0
- package/dist/src/lib/fs.js +58 -1
- package/dist/src/lib/settings.d.ts +7 -0
- package/dist/src/lib/settings.js +1 -0
- package/dist/src/lib/system.d.ts +19 -0
- package/dist/src/lib/system.js +26 -0
- package/dist/src/lib/templates.d.ts +34 -1
- package/dist/src/lib/templates.js +109 -5
- package/dist/src/lib/workflow/metrics-schema.d.ts +153 -0
- package/dist/src/lib/workflow/metrics-schema.js +138 -0
- package/dist/src/lib/workflow/metrics-writer.d.ts +102 -0
- package/dist/src/lib/workflow/metrics-writer.js +189 -0
- package/dist/src/lib/workflow/state-manager.d.ts +18 -1
- package/dist/src/lib/workflow/state-manager.js +61 -1
- package/dist/src/lib/workflow/state-schema.d.ts +152 -1
- package/dist/src/lib/workflow/state-schema.js +99 -0
- package/dist/src/lib/workflow/state-utils.d.ts +67 -3
- package/dist/src/lib/workflow/state-utils.js +289 -8
- package/dist/src/lib/workflow/types.d.ts +2 -0
- package/dist/src/lib/workflow/types.js +1 -0
- 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 -
|
|
2
|
+
* sequant stats - Local workflow analytics
|
|
3
3
|
*
|
|
4
|
-
* Provides success/failure rates,
|
|
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 -
|
|
2
|
+
* sequant stats - Local workflow analytics
|
|
3
3
|
*
|
|
4
|
-
* Provides success/failure rates,
|
|
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
|
-
*
|
|
206
|
+
* Load metrics from file
|
|
205
207
|
*/
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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 (
|
|
215
|
-
|
|
300
|
+
else if (avgFilesChanged <= 10) {
|
|
301
|
+
insights.push(`Avg files changed: ${avgFilesChanged.toFixed(1)} (moderate scope)`);
|
|
216
302
|
}
|
|
217
303
|
else {
|
|
218
|
-
|
|
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
|
-
//
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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 (
|
|
237
|
-
|
|
312
|
+
else if (avgLinesAdded > 500) {
|
|
313
|
+
insights.push(`Large changes (${avgLinesAdded.toFixed(0)} LOC avg) - consider splitting issues`);
|
|
238
314
|
}
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
//
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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>;
|