sequant 1.20.3 → 2.0.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 (137) hide show
  1. package/.claude-plugin/marketplace.json +2 -4
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +29 -9
  4. package/dist/bin/cli.js +25 -2
  5. package/dist/src/commands/doctor.js +42 -9
  6. package/dist/src/commands/init.d.ts +1 -0
  7. package/dist/src/commands/init.js +52 -0
  8. package/dist/src/commands/logs.d.ts +1 -0
  9. package/dist/src/commands/logs.js +18 -2
  10. package/dist/src/commands/run.d.ts +7 -0
  11. package/dist/src/commands/run.js +235 -68
  12. package/dist/src/commands/serve.d.ts +13 -0
  13. package/dist/src/commands/serve.js +131 -0
  14. package/dist/src/commands/stats.d.ts +1 -0
  15. package/dist/src/commands/stats.js +185 -26
  16. package/dist/src/commands/status.d.ts +2 -0
  17. package/dist/src/commands/status.js +99 -50
  18. package/dist/src/index.d.ts +2 -2
  19. package/dist/src/index.js +4 -1
  20. package/dist/src/lib/ac-parser.d.ts +2 -0
  21. package/dist/src/lib/ac-parser.js +12 -2
  22. package/dist/src/lib/assess-comment-parser.d.ts +137 -0
  23. package/dist/src/lib/assess-comment-parser.js +344 -0
  24. package/dist/src/lib/ci/config.d.ts +22 -0
  25. package/dist/src/lib/ci/config.js +134 -0
  26. package/dist/src/lib/ci/index.d.ts +12 -0
  27. package/dist/src/lib/ci/index.js +10 -0
  28. package/dist/src/lib/ci/inputs.d.ts +29 -0
  29. package/dist/src/lib/ci/inputs.js +103 -0
  30. package/dist/src/lib/ci/labels.d.ts +34 -0
  31. package/dist/src/lib/ci/labels.js +101 -0
  32. package/dist/src/lib/ci/outputs.d.ts +25 -0
  33. package/dist/src/lib/ci/outputs.js +84 -0
  34. package/dist/src/lib/ci/triggers.d.ts +9 -0
  35. package/dist/src/lib/ci/triggers.js +86 -0
  36. package/dist/src/lib/ci/types.d.ts +131 -0
  37. package/dist/src/lib/ci/types.js +47 -0
  38. package/dist/src/lib/mcp-config.d.ts +54 -0
  39. package/dist/src/lib/mcp-config.js +172 -0
  40. package/dist/src/lib/merge-check/index.js +6 -12
  41. package/dist/src/lib/merge-check/types.d.ts +20 -7
  42. package/dist/src/lib/merge-check/types.js +11 -0
  43. package/dist/src/lib/phase-signal.d.ts +3 -3
  44. package/dist/src/lib/phase-signal.js +5 -3
  45. package/dist/src/lib/settings.d.ts +52 -0
  46. package/dist/src/lib/settings.js +41 -0
  47. package/dist/src/lib/shutdown.d.ts +16 -5
  48. package/dist/src/lib/shutdown.js +32 -12
  49. package/dist/src/lib/solve-comment-parser.d.ts +9 -102
  50. package/dist/src/lib/solve-comment-parser.js +13 -248
  51. package/dist/src/lib/stacks.d.ts +8 -0
  52. package/dist/src/lib/stacks.js +34 -0
  53. package/dist/src/lib/system.js +3 -7
  54. package/dist/src/lib/test-tautology-detector.d.ts +10 -0
  55. package/dist/src/lib/test-tautology-detector.js +43 -4
  56. package/dist/src/lib/upstream/assessment.js +9 -59
  57. package/dist/src/lib/upstream/issues.js +12 -75
  58. package/dist/src/lib/version-check.d.ts +2 -2
  59. package/dist/src/lib/version-check.js +6 -3
  60. package/dist/src/lib/version.d.ts +4 -0
  61. package/dist/src/lib/version.js +25 -0
  62. package/dist/src/lib/workflow/batch-executor.d.ts +18 -86
  63. package/dist/src/lib/workflow/batch-executor.js +232 -55
  64. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +56 -0
  65. package/dist/src/lib/workflow/drivers/agent-driver.js +8 -0
  66. package/dist/src/lib/workflow/drivers/aider.d.ts +18 -0
  67. package/dist/src/lib/workflow/drivers/aider.js +160 -0
  68. package/dist/src/lib/workflow/drivers/claude-code.d.ts +17 -0
  69. package/dist/src/lib/workflow/drivers/claude-code.js +165 -0
  70. package/dist/src/lib/workflow/drivers/index.d.ts +20 -0
  71. package/dist/src/lib/workflow/drivers/index.js +27 -0
  72. package/dist/src/lib/workflow/error-classifier.d.ts +16 -0
  73. package/dist/src/lib/workflow/error-classifier.js +90 -0
  74. package/dist/src/lib/workflow/log-writer.d.ts +6 -3
  75. package/dist/src/lib/workflow/log-writer.js +57 -27
  76. package/dist/src/lib/workflow/metrics-schema.d.ts +9 -9
  77. package/dist/src/lib/workflow/phase-detection.d.ts +23 -0
  78. package/dist/src/lib/workflow/phase-detection.js +45 -29
  79. package/dist/src/lib/workflow/phase-executor.d.ts +42 -3
  80. package/dist/src/lib/workflow/phase-executor.js +340 -220
  81. package/dist/src/lib/workflow/phase-mapper.d.ts +1 -1
  82. package/dist/src/lib/workflow/phase-mapper.js +7 -7
  83. package/dist/src/lib/workflow/platforms/github.d.ts +157 -0
  84. package/dist/src/lib/workflow/platforms/github.js +466 -0
  85. package/dist/src/lib/workflow/platforms/index.d.ts +17 -0
  86. package/dist/src/lib/workflow/platforms/index.js +25 -0
  87. package/dist/src/lib/workflow/platforms/platform-provider.d.ts +67 -0
  88. package/dist/src/lib/workflow/platforms/platform-provider.js +8 -0
  89. package/dist/src/lib/workflow/pr-status.d.ts +2 -4
  90. package/dist/src/lib/workflow/pr-status.js +3 -16
  91. package/dist/src/lib/workflow/qa-cache.d.ts +58 -0
  92. package/dist/src/lib/workflow/qa-cache.js +88 -0
  93. package/dist/src/lib/workflow/reconcile.d.ts +69 -0
  94. package/dist/src/lib/workflow/reconcile.js +290 -0
  95. package/dist/src/lib/workflow/ring-buffer.d.ts +17 -0
  96. package/dist/src/lib/workflow/ring-buffer.js +37 -0
  97. package/dist/src/lib/workflow/run-log-schema.d.ts +115 -24
  98. package/dist/src/lib/workflow/run-log-schema.js +47 -12
  99. package/dist/src/lib/workflow/run-reflect.js +1 -1
  100. package/dist/src/lib/workflow/state-cleanup.js +21 -0
  101. package/dist/src/lib/workflow/state-manager.d.ts +34 -3
  102. package/dist/src/lib/workflow/state-manager.js +278 -126
  103. package/dist/src/lib/workflow/state-schema.d.ts +34 -30
  104. package/dist/src/lib/workflow/state-schema.js +35 -25
  105. package/dist/src/lib/workflow/state-utils.d.ts +3 -1
  106. package/dist/src/lib/workflow/state-utils.js +1 -0
  107. package/dist/src/lib/workflow/types.d.ts +208 -6
  108. package/dist/src/lib/workflow/types.js +20 -1
  109. package/dist/src/lib/workflow/worktree-discovery.d.ts +1 -1
  110. package/dist/src/lib/workflow/worktree-discovery.js +6 -14
  111. package/dist/src/lib/workflow/worktree-manager.js +33 -51
  112. package/dist/src/mcp/index.d.ts +4 -0
  113. package/dist/src/mcp/index.js +4 -0
  114. package/dist/src/mcp/resources.d.ts +7 -0
  115. package/dist/src/mcp/resources.js +111 -0
  116. package/dist/src/mcp/run-registry.d.ts +34 -0
  117. package/dist/src/mcp/run-registry.js +42 -0
  118. package/dist/src/mcp/server.d.ts +12 -0
  119. package/dist/src/mcp/server.js +50 -0
  120. package/dist/src/mcp/tools/logs.d.ts +7 -0
  121. package/dist/src/mcp/tools/logs.js +149 -0
  122. package/dist/src/mcp/tools/run.d.ts +121 -0
  123. package/dist/src/mcp/tools/run.js +591 -0
  124. package/dist/src/mcp/tools/status.d.ts +7 -0
  125. package/dist/src/mcp/tools/status.js +127 -0
  126. package/package.json +10 -1
  127. package/templates/hooks/post-tool.sh +19 -8
  128. package/templates/hooks/pre-tool.sh +36 -49
  129. package/templates/mcp.json +6 -0
  130. package/templates/skills/assess/SKILL.md +354 -352
  131. package/templates/skills/exec/SKILL.md +64 -1
  132. package/templates/skills/fullsolve/SKILL.md +35 -4
  133. package/templates/skills/qa/SKILL.md +486 -9
  134. package/templates/skills/qa/scripts/quality-checks.sh +1 -1
  135. package/templates/skills/setup/SKILL.md +386 -0
  136. package/templates/skills/solve/SKILL.md +38 -664
  137. package/templates/skills/spec/SKILL.md +90 -31
@@ -89,10 +89,13 @@ function calculateStats(logs) {
89
89
  existing.total += phase.durationSeconds;
90
90
  existing.count++;
91
91
  phaseDurations.set(phase.phase, existing);
92
- // Track failure patterns
93
- if (phase.status === "failure" && phase.error) {
94
- // Normalize error message (truncate and clean)
95
- const errorKey = `${phase.phase}: ${phase.error.slice(0, 100)}`;
92
+ // Track failure patterns — prefer errorContext category (#447 AC-4)
93
+ if (phase.status === "failure") {
94
+ const errorKey = phase.errorContext?.category
95
+ ? `${phase.phase}: [${phase.errorContext.category}]`
96
+ : phase.error
97
+ ? `${phase.phase}: ${phase.error.slice(0, 100)}`
98
+ : `${phase.phase}: unknown`;
96
99
  commonFailures.set(errorKey, (commonFailures.get(errorKey) ?? 0) + 1);
97
100
  }
98
101
  }
@@ -424,6 +427,145 @@ function displayMetricsAnalytics(analytics) {
424
427
  console.log(colors.muted("\n Data stored locally in .sequant/metrics.json"));
425
428
  console.log("");
426
429
  }
430
+ /**
431
+ * Calculate detailed analytics from run logs
432
+ */
433
+ function calculateDetailedAnalytics(logs) {
434
+ const allIssues = logs.flatMap((l) => l.issues);
435
+ // QA verdict distribution
436
+ const qaVerdictDistribution = {};
437
+ let totalQaPhases = 0;
438
+ for (const issue of allIssues) {
439
+ for (const phase of issue.phases) {
440
+ if (phase.phase === "qa") {
441
+ totalQaPhases++;
442
+ const verdict = phase.verdict ?? "no_verdict";
443
+ qaVerdictDistribution[verdict] =
444
+ (qaVerdictDistribution[verdict] ?? 0) + 1;
445
+ }
446
+ }
447
+ }
448
+ // First-pass QA rate: group by issue, check if first QA attempt was READY_FOR_MERGE
449
+ const qaByIssue = new Map();
450
+ for (const issue of allIssues) {
451
+ const issueQa = issue.phases
452
+ .filter((p) => p.phase === "qa")
453
+ .map((p) => ({ verdict: p.verdict, startTime: p.startTime }));
454
+ if (issueQa.length > 0) {
455
+ const existing = qaByIssue.get(issue.issueNumber) ?? [];
456
+ existing.push(...issueQa);
457
+ qaByIssue.set(issue.issueNumber, existing);
458
+ }
459
+ }
460
+ let firstPassSuccess = 0;
461
+ let totalIssuesWithQa = 0;
462
+ for (const [, phases] of qaByIssue) {
463
+ totalIssuesWithQa++;
464
+ const sorted = phases.sort((a, b) => a.startTime.localeCompare(b.startTime));
465
+ if (sorted[0]?.verdict === "READY_FOR_MERGE") {
466
+ firstPassSuccess++;
467
+ }
468
+ }
469
+ // Weekly trends
470
+ const weekBuckets = new Map();
471
+ for (const log of logs) {
472
+ const d = new Date(log.startTime);
473
+ const day = d.getUTCDay();
474
+ const diff = d.getUTCDate() - day + (day === 0 ? -6 : 1);
475
+ const monday = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), diff));
476
+ const week = monday.toISOString().slice(0, 10);
477
+ const existing = weekBuckets.get(week) ?? {
478
+ runs: 0,
479
+ issues: 0,
480
+ successes: 0,
481
+ };
482
+ existing.runs++;
483
+ existing.issues += log.issues.length;
484
+ existing.successes += log.issues.filter((i) => i.status === "success").length;
485
+ weekBuckets.set(week, existing);
486
+ }
487
+ const weeklyTrends = [...weekBuckets.entries()]
488
+ .sort(([a], [b]) => a.localeCompare(b))
489
+ .map(([week, data]) => ({
490
+ week,
491
+ runs: data.runs,
492
+ issues: data.issues,
493
+ successRate: data.issues > 0 ? (data.successes / data.issues) * 100 : 0,
494
+ }));
495
+ // Label segmentation
496
+ const labelAcc = new Map();
497
+ for (const issue of allIssues) {
498
+ for (const label of issue.labels) {
499
+ const existing = labelAcc.get(label) ?? { issues: 0, successes: 0 };
500
+ existing.issues++;
501
+ if (issue.status === "success")
502
+ existing.successes++;
503
+ labelAcc.set(label, existing);
504
+ }
505
+ }
506
+ const labelSegments = [...labelAcc.entries()]
507
+ .map(([label, data]) => ({
508
+ label,
509
+ issues: data.issues,
510
+ successRate: data.issues > 0 ? (data.successes / data.issues) * 100 : 0,
511
+ }))
512
+ .sort((a, b) => b.issues - a.issues)
513
+ .slice(0, 10);
514
+ return {
515
+ qaVerdictDistribution,
516
+ firstPassQaRate: totalIssuesWithQa > 0 ? (firstPassSuccess / totalIssuesWithQa) * 100 : 0,
517
+ totalQaPhases,
518
+ weeklyTrends,
519
+ labelSegments,
520
+ };
521
+ }
522
+ /**
523
+ * Display detailed analytics
524
+ */
525
+ function displayDetailedAnalytics(detailed) {
526
+ // QA Verdicts
527
+ console.log(ui.sectionHeader("QA Verdicts"));
528
+ console.log(` First-pass QA rate: ${colors.accent(detailed.firstPassQaRate.toFixed(1) + "%")}`);
529
+ console.log(` Total QA phases: ${detailed.totalQaPhases}\n`);
530
+ for (const [verdict, count] of Object.entries(detailed.qaVerdictDistribution).sort((a, b) => b[1] - a[1])) {
531
+ const pct = detailed.totalQaPhases > 0
532
+ ? ((count / detailed.totalQaPhases) * 100).toFixed(1)
533
+ : "0";
534
+ const bar = ui.progressBar(count, detailed.totalQaPhases, 10);
535
+ console.log(` ${verdict.padEnd(26)} ${String(count).padStart(3)} (${pct}%) ${bar}`);
536
+ }
537
+ // Weekly Trends
538
+ if (detailed.weeklyTrends.length > 0) {
539
+ console.log(ui.sectionHeader("Weekly Trends"));
540
+ const rows = detailed.weeklyTrends.map((w) => [
541
+ w.week,
542
+ String(w.runs),
543
+ String(w.issues),
544
+ `${w.successRate.toFixed(0)}%`,
545
+ ]);
546
+ console.log(ui.table(rows, {
547
+ columns: [
548
+ { header: "Week", width: 12 },
549
+ { header: "Runs", width: 6 },
550
+ { header: "Issues", width: 8 },
551
+ { header: "Success", width: 9 },
552
+ ],
553
+ }));
554
+ }
555
+ // Label Segmentation
556
+ if (detailed.labelSegments.length > 0) {
557
+ console.log(ui.sectionHeader("Success by Label"));
558
+ for (const seg of detailed.labelSegments) {
559
+ const bar = ui.progressBar(Math.round(seg.successRate), 100, 10);
560
+ const rateStr = seg.successRate >= 80
561
+ ? colors.success(`${seg.successRate.toFixed(0)}%`)
562
+ : seg.successRate >= 60
563
+ ? colors.warning(`${seg.successRate.toFixed(0)}%`)
564
+ : colors.error(`${seg.successRate.toFixed(0)}%`);
565
+ console.log(` ${seg.label.padEnd(20)} ${String(seg.issues).padStart(3)} issues ${rateStr} ${bar}`);
566
+ }
567
+ }
568
+ }
427
569
  /**
428
570
  * Main stats command
429
571
  */
@@ -517,29 +659,46 @@ export async function statsCommand(options) {
517
659
  if (metrics && metrics.runs.length > 0) {
518
660
  const analytics = calculateMetricsAnalytics(metrics);
519
661
  displayMetricsAnalytics(analytics);
520
- return;
521
662
  }
522
- // Fall back to run logs display
523
- const logDir = resolveLogPath(options.path);
524
- const logFiles = listLogFiles(logDir);
525
- if (logFiles.length === 0) {
526
- console.log(ui.headerBox("SEQUANT ANALYTICS"));
527
- console.log(colors.muted("\n Local data only - no telemetry\n"));
528
- console.log(colors.warning(" No data found."));
529
- console.log(colors.muted(" Run `npx sequant run <issues>` to collect metrics."));
530
- console.log("");
531
- return;
663
+ else {
664
+ // Fall back to run logs display
665
+ const logDir = resolveLogPath(options.path);
666
+ const logFiles = listLogFiles(logDir);
667
+ if (logFiles.length === 0) {
668
+ console.log(ui.headerBox("SEQUANT ANALYTICS"));
669
+ console.log(colors.muted("\n Local data only - no telemetry\n"));
670
+ console.log(colors.warning(" No data found."));
671
+ console.log(colors.muted(" Run `npx sequant run <issues>` to collect metrics."));
672
+ console.log("");
673
+ return;
674
+ }
675
+ const logs = logFiles
676
+ .map((filename) => {
677
+ const filePath = path.join(logDir, filename);
678
+ return parseLogFile(filePath);
679
+ })
680
+ .filter((log) => log !== null);
681
+ if (logs.length === 0) {
682
+ console.log(colors.warning("\n No valid log files found.\n"));
683
+ return;
684
+ }
685
+ const stats = calculateStats(logs);
686
+ displayStats(stats, logDir);
532
687
  }
533
- const logs = logFiles
534
- .map((filename) => {
535
- const filePath = path.join(logDir, filename);
536
- return parseLogFile(filePath);
537
- })
538
- .filter((log) => log !== null);
539
- if (logs.length === 0) {
540
- console.log(colors.warning("\n No valid log files found.\n"));
541
- return;
688
+ // Detailed analytics from run logs (--detailed flag)
689
+ if (options.detailed) {
690
+ const logDir = resolveLogPath(options.path);
691
+ const logFiles = listLogFiles(logDir);
692
+ const logs = logFiles
693
+ .map((filename) => {
694
+ const filePath = path.join(logDir, filename);
695
+ return parseLogFile(filePath);
696
+ })
697
+ .filter((log) => log !== null);
698
+ if (logs.length > 0) {
699
+ const detailed = calculateDetailedAnalytics(logs);
700
+ displayDetailedAnalytics(detailed);
701
+ console.log("");
702
+ }
542
703
  }
543
- const stats = calculateStats(logs);
544
- displayStats(stats, logDir);
545
704
  }
@@ -18,5 +18,7 @@ export interface StatusCommandOptions {
18
18
  maxAge?: number;
19
19
  /** Remove all orphaned entries (both merged and abandoned) in one step */
20
20
  all?: boolean;
21
+ /** Skip GitHub API queries (offline mode) */
22
+ offline?: boolean;
21
23
  }
22
24
  export declare function statusCommand(options?: StatusCommandOptions): Promise<void>;
@@ -7,27 +7,54 @@ import { getManifest, getPackageVersion } from "../lib/manifest.js";
7
7
  import { fileExists } from "../lib/fs.js";
8
8
  import { readdir } from "fs/promises";
9
9
  import { StateManager } from "../lib/workflow/state-manager.js";
10
- import { rebuildStateFromLogs, cleanupStaleEntries, checkPRMergeStatus, } from "../lib/workflow/state-utils.js";
10
+ import { rebuildStateFromLogs, cleanupStaleEntries, } from "../lib/workflow/state-utils.js";
11
+ import { reconcileState, getNextActionHint, formatRelativeTime, } from "../lib/workflow/reconcile.js";
11
12
  /**
12
- * Auto-detect merged PRs and update state for issues stuck at ready_for_merge.
13
- * Queries GitHub for each ready_for_merge issue that has a PR number.
13
+ * Run reconciliation and display warnings.
14
+ * Returns the reconcile result for use in display.
14
15
  */
15
- async function refreshMergedStatuses(stateManager, issues) {
16
- const readyIssues = issues.filter((i) => i.status === "ready_for_merge" && i.pr?.number);
17
- if (readyIssues.length === 0)
18
- return;
19
- for (const issue of readyIssues) {
20
- const prStatus = checkPRMergeStatus(issue.pr.number);
21
- if (prStatus === "MERGED") {
22
- issue.status = "merged";
23
- await stateManager.updateIssueStatus(issue.number, "merged");
16
+ async function runReconciliation(stateManager, options) {
17
+ const result = await reconcileState({
18
+ offline: options.offline,
19
+ stateManager,
20
+ });
21
+ if (!options.json) {
22
+ // Show reconciliation warnings
23
+ if (result.warnings.length > 0) {
24
+ console.log(chalk.yellow("\n āš ļø Drift detected:"));
25
+ for (const w of result.warnings) {
26
+ console.log(chalk.yellow(` #${w.issueNumber}: ${w.description}`));
27
+ }
28
+ }
29
+ // Show healed drift
30
+ if (result.healed.length > 0) {
31
+ for (const h of result.healed) {
32
+ console.log(chalk.gray(` āœ“ Auto-healed: ${h.description}`));
33
+ }
34
+ }
35
+ if (!result.githubReachable && !options.offline) {
36
+ console.log(chalk.yellow("\n āš ļø GitHub unreachable — showing cached data. Use --offline to suppress this warning."));
24
37
  }
25
38
  }
39
+ return result;
40
+ }
41
+ /**
42
+ * Format age in days from an ISO timestamp
43
+ */
44
+ function formatAgeDays(isoTimestamp) {
45
+ if (!isoTimestamp)
46
+ return "";
47
+ const age = Date.now() - new Date(isoTimestamp).getTime();
48
+ const days = Math.floor(age / 86_400_000);
49
+ if (days < 1)
50
+ return "today";
51
+ return `${days}d ago`;
26
52
  }
27
53
  /**
28
- * Color-code issue status
54
+ * Color-code issue status, with age indicator for resolved issues
29
55
  */
30
- function colorStatus(status) {
56
+ function colorStatus(status, resolvedAt) {
57
+ const age = resolvedAt ? ` (${formatAgeDays(resolvedAt)})` : "";
31
58
  switch (status) {
32
59
  case "not_started":
33
60
  return chalk.gray(status);
@@ -38,11 +65,11 @@ function colorStatus(status) {
38
65
  case "ready_for_merge":
39
66
  return chalk.green(status);
40
67
  case "merged":
41
- return chalk.green(status);
68
+ return chalk.green(status + age);
42
69
  case "blocked":
43
70
  return chalk.yellow(status);
44
71
  case "abandoned":
45
- return chalk.red(status);
72
+ return chalk.red(status + age);
46
73
  default:
47
74
  return status;
48
75
  }
@@ -76,7 +103,7 @@ function formatIssueState(issue) {
76
103
  // Issue header
77
104
  lines.push(chalk.bold(` #${issue.number}: ${issue.title.substring(0, 50)}${issue.title.length > 50 ? "..." : ""}`));
78
105
  // Status and current phase
79
- const status = colorStatus(issue.status);
106
+ const status = colorStatus(issue.status, issue.resolvedAt);
80
107
  const currentPhase = issue.currentPhase
81
108
  ? chalk.cyan(issue.currentPhase)
82
109
  : chalk.gray("none");
@@ -105,31 +132,10 @@ function formatIssueState(issue) {
105
132
  lines.push(chalk.gray(` Worktree: ${issue.worktree}`));
106
133
  }
107
134
  // Last activity
108
- const lastActivity = new Date(issue.lastActivity);
109
- const relativeTime = getRelativeTime(lastActivity);
135
+ const relativeTime = formatRelativeTime(issue.lastActivity);
110
136
  lines.push(chalk.gray(` Last activity: ${relativeTime}`));
111
137
  return lines.join("\n");
112
138
  }
113
- /**
114
- * Get relative time string
115
- */
116
- function getRelativeTime(date) {
117
- const now = new Date();
118
- const diffMs = now.getTime() - date.getTime();
119
- const diffSec = Math.floor(diffMs / 1000);
120
- const diffMin = Math.floor(diffSec / 60);
121
- const diffHour = Math.floor(diffMin / 60);
122
- const diffDay = Math.floor(diffHour / 24);
123
- if (diffSec < 60)
124
- return "just now";
125
- if (diffMin < 60)
126
- return `${diffMin} minute${diffMin > 1 ? "s" : ""} ago`;
127
- if (diffHour < 24)
128
- return `${diffHour} hour${diffHour > 1 ? "s" : ""} ago`;
129
- if (diffDay < 7)
130
- return `${diffDay} day${diffDay > 1 ? "s" : ""} ago`;
131
- return date.toLocaleDateString();
132
- }
133
139
  /**
134
140
  * Display issue state summary table
135
141
  */
@@ -170,11 +176,16 @@ function displayIssueSummary(issues) {
170
176
  const title = issue.title.length > 30
171
177
  ? issue.title.substring(0, 27) + "..."
172
178
  : issue.title;
179
+ const hint = getNextActionHint(issue);
180
+ const hintDisplay = hint
181
+ ? chalk.gray(`→ ${hint.length > 30 ? hint.substring(0, 27) + "..." : hint}`)
182
+ : "";
173
183
  rows.push([
174
184
  `#${issue.number}`,
175
185
  title,
176
- colorStatus(issue.status),
186
+ colorStatus(issue.status, issue.resolvedAt),
177
187
  issue.currentPhase || "-",
188
+ hintDisplay,
178
189
  ]);
179
190
  }
180
191
  }
@@ -186,6 +197,7 @@ function displayIssueSummary(issues) {
186
197
  { header: "Title", width: 32 },
187
198
  { header: "Status", width: 20 },
188
199
  { header: "Phase", width: 10 },
200
+ { header: "Next", width: 34 },
189
201
  ],
190
202
  }));
191
203
  // Summary counts
@@ -220,7 +232,7 @@ export async function statusCommand(options = {}) {
220
232
  return;
221
233
  }
222
234
  // If --issues or --issue flag, focus on issue state
223
- if (options.issues || options.issue !== undefined) {
235
+ if (options.issues || options.issue !== undefined || options.all) {
224
236
  await displayIssueState(options);
225
237
  return;
226
238
  }
@@ -263,12 +275,21 @@ export async function statusCommand(options = {}) {
263
275
  const stateManager = new StateManager();
264
276
  if (stateManager.stateExists()) {
265
277
  try {
278
+ // Reconcile state with GitHub before displaying
279
+ const reconcileResult = await runReconciliation(stateManager, options);
280
+ // Re-read state after reconciliation (may have been updated)
281
+ stateManager.clearCache();
266
282
  const allIssues = await stateManager.getAllIssueStates();
267
283
  const issues = Object.values(allIssues);
268
284
  if (issues.length > 0) {
269
- await refreshMergedStatuses(stateManager, issues);
270
285
  displayIssueSummary(issues);
271
286
  }
287
+ // Last synced footer
288
+ if (reconcileResult.lastSynced) {
289
+ const syncedAgo = formatRelativeTime(reconcileResult.lastSynced);
290
+ const offlineNote = options.offline ? " (offline)" : "";
291
+ console.log(chalk.gray(`\n Last synced: ${syncedAgo}${offlineNote}`));
292
+ }
272
293
  }
273
294
  catch {
274
295
  // Ignore state read errors
@@ -293,34 +314,62 @@ async function displayIssueState(options) {
293
314
  return;
294
315
  }
295
316
  try {
317
+ // Reconcile state with GitHub before displaying
318
+ const reconcileResult = await runReconciliation(stateManager, options);
319
+ stateManager.clearCache();
296
320
  if (options.issue !== undefined) {
297
321
  // Show single issue details
298
322
  const issueState = await stateManager.getIssueState(options.issue);
299
- if (issueState) {
300
- await refreshMergedStatuses(stateManager, [issueState]);
301
- }
302
323
  if (options.json) {
303
- console.log(JSON.stringify(issueState, null, 2));
324
+ const jsonData = issueState
325
+ ? {
326
+ ...issueState,
327
+ nextAction: getNextActionHint(issueState),
328
+ lastSynced: reconcileResult.lastSynced,
329
+ }
330
+ : null;
331
+ console.log(JSON.stringify(jsonData, null, 2));
304
332
  }
305
333
  else if (issueState) {
306
334
  console.log(chalk.bold(`\nšŸ“Š Issue #${options.issue} State\n`));
307
335
  console.log(formatIssueState(issueState));
336
+ const hint = getNextActionHint(issueState);
337
+ if (hint) {
338
+ console.log(chalk.cyan(`\n Next: ${hint}`));
339
+ }
308
340
  }
309
341
  else {
310
342
  console.log(chalk.yellow(`\nIssue #${options.issue} not found in state.`));
311
343
  }
312
344
  }
313
345
  else {
314
- // Show all issues
315
- const allIssues = await stateManager.getAllIssueStates();
346
+ // Show all issues (--all bypasses TTL filtering)
347
+ const allIssues = options.all
348
+ ? await stateManager.getAllIssueStatesUnfiltered()
349
+ : await stateManager.getAllIssueStates();
316
350
  const issues = Object.values(allIssues);
317
- await refreshMergedStatuses(stateManager, issues);
318
351
  if (options.json) {
319
- console.log(JSON.stringify({ issues: allIssues }, null, 2));
352
+ // Enrich JSON output with next-action hints and lastSynced
353
+ const enriched = {};
354
+ for (const [key, issue] of Object.entries((await stateManager.getState()).issues)) {
355
+ enriched[key] = {
356
+ ...issue,
357
+ nextAction: getNextActionHint(issue),
358
+ };
359
+ }
360
+ console.log(JSON.stringify({
361
+ issues: enriched,
362
+ lastSynced: reconcileResult.lastSynced,
363
+ githubReachable: reconcileResult.githubReachable,
364
+ }, null, 2));
320
365
  }
321
366
  else {
322
367
  console.log(chalk.bold("\nšŸ“Š Workflow State\n"));
323
368
  displayIssueSummary(issues);
369
+ // Last synced footer
370
+ const syncedAgo = formatRelativeTime(reconcileResult.lastSynced);
371
+ const offlineNote = options.offline ? " (offline)" : "";
372
+ console.log(chalk.gray(`\n Last synced: ${syncedAgo}${offlineNote}`));
324
373
  }
325
374
  }
326
375
  }
@@ -28,5 +28,5 @@ export { analyzeTitleForPhases, analyzeBodyForPhases, analyzeContentForPhases, i
28
28
  export type { ContentSignal, ContentAnalysisResult, } from "./lib/content-analyzer.js";
29
29
  export { mergePhaseSignals, signalFromLabel, signalsFromLabels, formatMergedPhases, SIGNAL_PRIORITY, } from "./lib/phase-signal.js";
30
30
  export type { SignalSource, SignalConfidence, PhaseSignal, MergedPhaseResult, } from "./lib/phase-signal.js";
31
- export { isSolveComment, findSolveComment, parseSolveWorkflow, solveWorkflowToSignals, solveCoversIssue, } from "./lib/solve-comment-parser.js";
32
- export type { SolveWorkflowResult, IssueComment, } from "./lib/solve-comment-parser.js";
31
+ export { isAssessComment, findAssessComment, parseAssessMarkers, parseAssessWorkflow, assessWorkflowToSignals, assessCoversIssue, isSolveComment, findSolveComment, parseSolveMarkers, parseSolveWorkflow, solveWorkflowToSignals, solveCoversIssue, } from "./lib/assess-comment-parser.js";
32
+ export type { AssessWorkflowResult, AssessMarkers, AssessAction, SolveWorkflowResult, SolveMarkers, IssueComment, } from "./lib/assess-comment-parser.js";
package/dist/src/index.js CHANGED
@@ -21,4 +21,7 @@ export { rebuildStateFromLogs, cleanupStaleEntries, } from "./lib/workflow/state
21
21
  // Content analysis exports
22
22
  export { analyzeTitleForPhases, analyzeBodyForPhases, analyzeContentForPhases, isTrivialWork, formatContentAnalysis, } from "./lib/content-analyzer.js";
23
23
  export { mergePhaseSignals, signalFromLabel, signalsFromLabels, formatMergedPhases, SIGNAL_PRIORITY, } from "./lib/phase-signal.js";
24
- export { isSolveComment, findSolveComment, parseSolveWorkflow, solveWorkflowToSignals, solveCoversIssue, } from "./lib/solve-comment-parser.js";
24
+ // Assess comment parser exports (unified from solve + assess)
25
+ export { isAssessComment, findAssessComment, parseAssessMarkers, parseAssessWorkflow, assessWorkflowToSignals, assessCoversIssue,
26
+ // Backward-compatible aliases (deprecated)
27
+ isSolveComment, findSolveComment, parseSolveMarkers, parseSolveWorkflow, solveWorkflowToSignals, solveCoversIssue, } from "./lib/assess-comment-parser.js";
@@ -4,6 +4,7 @@
4
4
  * Extracts acceptance criteria from GitHub issue markdown.
5
5
  * Supports checkbox format: `- [ ] **AC-1:** Description`
6
6
  * Also supports alternate formats: `- [ ] **B2:** Description`
7
+ * And bold-wrapped format: `- [ ] **AC-1: Description**`
7
8
  *
8
9
  * @example
9
10
  * ```typescript
@@ -37,6 +38,7 @@ export declare function inferVerificationMethod(description: string): ACVerifica
37
38
  * Supports multiple formats:
38
39
  * - `- [ ] **AC-1:** Description`
39
40
  * - `- [ ] **B2:** Description`
41
+ * - `- [ ] **AC-1: Description**` (bold wraps ID + description)
40
42
  * - `- [ ] AC-1: Description`
41
43
  *
42
44
  * @param issueBody - The full GitHub issue body markdown
@@ -4,6 +4,7 @@
4
4
  * Extracts acceptance criteria from GitHub issue markdown.
5
5
  * Supports checkbox format: `- [ ] **AC-1:** Description`
6
6
  * Also supports alternate formats: `- [ ] **B2:** Description`
7
+ * And bold-wrapped format: `- [ ] **AC-1: Description**`
7
8
  *
8
9
  * @example
9
10
  * ```typescript
@@ -31,13 +32,16 @@ import { createAcceptanceCriterion, createAcceptanceCriteria, } from "./workflow
31
32
  * - `- [x] **AC-1:** Description`
32
33
  * - `- [ ] **B2:** Description`
33
34
  * - `- [ ] **AC1:** Description`
35
+ * - `- [ ] **AC-1: Description**` (bold wraps ID + description)
34
36
  */
35
37
  const AC_PATTERNS = [
36
38
  // Pattern 1: `- [ ] **AC-1:** Description` or `- [x] **AC-1:** Description`
37
39
  /^-\s*\[[x\s]\]\s*\*\*([A-Za-z]+-?\d+):\*\*\s*(.+)$/gim,
38
40
  // Pattern 2: `- [ ] **B2:** Description` (letter + number without hyphen)
39
41
  /^-\s*\[[x\s]\]\s*\*\*([A-Za-z]\d+):\*\*\s*(.+)$/gim,
40
- // Pattern 3: `- [ ] AC-1: Description` (no bold)
42
+ // Pattern 3: `- [ ] **AC-1: Description.** optional text` (bold wraps ID + description)
43
+ /^-\s*\[[x\s]\]\s*\*\*([A-Za-z]+-?\d+):\s*(.+?)\*\*\s*(.*)$/gim,
44
+ // Pattern 4: `- [ ] AC-1: Description` (no bold)
41
45
  /^-\s*\[[x\s]\]\s*([A-Za-z]+-?\d+):\s*(.+)$/gim,
42
46
  ];
43
47
  /**
@@ -97,9 +101,14 @@ function parseACLine(line) {
97
101
  pattern.lastIndex = 0;
98
102
  const match = pattern.exec(line);
99
103
  if (match) {
104
+ // Combine groups 2 and 3 for bold-wrapped format (Pattern 3)
105
+ // where group 3 captures optional text after closing **
106
+ const description = match[3]
107
+ ? `${match[2].trim()} ${match[3].trim()}`.trim()
108
+ : match[2].trim();
100
109
  return {
101
110
  id: match[1].toUpperCase(),
102
- description: match[2].trim(),
111
+ description,
103
112
  };
104
113
  }
105
114
  }
@@ -112,6 +121,7 @@ function parseACLine(line) {
112
121
  * Supports multiple formats:
113
122
  * - `- [ ] **AC-1:** Description`
114
123
  * - `- [ ] **B2:** Description`
124
+ * - `- [ ] **AC-1: Description**` (bold wraps ID + description)
115
125
  * - `- [ ] AC-1: Description`
116
126
  *
117
127
  * @param issueBody - The full GitHub issue body markdown