sequant 1.20.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/.claude-plugin/marketplace.json +2 -4
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +29 -9
  4. package/dist/bin/cli.js +25 -2
  5. package/dist/src/commands/doctor.js +42 -9
  6. package/dist/src/commands/init.d.ts +1 -0
  7. package/dist/src/commands/init.js +52 -0
  8. package/dist/src/commands/logs.d.ts +1 -0
  9. package/dist/src/commands/logs.js +18 -2
  10. package/dist/src/commands/run.d.ts +7 -0
  11. package/dist/src/commands/run.js +235 -68
  12. package/dist/src/commands/serve.d.ts +13 -0
  13. package/dist/src/commands/serve.js +131 -0
  14. package/dist/src/commands/stats.d.ts +1 -0
  15. package/dist/src/commands/stats.js +185 -26
  16. package/dist/src/commands/status.d.ts +2 -0
  17. package/dist/src/commands/status.js +99 -50
  18. package/dist/src/index.d.ts +2 -2
  19. package/dist/src/index.js +4 -1
  20. package/dist/src/lib/ac-parser.d.ts +2 -0
  21. package/dist/src/lib/ac-parser.js +12 -2
  22. package/dist/src/lib/assess-comment-parser.d.ts +137 -0
  23. package/dist/src/lib/assess-comment-parser.js +344 -0
  24. package/dist/src/lib/ci/config.d.ts +22 -0
  25. package/dist/src/lib/ci/config.js +134 -0
  26. package/dist/src/lib/ci/index.d.ts +12 -0
  27. package/dist/src/lib/ci/index.js +10 -0
  28. package/dist/src/lib/ci/inputs.d.ts +29 -0
  29. package/dist/src/lib/ci/inputs.js +103 -0
  30. package/dist/src/lib/ci/labels.d.ts +34 -0
  31. package/dist/src/lib/ci/labels.js +101 -0
  32. package/dist/src/lib/ci/outputs.d.ts +25 -0
  33. package/dist/src/lib/ci/outputs.js +84 -0
  34. package/dist/src/lib/ci/triggers.d.ts +9 -0
  35. package/dist/src/lib/ci/triggers.js +86 -0
  36. package/dist/src/lib/ci/types.d.ts +131 -0
  37. package/dist/src/lib/ci/types.js +47 -0
  38. package/dist/src/lib/mcp-config.d.ts +54 -0
  39. package/dist/src/lib/mcp-config.js +172 -0
  40. package/dist/src/lib/merge-check/index.js +6 -12
  41. package/dist/src/lib/merge-check/types.d.ts +20 -7
  42. package/dist/src/lib/merge-check/types.js +11 -0
  43. package/dist/src/lib/phase-signal.d.ts +3 -3
  44. package/dist/src/lib/phase-signal.js +5 -3
  45. package/dist/src/lib/settings.d.ts +52 -0
  46. package/dist/src/lib/settings.js +41 -0
  47. package/dist/src/lib/shutdown.d.ts +16 -5
  48. package/dist/src/lib/shutdown.js +32 -12
  49. package/dist/src/lib/solve-comment-parser.d.ts +9 -102
  50. package/dist/src/lib/solve-comment-parser.js +13 -248
  51. package/dist/src/lib/stacks.d.ts +8 -0
  52. package/dist/src/lib/stacks.js +34 -0
  53. package/dist/src/lib/system.js +3 -7
  54. package/dist/src/lib/test-tautology-detector.d.ts +10 -0
  55. package/dist/src/lib/test-tautology-detector.js +43 -4
  56. package/dist/src/lib/upstream/assessment.js +9 -59
  57. package/dist/src/lib/upstream/issues.js +12 -75
  58. package/dist/src/lib/version-check.d.ts +2 -2
  59. package/dist/src/lib/version-check.js +6 -3
  60. package/dist/src/lib/version.d.ts +4 -0
  61. package/dist/src/lib/version.js +25 -0
  62. package/dist/src/lib/workflow/batch-executor.d.ts +18 -86
  63. package/dist/src/lib/workflow/batch-executor.js +232 -55
  64. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +56 -0
  65. package/dist/src/lib/workflow/drivers/agent-driver.js +8 -0
  66. package/dist/src/lib/workflow/drivers/aider.d.ts +18 -0
  67. package/dist/src/lib/workflow/drivers/aider.js +160 -0
  68. package/dist/src/lib/workflow/drivers/claude-code.d.ts +17 -0
  69. package/dist/src/lib/workflow/drivers/claude-code.js +165 -0
  70. package/dist/src/lib/workflow/drivers/index.d.ts +20 -0
  71. package/dist/src/lib/workflow/drivers/index.js +27 -0
  72. package/dist/src/lib/workflow/error-classifier.d.ts +16 -0
  73. package/dist/src/lib/workflow/error-classifier.js +90 -0
  74. package/dist/src/lib/workflow/log-writer.d.ts +6 -3
  75. package/dist/src/lib/workflow/log-writer.js +57 -27
  76. package/dist/src/lib/workflow/metrics-schema.d.ts +9 -9
  77. package/dist/src/lib/workflow/phase-detection.d.ts +23 -0
  78. package/dist/src/lib/workflow/phase-detection.js +45 -29
  79. package/dist/src/lib/workflow/phase-executor.d.ts +42 -3
  80. package/dist/src/lib/workflow/phase-executor.js +345 -220
  81. package/dist/src/lib/workflow/phase-mapper.d.ts +1 -1
  82. package/dist/src/lib/workflow/phase-mapper.js +7 -7
  83. package/dist/src/lib/workflow/platforms/github.d.ts +157 -0
  84. package/dist/src/lib/workflow/platforms/github.js +466 -0
  85. package/dist/src/lib/workflow/platforms/index.d.ts +17 -0
  86. package/dist/src/lib/workflow/platforms/index.js +25 -0
  87. package/dist/src/lib/workflow/platforms/platform-provider.d.ts +67 -0
  88. package/dist/src/lib/workflow/platforms/platform-provider.js +8 -0
  89. package/dist/src/lib/workflow/pr-status.d.ts +2 -4
  90. package/dist/src/lib/workflow/pr-status.js +3 -16
  91. package/dist/src/lib/workflow/qa-cache.d.ts +58 -0
  92. package/dist/src/lib/workflow/qa-cache.js +88 -0
  93. package/dist/src/lib/workflow/reconcile.d.ts +69 -0
  94. package/dist/src/lib/workflow/reconcile.js +290 -0
  95. package/dist/src/lib/workflow/ring-buffer.d.ts +17 -0
  96. package/dist/src/lib/workflow/ring-buffer.js +37 -0
  97. package/dist/src/lib/workflow/run-log-schema.d.ts +115 -24
  98. package/dist/src/lib/workflow/run-log-schema.js +47 -12
  99. package/dist/src/lib/workflow/run-reflect.js +1 -1
  100. package/dist/src/lib/workflow/state-cleanup.js +21 -0
  101. package/dist/src/lib/workflow/state-manager.d.ts +34 -3
  102. package/dist/src/lib/workflow/state-manager.js +278 -126
  103. package/dist/src/lib/workflow/state-schema.d.ts +34 -30
  104. package/dist/src/lib/workflow/state-schema.js +35 -25
  105. package/dist/src/lib/workflow/state-utils.d.ts +3 -1
  106. package/dist/src/lib/workflow/state-utils.js +1 -0
  107. package/dist/src/lib/workflow/types.d.ts +208 -6
  108. package/dist/src/lib/workflow/types.js +20 -1
  109. package/dist/src/lib/workflow/worktree-discovery.d.ts +1 -1
  110. package/dist/src/lib/workflow/worktree-discovery.js +6 -14
  111. package/dist/src/lib/workflow/worktree-manager.js +33 -51
  112. package/dist/src/mcp/index.d.ts +4 -0
  113. package/dist/src/mcp/index.js +4 -0
  114. package/dist/src/mcp/resources.d.ts +7 -0
  115. package/dist/src/mcp/resources.js +111 -0
  116. package/dist/src/mcp/run-registry.d.ts +34 -0
  117. package/dist/src/mcp/run-registry.js +42 -0
  118. package/dist/src/mcp/server.d.ts +12 -0
  119. package/dist/src/mcp/server.js +50 -0
  120. package/dist/src/mcp/tools/logs.d.ts +7 -0
  121. package/dist/src/mcp/tools/logs.js +149 -0
  122. package/dist/src/mcp/tools/run.d.ts +121 -0
  123. package/dist/src/mcp/tools/run.js +591 -0
  124. package/dist/src/mcp/tools/status.d.ts +7 -0
  125. package/dist/src/mcp/tools/status.js +127 -0
  126. package/package.json +10 -1
  127. package/templates/hooks/post-tool.sh +19 -8
  128. package/templates/hooks/pre-tool.sh +36 -49
  129. package/templates/mcp.json +6 -0
  130. package/templates/skills/assess/SKILL.md +354 -352
  131. package/templates/skills/exec/SKILL.md +64 -1
  132. package/templates/skills/fullsolve/SKILL.md +35 -4
  133. package/templates/skills/qa/SKILL.md +486 -9
  134. package/templates/skills/qa/scripts/quality-checks.sh +1 -1
  135. package/templates/skills/setup/SKILL.md +386 -0
  136. package/templates/skills/solve/SKILL.md +38 -664
  137. package/templates/skills/spec/SKILL.md +90 -31
@@ -9,7 +9,9 @@
9
9
  */
10
10
  import chalk from "chalk";
11
11
  import { spawnSync } from "child_process";
12
+ import pLimit from "p-limit";
12
13
  import { getManifest } from "../lib/manifest.js";
14
+ import { formatElapsedTime } from "../lib/phase-spinner.js";
13
15
  import { getSettings } from "../lib/settings.js";
14
16
  import { LogWriter } from "../lib/workflow/log-writer.js";
15
17
  import { StateManager } from "../lib/workflow/state-manager.js";
@@ -23,6 +25,13 @@ import { getCommitHash } from "../lib/workflow/git-diff-utils.js";
23
25
  import { getTokenUsageForRun } from "../lib/workflow/token-utils.js";
24
26
  import { reconcileStateAtStartup } from "../lib/workflow/state-utils.js";
25
27
  import { analyzeRun, formatReflection } from "../lib/workflow/run-reflect.js";
28
+ /** @internal Log a non-fatal warning: one-line summary always, detail in verbose. */
29
+ export function logNonFatalWarning(message, error, verbose) {
30
+ console.log(chalk.yellow(message));
31
+ if (verbose) {
32
+ console.log(chalk.gray(` ${error}`));
33
+ }
34
+ }
26
35
  // Extracted modules
27
36
  import { detectDefaultBranch, ensureWorktrees, ensureWorktreesChain, getWorktreeDiffStats, } from "../lib/workflow/worktree-manager.js";
28
37
  import { formatDuration } from "../lib/workflow/phase-executor.js";
@@ -32,6 +41,64 @@ export { parseQaVerdict, formatDuration, executePhaseWithRetry, } from "../lib/w
32
41
  export { detectDefaultBranch, checkWorktreeFreshness, removeStaleWorktree, listWorktrees, getWorktreeChangedFiles, getWorktreeDiffStats, readCacheMetrics, filterResumedPhases, ensureWorktree, createCheckpointCommit, reinstallIfLockfileChanged, rebaseBeforePR, createPR, } from "../lib/workflow/worktree-manager.js";
33
42
  export { detectPhasesFromLabels, parseRecommendedWorkflow, determinePhasesForIssue, } from "../lib/workflow/phase-mapper.js";
34
43
  export { getIssueInfo, sortByDependencies, parseBatches, getEnvConfig, executeBatch, runIssueWithLogging, } from "../lib/workflow/batch-executor.js";
44
+ /**
45
+ * Normalize Commander.js --no-X flags into typed RunOptions fields.
46
+ * Replaces the `as any` cast (#402 AC-4).
47
+ */
48
+ export function normalizeCommanderOptions(options) {
49
+ const raw = options;
50
+ return {
51
+ ...options,
52
+ ...(raw.log === false && { noLog: true }),
53
+ ...(raw.smartTests === false && { noSmartTests: true }),
54
+ ...(raw.mcp === false && { noMcp: true }),
55
+ ...(raw.retry === false && { noRetry: true }),
56
+ ...(raw.rebase === false && { noRebase: true }),
57
+ ...(raw.pr === false && { noPr: true }),
58
+ };
59
+ }
60
+ /**
61
+ * Execute a single issue with log bookkeeping (start/complete/PR info).
62
+ * Replaces the duplicated per-issue wrapper in sequential and parallel loops (#402 AC-1).
63
+ */
64
+ async function executeOneIssue(args) {
65
+ const { issueNumber, batchCtx, chain, parallelIssueNumber } = args;
66
+ const { config, options, issueInfoMap, worktreeMap, logWriter, stateManager, shutdownManager, packageManager, baseBranch, onProgress, } = batchCtx;
67
+ const issueInfo = issueInfoMap.get(issueNumber) ?? {
68
+ title: `Issue #${issueNumber}`,
69
+ labels: [],
70
+ };
71
+ const worktreeInfo = worktreeMap.get(issueNumber);
72
+ // Start issue logging
73
+ if (logWriter) {
74
+ logWriter.startIssue(issueNumber, issueInfo.title, issueInfo.labels);
75
+ }
76
+ const ctx = {
77
+ issueNumber,
78
+ title: issueInfo.title,
79
+ labels: issueInfo.labels,
80
+ config,
81
+ options,
82
+ services: { logWriter, stateManager, shutdownManager },
83
+ worktree: worktreeInfo
84
+ ? { path: worktreeInfo.path, branch: worktreeInfo.branch }
85
+ : undefined,
86
+ chain,
87
+ packageManager,
88
+ baseBranch,
89
+ onProgress,
90
+ };
91
+ const result = await runIssueWithLogging(ctx);
92
+ // Record PR info in log before completing issue
93
+ if (logWriter && result.prNumber && result.prUrl) {
94
+ logWriter.setPRInfo(result.prNumber, result.prUrl, parallelIssueNumber);
95
+ }
96
+ // Complete issue logging
97
+ if (logWriter) {
98
+ logWriter.completeIssue(parallelIssueNumber);
99
+ }
100
+ return result;
101
+ }
35
102
  /**
36
103
  * Main run command
37
104
  */
@@ -61,22 +128,11 @@ export async function runCommand(issues, options) {
61
128
  const envConfig = getEnvConfig();
62
129
  // Settings provide defaults, env overrides settings, CLI overrides all
63
130
  // Note: phases are auto-detected per-issue unless --phases is explicitly set
64
- // Commander.js converts --no-X to { X: false }, not { noX: true }.
65
- // Normalize these so RunOptions fields (noLog, noMcp, etc.) work correctly.
66
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
67
- const cliOpts = options;
68
- const normalizedOptions = {
69
- ...options,
70
- ...(cliOpts.log === false && { noLog: true }),
71
- ...(cliOpts.smartTests === false && { noSmartTests: true }),
72
- ...(cliOpts.mcp === false && { noMcp: true }),
73
- ...(cliOpts.retry === false && { noRetry: true }),
74
- ...(cliOpts.rebase === false && { noRebase: true }),
75
- ...(cliOpts.pr === false && { noPr: true }),
76
- };
131
+ const normalizedOptions = normalizeCommanderOptions(options);
77
132
  const mergedOptions = {
78
133
  // Settings defaults (phases removed - now auto-detected)
79
134
  sequential: normalizedOptions.sequential ?? settings.run.sequential,
135
+ concurrency: normalizedOptions.concurrency ?? settings.run.concurrency,
80
136
  timeout: normalizedOptions.timeout ?? settings.run.timeout,
81
137
  logPath: normalizedOptions.logPath ?? settings.run.logPath,
82
138
  qualityLoop: normalizedOptions.qualityLoop ?? settings.run.qualityLoop,
@@ -132,6 +188,13 @@ export async function runCommand(issues, options) {
132
188
  console.log("");
133
189
  }
134
190
  }
191
+ // Validate concurrency value
192
+ if (mergedOptions.concurrency !== undefined &&
193
+ (mergedOptions.concurrency < 1 ||
194
+ !Number.isInteger(mergedOptions.concurrency))) {
195
+ console.log(chalk.red(`❌ Invalid --concurrency value: ${mergedOptions.concurrency}. Must be a positive integer.`));
196
+ return;
197
+ }
135
198
  // Validate QA gate requirements
136
199
  if (mergedOptions.qaGate && !mergedOptions.chain) {
137
200
  console.log(chalk.red("❌ --qa-gate requires --chain flag"));
@@ -161,10 +224,14 @@ export async function runCommand(issues, options) {
161
224
  const retryEnabled = mergedOptions.noRetry
162
225
  ? false
163
226
  : (settings.run.retry ?? true);
227
+ const isSequential = mergedOptions.sequential ?? false;
228
+ const isParallel = !isSequential && issueNumbers.length > 1;
164
229
  const config = {
165
230
  ...DEFAULT_CONFIG,
166
231
  phases: explicitPhases ?? DEFAULT_PHASES,
167
- sequential: mergedOptions.sequential ?? false,
232
+ sequential: isSequential,
233
+ concurrency: mergedOptions.concurrency ?? DEFAULT_CONFIG.concurrency,
234
+ parallel: isParallel,
168
235
  dryRun: mergedOptions.dryRun ?? false,
169
236
  verbose: mergedOptions.verbose ?? false,
170
237
  phaseTimeout: mergedOptions.timeout ?? DEFAULT_CONFIG.phaseTimeout,
@@ -173,6 +240,8 @@ export async function runCommand(issues, options) {
173
240
  noSmartTests: mergedOptions.noSmartTests ?? false,
174
241
  mcp: mcpEnabled,
175
242
  retry: retryEnabled,
243
+ agent: mergedOptions.agent ?? settings.run.agent,
244
+ aiderSettings: settings.run.aider,
176
245
  };
177
246
  // Propagate verbose mode to UI config so spinners use text-only mode.
178
247
  // This prevents animated spinner control characters from colliding with
@@ -234,7 +303,7 @@ export async function runCommand(issues, options) {
234
303
  else {
235
304
  console.log(chalk.gray(` Phases: ${config.phases.join(" → ")}`));
236
305
  }
237
- console.log(chalk.gray(` Mode: ${config.sequential ? "stop-on-failure" : "continue-on-failure"}`));
306
+ console.log(chalk.gray(` Mode: ${config.sequential ? "sequential (stop-on-failure)" : `parallel (concurrency: ${config.concurrency})`}`));
238
307
  if (config.qualityLoop) {
239
308
  console.log(chalk.gray(` Quality loop: enabled (max ${config.maxIterations} iterations)`));
240
309
  }
@@ -270,11 +339,9 @@ export async function runCommand(issues, options) {
270
339
  console.log(chalk.gray(` State reconciled: ${reconcileResult.advanced.map((n) => `#${n}`).join(", ")} → merged`));
271
340
  }
272
341
  }
273
- catch {
342
+ catch (error) {
274
343
  // AC-8: Graceful degradation - don't block execution on reconciliation failure
275
- if (config.verbose) {
276
- console.log(chalk.yellow(` ⚠️ State reconciliation failed, continuing...`));
277
- }
344
+ logNonFatalWarning(` ⚠️ State reconciliation failed, continuing...`, error, config.verbose);
278
345
  }
279
346
  }
280
347
  // AC-1 & AC-2: Pre-flight state guard - skip completed issues unless --force
@@ -294,8 +361,9 @@ export async function runCommand(issues, options) {
294
361
  activeIssues.push(issueNumber);
295
362
  }
296
363
  }
297
- catch {
364
+ catch (error) {
298
365
  // AC-8: Graceful degradation - if state check fails, include the issue
366
+ logNonFatalWarning(` ⚠️ State lookup failed for #${issueNumber}, including anyway...`, error, config.verbose);
299
367
  activeIssues.push(issueNumber);
300
368
  }
301
369
  }
@@ -357,6 +425,18 @@ export async function runCommand(issues, options) {
357
425
  }
358
426
  }
359
427
  }
428
+ // Shared context for all execution paths
429
+ const batchCtx = {
430
+ config,
431
+ options: mergedOptions,
432
+ issueInfoMap,
433
+ worktreeMap,
434
+ logWriter,
435
+ stateManager,
436
+ shutdownManager: shutdown,
437
+ packageManager: manifest.packageManager,
438
+ baseBranch: resolvedBaseBranch,
439
+ };
360
440
  // Execute with graceful shutdown handling
361
441
  const results = [];
362
442
  let exitCode = 0;
@@ -366,7 +446,7 @@ export async function runCommand(issues, options) {
366
446
  for (let batchIdx = 0; batchIdx < batches.length; batchIdx++) {
367
447
  const batch = batches[batchIdx];
368
448
  console.log(chalk.blue(`\n Batch ${batchIdx + 1}/${batches.length}: Issues ${batch.map((n) => `#${n}`).join(", ")}`));
369
- const batchResults = await executeBatch(batch, config, logWriter, stateManager, mergedOptions, issueInfoMap, worktreeMap, shutdown, manifest.packageManager, resolvedBaseBranch);
449
+ const batchResults = await executeBatch(batch, batchCtx);
370
450
  results.push(...batchResults);
371
451
  // Check if batch failed and we should stop
372
452
  const batchFailed = batchResults.some((r) => !r.success);
@@ -380,28 +460,14 @@ export async function runCommand(issues, options) {
380
460
  // Sequential execution
381
461
  for (let i = 0; i < issueNumbers.length; i++) {
382
462
  const issueNumber = issueNumbers[i];
383
- const issueInfo = issueInfoMap.get(issueNumber) ?? {
384
- title: `Issue #${issueNumber}`,
385
- labels: [],
386
- };
387
- const worktreeInfo = worktreeMap.get(issueNumber);
388
- // Start issue logging
389
- if (logWriter) {
390
- logWriter.startIssue(issueNumber, issueInfo.title, issueInfo.labels);
391
- }
392
- const result = await runIssueWithLogging(issueNumber, config, logWriter, stateManager, issueInfo.title, issueInfo.labels, mergedOptions, worktreeInfo?.path, worktreeInfo?.branch, shutdown, mergedOptions.chain, // Enable checkpoint commits in chain mode
393
- manifest.packageManager,
394
- // In chain mode, only the last issue should trigger pre-PR rebase
395
- mergedOptions.chain ? i === issueNumbers.length - 1 : undefined, resolvedBaseBranch);
463
+ const result = await executeOneIssue({
464
+ issueNumber,
465
+ batchCtx,
466
+ chain: mergedOptions.chain
467
+ ? { enabled: true, isLast: i === issueNumbers.length - 1 }
468
+ : undefined,
469
+ });
396
470
  results.push(result);
397
- // Record PR info in log before completing issue
398
- if (logWriter && result.prNumber && result.prUrl) {
399
- logWriter.setPRInfo(result.prNumber, result.prUrl);
400
- }
401
- // Complete issue logging
402
- if (logWriter) {
403
- logWriter.completeIssue();
404
- }
405
471
  // Check if shutdown was triggered
406
472
  if (shutdown.shuttingDown) {
407
473
  break;
@@ -435,32 +501,135 @@ export async function runCommand(issues, options) {
435
501
  }
436
502
  }
437
503
  else {
438
- // Default mode: run issues serially but continue on failure (don't stop)
439
- // TODO: Add proper parallel execution with listr2
440
- for (const issueNumber of issueNumbers) {
441
- // Check if shutdown was triggered
442
- if (shutdown.shuttingDown) {
443
- break;
504
+ // Default mode: run issues concurrently with configurable concurrency limit
505
+ const limit = pLimit(config.concurrency);
506
+ // Track progress for concurrent issues
507
+ const issueStatus = new Map();
508
+ for (const num of issueNumbers) {
509
+ issueStatus.set(num, { state: "running" });
510
+ }
511
+ const renderProgressLine = () => {
512
+ const parts = issueNumbers.map((num) => {
513
+ const info = issueStatus.get(num);
514
+ if (info.state === "done")
515
+ return colors.success(`#${num} ✓`);
516
+ if (info.state === "failed")
517
+ return colors.error(`#${num} ✗`);
518
+ return colors.warning(`#${num} ⏳`);
519
+ });
520
+ return ` Progress: ${parts.join(" ")}`;
521
+ };
522
+ const updateProgress = (completedIssue) => {
523
+ if (mergedOptions.quiet)
524
+ return;
525
+ if (process.stdout.isTTY) {
526
+ // TTY: overwrite the progress line in place
527
+ process.stdout.write(`\r${renderProgressLine()}`);
444
528
  }
445
- const issueInfo = issueInfoMap.get(issueNumber) ?? {
446
- title: `Issue #${issueNumber}`,
447
- labels: [],
448
- };
449
- const worktreeInfo = worktreeMap.get(issueNumber);
450
- // Start issue logging
451
- if (logWriter) {
452
- logWriter.startIssue(issueNumber, issueInfo.title, issueInfo.labels);
529
+ // Print a per-issue completion summary (works on both TTY and non-TTY)
530
+ if (completedIssue != null) {
531
+ const info = issueStatus.get(completedIssue);
532
+ const duration = info.durationSeconds != null
533
+ ? ` (${formatElapsedTime(info.durationSeconds)})`
534
+ : "";
535
+ if (info.state === "done") {
536
+ const line = ` ${colors.success("✓")} Issue #${completedIssue} completed${duration}`;
537
+ if (process.stdout.isTTY) {
538
+ // Move to a new line before printing the summary, then re-render progress
539
+ process.stdout.write(`\n${line}\n${renderProgressLine()}`);
540
+ }
541
+ else {
542
+ console.log(line);
543
+ }
544
+ }
545
+ else {
546
+ const errorSuffix = info.error ? `: ${info.error}` : "";
547
+ const line = ` ${colors.error("✗")} Issue #${completedIssue} failed${duration}${errorSuffix}`;
548
+ if (process.stdout.isTTY) {
549
+ process.stdout.write(`\n${line}\n${renderProgressLine()}`);
550
+ }
551
+ else {
552
+ console.log(line);
553
+ }
554
+ }
453
555
  }
454
- const result = await runIssueWithLogging(issueNumber, config, logWriter, stateManager, issueInfo.title, issueInfo.labels, mergedOptions, worktreeInfo?.path, worktreeInfo?.branch, shutdown, false, // Parallel mode doesn't support chain
455
- manifest.packageManager, undefined, resolvedBaseBranch);
456
- results.push(result);
457
- // Record PR info in log before completing issue
458
- if (logWriter && result.prNumber && result.prUrl) {
459
- logWriter.setPRInfo(result.prNumber, result.prUrl);
556
+ };
557
+ // Per-phase progress callback for parallel mode (AC-1, AC-3)
558
+ const parallelStartTime = Date.now();
559
+ const onPhaseProgress = (issue, phase, event, extra) => {
560
+ if (mergedOptions.quiet)
561
+ return;
562
+ let line;
563
+ if (event === "start") {
564
+ line = ` ${colors.warning("●")} #${issue}: ${phase} started`;
460
565
  }
461
- // Complete issue logging
462
- if (logWriter) {
463
- logWriter.completeIssue();
566
+ else if (event === "complete") {
567
+ const dur = extra?.durationSeconds != null
568
+ ? ` (${formatElapsedTime(extra.durationSeconds)})`
569
+ : "";
570
+ line = ` ${colors.success("●")} #${issue}: ${phase} ✓${dur}`;
571
+ }
572
+ else {
573
+ line = ` ${colors.error("●")} #${issue}: ${phase} ✗`;
574
+ }
575
+ console.log(line);
576
+ };
577
+ // 60-second heartbeat timer so the terminal never appears frozen (AC-2)
578
+ const HEARTBEAT_INTERVAL_MS = 60_000;
579
+ const heartbeatTimer = setInterval(() => {
580
+ if (mergedOptions.quiet)
581
+ return;
582
+ const elapsedSec = Math.round((Date.now() - parallelStartTime) / 1000);
583
+ console.log(` ${colors.warning("⏳")} Still running... (${formatElapsedTime(elapsedSec)} elapsed)`);
584
+ }, HEARTBEAT_INTERVAL_MS);
585
+ updateProgress();
586
+ const settledResults = await Promise.allSettled(issueNumbers.map((issueNumber) => limit(async () => {
587
+ // Check if shutdown was triggered before starting
588
+ if (shutdown.shuttingDown) {
589
+ return {
590
+ issueNumber,
591
+ success: false,
592
+ phaseResults: [],
593
+ durationSeconds: 0,
594
+ loopTriggered: false,
595
+ };
596
+ }
597
+ const result = await executeOneIssue({
598
+ issueNumber,
599
+ batchCtx: { ...batchCtx, onProgress: onPhaseProgress },
600
+ parallelIssueNumber: issueNumber,
601
+ });
602
+ // Update progress with completion details
603
+ issueStatus.set(issueNumber, {
604
+ state: result.success ? "done" : "failed",
605
+ durationSeconds: result.durationSeconds,
606
+ error: result.phaseResults.find((p) => !p.success)?.error,
607
+ });
608
+ updateProgress(issueNumber);
609
+ return result;
610
+ })));
611
+ // Clean up heartbeat timer
612
+ clearInterval(heartbeatTimer);
613
+ // Clear the progress line
614
+ if (process.stdout.isTTY && !mergedOptions.quiet) {
615
+ process.stdout.write("\n");
616
+ }
617
+ // Collect results from settled promises
618
+ for (let i = 0; i < settledResults.length; i++) {
619
+ const settled = settledResults[i];
620
+ if (settled.status === "fulfilled") {
621
+ results.push(settled.value);
622
+ }
623
+ else {
624
+ // Defensive fallback — runIssueWithLogging catches errors internally,
625
+ // so this path is unreachable in normal operation.
626
+ results.push({
627
+ issueNumber: issueNumbers[i],
628
+ success: false,
629
+ phaseResults: [],
630
+ durationSeconds: 0,
631
+ loopTriggered: false,
632
+ });
464
633
  }
465
634
  }
466
635
  }
@@ -555,9 +724,7 @@ export async function runCommand(issues, options) {
555
724
  }
556
725
  catch (metricsError) {
557
726
  // Metrics recording errors shouldn't stop execution
558
- if (config.verbose) {
559
- console.log(chalk.yellow(` ⚠️ Metrics recording error: ${metricsError}`));
560
- }
727
+ logNonFatalWarning(` ⚠️ Metrics recording failed, continuing...`, metricsError, config.verbose);
561
728
  }
562
729
  }
563
730
  // Summary
@@ -0,0 +1,13 @@
1
+ /**
2
+ * sequant serve - Start MCP server
3
+ *
4
+ * Exposes Sequant workflow orchestration as tools and resources
5
+ * over the Model Context Protocol (MCP).
6
+ *
7
+ * Supports stdio (default) and SSE transports.
8
+ */
9
+ export interface ServeOptions {
10
+ transport?: "stdio" | "sse";
11
+ port?: number;
12
+ }
13
+ export declare function serveCommand(options: ServeOptions): Promise<void>;
@@ -0,0 +1,131 @@
1
+ /**
2
+ * sequant serve - Start MCP server
3
+ *
4
+ * Exposes Sequant workflow orchestration as tools and resources
5
+ * over the Model Context Protocol (MCP).
6
+ *
7
+ * Supports stdio (default) and SSE transports.
8
+ */
9
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
+ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
11
+ import { createServer as createHttpServer, } from "http";
12
+ import { createServer } from "../mcp/server.js";
13
+ import { getVersion } from "../lib/version.js";
14
+ export async function serveCommand(options) {
15
+ const version = getVersion();
16
+ const server = createServer(version);
17
+ const transportType = options.transport || "stdio";
18
+ if (transportType === "sse") {
19
+ await startSSE(server, options.port || 3100);
20
+ }
21
+ else {
22
+ await startStdio(server);
23
+ }
24
+ }
25
+ async function startStdio(server) {
26
+ const transport = new StdioServerTransport();
27
+ // Handle graceful shutdown
28
+ let shuttingDown = false;
29
+ const shutdown = async () => {
30
+ if (shuttingDown)
31
+ return;
32
+ shuttingDown = true;
33
+ try {
34
+ await server.close();
35
+ }
36
+ catch {
37
+ /* best-effort */
38
+ }
39
+ process.exit(0);
40
+ };
41
+ process.on("SIGINT", shutdown);
42
+ process.on("SIGTERM", shutdown);
43
+ await server.connect(transport);
44
+ // Write startup info to stderr (stdout is for MCP protocol)
45
+ process.stderr.write(`Sequant MCP server started (stdio)\n`);
46
+ }
47
+ async function startSSE(server, port) {
48
+ let sseTransport = null;
49
+ let clientConnected = false;
50
+ const httpServer = createHttpServer(async (req, res) => {
51
+ const url = new URL(req.url || "/", `http://localhost:${port}`);
52
+ if (url.pathname === "/sse" && req.method === "GET") {
53
+ // Reject if a client is already connected
54
+ if (clientConnected) {
55
+ res.writeHead(409, { "Content-Type": "application/json" });
56
+ res.end(JSON.stringify({
57
+ error: "conflict",
58
+ message: "Another SSE client is already connected",
59
+ }));
60
+ return;
61
+ }
62
+ // SSE endpoint - create transport and connect
63
+ sseTransport = new SSEServerTransport("/messages", res);
64
+ clientConnected = true;
65
+ // Clean up on client disconnect
66
+ res.on("close", () => {
67
+ clientConnected = false;
68
+ sseTransport = null;
69
+ });
70
+ try {
71
+ await server.connect(sseTransport);
72
+ }
73
+ catch {
74
+ clientConnected = false;
75
+ sseTransport = null;
76
+ if (!res.headersSent) {
77
+ res.writeHead(500, { "Content-Type": "application/json" });
78
+ res.end(JSON.stringify({
79
+ error: "connection_failed",
80
+ message: "Failed to establish MCP transport connection",
81
+ }));
82
+ }
83
+ }
84
+ }
85
+ else if (url.pathname === "/messages" &&
86
+ req.method === "POST" &&
87
+ sseTransport) {
88
+ // Message endpoint for client-to-server communication
89
+ await sseTransport.handlePostMessage(req, res);
90
+ }
91
+ else if (url.pathname === "/health" && req.method === "GET") {
92
+ res.writeHead(200, { "Content-Type": "application/json" });
93
+ res.end(JSON.stringify({
94
+ status: "ok",
95
+ transport: "sse",
96
+ connected: clientConnected,
97
+ }));
98
+ }
99
+ else {
100
+ res.writeHead(404);
101
+ res.end("Not found");
102
+ }
103
+ });
104
+ // Handle graceful shutdown
105
+ let shuttingDown = false;
106
+ const shutdown = async () => {
107
+ if (shuttingDown)
108
+ return;
109
+ shuttingDown = true;
110
+ try {
111
+ await server.close();
112
+ }
113
+ catch {
114
+ /* best-effort */
115
+ }
116
+ try {
117
+ await new Promise((resolve) => httpServer.close(() => resolve()));
118
+ }
119
+ catch {
120
+ /* best-effort */
121
+ }
122
+ process.exit(0);
123
+ };
124
+ process.on("SIGINT", shutdown);
125
+ process.on("SIGTERM", shutdown);
126
+ httpServer.listen(port, "127.0.0.1", () => {
127
+ console.log(`Sequant MCP server started (SSE) on 127.0.0.1:${port}`);
128
+ console.log(` SSE endpoint: http://127.0.0.1:${port}/sse`);
129
+ console.log(` Health check: http://127.0.0.1:${port}/health`);
130
+ });
131
+ }
@@ -8,6 +8,7 @@ interface StatsOptions {
8
8
  path?: string;
9
9
  csv?: boolean;
10
10
  json?: boolean;
11
+ detailed?: boolean;
11
12
  }
12
13
  /**
13
14
  * Main stats command