sequant 1.12.0 → 1.13.1

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 (53) hide show
  1. package/README.md +10 -8
  2. package/dist/bin/cli.js +19 -9
  3. package/dist/src/commands/doctor.js +42 -20
  4. package/dist/src/commands/init.js +152 -65
  5. package/dist/src/commands/logs.js +7 -6
  6. package/dist/src/commands/run.d.ts +13 -1
  7. package/dist/src/commands/run.js +122 -32
  8. package/dist/src/commands/stats.js +67 -48
  9. package/dist/src/commands/status.js +30 -12
  10. package/dist/src/commands/sync.d.ts +28 -0
  11. package/dist/src/commands/sync.js +102 -0
  12. package/dist/src/index.d.ts +6 -0
  13. package/dist/src/index.js +4 -0
  14. package/dist/src/lib/cli-ui.d.ts +196 -0
  15. package/dist/src/lib/cli-ui.js +544 -0
  16. package/dist/src/lib/content-analyzer.d.ts +89 -0
  17. package/dist/src/lib/content-analyzer.js +437 -0
  18. package/dist/src/lib/phase-signal.d.ts +94 -0
  19. package/dist/src/lib/phase-signal.js +171 -0
  20. package/dist/src/lib/phase-spinner.d.ts +146 -0
  21. package/dist/src/lib/phase-spinner.js +255 -0
  22. package/dist/src/lib/solve-comment-parser.d.ts +84 -0
  23. package/dist/src/lib/solve-comment-parser.js +200 -0
  24. package/dist/src/lib/stack-config.d.ts +51 -0
  25. package/dist/src/lib/stack-config.js +77 -0
  26. package/dist/src/lib/stacks.d.ts +52 -0
  27. package/dist/src/lib/stacks.js +173 -0
  28. package/dist/src/lib/templates.d.ts +2 -0
  29. package/dist/src/lib/templates.js +9 -2
  30. package/dist/src/lib/upstream/assessment.d.ts +70 -0
  31. package/dist/src/lib/upstream/assessment.js +385 -0
  32. package/dist/src/lib/upstream/index.d.ts +11 -0
  33. package/dist/src/lib/upstream/index.js +14 -0
  34. package/dist/src/lib/upstream/issues.d.ts +38 -0
  35. package/dist/src/lib/upstream/issues.js +267 -0
  36. package/dist/src/lib/upstream/relevance.d.ts +50 -0
  37. package/dist/src/lib/upstream/relevance.js +209 -0
  38. package/dist/src/lib/upstream/report.d.ts +29 -0
  39. package/dist/src/lib/upstream/report.js +391 -0
  40. package/dist/src/lib/upstream/types.d.ts +207 -0
  41. package/dist/src/lib/upstream/types.js +5 -0
  42. package/dist/src/lib/workflow/log-writer.d.ts +1 -1
  43. package/dist/src/lib/workflow/metrics-schema.d.ts +3 -3
  44. package/dist/src/lib/workflow/qa-cache.d.ts +199 -0
  45. package/dist/src/lib/workflow/qa-cache.js +440 -0
  46. package/dist/src/lib/workflow/run-log-schema.d.ts +34 -6
  47. package/dist/src/lib/workflow/run-log-schema.js +12 -1
  48. package/dist/src/lib/workflow/state-schema.d.ts +4 -4
  49. package/dist/src/lib/workflow/types.d.ts +4 -0
  50. package/package.json +6 -1
  51. package/templates/skills/qa/scripts/quality-checks.sh +509 -53
  52. package/templates/skills/solve/SKILL.md +375 -83
  53. package/templates/skills/spec/SKILL.md +107 -5
@@ -20,6 +20,8 @@ import { getMcpServersConfig } from "../lib/system.js";
20
20
  import { checkVersionCached, getVersionWarning } from "../lib/version-check.js";
21
21
  import { MetricsWriter } from "../lib/workflow/metrics-writer.js";
22
22
  import { determineOutcome, } from "../lib/workflow/metrics-schema.js";
23
+ import { ui, colors } from "../lib/cli-ui.js";
24
+ import { PhaseSpinner } from "../lib/phase-spinner.js";
23
25
  /**
24
26
  * Slugify a title for branch naming
25
27
  */
@@ -30,6 +32,33 @@ function slugify(title) {
30
32
  .replace(/^-+|-+$/g, "")
31
33
  .substring(0, 50);
32
34
  }
35
+ /**
36
+ * Parse QA verdict from phase output
37
+ *
38
+ * Looks for verdict patterns in the QA output:
39
+ * - "### Verdict: READY_FOR_MERGE"
40
+ * - "**Verdict:** AC_NOT_MET"
41
+ * - "Verdict: AC_MET_BUT_NOT_A_PLUS"
42
+ *
43
+ * @param output - The captured output from QA phase
44
+ * @returns The parsed verdict or null if not found
45
+ */
46
+ export function parseQaVerdict(output) {
47
+ if (!output)
48
+ return null;
49
+ // Match various verdict formats:
50
+ // - "### Verdict: X" (markdown header)
51
+ // - "**Verdict:** X" (bold label with colon inside)
52
+ // - "**Verdict:** **X**" (bold label and bold value)
53
+ // - "Verdict: X" (plain)
54
+ // Case insensitive, handles optional markdown formatting
55
+ const verdictMatch = output.match(/(?:###?\s*)?(?:\*\*)?Verdict:?\*?\*?\s*\*?\*?\s*(READY_FOR_MERGE|AC_MET_BUT_NOT_A_PLUS|AC_NOT_MET|NEEDS_VERIFICATION)\*?\*?/i);
56
+ if (!verdictMatch)
57
+ return null;
58
+ // Normalize to uppercase with underscores
59
+ const verdict = verdictMatch[1].toUpperCase().replace(/-/g, "_");
60
+ return verdict;
61
+ }
33
62
  /**
34
63
  * Get the git repository root directory
35
64
  */
@@ -553,7 +582,7 @@ const ISOLATED_PHASES = ["exec", "test", "qa"];
553
582
  /**
554
583
  * Execute a single phase for an issue using Claude Agent SDK
555
584
  */
556
- async function executePhase(issueNumber, phase, config, sessionId, worktreePath, shutdownManager) {
585
+ async function executePhase(issueNumber, phase, config, sessionId, worktreePath, shutdownManager, spinner) {
557
586
  const startTime = Date.now();
558
587
  if (config.dryRun) {
559
588
  // Dry run - just simulate
@@ -658,7 +687,10 @@ async function executePhase(issueNumber, phase, config, sessionId, worktreePath,
658
687
  capturedOutput += textContent;
659
688
  // Show streaming output in verbose mode
660
689
  if (config.verbose) {
690
+ // Pause spinner during verbose streaming to avoid terminal corruption
691
+ spinner?.pause();
661
692
  process.stdout.write(chalk.gray(textContent));
693
+ spinner?.resume();
662
694
  }
663
695
  }
664
696
  }
@@ -676,6 +708,35 @@ async function executePhase(issueNumber, phase, config, sessionId, worktreePath,
676
708
  // Check result status
677
709
  if (resultMessage) {
678
710
  if (resultMessage.subtype === "success") {
711
+ // For QA phase, check the verdict to determine actual success
712
+ // SDK "success" just means the query completed - we need to parse the verdict
713
+ if (phase === "qa" && capturedOutput) {
714
+ const verdict = parseQaVerdict(capturedOutput);
715
+ // Only READY_FOR_MERGE and NEEDS_VERIFICATION are considered passing
716
+ // NEEDS_VERIFICATION is external verification, not a code quality issue
717
+ if (verdict &&
718
+ verdict !== "READY_FOR_MERGE" &&
719
+ verdict !== "NEEDS_VERIFICATION") {
720
+ return {
721
+ phase,
722
+ success: false,
723
+ durationSeconds,
724
+ error: `QA verdict: ${verdict}`,
725
+ sessionId: resultSessionId,
726
+ output: capturedOutput,
727
+ verdict, // Include parsed verdict
728
+ };
729
+ }
730
+ // Pass case - include verdict for logging
731
+ return {
732
+ phase,
733
+ success: true,
734
+ durationSeconds,
735
+ sessionId: resultSessionId,
736
+ output: capturedOutput,
737
+ verdict: verdict ?? undefined, // Include if found
738
+ };
739
+ }
679
740
  return {
680
741
  phase,
681
742
  success: true,
@@ -924,7 +985,7 @@ function parseBatches(batchArgs) {
924
985
  * Main run command
925
986
  */
926
987
  export async function runCommand(issues, options) {
927
- console.log(chalk.blue("\n🌐 Sequant Workflow Execution\n"));
988
+ console.log(ui.headerBox("SEQUANT WORKFLOW"));
928
989
  // Version freshness check (cached, non-blocking, respects --quiet)
929
990
  if (!options.quiet) {
930
991
  try {
@@ -1337,28 +1398,30 @@ export async function runCommand(issues, options) {
1337
1398
  }
1338
1399
  }
1339
1400
  // Summary
1340
- console.log(chalk.blue("\n" + "━".repeat(50)));
1341
- console.log(chalk.blue(" Summary"));
1342
- console.log(chalk.blue("━".repeat(50)));
1343
- console.log(chalk.gray(`\n Results: ${chalk.green(`${passed} passed`)}, ${chalk.red(`${failed} failed`)}`));
1401
+ console.log("\n" + ui.divider());
1402
+ console.log(colors.info(" Summary"));
1403
+ console.log(ui.divider());
1404
+ console.log(colors.muted(`\n Results: ${colors.success(`${passed} passed`)}, ${colors.error(`${failed} failed`)}`));
1344
1405
  for (const result of results) {
1345
- const status = result.success ? chalk.green("āœ“") : chalk.red("āœ—");
1406
+ const status = result.success
1407
+ ? ui.statusIcon("success")
1408
+ : ui.statusIcon("error");
1346
1409
  const duration = result.durationSeconds
1347
- ? chalk.gray(` (${formatDuration(result.durationSeconds)})`)
1410
+ ? colors.muted(` (${formatDuration(result.durationSeconds)})`)
1348
1411
  : "";
1349
1412
  const phases = result.phaseResults
1350
- .map((p) => (p.success ? chalk.green(p.phase) : chalk.red(p.phase)))
1413
+ .map((p) => p.success ? colors.success(p.phase) : colors.error(p.phase))
1351
1414
  .join(" → ");
1352
- const loopInfo = result.loopTriggered ? chalk.yellow(" [loop]") : "";
1415
+ const loopInfo = result.loopTriggered ? colors.warning(" [loop]") : "";
1353
1416
  console.log(` ${status} #${result.issueNumber}: ${phases}${loopInfo}${duration}`);
1354
1417
  }
1355
1418
  console.log("");
1356
1419
  if (logPath) {
1357
- console.log(chalk.gray(` šŸ“ Log: ${logPath}`));
1420
+ console.log(colors.muted(` šŸ“ Log: ${logPath}`));
1358
1421
  console.log("");
1359
1422
  }
1360
1423
  if (config.dryRun) {
1361
- console.log(chalk.yellow(" ā„¹ļø This was a dry run. Use without --dry-run to execute."));
1424
+ console.log(colors.warning(" ā„¹ļø This was a dry run. Use without --dry-run to execute."));
1362
1425
  console.log("");
1363
1426
  }
1364
1427
  // Set exit code if any failed
@@ -1457,7 +1520,14 @@ async function runIssueWithLogging(issueNumber, config, logWriter, stateManager,
1457
1520
  else {
1458
1521
  // Run spec first to get recommended workflow
1459
1522
  console.log(chalk.gray(` Running spec to determine workflow...`));
1460
- console.log(chalk.gray(` ā³ spec...`));
1523
+ // Create spinner for spec phase (1 of estimated 3: spec, exec, qa)
1524
+ const specSpinner = new PhaseSpinner({
1525
+ phase: "spec",
1526
+ phaseIndex: 1,
1527
+ totalPhases: 3, // Estimate; will be refined after spec
1528
+ shutdownManager,
1529
+ });
1530
+ specSpinner.start();
1461
1531
  // Track spec phase start in state
1462
1532
  if (stateManager) {
1463
1533
  try {
@@ -1470,7 +1540,7 @@ async function runIssueWithLogging(issueNumber, config, logWriter, stateManager,
1470
1540
  const specStartTime = new Date();
1471
1541
  // Note: spec runs in main repo (not worktree) for planning
1472
1542
  const specResult = await executePhase(issueNumber, "spec", config, sessionId, worktreePath, // Will be ignored for spec (non-isolated phase)
1473
- shutdownManager);
1543
+ shutdownManager, specSpinner);
1474
1544
  const specEndTime = new Date();
1475
1545
  if (specResult.sessionId) {
1476
1546
  sessionId = specResult.sessionId;
@@ -1508,7 +1578,7 @@ async function runIssueWithLogging(issueNumber, config, logWriter, stateManager,
1508
1578
  }
1509
1579
  }
1510
1580
  if (!specResult.success) {
1511
- console.log(chalk.red(` āœ— spec: ${specResult.error}`));
1581
+ specSpinner.fail(specResult.error);
1512
1582
  const durationSeconds = (Date.now() - startTime) / 1000;
1513
1583
  return {
1514
1584
  issueNumber,
@@ -1518,10 +1588,7 @@ async function runIssueWithLogging(issueNumber, config, logWriter, stateManager,
1518
1588
  loopTriggered: false,
1519
1589
  };
1520
1590
  }
1521
- const duration = specResult.durationSeconds
1522
- ? ` (${formatDuration(specResult.durationSeconds)})`
1523
- : "";
1524
- console.log(chalk.green(` āœ“ spec${duration}`));
1591
+ specSpinner.succeed();
1525
1592
  // Parse recommended workflow from spec output
1526
1593
  const parsedWorkflow = specResult.output
1527
1594
  ? parseRecommendedWorkflow(specResult.output)
@@ -1575,8 +1642,22 @@ async function runIssueWithLogging(issueNumber, config, logWriter, stateManager,
1575
1642
  loopTriggered = true;
1576
1643
  }
1577
1644
  let phasesFailed = false;
1578
- for (const phase of phases) {
1579
- console.log(chalk.gray(` ā³ ${phase}...`));
1645
+ // Calculate total phases for progress indicator
1646
+ // If spec already ran in auto-detect mode, it's counted separately
1647
+ const totalPhases = specAlreadyRan ? phases.length + 1 : phases.length;
1648
+ const phaseIndexOffset = specAlreadyRan ? 1 : 0;
1649
+ for (let phaseIdx = 0; phaseIdx < phases.length; phaseIdx++) {
1650
+ const phase = phases[phaseIdx];
1651
+ const phaseNumber = phaseIdx + 1 + phaseIndexOffset;
1652
+ // Create spinner for this phase
1653
+ const phaseSpinner = new PhaseSpinner({
1654
+ phase,
1655
+ phaseIndex: phaseNumber,
1656
+ totalPhases,
1657
+ shutdownManager,
1658
+ iteration: useQualityLoop ? iteration : undefined,
1659
+ });
1660
+ phaseSpinner.start();
1580
1661
  // Track phase start in state
1581
1662
  if (stateManager) {
1582
1663
  try {
@@ -1587,7 +1668,7 @@ async function runIssueWithLogging(issueNumber, config, logWriter, stateManager,
1587
1668
  }
1588
1669
  }
1589
1670
  const phaseStartTime = new Date();
1590
- const result = await executePhase(issueNumber, phase, config, sessionId, worktreePath, shutdownManager);
1671
+ const result = await executePhase(issueNumber, phase, config, sessionId, worktreePath, shutdownManager, phaseSpinner);
1591
1672
  const phaseEndTime = new Date();
1592
1673
  // Capture session ID for subsequent phases
1593
1674
  if (result.sessionId) {
@@ -1609,7 +1690,11 @@ async function runIssueWithLogging(issueNumber, config, logWriter, stateManager,
1609
1690
  ? "success"
1610
1691
  : result.error?.includes("Timeout")
1611
1692
  ? "timeout"
1612
- : "failure", { error: result.error });
1693
+ : "failure", {
1694
+ error: result.error,
1695
+ // Include verdict for QA phase (AC-6)
1696
+ verdict: result.verdict,
1697
+ });
1613
1698
  logWriter.logPhase(phaseLog);
1614
1699
  }
1615
1700
  // Track phase completion in state
@@ -1627,29 +1712,34 @@ async function runIssueWithLogging(issueNumber, config, logWriter, stateManager,
1627
1712
  }
1628
1713
  }
1629
1714
  if (result.success) {
1630
- const duration = result.durationSeconds
1631
- ? ` (${formatDuration(result.durationSeconds)})`
1632
- : "";
1633
- console.log(chalk.green(` āœ“ ${phase}${duration}`));
1715
+ phaseSpinner.succeed();
1634
1716
  }
1635
1717
  else {
1636
- console.log(chalk.red(` āœ— ${phase}: ${result.error}`));
1718
+ phaseSpinner.fail(result.error);
1637
1719
  phasesFailed = true;
1638
1720
  // If quality loop enabled, run loop phase to fix issues
1639
1721
  if (useQualityLoop && iteration < maxIterations) {
1640
- console.log(chalk.yellow(` Running /loop to fix issues...`));
1641
- const loopResult = await executePhase(issueNumber, "loop", config, sessionId, worktreePath, shutdownManager);
1722
+ // Create spinner for loop phase
1723
+ const loopSpinner = new PhaseSpinner({
1724
+ phase: "loop",
1725
+ phaseIndex: phaseNumber,
1726
+ totalPhases,
1727
+ shutdownManager,
1728
+ iteration,
1729
+ });
1730
+ loopSpinner.start();
1731
+ const loopResult = await executePhase(issueNumber, "loop", config, sessionId, worktreePath, shutdownManager, loopSpinner);
1642
1732
  phaseResults.push(loopResult);
1643
1733
  if (loopResult.sessionId) {
1644
1734
  sessionId = loopResult.sessionId;
1645
1735
  }
1646
1736
  if (loopResult.success) {
1647
- console.log(chalk.green(` āœ“ loop - retrying phases`));
1737
+ loopSpinner.succeed();
1648
1738
  // Continue to next iteration
1649
1739
  break;
1650
1740
  }
1651
1741
  else {
1652
- console.log(chalk.red(` āœ— loop: ${loopResult.error}`));
1742
+ loopSpinner.fail(loopResult.error);
1653
1743
  }
1654
1744
  }
1655
1745
  // Stop on first failure (if not in quality loop or loop failed)
@@ -4,10 +4,10 @@
4
4
  * Provides success/failure rates, workflow insights, and aggregate statistics.
5
5
  * All data is local - no telemetry is ever sent remotely.
6
6
  */
7
- import chalk from "chalk";
8
7
  import * as fs from "fs";
9
8
  import * as path from "path";
10
9
  import * as os from "os";
10
+ import { ui, colors } from "../lib/cli-ui.js";
11
11
  import { RunLogSchema, LOG_PATHS, } from "../lib/workflow/run-log-schema.js";
12
12
  import { MetricsSchema, METRICS_FILE_PATH, } from "../lib/workflow/metrics-schema.js";
13
13
  /**
@@ -158,46 +158,57 @@ function generateCsv(logs) {
158
158
  return [header, ...csvRows].join("\n");
159
159
  }
160
160
  /**
161
- * Display human-readable statistics
161
+ * Display human-readable statistics with dashboard-style boxes
162
162
  */
163
163
  function displayStats(stats, logDir) {
164
- console.log(chalk.blue("\nšŸ“Š Sequant Run Statistics\n"));
165
- console.log(chalk.gray(` Log directory: ${logDir}`));
166
- // Overall summary
167
- console.log(chalk.blue("\n Overview"));
168
- console.log(chalk.gray(` ─────────────────────────────────`));
169
- console.log(chalk.gray(` Total runs analyzed: ${stats.totalRuns}`));
170
- console.log(chalk.gray(` Total issues processed: ${stats.totalIssues}`));
171
- console.log(chalk.gray(` Average run duration: ${formatDuration(stats.avgDurationSeconds)}`));
172
- // Success/failure rates
173
- console.log(chalk.blue("\n Success/Failure Rates"));
174
- console.log(chalk.gray(` ─────────────────────────────────`));
175
- console.log(chalk.green(` Passed: ${stats.passed} (${stats.successRate.toFixed(1)}%)`));
176
- console.log(chalk.red(` Failed: ${stats.failed} (${stats.failureRate.toFixed(1)}%)`));
164
+ console.log(ui.headerBox("SEQUANT ANALYTICS"));
165
+ console.log(colors.muted(`\n Log directory: ${logDir}`));
166
+ console.log(colors.muted(" Local data only - no telemetry\n"));
167
+ // Overview table
168
+ const overviewData = {
169
+ "Total Runs": stats.totalRuns,
170
+ "Issues Processed": stats.totalIssues,
171
+ "Avg Duration": formatDuration(stats.avgDurationSeconds),
172
+ };
173
+ console.log(ui.keyValueTable(overviewData));
174
+ // Success rates with progress bar
175
+ console.log(ui.sectionHeader("Success Rates"));
176
+ const passedBar = ui.progressBar(stats.passed, stats.totalIssues, 12);
177
+ const failedBar = ui.progressBar(stats.failed, stats.totalIssues, 12);
178
+ console.log(` ${colors.success("\u2713 Passed")} ${stats.passed} (${stats.successRate.toFixed(1)}%) ${passedBar}`);
179
+ console.log(` ${colors.error("\u2717 Failed")} ${stats.failed} (${stats.failureRate.toFixed(1)}%) ${failedBar}`);
177
180
  if (stats.partial > 0) {
178
- console.log(chalk.yellow(` Partial: ${stats.partial}`));
181
+ const partialRate = (stats.partial / stats.totalIssues) * 100;
182
+ const partialBar = ui.progressBar(stats.partial, stats.totalIssues, 12);
183
+ console.log(` ${colors.warning("\u26A0 Partial")} ${stats.partial} (${partialRate.toFixed(1)}%) ${partialBar}`);
179
184
  }
180
- // Phase durations
185
+ // Phase durations table
181
186
  if (stats.phaseDurations.size > 0) {
182
- console.log(chalk.blue("\n Average Phase Durations"));
183
- console.log(chalk.gray(` ─────────────────────────────────`));
187
+ console.log(ui.sectionHeader("Phase Durations"));
184
188
  // Sort phases by count (most common first)
185
189
  const sortedPhases = [...stats.phaseDurations.entries()].sort((a, b) => b[1].count - a[1].count);
186
- for (const [phase, data] of sortedPhases) {
187
- const avgFormatted = formatDuration(data.avg);
188
- console.log(chalk.gray(` ${phase.padEnd(10)} ${avgFormatted.padStart(8)} avg (${data.count} runs)`));
189
- }
190
+ const phaseRows = sortedPhases.map(([phase, data]) => [
191
+ phase,
192
+ formatDuration(data.avg),
193
+ data.count,
194
+ ]);
195
+ console.log(ui.table(phaseRows, {
196
+ columns: [
197
+ { header: "Phase", width: 12 },
198
+ { header: "Avg Time", width: 12 },
199
+ { header: "Runs", width: 8 },
200
+ ],
201
+ }));
190
202
  }
191
203
  // Common failures
192
204
  if (stats.commonFailures.size > 0) {
193
- console.log(chalk.blue("\n Common Failure Points"));
194
- console.log(chalk.gray(` ─────────────────────────────────`));
205
+ console.log(ui.sectionHeader("Common Failures"));
195
206
  // Sort by frequency
196
207
  const sortedFailures = [...stats.commonFailures.entries()]
197
208
  .sort((a, b) => b[1] - a[1])
198
209
  .slice(0, 5); // Top 5
199
210
  for (const [error, count] of sortedFailures) {
200
- console.log(chalk.red(` ${count}x ${error}`));
211
+ console.log(` ${colors.error(`${count}x`)} ${error}`);
201
212
  }
202
213
  }
203
214
  console.log("");
@@ -332,39 +343,46 @@ function generateInsights(runs, successRate, avgFilesChanged, avgLinesAdded, cha
332
343
  return insights;
333
344
  }
334
345
  /**
335
- * Display local metrics analytics
346
+ * Display local metrics analytics with dashboard-style boxes
336
347
  */
337
348
  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)}%)`));
349
+ console.log(ui.headerBox("SEQUANT ANALYTICS"));
350
+ console.log(colors.muted("\n Local data only - no telemetry\n"));
351
+ // Overview with progress bars
352
+ const total = analytics.successCount + analytics.partialCount + analytics.failedCount;
353
+ const successBar = ui.progressBar(analytics.successCount, total, 12);
354
+ const failedBar = ui.progressBar(analytics.failedCount, total, 12);
355
+ console.log(` Runs: ${analytics.totalRuns} total\n`);
356
+ console.log(` ${colors.success("\u2713 Success")} ${analytics.successCount} (${analytics.successRate.toFixed(0)}%) ${successBar}`);
342
357
  if (analytics.partialCount > 0) {
343
- console.log(chalk.yellow(` Partial: ${analytics.partialCount}`));
358
+ const partialRate = (analytics.partialCount / total) * 100;
359
+ const partialBar = ui.progressBar(analytics.partialCount, total, 12);
360
+ console.log(` ${colors.warning("\u26A0 Partial")} ${analytics.partialCount} (${partialRate.toFixed(0)}%) ${partialBar}`);
344
361
  }
345
362
  if (analytics.failedCount > 0) {
346
- console.log(chalk.red(` Failed: ${analytics.failedCount}`));
363
+ const failedRate = (analytics.failedCount / total) * 100;
364
+ console.log(` ${colors.error("\u2717 Failed")} ${analytics.failedCount} (${failedRate.toFixed(0)}%) ${failedBar}`);
347
365
  }
348
- // Averages
349
- console.log(chalk.blue("\n Averages"));
350
- console.log(chalk.gray(` ─────────────────────────────────`));
366
+ // Averages table
367
+ console.log(ui.sectionHeader("Averages"));
368
+ const avgData = {};
351
369
  if (analytics.avgTokensPerRun > 0) {
352
- console.log(chalk.gray(` Tokens per run: ${analytics.avgTokensPerRun.toLocaleString()}`));
370
+ avgData["Tokens/run"] = analytics.avgTokensPerRun.toLocaleString();
353
371
  }
354
- console.log(chalk.gray(` Files changed: ${analytics.avgFilesChanged.toFixed(1)}`));
372
+ avgData["Files changed"] = analytics.avgFilesChanged.toFixed(1);
355
373
  if (analytics.avgLinesAdded > 0) {
356
- console.log(chalk.gray(` Lines added: ${analytics.avgLinesAdded.toFixed(0)}`));
374
+ avgData["Lines added"] = analytics.avgLinesAdded.toFixed(0);
357
375
  }
358
- console.log(chalk.gray(` Duration: ${formatDuration(analytics.avgDuration)}`));
376
+ avgData["Duration"] = formatDuration(analytics.avgDuration);
377
+ console.log(ui.keyValueTable(avgData));
359
378
  // Insights
360
379
  if (analytics.insights.length > 0) {
361
- console.log(chalk.blue("\n Insights"));
362
- console.log(chalk.gray(` ─────────────────────────────────`));
380
+ console.log(ui.sectionHeader("Insights"));
363
381
  for (const insight of analytics.insights) {
364
- console.log(chalk.gray(` • ${insight}`));
382
+ console.log(` ${colors.accent("\u2022")} ${insight}`);
365
383
  }
366
384
  }
367
- console.log(chalk.gray("\n Data stored locally in .sequant/metrics.json"));
385
+ console.log(colors.muted("\n Data stored locally in .sequant/metrics.json"));
368
386
  console.log("");
369
387
  }
370
388
  /**
@@ -457,9 +475,10 @@ export async function statsCommand(options) {
457
475
  const logDir = resolveLogPath(options.path);
458
476
  const logFiles = listLogFiles(logDir);
459
477
  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."));
478
+ console.log(ui.headerBox("SEQUANT ANALYTICS"));
479
+ console.log(colors.muted("\n Local data only - no telemetry\n"));
480
+ console.log(colors.warning(" No data found."));
481
+ console.log(colors.muted(" Run `npx sequant run <issues>` to collect metrics."));
463
482
  console.log("");
464
483
  return;
465
484
  }
@@ -470,7 +489,7 @@ export async function statsCommand(options) {
470
489
  })
471
490
  .filter((log) => log !== null);
472
491
  if (logs.length === 0) {
473
- console.log(chalk.yellow("\n No valid log files found.\n"));
492
+ console.log(colors.warning("\n No valid log files found.\n"));
474
493
  return;
475
494
  }
476
495
  const stats = calculateStats(logs);
@@ -2,6 +2,7 @@
2
2
  * sequant status - Show version, configuration, and workflow state
3
3
  */
4
4
  import chalk from "chalk";
5
+ import { ui, colors } from "../lib/cli-ui.js";
5
6
  import { getManifest, getPackageVersion } from "../lib/manifest.js";
6
7
  import { fileExists } from "../lib/fs.js";
7
8
  import { readdir } from "fs/promises";
@@ -122,7 +123,6 @@ function displayIssueSummary(issues) {
122
123
  console.log(chalk.gray(" Run `sequant run <issue>` to start tracking."));
123
124
  return;
124
125
  }
125
- console.log(chalk.bold("\n Tracked Issues:\n"));
126
126
  // Group by status
127
127
  const byStatus = {
128
128
  in_progress: [],
@@ -136,7 +136,7 @@ function displayIssueSummary(issues) {
136
136
  for (const issue of issues) {
137
137
  byStatus[issue.status].push(issue);
138
138
  }
139
- // Display in priority order
139
+ // Display in priority order using a table
140
140
  const statusOrder = [
141
141
  "in_progress",
142
142
  "waiting_for_qa_gate",
@@ -146,34 +146,51 @@ function displayIssueSummary(issues) {
146
146
  "merged",
147
147
  "abandoned",
148
148
  ];
149
+ // Build rows for the table
150
+ const rows = [];
149
151
  for (const status of statusOrder) {
150
152
  const statusIssues = byStatus[status];
151
- if (statusIssues.length === 0)
152
- continue;
153
153
  for (const issue of statusIssues) {
154
- console.log(formatIssueState(issue));
155
- console.log("");
154
+ const title = issue.title.length > 30
155
+ ? issue.title.substring(0, 27) + "..."
156
+ : issue.title;
157
+ rows.push([
158
+ `#${issue.number}`,
159
+ title,
160
+ colorStatus(issue.status),
161
+ issue.currentPhase || "-",
162
+ ]);
156
163
  }
157
164
  }
165
+ // Display table
166
+ console.log("\n" +
167
+ ui.table(rows, {
168
+ columns: [
169
+ { header: "Issue", width: 8 },
170
+ { header: "Title", width: 32 },
171
+ { header: "Status", width: 20 },
172
+ { header: "Phase", width: 10 },
173
+ ],
174
+ }));
158
175
  // Summary counts
159
176
  const summary = [
160
177
  `Total: ${issues.length}`,
161
178
  byStatus.in_progress.length > 0
162
- ? chalk.blue(`In Progress: ${byStatus.in_progress.length}`)
179
+ ? colors.info(`In Progress: ${byStatus.in_progress.length}`)
163
180
  : null,
164
181
  byStatus.waiting_for_qa_gate.length > 0
165
- ? chalk.yellow(`QA Gate: ${byStatus.waiting_for_qa_gate.length}`)
182
+ ? colors.warning(`QA Gate: ${byStatus.waiting_for_qa_gate.length}`)
166
183
  : null,
167
184
  byStatus.ready_for_merge.length > 0
168
- ? chalk.green(`Ready: ${byStatus.ready_for_merge.length}`)
185
+ ? colors.success(`Ready: ${byStatus.ready_for_merge.length}`)
169
186
  : null,
170
187
  byStatus.blocked.length > 0
171
- ? chalk.yellow(`Blocked: ${byStatus.blocked.length}`)
188
+ ? colors.warning(`Blocked: ${byStatus.blocked.length}`)
172
189
  : null,
173
190
  ]
174
191
  .filter(Boolean)
175
192
  .join(" ");
176
- console.log(chalk.gray(` ${summary}`));
193
+ console.log(`\n ${summary}`);
177
194
  }
178
195
  export async function statusCommand(options = {}) {
179
196
  // Handle --rebuild flag
@@ -191,7 +208,8 @@ export async function statusCommand(options = {}) {
191
208
  await displayIssueState(options);
192
209
  return;
193
210
  }
194
- console.log(chalk.bold("\nšŸ“Š Sequant Status\n"));
211
+ console.log(ui.headerBox("SEQUANT STATUS"));
212
+ console.log();
195
213
  // Package version
196
214
  console.log(chalk.gray(`Package version: ${getPackageVersion()}`));
197
215
  // Check initialization
@@ -0,0 +1,28 @@
1
+ /**
2
+ * sequant sync - Fast, non-interactive template sync
3
+ *
4
+ * Syncs skills and other templates from the package to the local project.
5
+ * Designed for plugin users who need to update after upgrading sequant.
6
+ */
7
+ interface SyncOptions {
8
+ force?: boolean;
9
+ quiet?: boolean;
10
+ }
11
+ /**
12
+ * Get the version of skills currently installed
13
+ */
14
+ export declare function getSkillsVersion(): Promise<string | null>;
15
+ /**
16
+ * Check if skills are outdated compared to package version
17
+ */
18
+ export declare function areSkillsOutdated(): Promise<{
19
+ outdated: boolean;
20
+ currentVersion: string | null;
21
+ packageVersion: string;
22
+ }>;
23
+ export declare function syncCommand(options?: SyncOptions): Promise<void>;
24
+ /**
25
+ * Check and warn if skills are outdated (for use by other commands)
26
+ */
27
+ export declare function checkAndWarnSkillsOutdated(): Promise<boolean>;
28
+ export {};