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.
Files changed (63) hide show
  1. package/README.md +92 -3
  2. package/dist/bin/cli.js +55 -2
  3. package/dist/dashboard/server.d.ts +37 -0
  4. package/dist/dashboard/server.js +968 -0
  5. package/dist/src/commands/dashboard.d.ts +25 -0
  6. package/dist/src/commands/dashboard.js +44 -0
  7. package/dist/src/commands/doctor.d.ts +18 -1
  8. package/dist/src/commands/doctor.js +105 -2
  9. package/dist/src/commands/init.d.ts +1 -0
  10. package/dist/src/commands/init.js +26 -2
  11. package/dist/src/commands/run.d.ts +20 -0
  12. package/dist/src/commands/run.js +151 -3
  13. package/dist/src/commands/state.d.ts +60 -0
  14. package/dist/src/commands/state.js +267 -0
  15. package/dist/src/commands/stats.d.ts +3 -2
  16. package/dist/src/commands/stats.js +246 -38
  17. package/dist/src/commands/status.d.ts +2 -0
  18. package/dist/src/commands/status.js +28 -3
  19. package/dist/src/lib/ac-linter.d.ts +116 -0
  20. package/dist/src/lib/ac-linter.js +304 -0
  21. package/dist/src/lib/ac-parser.d.ts +61 -0
  22. package/dist/src/lib/ac-parser.js +156 -0
  23. package/dist/src/lib/fs.d.ts +19 -0
  24. package/dist/src/lib/fs.js +58 -1
  25. package/dist/src/lib/plugin-version-sync.d.ts +26 -0
  26. package/dist/src/lib/plugin-version-sync.js +91 -0
  27. package/dist/src/lib/project-name.d.ts +40 -0
  28. package/dist/src/lib/project-name.js +191 -0
  29. package/dist/src/lib/semgrep.d.ts +136 -0
  30. package/dist/src/lib/semgrep.js +406 -0
  31. package/dist/src/lib/settings.d.ts +7 -0
  32. package/dist/src/lib/settings.js +1 -0
  33. package/dist/src/lib/stacks.d.ts +14 -0
  34. package/dist/src/lib/stacks.js +159 -0
  35. package/dist/src/lib/system.d.ts +19 -0
  36. package/dist/src/lib/system.js +26 -0
  37. package/dist/src/lib/templates.d.ts +34 -1
  38. package/dist/src/lib/templates.js +117 -7
  39. package/dist/src/lib/workflow/metrics-schema.d.ts +153 -0
  40. package/dist/src/lib/workflow/metrics-schema.js +138 -0
  41. package/dist/src/lib/workflow/metrics-writer.d.ts +102 -0
  42. package/dist/src/lib/workflow/metrics-writer.js +189 -0
  43. package/dist/src/lib/workflow/state-manager.d.ts +18 -1
  44. package/dist/src/lib/workflow/state-manager.js +61 -1
  45. package/dist/src/lib/workflow/state-schema.d.ts +152 -1
  46. package/dist/src/lib/workflow/state-schema.js +99 -0
  47. package/dist/src/lib/workflow/state-utils.d.ts +67 -3
  48. package/dist/src/lib/workflow/state-utils.js +289 -8
  49. package/dist/src/lib/workflow/types.d.ts +2 -0
  50. package/dist/src/lib/workflow/types.js +1 -0
  51. package/package.json +5 -1
  52. package/templates/hooks/pre-tool.sh +6 -0
  53. package/templates/memory/constitution.md +1 -5
  54. package/templates/skills/_shared/references/prompt-templates.md +350 -0
  55. package/templates/skills/_shared/references/subagent-types.md +131 -0
  56. package/templates/skills/exec/SKILL.md +82 -0
  57. package/templates/skills/fullsolve/SKILL.md +19 -2
  58. package/templates/skills/loop/SKILL.md +3 -1
  59. package/templates/skills/qa/SKILL.md +79 -9
  60. package/templates/skills/qa/references/quality-gates.md +85 -1
  61. package/templates/skills/qa/references/semgrep-rules.md +207 -0
  62. package/templates/skills/qa/scripts/quality-checks.sh +54 -0
  63. 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 declare function doctorCommand(): Promise<void>;
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
- export async function doctorCommand() {
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;
@@ -7,6 +7,7 @@ interface InitOptions {
7
7
  force?: boolean;
8
8
  interactive?: boolean;
9
9
  skipSetup?: boolean;
10
+ noSymlinks?: boolean;
10
11
  }
11
12
  export declare function initCommand(options: InitOptions): Promise<void>;
12
13
  export {};
@@ -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
@@ -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, } from "../lib/workflow/state-manager.js";
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>;