sequant 2.1.2 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/dist/bin/cli.js +1 -0
- package/dist/src/commands/init.d.ts +1 -0
- package/dist/src/commands/init.js +118 -0
- package/dist/src/commands/run-display.d.ts +17 -0
- package/dist/src/commands/run-display.js +116 -0
- package/dist/src/commands/run.js +12 -64
- package/dist/src/commands/status.js +15 -1
- package/dist/src/lib/skill-version.d.ts +19 -0
- package/dist/src/lib/skill-version.js +68 -0
- package/dist/src/lib/templates.d.ts +1 -0
- package/dist/src/lib/templates.js +1 -1
- package/dist/src/lib/workflow/batch-executor.js +1 -1
- package/dist/src/lib/workflow/phase-executor.d.ts +31 -0
- package/dist/src/lib/workflow/phase-executor.js +129 -46
- package/dist/src/lib/workflow/run-orchestrator.d.ts +37 -0
- package/dist/src/lib/workflow/run-orchestrator.js +42 -14
- package/dist/src/lib/workflow/worktree-manager.d.ts +4 -3
- package/dist/src/lib/workflow/worktree-manager.js +61 -11
- package/package.json +1 -1
- package/templates/skills/assess/SKILL.md +176 -63
- package/templates/skills/exec/SKILL.md +0 -41
- package/templates/skills/fullsolve/SKILL.md +1 -27
- package/templates/skills/qa/SKILL.md +19 -0
- package/templates/skills/qa/scripts/quality-checks.sh +47 -1
- package/templates/skills/spec/SKILL.md +183 -982
- package/templates/skills/spec/references/quality-checklist.md +75 -0
- package/templates/skills/test/SKILL.md +0 -27
- package/templates/skills/testgen/SKILL.md +0 -27
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
{
|
|
9
9
|
"name": "sequant",
|
|
10
10
|
"description": "Structured workflow system for Claude Code - GitHub issue resolution with spec, exec, test, and QA phases. Includes 17 skills, MCP server with workflow tools, and pre/post-tool hooks.",
|
|
11
|
-
"version": "2.
|
|
11
|
+
"version": "2.2.0",
|
|
12
12
|
"author": {
|
|
13
13
|
"name": "sequant-io",
|
|
14
14
|
"email": "hello@sequant.io"
|
package/dist/bin/cli.js
CHANGED
|
@@ -86,6 +86,7 @@ program
|
|
|
86
86
|
.option("--no-symlinks", "Use copies instead of symlinks for scripts/dev/ files")
|
|
87
87
|
.option("--no-agents-md", "Skip AGENTS.md generation")
|
|
88
88
|
.option("--mcp", "Add Sequant MCP server to detected clients (use with --yes)")
|
|
89
|
+
.option("--upgrade-skills", "Upgrade skill files from installed package templates (with diff preview)")
|
|
89
90
|
.action(initCommand);
|
|
90
91
|
program
|
|
91
92
|
.command("update")
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
* sequant init - Initialize Sequant in a project
|
|
3
3
|
*/
|
|
4
4
|
import chalk from "chalk";
|
|
5
|
+
import { diffLines } from "diff";
|
|
5
6
|
import inquirer from "inquirer";
|
|
7
|
+
import { join, dirname } from "path";
|
|
8
|
+
import { readdir } from "fs/promises";
|
|
6
9
|
import { ui, colors } from "../lib/cli-ui.js";
|
|
7
10
|
import { detectStack, detectAllStacks, getStackConfig, detectPackageManager, getPackageManagerCommands, STACKS, } from "../lib/stacks.js";
|
|
8
11
|
import { copyTemplates } from "../lib/templates.js";
|
|
@@ -70,6 +73,11 @@ function logDefault(label, value) {
|
|
|
70
73
|
console.log(chalk.blue(`${label}: ${value} (default)`));
|
|
71
74
|
}
|
|
72
75
|
export async function initCommand(options) {
|
|
76
|
+
// Handle --upgrade-skills: update skill files from installed package templates
|
|
77
|
+
if (options.upgradeSkills) {
|
|
78
|
+
await upgradeSkills();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
73
81
|
// Show banner
|
|
74
82
|
console.log(ui.banner());
|
|
75
83
|
console.log(colors.success("\nInitializing Sequant...\n"));
|
|
@@ -503,3 +511,113 @@ export async function initCommand(options) {
|
|
|
503
511
|
}
|
|
504
512
|
console.log(chalk.gray("\nDocumentation: https://github.com/sequant-io/sequant#readme\n"));
|
|
505
513
|
}
|
|
514
|
+
/**
|
|
515
|
+
* Upgrade installed skill files from the sequant package's templates.
|
|
516
|
+
* Shows a diff preview for each changed file and asks for confirmation.
|
|
517
|
+
*/
|
|
518
|
+
async function upgradeSkills() {
|
|
519
|
+
console.log(chalk.bold("\nUpgrading skills from package templates...\n"));
|
|
520
|
+
const installedDir = ".claude/skills";
|
|
521
|
+
if (!(await fileExists(installedDir))) {
|
|
522
|
+
console.log(chalk.red("No skills directory found. Run `sequant init` first."));
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
// Resolve the package's templates/skills directory
|
|
526
|
+
const { getTemplatesDir } = await import("../lib/templates.js");
|
|
527
|
+
const templateSkillsDir = join(getTemplatesDir(), "skills");
|
|
528
|
+
// Collect all files from both directories
|
|
529
|
+
const changes = [];
|
|
530
|
+
const newFiles = [];
|
|
531
|
+
async function compareDir(templateDir, installedBaseDir, relativePrefix) {
|
|
532
|
+
let entries;
|
|
533
|
+
try {
|
|
534
|
+
entries = await readdir(templateDir, { withFileTypes: true });
|
|
535
|
+
}
|
|
536
|
+
catch {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
for (const entry of entries) {
|
|
540
|
+
const relPath = join(relativePrefix, entry.name);
|
|
541
|
+
const templatePath = join(templateDir, entry.name);
|
|
542
|
+
const installedPath = join(installedBaseDir, relPath);
|
|
543
|
+
if (entry.isDirectory()) {
|
|
544
|
+
await compareDir(templatePath, installedBaseDir, relPath);
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
const templateContent = await readFile(templatePath);
|
|
548
|
+
const exists = await fileExists(installedPath);
|
|
549
|
+
if (!exists) {
|
|
550
|
+
newFiles.push({ path: relPath, content: templateContent });
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
const installedContent = await readFile(installedPath);
|
|
554
|
+
if (installedContent !== templateContent) {
|
|
555
|
+
changes.push({
|
|
556
|
+
path: relPath,
|
|
557
|
+
installed: installedContent,
|
|
558
|
+
template: templateContent,
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
await compareDir(templateSkillsDir, installedDir, "");
|
|
566
|
+
if (changes.length === 0 && newFiles.length === 0) {
|
|
567
|
+
console.log(chalk.green("All skills are up to date."));
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
// Show summary
|
|
571
|
+
console.log(chalk.bold("Changes found:"));
|
|
572
|
+
if (changes.length > 0) {
|
|
573
|
+
console.log(chalk.yellow(` Modified: ${changes.length} file(s)`));
|
|
574
|
+
}
|
|
575
|
+
if (newFiles.length > 0) {
|
|
576
|
+
console.log(chalk.green(` New: ${newFiles.length} file(s)`));
|
|
577
|
+
}
|
|
578
|
+
console.log();
|
|
579
|
+
// Show diffs for modified files
|
|
580
|
+
for (const change of changes) {
|
|
581
|
+
console.log(chalk.yellow(`--- ${change.path} ---`));
|
|
582
|
+
const diff = diffLines(change.installed, change.template);
|
|
583
|
+
for (const part of diff) {
|
|
584
|
+
if (part.added) {
|
|
585
|
+
process.stdout.write(chalk.green(part.value));
|
|
586
|
+
}
|
|
587
|
+
else if (part.removed) {
|
|
588
|
+
process.stdout.write(chalk.red(part.value));
|
|
589
|
+
}
|
|
590
|
+
// Skip unchanged lines in diff output
|
|
591
|
+
}
|
|
592
|
+
console.log();
|
|
593
|
+
}
|
|
594
|
+
// Show new files
|
|
595
|
+
for (const file of newFiles) {
|
|
596
|
+
console.log(chalk.green(`+++ ${file.path} (new)`));
|
|
597
|
+
}
|
|
598
|
+
// Ask for confirmation
|
|
599
|
+
const isInteractive = shouldUseInteractiveMode();
|
|
600
|
+
if (isInteractive) {
|
|
601
|
+
const { proceed } = await inquirer.prompt([
|
|
602
|
+
{
|
|
603
|
+
type: "confirm",
|
|
604
|
+
name: "proceed",
|
|
605
|
+
message: `Apply ${changes.length + newFiles.length} skill update(s)?`,
|
|
606
|
+
default: true,
|
|
607
|
+
},
|
|
608
|
+
]);
|
|
609
|
+
if (!proceed) {
|
|
610
|
+
console.log(chalk.gray("Aborted."));
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
// Apply changes
|
|
615
|
+
for (const change of changes) {
|
|
616
|
+
await writeFile(join(installedDir, change.path), change.template);
|
|
617
|
+
}
|
|
618
|
+
for (const file of newFiles) {
|
|
619
|
+
await ensureDir(dirname(join(installedDir, file.path)));
|
|
620
|
+
await writeFile(join(installedDir, file.path), file.content);
|
|
621
|
+
}
|
|
622
|
+
console.log(chalk.green(`\nUpgraded ${changes.length + newFiles.length} skill file(s).`));
|
|
623
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Display helpers for `sequant run` — pre-run config block + post-run summary.
|
|
3
|
+
*
|
|
4
|
+
* Kept separate from run.ts so the adapter stays thin (see AC-2 of #503).
|
|
5
|
+
*/
|
|
6
|
+
import type { ResolvedRun, RunResult } from "../lib/workflow/run-orchestrator.js";
|
|
7
|
+
/**
|
|
8
|
+
* Print pre-run config block.
|
|
9
|
+
*
|
|
10
|
+
* Columnar alignment via 15-char label padding. Conditional rows only
|
|
11
|
+
* appear when non-default, matching the pre-#503 format.
|
|
12
|
+
*/
|
|
13
|
+
export declare function displayConfig(r: ResolvedRun): void;
|
|
14
|
+
/**
|
|
15
|
+
* Print post-run summary: per-issue status, log path, reflection, tips.
|
|
16
|
+
*/
|
|
17
|
+
export declare function displaySummary(result: RunResult): void;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Display helpers for `sequant run` — pre-run config block + post-run summary.
|
|
3
|
+
*
|
|
4
|
+
* Kept separate from run.ts so the adapter stays thin (see AC-2 of #503).
|
|
5
|
+
*/
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import { ui, colors } from "../lib/cli-ui.js";
|
|
8
|
+
import { formatDuration } from "../lib/workflow/phase-executor.js";
|
|
9
|
+
import { analyzeRun, formatReflection } from "../lib/workflow/run-reflect.js";
|
|
10
|
+
/**
|
|
11
|
+
* Print pre-run config block.
|
|
12
|
+
*
|
|
13
|
+
* Columnar alignment via 15-char label padding. Conditional rows only
|
|
14
|
+
* appear when non-default, matching the pre-#503 format.
|
|
15
|
+
*/
|
|
16
|
+
export function displayConfig(r) {
|
|
17
|
+
const pad = (label) => label.padEnd(15);
|
|
18
|
+
const row = (label, value) => console.log(chalk.gray(` ${pad(label)}${value}`));
|
|
19
|
+
row("Stack", r.stack);
|
|
20
|
+
if (r.autoDetectPhases) {
|
|
21
|
+
row("Phases", "auto-detect from labels");
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
row("Phases", r.config.phases.join(" \u2192 "));
|
|
25
|
+
}
|
|
26
|
+
row("Mode", r.config.sequential
|
|
27
|
+
? "sequential (stop-on-failure)"
|
|
28
|
+
: `parallel (concurrency: ${r.config.concurrency})`);
|
|
29
|
+
if (r.config.qualityLoop) {
|
|
30
|
+
row("Quality loop", `enabled (max ${r.config.maxIterations} iterations)`);
|
|
31
|
+
}
|
|
32
|
+
if (r.mergedOptions.testgen)
|
|
33
|
+
row("Testgen", "enabled");
|
|
34
|
+
if (r.config.noSmartTests)
|
|
35
|
+
row("Smart tests", "disabled");
|
|
36
|
+
if (r.config.dryRun) {
|
|
37
|
+
console.log(chalk.yellow(` ! DRY RUN - no actual execution`));
|
|
38
|
+
}
|
|
39
|
+
if (r.logEnabled)
|
|
40
|
+
row("Logging", "JSON");
|
|
41
|
+
if (r.stateEnabled)
|
|
42
|
+
row("State", "enabled");
|
|
43
|
+
if (r.mergedOptions.force) {
|
|
44
|
+
console.log(chalk.yellow(` ${pad("Force")}enabled (bypass state guard)`));
|
|
45
|
+
}
|
|
46
|
+
if (r.issueNumbers.length > 0) {
|
|
47
|
+
row("Issues", r.issueNumbers.map((n) => `#${n}`).join(", "));
|
|
48
|
+
}
|
|
49
|
+
if (r.worktreeIsolationEnabled) {
|
|
50
|
+
console.log(chalk.gray(` Worktree isolation: enabled`));
|
|
51
|
+
}
|
|
52
|
+
if (r.baseBranch) {
|
|
53
|
+
console.log(chalk.gray(` Base branch: ${r.baseBranch}`));
|
|
54
|
+
}
|
|
55
|
+
if (r.mergedOptions.chain) {
|
|
56
|
+
console.log(chalk.gray(` Chain mode: enabled (each issue branches from previous)`));
|
|
57
|
+
}
|
|
58
|
+
if (r.mergedOptions.qaGate) {
|
|
59
|
+
console.log(chalk.gray(` QA gate: enabled (chain waits for QA pass)`));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Print post-run summary: per-issue status, log path, reflection, tips.
|
|
64
|
+
*/
|
|
65
|
+
export function displaySummary(result) {
|
|
66
|
+
const { results, logPath, config, mergedOptions } = result;
|
|
67
|
+
if (results.length === 0)
|
|
68
|
+
return;
|
|
69
|
+
const passed = results.filter((r) => r.success).length;
|
|
70
|
+
const failed = results.filter((r) => !r.success).length;
|
|
71
|
+
console.log("\n" + ui.divider());
|
|
72
|
+
console.log(colors.info(" Summary"));
|
|
73
|
+
console.log(ui.divider());
|
|
74
|
+
console.log(`\n ${colors.success(`${passed} passed`)} ${colors.muted("·")} ${colors.error(`${failed} failed`)}`);
|
|
75
|
+
for (const r of results) {
|
|
76
|
+
const status = r.success
|
|
77
|
+
? ui.statusIcon("success")
|
|
78
|
+
: ui.statusIcon("error");
|
|
79
|
+
const duration = r.durationSeconds
|
|
80
|
+
? colors.muted(` (${formatDuration(r.durationSeconds)})`)
|
|
81
|
+
: "";
|
|
82
|
+
const phases = r.phaseResults
|
|
83
|
+
.map((p) => (p.success ? colors.success(p.phase) : colors.error(p.phase)))
|
|
84
|
+
.join(" → ");
|
|
85
|
+
const loopInfo = r.loopTriggered ? colors.warning(" [loop]") : "";
|
|
86
|
+
const prInfo = r.prUrl ? colors.muted(` → PR #${r.prNumber}`) : "";
|
|
87
|
+
console.log(` ${status} #${r.issueNumber}: ${phases}${loopInfo}${prInfo}${duration}`);
|
|
88
|
+
}
|
|
89
|
+
console.log("");
|
|
90
|
+
if (logPath) {
|
|
91
|
+
console.log(colors.muted(` Log: ${logPath}`));
|
|
92
|
+
console.log("");
|
|
93
|
+
}
|
|
94
|
+
if (mergedOptions.reflect && results.length > 0) {
|
|
95
|
+
const reflection = analyzeRun({
|
|
96
|
+
results,
|
|
97
|
+
issueInfoMap: result.issueInfoMap,
|
|
98
|
+
runLog: result.logWriter?.getRunLog() ?? null,
|
|
99
|
+
config: { phases: config.phases, qualityLoop: config.qualityLoop },
|
|
100
|
+
});
|
|
101
|
+
const reflectionOutput = formatReflection(reflection);
|
|
102
|
+
if (reflectionOutput) {
|
|
103
|
+
console.log(reflectionOutput);
|
|
104
|
+
console.log("");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (results.length > 1 && passed > 0 && !config.dryRun) {
|
|
108
|
+
console.log(colors.muted(" Tip: Verify batch integration before merging:"));
|
|
109
|
+
console.log(colors.muted(" sequant merge --check"));
|
|
110
|
+
console.log("");
|
|
111
|
+
}
|
|
112
|
+
if (config.dryRun) {
|
|
113
|
+
console.log(colors.warning(" ℹ️ This was a dry run. Use without --dry-run to execute."));
|
|
114
|
+
console.log("");
|
|
115
|
+
}
|
|
116
|
+
}
|
package/dist/src/commands/run.js
CHANGED
|
@@ -5,10 +5,9 @@ import { formatElapsedTime } from "../lib/phase-spinner.js";
|
|
|
5
5
|
import { getSettings } from "../lib/settings.js";
|
|
6
6
|
import { checkVersionCached, getVersionWarning } from "../lib/version-check.js";
|
|
7
7
|
import { ui, colors } from "../lib/cli-ui.js";
|
|
8
|
-
import { formatDuration } from "../lib/workflow/phase-executor.js";
|
|
9
8
|
import { parseBatches } from "../lib/workflow/batch-executor.js";
|
|
10
9
|
import { RunOrchestrator } from "../lib/workflow/run-orchestrator.js";
|
|
11
|
-
import {
|
|
10
|
+
import { displayConfig, displaySummary } from "./run-display.js";
|
|
12
11
|
// Re-export public API for backwards compatibility
|
|
13
12
|
export * from "./run-compat.js";
|
|
14
13
|
/** Parse CLI args → validate → delegate to RunOrchestrator.run() → display summary. */
|
|
@@ -51,7 +50,16 @@ export async function runCommand(issues, options) {
|
|
|
51
50
|
batches = parseBatches(options.batch);
|
|
52
51
|
console.log(chalk.gray(` Batch mode: ${batches.map((b) => `[${b.join(", ")}]`).join(" → ")}`));
|
|
53
52
|
}
|
|
54
|
-
|
|
53
|
+
const init = {
|
|
54
|
+
options,
|
|
55
|
+
settings,
|
|
56
|
+
manifest: {
|
|
57
|
+
stack: manifest.stack,
|
|
58
|
+
packageManager: manifest.packageManager ?? "npm",
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
const resolved = RunOrchestrator.resolveConfig(init, issues, batches);
|
|
62
|
+
displayConfig(resolved);
|
|
55
63
|
const onProgress = !options.quiet
|
|
56
64
|
? (issue, phase, event, extra) => {
|
|
57
65
|
if (event === "start")
|
|
@@ -66,68 +74,8 @@ export async function runCommand(issues, options) {
|
|
|
66
74
|
console.log(` ${colors.error("✖")} #${issue} ${phase}`);
|
|
67
75
|
}
|
|
68
76
|
: undefined;
|
|
69
|
-
const result = await RunOrchestrator.run({
|
|
70
|
-
options,
|
|
71
|
-
settings,
|
|
72
|
-
manifest: {
|
|
73
|
-
stack: manifest.stack,
|
|
74
|
-
packageManager: manifest.packageManager ?? "npm",
|
|
75
|
-
},
|
|
76
|
-
onProgress,
|
|
77
|
-
}, issues, batches);
|
|
77
|
+
const result = await RunOrchestrator.run({ ...init, onProgress }, issues, batches);
|
|
78
78
|
displaySummary(result);
|
|
79
79
|
if (result.exitCode !== 0)
|
|
80
80
|
process.exit(result.exitCode);
|
|
81
81
|
}
|
|
82
|
-
function displaySummary(result) {
|
|
83
|
-
const { results, logPath, config, mergedOptions } = result;
|
|
84
|
-
if (results.length === 0)
|
|
85
|
-
return;
|
|
86
|
-
const passed = results.filter((r) => r.success).length;
|
|
87
|
-
const failed = results.filter((r) => !r.success).length;
|
|
88
|
-
console.log("\n" + ui.divider());
|
|
89
|
-
console.log(colors.info(" Summary"));
|
|
90
|
-
console.log(ui.divider());
|
|
91
|
-
console.log(`\n ${colors.success(`${passed} passed`)} ${colors.muted("·")} ${colors.error(`${failed} failed`)}`);
|
|
92
|
-
for (const r of results) {
|
|
93
|
-
const status = r.success
|
|
94
|
-
? ui.statusIcon("success")
|
|
95
|
-
: ui.statusIcon("error");
|
|
96
|
-
const duration = r.durationSeconds
|
|
97
|
-
? colors.muted(` (${formatDuration(r.durationSeconds)})`)
|
|
98
|
-
: "";
|
|
99
|
-
const phases = r.phaseResults
|
|
100
|
-
.map((p) => (p.success ? colors.success(p.phase) : colors.error(p.phase)))
|
|
101
|
-
.join(" → ");
|
|
102
|
-
const loopInfo = r.loopTriggered ? colors.warning(" [loop]") : "";
|
|
103
|
-
const prInfo = r.prUrl ? colors.muted(` → PR #${r.prNumber}`) : "";
|
|
104
|
-
console.log(` ${status} #${r.issueNumber}: ${phases}${loopInfo}${prInfo}${duration}`);
|
|
105
|
-
}
|
|
106
|
-
console.log("");
|
|
107
|
-
if (logPath) {
|
|
108
|
-
console.log(colors.muted(` Log: ${logPath}`));
|
|
109
|
-
console.log("");
|
|
110
|
-
}
|
|
111
|
-
if (mergedOptions.reflect && results.length > 0) {
|
|
112
|
-
const reflection = analyzeRun({
|
|
113
|
-
results,
|
|
114
|
-
issueInfoMap: result.issueInfoMap,
|
|
115
|
-
runLog: result.logWriter?.getRunLog() ?? null,
|
|
116
|
-
config: { phases: config.phases, qualityLoop: config.qualityLoop },
|
|
117
|
-
});
|
|
118
|
-
const reflectionOutput = formatReflection(reflection);
|
|
119
|
-
if (reflectionOutput) {
|
|
120
|
-
console.log(reflectionOutput);
|
|
121
|
-
console.log("");
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
if (results.length > 1 && passed > 0 && !config.dryRun) {
|
|
125
|
-
console.log(colors.muted(" Tip: Verify batch integration before merging:"));
|
|
126
|
-
console.log(colors.muted(" sequant merge --check"));
|
|
127
|
-
console.log("");
|
|
128
|
-
}
|
|
129
|
-
if (config.dryRun) {
|
|
130
|
-
console.log(colors.warning(" ℹ️ This was a dry run. Use without --dry-run to execute."));
|
|
131
|
-
console.log("");
|
|
132
|
-
}
|
|
133
|
-
}
|
|
@@ -10,6 +10,7 @@ import { StateManager } from "../lib/workflow/state-manager.js";
|
|
|
10
10
|
import { rebuildStateFromLogs, cleanupStaleEntries, } from "../lib/workflow/state-utils.js";
|
|
11
11
|
import { reconcileState, getNextActionHint, formatRelativeTime, } from "../lib/workflow/reconcile.js";
|
|
12
12
|
import { getSettingsWithWarnings } from "../lib/settings.js";
|
|
13
|
+
import { getSkillVersions } from "../lib/skill-version.js";
|
|
13
14
|
/**
|
|
14
15
|
* Run reconciliation and display warnings.
|
|
15
16
|
* Returns the reconcile result for use in display.
|
|
@@ -263,13 +264,26 @@ export async function statusCommand(options = {}) {
|
|
|
263
264
|
console.log(chalk.yellow(` ${w.message}`));
|
|
264
265
|
}
|
|
265
266
|
}
|
|
266
|
-
// Count skills
|
|
267
|
+
// Count skills and check versions
|
|
267
268
|
const skillsDir = ".claude/skills";
|
|
268
269
|
if (await fileExists(skillsDir)) {
|
|
269
270
|
try {
|
|
270
271
|
const skills = await readdir(skillsDir);
|
|
271
272
|
const skillCount = skills.filter((s) => !s.startsWith(".")).length;
|
|
272
273
|
console.log(chalk.gray(`Skills: ${skillCount}`));
|
|
274
|
+
// Show skill version info
|
|
275
|
+
const { getTemplatesDir } = await import("../lib/templates.js");
|
|
276
|
+
const { join } = await import("path");
|
|
277
|
+
const templateSkillsDir = join(getTemplatesDir(), "skills");
|
|
278
|
+
const skillVersions = await getSkillVersions(templateSkillsDir);
|
|
279
|
+
const outdated = skillVersions.filter((s) => s.updateAvailable);
|
|
280
|
+
if (outdated.length > 0) {
|
|
281
|
+
console.log(chalk.yellow(`\n Skill updates available (${outdated.length}):`));
|
|
282
|
+
for (const s of outdated) {
|
|
283
|
+
console.log(chalk.yellow(` ${s.name}: v${s.installedVersion} → v${s.templateVersion}`));
|
|
284
|
+
}
|
|
285
|
+
console.log(chalk.gray(" Run `sequant init --upgrade-skills` to update."));
|
|
286
|
+
}
|
|
273
287
|
}
|
|
274
288
|
catch {
|
|
275
289
|
// Ignore errors
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill version utilities — reads version from SKILL.md YAML frontmatter
|
|
3
|
+
*/
|
|
4
|
+
export interface SkillVersionInfo {
|
|
5
|
+
name: string;
|
|
6
|
+
installedVersion: string | null;
|
|
7
|
+
templateVersion: string | null;
|
|
8
|
+
updateAvailable: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Parse YAML frontmatter from a SKILL.md file and extract the version field.
|
|
12
|
+
* Returns null if no version is found or file doesn't exist.
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseSkillVersion(content: string): string | null;
|
|
15
|
+
/**
|
|
16
|
+
* Get version info for all installed skills, comparing installed (.claude/skills/)
|
|
17
|
+
* with template versions (from the sequant package's templates/skills/).
|
|
18
|
+
*/
|
|
19
|
+
export declare function getSkillVersions(templateDir?: string): Promise<SkillVersionInfo[]>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill version utilities — reads version from SKILL.md YAML frontmatter
|
|
3
|
+
*/
|
|
4
|
+
import { readdir } from "fs/promises";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import { parse as parseYaml } from "yaml";
|
|
7
|
+
import { readFile, fileExists } from "./fs.js";
|
|
8
|
+
/**
|
|
9
|
+
* Parse YAML frontmatter from a SKILL.md file and extract the version field.
|
|
10
|
+
* Returns null if no version is found or file doesn't exist.
|
|
11
|
+
*/
|
|
12
|
+
export function parseSkillVersion(content) {
|
|
13
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
14
|
+
if (!match)
|
|
15
|
+
return null;
|
|
16
|
+
try {
|
|
17
|
+
const frontmatter = parseYaml(match[1]);
|
|
18
|
+
return frontmatter?.version ?? frontmatter?.metadata?.version ?? null;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Read version from a single skill's SKILL.md file.
|
|
26
|
+
*/
|
|
27
|
+
async function readSkillVersion(skillDir) {
|
|
28
|
+
const skillPath = join(skillDir, "SKILL.md");
|
|
29
|
+
if (!(await fileExists(skillPath)))
|
|
30
|
+
return null;
|
|
31
|
+
const content = await readFile(skillPath);
|
|
32
|
+
return parseSkillVersion(content);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Get version info for all installed skills, comparing installed (.claude/skills/)
|
|
36
|
+
* with template versions (from the sequant package's templates/skills/).
|
|
37
|
+
*/
|
|
38
|
+
export async function getSkillVersions(templateDir) {
|
|
39
|
+
const installedDir = ".claude/skills";
|
|
40
|
+
const results = [];
|
|
41
|
+
if (!(await fileExists(installedDir)))
|
|
42
|
+
return results;
|
|
43
|
+
try {
|
|
44
|
+
const entries = await readdir(installedDir, { withFileTypes: true });
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
if (!entry.isDirectory() || entry.name.startsWith("."))
|
|
47
|
+
continue;
|
|
48
|
+
const installedVersion = await readSkillVersion(join(installedDir, entry.name));
|
|
49
|
+
let templateVersion = null;
|
|
50
|
+
if (templateDir) {
|
|
51
|
+
templateVersion = await readSkillVersion(join(templateDir, entry.name));
|
|
52
|
+
}
|
|
53
|
+
const updateAvailable = installedVersion !== null &&
|
|
54
|
+
templateVersion !== null &&
|
|
55
|
+
installedVersion !== templateVersion;
|
|
56
|
+
results.push({
|
|
57
|
+
name: entry.name,
|
|
58
|
+
installedVersion,
|
|
59
|
+
templateVersion,
|
|
60
|
+
updateAvailable,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Ignore errors reading skills directory
|
|
66
|
+
}
|
|
67
|
+
return results.sort((a, b) => a.name.localeCompare(b.name));
|
|
68
|
+
}
|
|
@@ -11,7 +11,7 @@ import { getStackConfig, getStackNotes, getMultiStackNotes } from "./stacks.js";
|
|
|
11
11
|
import { isNativeWindows } from "./system.js";
|
|
12
12
|
import { getProjectName } from "./project-name.js";
|
|
13
13
|
// Get the package templates directory
|
|
14
|
-
function getTemplatesDir() {
|
|
14
|
+
export function getTemplatesDir() {
|
|
15
15
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
16
|
// Compiled structure: dist/src/lib/templates.js
|
|
17
17
|
// So we need ../../../templates to reach project root templates/
|
|
@@ -747,7 +747,7 @@ export async function runIssueWithLogging(ctx) {
|
|
|
747
747
|
}
|
|
748
748
|
// Create checkpoint commit in chain mode after QA passes
|
|
749
749
|
if (success && chainMode && worktreePath) {
|
|
750
|
-
createCheckpointCommit(worktreePath, issueNumber, config.verbose);
|
|
750
|
+
createCheckpointCommit(worktreePath, issueNumber, config.verbose, baseBranch);
|
|
751
751
|
}
|
|
752
752
|
// Rebase onto the base branch before PR creation (unless --no-rebase)
|
|
753
753
|
// This ensures the branch is up-to-date and prevents lockfile drift
|
|
@@ -11,6 +11,7 @@ import { ShutdownManager } from "../shutdown.js";
|
|
|
11
11
|
import { PhaseSpinner } from "../phase-spinner.js";
|
|
12
12
|
import { Phase, ExecutionConfig, PhaseResult, QaVerdict } from "./types.js";
|
|
13
13
|
import type { QaSummary } from "./run-log-schema.js";
|
|
14
|
+
import type { AgentPhaseResult } from "./drivers/index.js";
|
|
14
15
|
/**
|
|
15
16
|
* Spec-specific retry configuration.
|
|
16
17
|
* Spec failures have a higher failure rate (~8.6%) than other phases due to
|
|
@@ -40,6 +41,36 @@ export declare function parseQaSummary(output: string): QaSummary | null;
|
|
|
40
41
|
* Format duration in human-readable format
|
|
41
42
|
*/
|
|
42
43
|
export declare function formatDuration(seconds: number): string;
|
|
44
|
+
/**
|
|
45
|
+
* Check whether the exec phase produced any changes in the worktree.
|
|
46
|
+
* Returns true if HEAD has commits unique to it relative to origin/main
|
|
47
|
+
* OR uncommitted work is present.
|
|
48
|
+
*
|
|
49
|
+
* Uses `git rev-list --count origin/main..HEAD` (commits reachable from HEAD
|
|
50
|
+
* but not origin/main) instead of `git diff origin/main..HEAD`, because the
|
|
51
|
+
* two-dot diff also fires in reverse when origin/main has advanced past HEAD
|
|
52
|
+
* — on stale branches that would falsely report "has commits" even when the
|
|
53
|
+
* exec phase produced nothing, reintroducing the bug #534 is fixing.
|
|
54
|
+
*
|
|
55
|
+
* Fails open (returns true) on git errors — a missing origin ref is better
|
|
56
|
+
* diagnosed as a real zero-diff run than as a false phase failure.
|
|
57
|
+
*
|
|
58
|
+
* @internal Exported for testing only.
|
|
59
|
+
*/
|
|
60
|
+
export declare function hasExecChanges(cwd: string): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Map a successful AgentPhaseResult to a PhaseResult, applying phase-specific
|
|
63
|
+
* guards that catch agent sessions which returned success without producing
|
|
64
|
+
* usable work (#534):
|
|
65
|
+
*
|
|
66
|
+
* - `qa`: fails when no parseable verdict is found (empty or malformed output).
|
|
67
|
+
* - `exec`: fails when no commits and no uncommitted changes exist.
|
|
68
|
+
*
|
|
69
|
+
* @internal Exported for testing only.
|
|
70
|
+
*/
|
|
71
|
+
export declare function mapAgentSuccessToPhaseResult(phase: Phase, agentResult: AgentPhaseResult, durationSeconds: number, cwd: string): PhaseResult & {
|
|
72
|
+
sessionId?: string;
|
|
73
|
+
};
|
|
43
74
|
/**
|
|
44
75
|
* Get the prompt for a phase with the issue number substituted.
|
|
45
76
|
* Selects self-contained prompts for non-Claude agents.
|