sequant 1.20.3 ā 2.0.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.
- package/.claude-plugin/marketplace.json +2 -4
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +36 -15
- package/dist/bin/cli.js +25 -2
- package/dist/src/commands/doctor.js +42 -9
- package/dist/src/commands/init.d.ts +1 -0
- package/dist/src/commands/init.js +52 -0
- package/dist/src/commands/logs.d.ts +1 -0
- package/dist/src/commands/logs.js +18 -2
- package/dist/src/commands/run.d.ts +7 -0
- package/dist/src/commands/run.js +235 -68
- package/dist/src/commands/serve.d.ts +13 -0
- package/dist/src/commands/serve.js +131 -0
- package/dist/src/commands/stats.d.ts +1 -0
- package/dist/src/commands/stats.js +185 -26
- package/dist/src/commands/status.d.ts +2 -0
- package/dist/src/commands/status.js +99 -50
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +4 -1
- package/dist/src/lib/ac-parser.d.ts +2 -0
- package/dist/src/lib/ac-parser.js +12 -2
- package/dist/src/lib/assess-comment-parser.d.ts +137 -0
- package/dist/src/lib/assess-comment-parser.js +344 -0
- package/dist/src/lib/ci/config.d.ts +22 -0
- package/dist/src/lib/ci/config.js +134 -0
- package/dist/src/lib/ci/index.d.ts +12 -0
- package/dist/src/lib/ci/index.js +10 -0
- package/dist/src/lib/ci/inputs.d.ts +29 -0
- package/dist/src/lib/ci/inputs.js +103 -0
- package/dist/src/lib/ci/labels.d.ts +34 -0
- package/dist/src/lib/ci/labels.js +101 -0
- package/dist/src/lib/ci/outputs.d.ts +25 -0
- package/dist/src/lib/ci/outputs.js +84 -0
- package/dist/src/lib/ci/triggers.d.ts +9 -0
- package/dist/src/lib/ci/triggers.js +86 -0
- package/dist/src/lib/ci/types.d.ts +131 -0
- package/dist/src/lib/ci/types.js +47 -0
- package/dist/src/lib/mcp-config.d.ts +54 -0
- package/dist/src/lib/mcp-config.js +172 -0
- package/dist/src/lib/merge-check/index.js +6 -12
- package/dist/src/lib/merge-check/types.d.ts +20 -7
- package/dist/src/lib/merge-check/types.js +11 -0
- package/dist/src/lib/phase-signal.d.ts +3 -3
- package/dist/src/lib/phase-signal.js +5 -3
- package/dist/src/lib/settings.d.ts +52 -0
- package/dist/src/lib/settings.js +41 -0
- package/dist/src/lib/shutdown.d.ts +16 -5
- package/dist/src/lib/shutdown.js +32 -12
- package/dist/src/lib/solve-comment-parser.d.ts +9 -102
- package/dist/src/lib/solve-comment-parser.js +13 -248
- package/dist/src/lib/stacks.d.ts +8 -0
- package/dist/src/lib/stacks.js +34 -0
- package/dist/src/lib/system.js +3 -7
- package/dist/src/lib/test-tautology-detector.d.ts +10 -0
- package/dist/src/lib/test-tautology-detector.js +43 -4
- package/dist/src/lib/upstream/assessment.js +9 -59
- package/dist/src/lib/upstream/issues.js +12 -75
- package/dist/src/lib/version-check.d.ts +2 -2
- package/dist/src/lib/version-check.js +6 -3
- package/dist/src/lib/version.d.ts +4 -0
- package/dist/src/lib/version.js +25 -0
- package/dist/src/lib/workflow/batch-executor.d.ts +26 -86
- package/dist/src/lib/workflow/batch-executor.js +269 -55
- package/dist/src/lib/workflow/drivers/agent-driver.d.ts +56 -0
- package/dist/src/lib/workflow/drivers/agent-driver.js +8 -0
- package/dist/src/lib/workflow/drivers/aider.d.ts +18 -0
- package/dist/src/lib/workflow/drivers/aider.js +160 -0
- package/dist/src/lib/workflow/drivers/claude-code.d.ts +17 -0
- package/dist/src/lib/workflow/drivers/claude-code.js +165 -0
- package/dist/src/lib/workflow/drivers/index.d.ts +20 -0
- package/dist/src/lib/workflow/drivers/index.js +27 -0
- package/dist/src/lib/workflow/error-classifier.d.ts +16 -0
- package/dist/src/lib/workflow/error-classifier.js +90 -0
- package/dist/src/lib/workflow/log-writer.d.ts +6 -3
- package/dist/src/lib/workflow/log-writer.js +57 -27
- package/dist/src/lib/workflow/metrics-schema.d.ts +9 -9
- package/dist/src/lib/workflow/phase-detection.d.ts +23 -0
- package/dist/src/lib/workflow/phase-detection.js +45 -29
- package/dist/src/lib/workflow/phase-executor.d.ts +42 -3
- package/dist/src/lib/workflow/phase-executor.js +375 -229
- package/dist/src/lib/workflow/phase-mapper.d.ts +1 -1
- package/dist/src/lib/workflow/phase-mapper.js +7 -7
- package/dist/src/lib/workflow/platforms/github.d.ts +157 -0
- package/dist/src/lib/workflow/platforms/github.js +466 -0
- package/dist/src/lib/workflow/platforms/index.d.ts +17 -0
- package/dist/src/lib/workflow/platforms/index.js +25 -0
- package/dist/src/lib/workflow/platforms/platform-provider.d.ts +67 -0
- package/dist/src/lib/workflow/platforms/platform-provider.js +8 -0
- package/dist/src/lib/workflow/pr-status.d.ts +2 -4
- package/dist/src/lib/workflow/pr-status.js +3 -16
- package/dist/src/lib/workflow/qa-cache.d.ts +58 -0
- package/dist/src/lib/workflow/qa-cache.js +88 -0
- package/dist/src/lib/workflow/reconcile.d.ts +69 -0
- package/dist/src/lib/workflow/reconcile.js +290 -0
- package/dist/src/lib/workflow/ring-buffer.d.ts +17 -0
- package/dist/src/lib/workflow/ring-buffer.js +37 -0
- package/dist/src/lib/workflow/run-log-schema.d.ts +115 -24
- package/dist/src/lib/workflow/run-log-schema.js +47 -12
- package/dist/src/lib/workflow/run-reflect.js +1 -1
- package/dist/src/lib/workflow/state-cleanup.js +21 -0
- package/dist/src/lib/workflow/state-manager.d.ts +34 -3
- package/dist/src/lib/workflow/state-manager.js +278 -126
- package/dist/src/lib/workflow/state-schema.d.ts +34 -30
- package/dist/src/lib/workflow/state-schema.js +35 -25
- package/dist/src/lib/workflow/state-utils.d.ts +3 -1
- package/dist/src/lib/workflow/state-utils.js +1 -0
- package/dist/src/lib/workflow/types.d.ts +224 -6
- package/dist/src/lib/workflow/types.js +20 -1
- package/dist/src/lib/workflow/worktree-discovery.d.ts +1 -1
- package/dist/src/lib/workflow/worktree-discovery.js +6 -14
- package/dist/src/lib/workflow/worktree-manager.js +33 -51
- package/dist/src/mcp/index.d.ts +4 -0
- package/dist/src/mcp/index.js +4 -0
- package/dist/src/mcp/resources.d.ts +7 -0
- package/dist/src/mcp/resources.js +111 -0
- package/dist/src/mcp/run-registry.d.ts +34 -0
- package/dist/src/mcp/run-registry.js +42 -0
- package/dist/src/mcp/server.d.ts +12 -0
- package/dist/src/mcp/server.js +50 -0
- package/dist/src/mcp/tools/logs.d.ts +7 -0
- package/dist/src/mcp/tools/logs.js +149 -0
- package/dist/src/mcp/tools/run.d.ts +121 -0
- package/dist/src/mcp/tools/run.js +591 -0
- package/dist/src/mcp/tools/status.d.ts +7 -0
- package/dist/src/mcp/tools/status.js +127 -0
- package/package.json +26 -7
- package/templates/hooks/post-tool.sh +19 -8
- package/templates/hooks/pre-tool.sh +36 -49
- package/templates/mcp.json +6 -0
- package/templates/skills/assess/SKILL.md +354 -352
- package/templates/skills/exec/SKILL.md +64 -1
- package/templates/skills/fullsolve/SKILL.md +35 -4
- package/templates/skills/qa/SKILL.md +486 -9
- package/templates/skills/qa/scripts/quality-checks.sh +1 -1
- package/templates/skills/setup/SKILL.md +386 -0
- package/templates/skills/solve/SKILL.md +38 -664
- 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"
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
-
|
|
534
|
-
|
|
535
|
-
const
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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,
|
|
10
|
+
import { rebuildStateFromLogs, cleanupStaleEntries, } from "../lib/workflow/state-utils.js";
|
|
11
|
+
import { reconcileState, getNextActionHint, formatRelativeTime, } from "../lib/workflow/reconcile.js";
|
|
11
12
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
13
|
+
* Run reconciliation and display warnings.
|
|
14
|
+
* Returns the reconcile result for use in display.
|
|
14
15
|
*/
|
|
15
|
-
async function
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -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/
|
|
32
|
-
export type { SolveWorkflowResult, IssueComment, } from "./lib/
|
|
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
|
-
|
|
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` (
|
|
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
|
|
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
|