sequant 1.15.3 → 1.15.4

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sequant",
3
3
  "description": "Structured workflow system for Claude Code - GitHub issue resolution with spec, exec, test, and QA phases",
4
- "version": "1.15.3",
4
+ "version": "1.15.4",
5
5
  "author": {
6
6
  "name": "sequant-io",
7
7
  "email": "hello@sequant.io"
package/dist/bin/cli.js CHANGED
@@ -142,6 +142,7 @@ program
142
142
  .option("--no-retry", "Disable automatic retry with MCP fallback (useful for debugging)")
143
143
  .option("--resume", "Resume from last completed phase (reads phase markers from GitHub)")
144
144
  .option("--no-rebase", "Skip pre-PR rebase onto origin/main (use when you want to handle rebasing manually)")
145
+ .option("--no-pr", "Skip PR creation after successful QA (manual PR workflow)")
145
146
  .option("-f, --force", "Force re-execution of completed issues (bypass pre-flight state guard)")
146
147
  .action(runCommand);
147
148
  program
@@ -130,6 +130,36 @@ export interface RebaseResult {
130
130
  * @internal Exported for testing
131
131
  */
132
132
  export declare function rebaseBeforePR(worktreePath: string, issueNumber: number, packageManager: string | undefined, verbose: boolean): RebaseResult;
133
+ /**
134
+ * Result of PR creation
135
+ */
136
+ export interface PRCreationResult {
137
+ /** Whether PR creation was attempted */
138
+ attempted: boolean;
139
+ /** Whether PR was created successfully (or already existed) */
140
+ success: boolean;
141
+ /** PR number */
142
+ prNumber?: number;
143
+ /** PR URL */
144
+ prUrl?: string;
145
+ /** Error message if failed */
146
+ error?: string;
147
+ }
148
+ /**
149
+ * Push branch and create a PR after successful QA.
150
+ *
151
+ * Handles both fresh PR creation and detection of existing PRs.
152
+ * Failures are warnings — they don't fail the run.
153
+ *
154
+ * @param worktreePath Path to the worktree
155
+ * @param issueNumber Issue number
156
+ * @param issueTitle Issue title (for PR title)
157
+ * @param branch Branch name
158
+ * @param verbose Whether to show verbose output
159
+ * @returns PRCreationResult with PR info or error
160
+ * @internal Exported for testing
161
+ */
162
+ export declare function createPR(worktreePath: string, issueNumber: number, issueTitle: string, branch: string, verbose: boolean, labels?: string[]): PRCreationResult;
133
163
  /**
134
164
  * Detect phases based on issue labels (like /solve logic)
135
165
  */
@@ -207,6 +237,12 @@ interface RunOptions {
207
237
  * Use when you want to preserve branch state or handle rebasing manually.
208
238
  */
209
239
  noRebase?: boolean;
240
+ /**
241
+ * Skip PR creation after successful QA.
242
+ * When true, branches are pushed but no PR is created.
243
+ * Useful for manual workflows where PRs are created separately.
244
+ */
245
+ noPr?: boolean;
210
246
  /**
211
247
  * Force re-execution of issues even if they have completed status.
212
248
  * Bypasses the pre-flight state guard that skips ready_for_merge/merged issues.
@@ -772,6 +772,140 @@ export function rebaseBeforePR(worktreePath, issueNumber, packageManager, verbos
772
772
  reinstalled,
773
773
  };
774
774
  }
775
+ /**
776
+ * Push branch and create a PR after successful QA.
777
+ *
778
+ * Handles both fresh PR creation and detection of existing PRs.
779
+ * Failures are warnings — they don't fail the run.
780
+ *
781
+ * @param worktreePath Path to the worktree
782
+ * @param issueNumber Issue number
783
+ * @param issueTitle Issue title (for PR title)
784
+ * @param branch Branch name
785
+ * @param verbose Whether to show verbose output
786
+ * @returns PRCreationResult with PR info or error
787
+ * @internal Exported for testing
788
+ */
789
+ export function createPR(worktreePath, issueNumber, issueTitle, branch, verbose, labels) {
790
+ // Step 1: Check for existing PR on this branch
791
+ const existingPR = spawnSync("gh", ["pr", "view", branch, "--json", "number,url"], { stdio: "pipe", cwd: worktreePath, timeout: 15000 });
792
+ if (existingPR.status === 0 && existingPR.stdout) {
793
+ try {
794
+ const prInfo = JSON.parse(existingPR.stdout.toString());
795
+ if (prInfo.number && prInfo.url) {
796
+ if (verbose) {
797
+ console.log(chalk.gray(` ℹ️ PR #${prInfo.number} already exists for branch ${branch}`));
798
+ }
799
+ return {
800
+ attempted: true,
801
+ success: true,
802
+ prNumber: prInfo.number,
803
+ prUrl: prInfo.url,
804
+ };
805
+ }
806
+ }
807
+ catch {
808
+ // JSON parse failed — no existing PR, continue to create
809
+ }
810
+ }
811
+ // Step 2: Push branch to remote
812
+ if (verbose) {
813
+ console.log(chalk.gray(` 🚀 Pushing branch ${branch} to origin...`));
814
+ }
815
+ const pushResult = spawnSync("git", ["-C", worktreePath, "push", "-u", "origin", branch], { stdio: "pipe", timeout: 60000 });
816
+ if (pushResult.status !== 0) {
817
+ const pushError = pushResult.stderr?.toString().trim() ?? "Unknown error";
818
+ console.log(chalk.yellow(` ⚠️ git push failed: ${pushError}`));
819
+ return {
820
+ attempted: true,
821
+ success: false,
822
+ error: `git push failed: ${pushError}`,
823
+ };
824
+ }
825
+ // Step 3: Create PR
826
+ if (verbose) {
827
+ console.log(chalk.gray(` 📝 Creating PR for #${issueNumber}...`));
828
+ }
829
+ const isBug = labels?.some((l) => /^bug/i.test(l));
830
+ const prefix = isBug ? "fix" : "feat";
831
+ const prTitle = `${prefix}(#${issueNumber}): ${issueTitle}`;
832
+ const prBody = [
833
+ `## Summary`,
834
+ ``,
835
+ `Automated PR for issue #${issueNumber}.`,
836
+ ``,
837
+ `Fixes #${issueNumber}`,
838
+ ``,
839
+ `---`,
840
+ `🤖 Generated by \`sequant run\``,
841
+ ].join("\n");
842
+ const prResult = spawnSync("gh", ["pr", "create", "--title", prTitle, "--body", prBody, "--head", branch], { stdio: "pipe", cwd: worktreePath, timeout: 30000 });
843
+ if (prResult.status !== 0) {
844
+ const prError = prResult.stderr?.toString().trim() ?? "Unknown error";
845
+ // Check if PR already exists (race condition or push-before-PR scenarios)
846
+ if (prError.includes("already exists")) {
847
+ const retryView = spawnSync("gh", ["pr", "view", branch, "--json", "number,url"], { stdio: "pipe", cwd: worktreePath, timeout: 15000 });
848
+ if (retryView.status === 0 && retryView.stdout) {
849
+ try {
850
+ const prInfo = JSON.parse(retryView.stdout.toString());
851
+ return {
852
+ attempted: true,
853
+ success: true,
854
+ prNumber: prInfo.number,
855
+ prUrl: prInfo.url,
856
+ };
857
+ }
858
+ catch {
859
+ // Fall through to error
860
+ }
861
+ }
862
+ }
863
+ console.log(chalk.yellow(` ⚠️ PR creation failed: ${prError}`));
864
+ return {
865
+ attempted: true,
866
+ success: false,
867
+ error: `gh pr create failed: ${prError}`,
868
+ };
869
+ }
870
+ // Step 4: Extract PR URL from output and get PR details
871
+ const prOutput = prResult.stdout?.toString().trim() ?? "";
872
+ const prUrlMatch = prOutput.match(/https:\/\/github\.com\/[^\s]+\/pull\/(\d+)/);
873
+ if (prUrlMatch) {
874
+ const prNumber = parseInt(prUrlMatch[1], 10);
875
+ const prUrl = prUrlMatch[0];
876
+ console.log(chalk.green(` ✅ PR #${prNumber} created: ${prUrl}`));
877
+ return {
878
+ attempted: true,
879
+ success: true,
880
+ prNumber,
881
+ prUrl,
882
+ };
883
+ }
884
+ // Fallback: try gh pr view to get details
885
+ const viewResult = spawnSync("gh", ["pr", "view", branch, "--json", "number,url"], { stdio: "pipe", cwd: worktreePath, timeout: 15000 });
886
+ if (viewResult.status === 0 && viewResult.stdout) {
887
+ try {
888
+ const prInfo = JSON.parse(viewResult.stdout.toString());
889
+ console.log(chalk.green(` ✅ PR #${prInfo.number} created: ${prInfo.url}`));
890
+ return {
891
+ attempted: true,
892
+ success: true,
893
+ prNumber: prInfo.number,
894
+ prUrl: prInfo.url,
895
+ };
896
+ }
897
+ catch {
898
+ // Fall through
899
+ }
900
+ }
901
+ // PR was created but we couldn't parse the URL
902
+ console.log(chalk.yellow(` ⚠️ PR created but could not extract URL from output: ${prOutput}`));
903
+ return {
904
+ attempted: true,
905
+ success: true,
906
+ error: "PR created but URL extraction failed",
907
+ };
908
+ }
775
909
  /**
776
910
  * Natural language prompts for each phase
777
911
  * These prompts will invoke the corresponding skills via natural language
@@ -1446,18 +1580,31 @@ export async function runCommand(issues, options) {
1446
1580
  const envConfig = getEnvConfig();
1447
1581
  // Settings provide defaults, env overrides settings, CLI overrides all
1448
1582
  // Note: phases are auto-detected per-issue unless --phases is explicitly set
1583
+ // Commander.js converts --no-X to { X: false }, not { noX: true }.
1584
+ // Normalize these so RunOptions fields (noLog, noMcp, etc.) work correctly.
1585
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1586
+ const cliOpts = options;
1587
+ const normalizedOptions = {
1588
+ ...options,
1589
+ ...(cliOpts.log === false && { noLog: true }),
1590
+ ...(cliOpts.smartTests === false && { noSmartTests: true }),
1591
+ ...(cliOpts.mcp === false && { noMcp: true }),
1592
+ ...(cliOpts.retry === false && { noRetry: true }),
1593
+ ...(cliOpts.rebase === false && { noRebase: true }),
1594
+ ...(cliOpts.pr === false && { noPr: true }),
1595
+ };
1449
1596
  const mergedOptions = {
1450
1597
  // Settings defaults (phases removed - now auto-detected)
1451
- sequential: options.sequential ?? settings.run.sequential,
1452
- timeout: options.timeout ?? settings.run.timeout,
1453
- logPath: options.logPath ?? settings.run.logPath,
1454
- qualityLoop: options.qualityLoop ?? settings.run.qualityLoop,
1455
- maxIterations: options.maxIterations ?? settings.run.maxIterations,
1456
- noSmartTests: options.noSmartTests ?? !settings.run.smartTests,
1598
+ sequential: normalizedOptions.sequential ?? settings.run.sequential,
1599
+ timeout: normalizedOptions.timeout ?? settings.run.timeout,
1600
+ logPath: normalizedOptions.logPath ?? settings.run.logPath,
1601
+ qualityLoop: normalizedOptions.qualityLoop ?? settings.run.qualityLoop,
1602
+ maxIterations: normalizedOptions.maxIterations ?? settings.run.maxIterations,
1603
+ noSmartTests: normalizedOptions.noSmartTests ?? !settings.run.smartTests,
1457
1604
  // Env overrides
1458
1605
  ...envConfig,
1459
1606
  // CLI explicit options override all
1460
- ...options,
1607
+ ...normalizedOptions,
1461
1608
  };
1462
1609
  // Determine if we should auto-detect phases from labels
1463
1610
  const autoDetectPhases = !options.phases && settings.run.autoDetectPhases;
@@ -1766,6 +1913,10 @@ export async function runCommand(issues, options) {
1766
1913
  // In chain mode, only the last issue should trigger pre-PR rebase
1767
1914
  mergedOptions.chain ? i === issueNumbers.length - 1 : undefined);
1768
1915
  results.push(result);
1916
+ // Record PR info in log before completing issue
1917
+ if (logWriter && result.prNumber && result.prUrl) {
1918
+ logWriter.setPRInfo(result.prNumber, result.prUrl);
1919
+ }
1769
1920
  // Complete issue logging
1770
1921
  if (logWriter) {
1771
1922
  logWriter.completeIssue();
@@ -1822,6 +1973,10 @@ export async function runCommand(issues, options) {
1822
1973
  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
1823
1974
  manifest.packageManager);
1824
1975
  results.push(result);
1976
+ // Record PR info in log before completing issue
1977
+ if (logWriter && result.prNumber && result.prUrl) {
1978
+ logWriter.setPRInfo(result.prNumber, result.prUrl);
1979
+ }
1825
1980
  // Complete issue logging
1826
1981
  if (logWriter) {
1827
1982
  logWriter.completeIssue();
@@ -1940,7 +2095,10 @@ export async function runCommand(issues, options) {
1940
2095
  .map((p) => p.success ? colors.success(p.phase) : colors.error(p.phase))
1941
2096
  .join(" → ");
1942
2097
  const loopInfo = result.loopTriggered ? colors.warning(" [loop]") : "";
1943
- console.log(` ${status} #${result.issueNumber}: ${phases}${loopInfo}${duration}`);
2098
+ const prInfo = result.prUrl
2099
+ ? colors.muted(` → PR #${result.prNumber}`)
2100
+ : "";
2101
+ console.log(` ${status} #${result.issueNumber}: ${phases}${loopInfo}${prInfo}${duration}`);
1944
2102
  }
1945
2103
  console.log("");
1946
2104
  if (logPath) {
@@ -1987,6 +2145,10 @@ async function executeBatch(issueNumbers, config, logWriter, stateManager, optio
1987
2145
  const result = await runIssueWithLogging(issueNumber, config, logWriter, stateManager, issueInfo.title, issueInfo.labels, options, worktreeInfo?.path, worktreeInfo?.branch, shutdownManager, false, // Batch mode doesn't support chain
1988
2146
  packageManager);
1989
2147
  results.push(result);
2148
+ // Record PR info in log before completing issue
2149
+ if (logWriter && result.prNumber && result.prUrl) {
2150
+ logWriter.setPRInfo(result.prNumber, result.prUrl);
2151
+ }
1990
2152
  // Complete issue logging
1991
2153
  if (logWriter) {
1992
2154
  logWriter.completeIssue();
@@ -2343,11 +2505,36 @@ async function runIssueWithLogging(issueNumber, config, logWriter, stateManager,
2343
2505
  if (shouldRebase) {
2344
2506
  rebaseBeforePR(worktreePath, issueNumber, packageManager, config.verbose);
2345
2507
  }
2508
+ // Create PR after successful QA + rebase (unless --no-pr)
2509
+ let prNumber;
2510
+ let prUrl;
2511
+ const shouldCreatePR = success && worktreePath && branch && !options.noPr;
2512
+ if (shouldCreatePR) {
2513
+ const prResult = createPR(worktreePath, issueNumber, issueTitle, branch, config.verbose, labels);
2514
+ if (prResult.success && prResult.prNumber && prResult.prUrl) {
2515
+ prNumber = prResult.prNumber;
2516
+ prUrl = prResult.prUrl;
2517
+ // Update workflow state with PR info
2518
+ if (stateManager) {
2519
+ try {
2520
+ await stateManager.updatePRInfo(issueNumber, {
2521
+ number: prResult.prNumber,
2522
+ url: prResult.prUrl,
2523
+ });
2524
+ }
2525
+ catch {
2526
+ // State tracking errors shouldn't stop execution
2527
+ }
2528
+ }
2529
+ }
2530
+ }
2346
2531
  return {
2347
2532
  issueNumber,
2348
2533
  success,
2349
2534
  phaseResults,
2350
2535
  durationSeconds,
2351
2536
  loopTriggered,
2537
+ prNumber,
2538
+ prUrl,
2352
2539
  };
2353
2540
  }
@@ -59,6 +59,10 @@ export declare class LogWriter {
59
59
  * @param phaseLog - Complete phase log entry
60
60
  */
61
61
  logPhase(phaseLog: PhaseLog): void;
62
+ /**
63
+ * Set PR info on the current issue (call before completeIssue)
64
+ */
65
+ setPRInfo(prNumber: number, prUrl: string): void;
62
66
  /**
63
67
  * Complete the current issue and add it to the run log
64
68
  */
@@ -98,6 +98,16 @@ export class LogWriter {
98
98
  console.log(`📝 Logged phase: ${phaseLog.phase} (${phaseLog.status}) - ${phaseLog.durationSeconds.toFixed(1)}s`);
99
99
  }
100
100
  }
101
+ /**
102
+ * Set PR info on the current issue (call before completeIssue)
103
+ */
104
+ setPRInfo(prNumber, prUrl) {
105
+ if (!this.currentIssue) {
106
+ return;
107
+ }
108
+ this.currentIssue.prNumber = prNumber;
109
+ this.currentIssue.prUrl = prUrl;
110
+ }
101
111
  /**
102
112
  * Complete the current issue and add it to the run log
103
113
  */
@@ -114,6 +124,12 @@ export class LogWriter {
114
124
  status: this.currentIssue.status,
115
125
  phases: this.currentIssue.phases,
116
126
  totalDurationSeconds,
127
+ ...(this.currentIssue.prNumber != null && {
128
+ prNumber: this.currentIssue.prNumber,
129
+ }),
130
+ ...(this.currentIssue.prUrl != null && {
131
+ prUrl: this.currentIssue.prUrl,
132
+ }),
117
133
  };
118
134
  this.runLog.issues.push(issueLog);
119
135
  this.currentIssue = null;
@@ -197,6 +197,8 @@ export declare const IssueLogSchema: z.ZodObject<{
197
197
  }, z.core.$strip>>;
198
198
  }, z.core.$strip>>;
199
199
  totalDurationSeconds: z.ZodNumber;
200
+ prNumber: z.ZodOptional<z.ZodNumber>;
201
+ prUrl: z.ZodOptional<z.ZodString>;
200
202
  }, z.core.$strip>;
201
203
  export type IssueLog = z.infer<typeof IssueLogSchema>;
202
204
  /**
@@ -314,6 +316,8 @@ export declare const RunLogSchema: z.ZodObject<{
314
316
  }, z.core.$strip>>;
315
317
  }, z.core.$strip>>;
316
318
  totalDurationSeconds: z.ZodNumber;
319
+ prNumber: z.ZodOptional<z.ZodNumber>;
320
+ prUrl: z.ZodOptional<z.ZodString>;
317
321
  }, z.core.$strip>>;
318
322
  summary: z.ZodObject<{
319
323
  totalIssues: z.ZodNumber;
@@ -126,6 +126,10 @@ export const IssueLogSchema = z.object({
126
126
  phases: z.array(PhaseLogSchema),
127
127
  /** Total execution time in seconds */
128
128
  totalDurationSeconds: z.number().nonnegative(),
129
+ /** PR number if created after successful QA */
130
+ prNumber: z.number().int().positive().optional(),
131
+ /** PR URL if created after successful QA */
132
+ prUrl: z.string().optional(),
129
133
  });
130
134
  /**
131
135
  * Run configuration
@@ -71,6 +71,10 @@ export interface IssueResult {
71
71
  abortReason?: string;
72
72
  loopTriggered?: boolean;
73
73
  durationSeconds?: number;
74
+ /** PR number if created after successful QA */
75
+ prNumber?: number;
76
+ /** PR URL if created after successful QA */
77
+ prUrl?: string;
74
78
  }
75
79
  /**
76
80
  * CLI arguments for run command
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sequant",
3
- "version": "1.15.3",
3
+ "version": "1.15.4",
4
4
  "description": "Quantize your development workflow - Sequential AI phases with quality gates",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,6 +23,7 @@ allowed-tools:
23
23
  - Bash(git add:*)
24
24
  - Bash(git commit:*)
25
25
  - Bash(git log:*)
26
+ - Bash(git push:*)
26
27
  - Bash(git worktree:*)
27
28
  # Worktree management
28
29
  - Bash(./scripts/dev/new-feature.sh:*)