sequant 1.10.1 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -3
- package/dist/bin/cli.js +55 -2
- package/dist/dashboard/server.d.ts +37 -0
- package/dist/dashboard/server.js +968 -0
- package/dist/src/commands/dashboard.d.ts +25 -0
- package/dist/src/commands/dashboard.js +44 -0
- package/dist/src/commands/doctor.d.ts +18 -1
- package/dist/src/commands/doctor.js +105 -2
- package/dist/src/commands/init.d.ts +1 -0
- package/dist/src/commands/init.js +26 -2
- package/dist/src/commands/run.d.ts +20 -0
- package/dist/src/commands/run.js +151 -3
- package/dist/src/commands/state.d.ts +60 -0
- package/dist/src/commands/state.js +267 -0
- package/dist/src/commands/stats.d.ts +3 -2
- package/dist/src/commands/stats.js +246 -38
- package/dist/src/commands/status.d.ts +2 -0
- package/dist/src/commands/status.js +28 -3
- package/dist/src/lib/ac-linter.d.ts +116 -0
- package/dist/src/lib/ac-linter.js +304 -0
- package/dist/src/lib/ac-parser.d.ts +61 -0
- package/dist/src/lib/ac-parser.js +156 -0
- package/dist/src/lib/fs.d.ts +19 -0
- package/dist/src/lib/fs.js +58 -1
- package/dist/src/lib/plugin-version-sync.d.ts +26 -0
- package/dist/src/lib/plugin-version-sync.js +91 -0
- package/dist/src/lib/project-name.d.ts +40 -0
- package/dist/src/lib/project-name.js +191 -0
- package/dist/src/lib/semgrep.d.ts +136 -0
- package/dist/src/lib/semgrep.js +406 -0
- package/dist/src/lib/settings.d.ts +7 -0
- package/dist/src/lib/settings.js +1 -0
- package/dist/src/lib/stacks.d.ts +14 -0
- package/dist/src/lib/stacks.js +159 -0
- package/dist/src/lib/system.d.ts +19 -0
- package/dist/src/lib/system.js +26 -0
- package/dist/src/lib/templates.d.ts +34 -1
- package/dist/src/lib/templates.js +117 -7
- package/dist/src/lib/workflow/metrics-schema.d.ts +153 -0
- package/dist/src/lib/workflow/metrics-schema.js +138 -0
- package/dist/src/lib/workflow/metrics-writer.d.ts +102 -0
- package/dist/src/lib/workflow/metrics-writer.js +189 -0
- package/dist/src/lib/workflow/state-manager.d.ts +18 -1
- package/dist/src/lib/workflow/state-manager.js +61 -1
- package/dist/src/lib/workflow/state-schema.d.ts +152 -1
- package/dist/src/lib/workflow/state-schema.js +99 -0
- package/dist/src/lib/workflow/state-utils.d.ts +67 -3
- package/dist/src/lib/workflow/state-utils.js +289 -8
- package/dist/src/lib/workflow/types.d.ts +2 -0
- package/dist/src/lib/workflow/types.js +1 -0
- package/package.json +5 -1
- package/templates/hooks/pre-tool.sh +6 -0
- package/templates/memory/constitution.md +1 -5
- package/templates/skills/_shared/references/prompt-templates.md +350 -0
- package/templates/skills/_shared/references/subagent-types.md +131 -0
- package/templates/skills/exec/SKILL.md +82 -0
- package/templates/skills/fullsolve/SKILL.md +19 -2
- package/templates/skills/loop/SKILL.md +3 -1
- package/templates/skills/qa/SKILL.md +79 -9
- package/templates/skills/qa/references/quality-gates.md +85 -1
- package/templates/skills/qa/references/semgrep-rules.md +207 -0
- package/templates/skills/qa/scripts/quality-checks.sh +54 -0
- package/templates/skills/spec/SKILL.md +215 -4
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sequant dashboard - Visual workflow state dashboard
|
|
3
|
+
*
|
|
4
|
+
* Starts a local web server displaying workflow state in a browser-based
|
|
5
|
+
* dashboard with live updates via SSE.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```bash
|
|
9
|
+
* sequant dashboard # Start on default port 3456
|
|
10
|
+
* sequant dashboard --port 8080 # Custom port
|
|
11
|
+
* sequant dashboard --no-open # Don't auto-open browser
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export interface DashboardCommandOptions {
|
|
15
|
+
/** Port to run the server on */
|
|
16
|
+
port?: number;
|
|
17
|
+
/** Don't automatically open browser */
|
|
18
|
+
noOpen?: boolean;
|
|
19
|
+
/** Enable verbose logging */
|
|
20
|
+
verbose?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Dashboard command handler
|
|
24
|
+
*/
|
|
25
|
+
export declare function dashboardCommand(options?: DashboardCommandOptions): Promise<void>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sequant dashboard - Visual workflow state dashboard
|
|
3
|
+
*
|
|
4
|
+
* Starts a local web server displaying workflow state in a browser-based
|
|
5
|
+
* dashboard with live updates via SSE.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```bash
|
|
9
|
+
* sequant dashboard # Start on default port 3456
|
|
10
|
+
* sequant dashboard --port 8080 # Custom port
|
|
11
|
+
* sequant dashboard --no-open # Don't auto-open browser
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
import chalk from "chalk";
|
|
15
|
+
/**
|
|
16
|
+
* Dashboard command handler
|
|
17
|
+
*/
|
|
18
|
+
export async function dashboardCommand(options = {}) {
|
|
19
|
+
const port = options.port ?? 3456;
|
|
20
|
+
const shouldOpen = !options.noOpen;
|
|
21
|
+
const verbose = options.verbose ?? false;
|
|
22
|
+
console.log(chalk.bold("\n📊 Sequant Dashboard\n"));
|
|
23
|
+
try {
|
|
24
|
+
// Dynamic import to avoid loading dashboard code unless needed
|
|
25
|
+
const { startDashboard } = await import("../../dashboard/server.js");
|
|
26
|
+
const server = await startDashboard({
|
|
27
|
+
port,
|
|
28
|
+
open: shouldOpen,
|
|
29
|
+
verbose,
|
|
30
|
+
});
|
|
31
|
+
// Handle graceful shutdown
|
|
32
|
+
const shutdown = () => {
|
|
33
|
+
server.close();
|
|
34
|
+
process.exit(0);
|
|
35
|
+
};
|
|
36
|
+
process.on("SIGINT", shutdown);
|
|
37
|
+
process.on("SIGTERM", shutdown);
|
|
38
|
+
console.log(chalk.gray("Press Ctrl+C to stop the server\n"));
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
console.error(chalk.red(`\n✗ Failed to start dashboard: ${error}\n`));
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -1,4 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* sequant doctor - Check installation health
|
|
3
3
|
*/
|
|
4
|
-
export
|
|
4
|
+
export interface DoctorOptions {
|
|
5
|
+
skipIssueCheck?: boolean;
|
|
6
|
+
}
|
|
7
|
+
interface ClosedIssue {
|
|
8
|
+
number: number;
|
|
9
|
+
title: string;
|
|
10
|
+
closedAt: string;
|
|
11
|
+
labels: Array<{
|
|
12
|
+
name: string;
|
|
13
|
+
}>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Check recently closed issues for missing commits in main branch
|
|
17
|
+
* Returns issues that were closed but have no commit referencing them
|
|
18
|
+
*/
|
|
19
|
+
export declare function checkClosedIssues(): ClosedIssue[];
|
|
20
|
+
export declare function doctorCommand(options?: DoctorOptions): Promise<void>;
|
|
21
|
+
export {};
|
|
@@ -2,13 +2,76 @@
|
|
|
2
2
|
* sequant doctor - Check installation health
|
|
3
3
|
*/
|
|
4
4
|
import chalk from "chalk";
|
|
5
|
+
import { execSync } from "child_process";
|
|
5
6
|
import { fileExists, isExecutable } from "../lib/fs.js";
|
|
6
7
|
import { getManifest } from "../lib/manifest.js";
|
|
7
|
-
import { commandExists, isGhAuthenticated, isNativeWindows, isWSL, checkOptionalMcpServers, OPTIONAL_MCP_SERVERS, } from "../lib/system.js";
|
|
8
|
+
import { commandExists, isGhAuthenticated, isNativeWindows, isWSL, checkOptionalMcpServers, getMcpServersConfig, OPTIONAL_MCP_SERVERS, } from "../lib/system.js";
|
|
8
9
|
import { checkVersionThorough, getVersionWarning, } from "../lib/version-check.js";
|
|
9
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Labels that indicate an issue should be skipped from closed-issue verification
|
|
12
|
+
* (case-insensitive matching)
|
|
13
|
+
*/
|
|
14
|
+
const SKIP_ISSUE_LABELS = [
|
|
15
|
+
"wontfix",
|
|
16
|
+
"won't fix",
|
|
17
|
+
"duplicate",
|
|
18
|
+
"invalid",
|
|
19
|
+
"question",
|
|
20
|
+
"documentation",
|
|
21
|
+
"docs",
|
|
22
|
+
];
|
|
23
|
+
/**
|
|
24
|
+
* Check recently closed issues for missing commits in main branch
|
|
25
|
+
* Returns issues that were closed but have no commit referencing them
|
|
26
|
+
*/
|
|
27
|
+
export function checkClosedIssues() {
|
|
28
|
+
const sevenDaysAgo = new Date();
|
|
29
|
+
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
|
30
|
+
const sevenDaysAgoISO = sevenDaysAgo.toISOString();
|
|
31
|
+
// Fetch closed issues from last 7 days
|
|
32
|
+
let closedIssues;
|
|
33
|
+
try {
|
|
34
|
+
const output = execSync(`gh issue list --state closed --json number,title,closedAt,labels --limit 100`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
35
|
+
closedIssues = JSON.parse(output);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// gh command failed - return empty (graceful degradation)
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
// Filter to last 7 days
|
|
42
|
+
const recentIssues = closedIssues.filter((issue) => issue.closedAt >= sevenDaysAgoISO);
|
|
43
|
+
// Filter out issues with skip labels
|
|
44
|
+
const issuesToCheck = recentIssues.filter((issue) => {
|
|
45
|
+
const labels = issue.labels.map((l) => l.name.toLowerCase());
|
|
46
|
+
return !SKIP_ISSUE_LABELS.some((skipLabel) => labels.some((label) => label.includes(skipLabel.toLowerCase())));
|
|
47
|
+
});
|
|
48
|
+
// Check each issue for a commit in main
|
|
49
|
+
const missingCommitIssues = [];
|
|
50
|
+
for (const issue of issuesToCheck) {
|
|
51
|
+
try {
|
|
52
|
+
// Look for commit mentioning this issue number
|
|
53
|
+
const result = execSync(`git log --oneline --grep="#${issue.number}" -1`, {
|
|
54
|
+
encoding: "utf-8",
|
|
55
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
56
|
+
});
|
|
57
|
+
// If no output, no commit found
|
|
58
|
+
if (!result.trim()) {
|
|
59
|
+
missingCommitIssues.push(issue);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// git log failed or no match - treat as missing
|
|
64
|
+
missingCommitIssues.push(issue);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return missingCommitIssues;
|
|
68
|
+
}
|
|
69
|
+
export async function doctorCommand(options = {}) {
|
|
10
70
|
console.log(chalk.blue("\n🔍 Running health checks...\n"));
|
|
11
71
|
const checks = [];
|
|
72
|
+
// Track gh availability and auth for conditional checks later
|
|
73
|
+
let ghAvailable = false;
|
|
74
|
+
let ghAuthenticated = false;
|
|
12
75
|
// Check 0: Version freshness
|
|
13
76
|
const versionResult = await checkVersionThorough();
|
|
14
77
|
if (versionResult.latestVersion) {
|
|
@@ -160,6 +223,7 @@ export async function doctorCommand() {
|
|
|
160
223
|
}
|
|
161
224
|
// Check 8: GitHub CLI installed
|
|
162
225
|
if (commandExists("gh")) {
|
|
226
|
+
ghAvailable = true;
|
|
163
227
|
checks.push({
|
|
164
228
|
name: "GitHub CLI",
|
|
165
229
|
status: "pass",
|
|
@@ -167,6 +231,7 @@ export async function doctorCommand() {
|
|
|
167
231
|
});
|
|
168
232
|
// Check 9: GitHub CLI authenticated (only if gh exists)
|
|
169
233
|
if (isGhAuthenticated()) {
|
|
234
|
+
ghAuthenticated = true;
|
|
170
235
|
checks.push({
|
|
171
236
|
name: "GitHub Auth",
|
|
172
237
|
status: "pass",
|
|
@@ -266,6 +331,44 @@ export async function doctorCommand() {
|
|
|
266
331
|
message: "No optional MCPs configured (Sequant works without them, but they enhance functionality)",
|
|
267
332
|
});
|
|
268
333
|
}
|
|
334
|
+
// Check: MCP availability for headless mode (sequant run)
|
|
335
|
+
const mcpServersConfig = getMcpServersConfig();
|
|
336
|
+
if (mcpServersConfig) {
|
|
337
|
+
const serverCount = Object.keys(mcpServersConfig).length;
|
|
338
|
+
checks.push({
|
|
339
|
+
name: "MCP Servers (headless)",
|
|
340
|
+
status: "pass",
|
|
341
|
+
message: `Available for sequant run (${serverCount} server${serverCount !== 1 ? "s" : ""} configured)`,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
checks.push({
|
|
346
|
+
name: "MCP Servers (headless)",
|
|
347
|
+
status: "warn",
|
|
348
|
+
message: "Not available for sequant run (no Claude Desktop config found or empty mcpServers)",
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
// Check: Closed issue verification (only if gh available, authenticated, and not skipped)
|
|
352
|
+
if (!options.skipIssueCheck && ghAvailable && ghAuthenticated && gitExists) {
|
|
353
|
+
const missingCommitIssues = checkClosedIssues();
|
|
354
|
+
if (missingCommitIssues.length === 0) {
|
|
355
|
+
checks.push({
|
|
356
|
+
name: "Closed Issues",
|
|
357
|
+
status: "pass",
|
|
358
|
+
message: "All recently closed issues have commits in main",
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
// Add a warning for each issue missing commits
|
|
363
|
+
for (const issue of missingCommitIssues) {
|
|
364
|
+
checks.push({
|
|
365
|
+
name: `Issue #${issue.number}`,
|
|
366
|
+
status: "warn",
|
|
367
|
+
message: `Closed but no commit found in main: "${issue.title}"`,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
269
372
|
// Display results
|
|
270
373
|
let passCount = 0;
|
|
271
374
|
let warnCount = 0;
|
|
@@ -266,9 +266,33 @@ export async function initCommand(options) {
|
|
|
266
266
|
// Create default settings
|
|
267
267
|
console.log(chalk.blue("⚙️ Creating default settings..."));
|
|
268
268
|
await createDefaultSettings();
|
|
269
|
-
// Copy templates
|
|
269
|
+
// Copy templates (with symlinks for scripts unless --no-symlinks)
|
|
270
270
|
console.log(chalk.blue("📄 Copying templates..."));
|
|
271
|
-
await copyTemplates(stack, tokens
|
|
271
|
+
const { scriptsSymlinked, symlinkResults } = await copyTemplates(stack, tokens, {
|
|
272
|
+
noSymlinks: options.noSymlinks,
|
|
273
|
+
force: options.force,
|
|
274
|
+
});
|
|
275
|
+
// Report symlink status
|
|
276
|
+
if (scriptsSymlinked) {
|
|
277
|
+
console.log(chalk.blue("🔗 Created symlinks for scripts/dev/"));
|
|
278
|
+
}
|
|
279
|
+
else if (!options.noSymlinks && symlinkResults) {
|
|
280
|
+
// Some symlinks may have fallen back to copies
|
|
281
|
+
const fallbacks = symlinkResults.filter((r) => r.fallbackToCopy);
|
|
282
|
+
if (fallbacks.length > 0) {
|
|
283
|
+
console.log(chalk.yellow("⚠️ Some scripts were copied instead of symlinked:"));
|
|
284
|
+
for (const fb of fallbacks) {
|
|
285
|
+
console.log(chalk.gray(` ${fb.path}: ${fb.reason}`));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const skipped = symlinkResults.filter((r) => r.skipped);
|
|
289
|
+
if (skipped.length > 0) {
|
|
290
|
+
console.log(chalk.yellow("⚠️ Some scripts were skipped (existing files found):"));
|
|
291
|
+
for (const s of skipped) {
|
|
292
|
+
console.log(chalk.gray(` ${s.path}: ${s.reason}`));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
272
296
|
// Create manifest
|
|
273
297
|
console.log(chalk.blue("📋 Creating manifest..."));
|
|
274
298
|
await createManifest(stack, packageManager ?? undefined);
|
|
@@ -17,6 +17,14 @@ export declare function listWorktrees(): Array<{
|
|
|
17
17
|
* Get changed files in a worktree compared to main
|
|
18
18
|
*/
|
|
19
19
|
export declare function getWorktreeChangedFiles(worktreePath: string): string[];
|
|
20
|
+
/**
|
|
21
|
+
* Get diff stats for a worktree (files changed, lines added)
|
|
22
|
+
* Returns aggregate metrics only - no file paths to preserve privacy
|
|
23
|
+
*/
|
|
24
|
+
export declare function getWorktreeDiffStats(worktreePath: string): {
|
|
25
|
+
filesChanged: number;
|
|
26
|
+
linesAdded: number;
|
|
27
|
+
};
|
|
20
28
|
/**
|
|
21
29
|
* Create a checkpoint commit in the worktree after QA passes
|
|
22
30
|
* This allows recovery in case later issues in the chain fail
|
|
@@ -66,11 +74,23 @@ interface RunOptions {
|
|
|
66
74
|
quiet?: boolean;
|
|
67
75
|
/** Chain issues: each branches from previous (requires --sequential) */
|
|
68
76
|
chain?: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Wait for QA pass before starting next issue in chain mode.
|
|
79
|
+
* When enabled, the chain pauses if QA fails, preventing downstream issues
|
|
80
|
+
* from building on potentially broken code.
|
|
81
|
+
*/
|
|
82
|
+
qaGate?: boolean;
|
|
69
83
|
/**
|
|
70
84
|
* Base branch for worktree creation.
|
|
71
85
|
* Resolution priority: this CLI flag → settings.run.defaultBase → 'main'
|
|
72
86
|
*/
|
|
73
87
|
base?: string;
|
|
88
|
+
/**
|
|
89
|
+
* Disable MCP servers in headless mode.
|
|
90
|
+
* When true, MCPs are not passed to the SDK (faster/cheaper runs).
|
|
91
|
+
* Resolution priority: this CLI flag → settings.run.mcp → default (true)
|
|
92
|
+
*/
|
|
93
|
+
noMcp?: boolean;
|
|
74
94
|
}
|
|
75
95
|
/**
|
|
76
96
|
* Main run command
|
package/dist/src/commands/run.js
CHANGED
|
@@ -13,10 +13,13 @@ import { getManifest } from "../lib/manifest.js";
|
|
|
13
13
|
import { getSettings } from "../lib/settings.js";
|
|
14
14
|
import { PM_CONFIG } from "../lib/stacks.js";
|
|
15
15
|
import { LogWriter, createPhaseLogFromTiming, } from "../lib/workflow/log-writer.js";
|
|
16
|
-
import { StateManager
|
|
16
|
+
import { StateManager } from "../lib/workflow/state-manager.js";
|
|
17
17
|
import { DEFAULT_PHASES, DEFAULT_CONFIG, } from "../lib/workflow/types.js";
|
|
18
18
|
import { ShutdownManager } from "../lib/shutdown.js";
|
|
19
|
+
import { getMcpServersConfig } from "../lib/system.js";
|
|
19
20
|
import { checkVersionCached, getVersionWarning } from "../lib/version-check.js";
|
|
21
|
+
import { MetricsWriter } from "../lib/workflow/metrics-writer.js";
|
|
22
|
+
import { determineOutcome, } from "../lib/workflow/metrics-schema.js";
|
|
20
23
|
/**
|
|
21
24
|
* Slugify a title for branch naming
|
|
22
25
|
*/
|
|
@@ -104,6 +107,29 @@ export function getWorktreeChangedFiles(worktreePath) {
|
|
|
104
107
|
.split("\n")
|
|
105
108
|
.filter((f) => f.length > 0);
|
|
106
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Get diff stats for a worktree (files changed, lines added)
|
|
112
|
+
* Returns aggregate metrics only - no file paths to preserve privacy
|
|
113
|
+
*/
|
|
114
|
+
export function getWorktreeDiffStats(worktreePath) {
|
|
115
|
+
const result = spawnSync("git", ["-C", worktreePath, "diff", "--stat", "main...HEAD"], { stdio: "pipe" });
|
|
116
|
+
if (result.status !== 0) {
|
|
117
|
+
return { filesChanged: 0, linesAdded: 0 };
|
|
118
|
+
}
|
|
119
|
+
const output = result.stdout.toString();
|
|
120
|
+
const lines = output.trim().split("\n");
|
|
121
|
+
// Summary line is last and looks like: " 5 files changed, 100 insertions(+), 20 deletions(-)"
|
|
122
|
+
const summaryLine = lines[lines.length - 1];
|
|
123
|
+
if (!summaryLine) {
|
|
124
|
+
return { filesChanged: 0, linesAdded: 0 };
|
|
125
|
+
}
|
|
126
|
+
const filesMatch = summaryLine.match(/(\d+)\s+files?\s+changed/);
|
|
127
|
+
const insertionsMatch = summaryLine.match(/(\d+)\s+insertions?\(\+\)/);
|
|
128
|
+
return {
|
|
129
|
+
filesChanged: filesMatch ? parseInt(filesMatch[1], 10) : 0,
|
|
130
|
+
linesAdded: insertionsMatch ? parseInt(insertionsMatch[1], 10) : 0,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
107
133
|
/**
|
|
108
134
|
* Create or reuse a worktree for an issue
|
|
109
135
|
* @param baseBranch - Optional branch to use as base instead of origin/main (for chain mode)
|
|
@@ -590,6 +616,9 @@ async function executePhase(issueNumber, phase, config, sessionId, worktreePath,
|
|
|
590
616
|
// Execute using Claude Agent SDK
|
|
591
617
|
// Note: Don't resume sessions when switching to worktree (different cwd breaks resume)
|
|
592
618
|
const canResume = sessionId && !shouldUseWorktree;
|
|
619
|
+
// Get MCP servers config if enabled
|
|
620
|
+
// Reads from Claude Desktop config and passes to SDK for headless MCP support
|
|
621
|
+
const mcpServers = config.mcp ? getMcpServersConfig() : undefined;
|
|
593
622
|
const queryInstance = query({
|
|
594
623
|
prompt,
|
|
595
624
|
options: {
|
|
@@ -607,6 +636,8 @@ async function executePhase(issueNumber, phase, config, sessionId, worktreePath,
|
|
|
607
636
|
...(canResume ? { resume: sessionId } : {}),
|
|
608
637
|
// Configure smart tests and worktree isolation via environment
|
|
609
638
|
env,
|
|
639
|
+
// Pass MCP servers for headless mode (AC-2)
|
|
640
|
+
...(mcpServers ? { mcpServers } : {}),
|
|
610
641
|
},
|
|
611
642
|
});
|
|
612
643
|
// Stream and process messages
|
|
@@ -976,6 +1007,13 @@ export async function runCommand(issues, options) {
|
|
|
976
1007
|
console.log("");
|
|
977
1008
|
}
|
|
978
1009
|
}
|
|
1010
|
+
// Validate QA gate requirements
|
|
1011
|
+
if (mergedOptions.qaGate && !mergedOptions.chain) {
|
|
1012
|
+
console.log(chalk.red("❌ --qa-gate requires --chain flag"));
|
|
1013
|
+
console.log(chalk.gray(" QA gate ensures each issue passes QA before the next issue starts."));
|
|
1014
|
+
console.log(chalk.gray(" Usage: npx sequant run 1 2 3 --sequential --chain --qa-gate"));
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
979
1017
|
// Sort issues by dependencies (if more than one issue)
|
|
980
1018
|
if (issueNumbers.length > 1 && !batches) {
|
|
981
1019
|
const originalOrder = [...issueNumbers];
|
|
@@ -990,6 +1028,10 @@ export async function runCommand(issues, options) {
|
|
|
990
1028
|
const explicitPhases = mergedOptions.phases
|
|
991
1029
|
? mergedOptions.phases.split(",").map((p) => p.trim())
|
|
992
1030
|
: null;
|
|
1031
|
+
// Determine MCP enablement: CLI flag (--no-mcp) → settings.run.mcp → default (true)
|
|
1032
|
+
const mcpEnabled = mergedOptions.noMcp
|
|
1033
|
+
? false
|
|
1034
|
+
: (settings.run.mcp ?? DEFAULT_CONFIG.mcp);
|
|
993
1035
|
const config = {
|
|
994
1036
|
...DEFAULT_CONFIG,
|
|
995
1037
|
phases: explicitPhases ?? DEFAULT_PHASES,
|
|
@@ -1000,6 +1042,7 @@ export async function runCommand(issues, options) {
|
|
|
1000
1042
|
qualityLoop: mergedOptions.qualityLoop ?? false,
|
|
1001
1043
|
maxIterations: mergedOptions.maxIterations ?? DEFAULT_CONFIG.maxIterations,
|
|
1002
1044
|
noSmartTests: mergedOptions.noSmartTests ?? false,
|
|
1045
|
+
mcp: mcpEnabled,
|
|
1003
1046
|
};
|
|
1004
1047
|
// Initialize log writer if JSON logging enabled
|
|
1005
1048
|
// Default: enabled via settings (logJson: true), can be disabled with --no-log
|
|
@@ -1074,6 +1117,9 @@ export async function runCommand(issues, options) {
|
|
|
1074
1117
|
if (mergedOptions.chain) {
|
|
1075
1118
|
console.log(chalk.gray(` Chain mode: enabled (each issue branches from previous)`));
|
|
1076
1119
|
}
|
|
1120
|
+
if (mergedOptions.qaGate) {
|
|
1121
|
+
console.log(chalk.gray(` QA gate: enabled (chain waits for QA pass)`));
|
|
1122
|
+
}
|
|
1077
1123
|
// Fetch issue info for all issues first
|
|
1078
1124
|
const issueInfoMap = new Map();
|
|
1079
1125
|
for (const issueNumber of issueNumbers) {
|
|
@@ -1150,6 +1196,27 @@ export async function runCommand(issues, options) {
|
|
|
1150
1196
|
break;
|
|
1151
1197
|
}
|
|
1152
1198
|
if (!result.success) {
|
|
1199
|
+
// Check if QA gate is enabled and QA specifically failed
|
|
1200
|
+
if (mergedOptions.qaGate) {
|
|
1201
|
+
const qaResult = result.phaseResults.find((p) => p.phase === "qa");
|
|
1202
|
+
const qaFailed = qaResult && !qaResult.success;
|
|
1203
|
+
if (qaFailed) {
|
|
1204
|
+
// QA gate: pause chain with clear messaging
|
|
1205
|
+
console.log(chalk.yellow("\n ⏸️ QA Gate"));
|
|
1206
|
+
console.log(chalk.yellow(` Issue #${issueNumber} QA did not pass. Chain paused.`));
|
|
1207
|
+
console.log(chalk.gray(" Fix QA issues and re-run, or run /loop to auto-fix."));
|
|
1208
|
+
// Update state to waiting_for_qa_gate
|
|
1209
|
+
if (stateManager) {
|
|
1210
|
+
try {
|
|
1211
|
+
await stateManager.updateIssueStatus(issueNumber, "waiting_for_qa_gate");
|
|
1212
|
+
}
|
|
1213
|
+
catch {
|
|
1214
|
+
// State tracking errors shouldn't stop execution
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
break;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1153
1220
|
const chainInfo = mergedOptions.chain ? " (chain stopped)" : "";
|
|
1154
1221
|
console.log(chalk.yellow(`\n ⚠️ Issue #${issueNumber} failed, stopping sequential execution${chainInfo}`));
|
|
1155
1222
|
break;
|
|
@@ -1186,12 +1253,93 @@ export async function runCommand(issues, options) {
|
|
|
1186
1253
|
if (logWriter) {
|
|
1187
1254
|
logPath = await logWriter.finalize();
|
|
1188
1255
|
}
|
|
1256
|
+
// Calculate success/failure counts
|
|
1257
|
+
const passed = results.filter((r) => r.success).length;
|
|
1258
|
+
const failed = results.filter((r) => !r.success).length;
|
|
1259
|
+
// Record metrics (local analytics)
|
|
1260
|
+
if (!config.dryRun && results.length > 0) {
|
|
1261
|
+
try {
|
|
1262
|
+
const metricsWriter = new MetricsWriter({ verbose: config.verbose });
|
|
1263
|
+
// Calculate total duration
|
|
1264
|
+
const totalDuration = results.reduce((sum, r) => sum + (r.durationSeconds ?? 0), 0);
|
|
1265
|
+
// Get unique phases from all results
|
|
1266
|
+
const allPhases = new Set();
|
|
1267
|
+
for (const result of results) {
|
|
1268
|
+
for (const phaseResult of result.phaseResults) {
|
|
1269
|
+
// Only include phases that are valid MetricPhases
|
|
1270
|
+
const phase = phaseResult.phase;
|
|
1271
|
+
if ([
|
|
1272
|
+
"spec",
|
|
1273
|
+
"security-review",
|
|
1274
|
+
"testgen",
|
|
1275
|
+
"exec",
|
|
1276
|
+
"test",
|
|
1277
|
+
"qa",
|
|
1278
|
+
"loop",
|
|
1279
|
+
].includes(phase)) {
|
|
1280
|
+
allPhases.add(phase);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
// Calculate aggregate metrics from worktrees
|
|
1285
|
+
let totalFilesChanged = 0;
|
|
1286
|
+
let totalLinesAdded = 0;
|
|
1287
|
+
let totalQaIterations = 0;
|
|
1288
|
+
for (const result of results) {
|
|
1289
|
+
const worktreeInfo = worktreeMap.get(result.issueNumber);
|
|
1290
|
+
if (worktreeInfo?.path) {
|
|
1291
|
+
const stats = getWorktreeDiffStats(worktreeInfo.path);
|
|
1292
|
+
totalFilesChanged += stats.filesChanged;
|
|
1293
|
+
totalLinesAdded += stats.linesAdded;
|
|
1294
|
+
}
|
|
1295
|
+
// Count QA iterations (loop phases indicate retries)
|
|
1296
|
+
if (result.loopTriggered) {
|
|
1297
|
+
totalQaIterations += result.phaseResults.filter((p) => p.phase === "loop").length;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
// Build CLI flags for metrics
|
|
1301
|
+
const cliFlags = [];
|
|
1302
|
+
if (mergedOptions.sequential)
|
|
1303
|
+
cliFlags.push("--sequential");
|
|
1304
|
+
if (mergedOptions.chain)
|
|
1305
|
+
cliFlags.push("--chain");
|
|
1306
|
+
if (mergedOptions.qaGate)
|
|
1307
|
+
cliFlags.push("--qa-gate");
|
|
1308
|
+
if (mergedOptions.qualityLoop)
|
|
1309
|
+
cliFlags.push("--quality-loop");
|
|
1310
|
+
if (mergedOptions.testgen)
|
|
1311
|
+
cliFlags.push("--testgen");
|
|
1312
|
+
// Record the run
|
|
1313
|
+
await metricsWriter.recordRun({
|
|
1314
|
+
issues: issueNumbers,
|
|
1315
|
+
phases: Array.from(allPhases),
|
|
1316
|
+
outcome: determineOutcome(passed, results.length),
|
|
1317
|
+
duration: totalDuration,
|
|
1318
|
+
model: process.env.ANTHROPIC_MODEL ?? "opus",
|
|
1319
|
+
flags: cliFlags,
|
|
1320
|
+
metrics: {
|
|
1321
|
+
tokensUsed: 0, // Token tracking not available from SDK
|
|
1322
|
+
filesChanged: totalFilesChanged,
|
|
1323
|
+
linesAdded: totalLinesAdded,
|
|
1324
|
+
acceptanceCriteria: 0, // Would need to parse from issue
|
|
1325
|
+
qaIterations: totalQaIterations,
|
|
1326
|
+
},
|
|
1327
|
+
});
|
|
1328
|
+
if (config.verbose) {
|
|
1329
|
+
console.log(chalk.gray(` 📊 Metrics recorded to .sequant/metrics.json`));
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
catch (metricsError) {
|
|
1333
|
+
// Metrics recording errors shouldn't stop execution
|
|
1334
|
+
if (config.verbose) {
|
|
1335
|
+
console.log(chalk.yellow(` ⚠️ Metrics recording error: ${metricsError}`));
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1189
1339
|
// Summary
|
|
1190
1340
|
console.log(chalk.blue("\n" + "━".repeat(50)));
|
|
1191
1341
|
console.log(chalk.blue(" Summary"));
|
|
1192
1342
|
console.log(chalk.blue("━".repeat(50)));
|
|
1193
|
-
const passed = results.filter((r) => r.success).length;
|
|
1194
|
-
const failed = results.filter((r) => !r.success).length;
|
|
1195
1343
|
console.log(chalk.gray(`\n Results: ${chalk.green(`${passed} passed`)}, ${chalk.red(`${failed} failed`)}`));
|
|
1196
1344
|
for (const result of results) {
|
|
1197
1345
|
const status = result.success ? chalk.green("✓") : chalk.red("✗");
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sequant state - Manage workflow state for existing worktrees
|
|
3
|
+
*
|
|
4
|
+
* Provides commands to bootstrap, rebuild, and clean up workflow state:
|
|
5
|
+
* - init: Populate state for untracked worktrees
|
|
6
|
+
* - rebuild: Recreate entire state from git worktrees + logs
|
|
7
|
+
* - clean: Remove entries for worktrees that no longer exist
|
|
8
|
+
*/
|
|
9
|
+
export interface StateInitOptions {
|
|
10
|
+
/** Output as JSON */
|
|
11
|
+
json?: boolean;
|
|
12
|
+
/** Enable verbose output */
|
|
13
|
+
verbose?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface StateRebuildOptions {
|
|
16
|
+
/** Output as JSON */
|
|
17
|
+
json?: boolean;
|
|
18
|
+
/** Enable verbose output */
|
|
19
|
+
verbose?: boolean;
|
|
20
|
+
/** Force rebuild without confirmation (skip backup warning) */
|
|
21
|
+
force?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface StateCleanOptions {
|
|
24
|
+
/** Output as JSON */
|
|
25
|
+
json?: boolean;
|
|
26
|
+
/** Enable verbose output */
|
|
27
|
+
verbose?: boolean;
|
|
28
|
+
/** Only show what would be cleaned (don't modify) */
|
|
29
|
+
dryRun?: boolean;
|
|
30
|
+
/** Remove entries older than this many days */
|
|
31
|
+
maxAge?: number;
|
|
32
|
+
/** Remove all orphaned entries (both merged and abandoned) in one step */
|
|
33
|
+
all?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Initialize state for untracked worktrees
|
|
37
|
+
*
|
|
38
|
+
* Scans for worktrees with issue-* or feature/* patterns,
|
|
39
|
+
* extracts issue numbers, fetches titles from GitHub,
|
|
40
|
+
* and populates state file with reasonable defaults.
|
|
41
|
+
*/
|
|
42
|
+
export declare function stateInitCommand(options?: StateInitOptions): Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* Rebuild entire state from scratch
|
|
45
|
+
*
|
|
46
|
+
* Combines worktree discovery with log-based state reconstruction.
|
|
47
|
+
* Creates backup of existing state before rebuilding.
|
|
48
|
+
*/
|
|
49
|
+
export declare function stateRebuildCommand(options?: StateRebuildOptions): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Clean up orphaned state entries
|
|
52
|
+
*
|
|
53
|
+
* Removes entries for worktrees that no longer exist.
|
|
54
|
+
* Detects merged PRs and auto-removes them.
|
|
55
|
+
*/
|
|
56
|
+
export declare function stateCleanCommand(options?: StateCleanOptions): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Main state command handler (routes to subcommands)
|
|
59
|
+
*/
|
|
60
|
+
export declare function stateCommand(subcommand: string | undefined, options?: StateInitOptions & StateRebuildOptions & StateCleanOptions): Promise<void>;
|