sequant 1.16.0 → 1.17.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.
@@ -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.16.0",
4
+ "version": "1.16.1",
5
5
  "author": {
6
6
  "name": "sequant-io",
7
7
  "email": "hello@sequant.io"
package/README.md CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  Solve GitHub issues with structured phases and quality gates — from issue to merge-ready PR.
6
6
 
7
+ **[sequant.io](https://sequant.io)** — docs, guides, and getting started.
8
+
7
9
  [![npm version](https://img.shields.io/npm/v/sequant.svg)](https://www.npmjs.com/package/sequant)
8
10
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
9
11
 
@@ -128,6 +130,7 @@ npx sequant run 123 # Single issue
128
130
  npx sequant run 1 2 3 # Batch (parallel)
129
131
  npx sequant run 123 --quality-loop
130
132
  npx sequant run 123 --base feature/dashboard # Custom base branch
133
+ npx sequant merge --check # Verify batch before merging
131
134
  ```
132
135
 
133
136
  ---
@@ -155,6 +158,7 @@ npx sequant run 123 --base feature/dashboard # Custom base branch
155
158
 
156
159
  | Command | Purpose |
157
160
  |---------|---------|
161
+ | `/merger` | Multi-issue merge coordination |
158
162
  | `/testgen` | Generate test stubs from spec |
159
163
  | `/verify` | CLI/script execution verification |
160
164
  | `/setup` | Initialize Sequant in a project |
@@ -180,12 +184,13 @@ npx sequant update # Update skill templates
180
184
  npx sequant doctor # Check installation
181
185
  npx sequant status # Show version and config
182
186
  npx sequant run <issues...> # Execute workflow
187
+ npx sequant merge <issues...> # Batch integration QA before merging
183
188
  npx sequant state <cmd> # Manage workflow state (init/rebuild/clean)
184
189
  npx sequant stats # View local workflow analytics
185
190
  npx sequant dashboard # Launch real-time workflow dashboard
186
191
  ```
187
192
 
188
- See [Run Command Options](docs/reference/run-command.md), [State Command](docs/reference/state-command.md), and [Analytics](docs/reference/analytics.md) for details.
193
+ See [Run Command Options](docs/reference/run-command.md), [Merge Command](docs/reference/merge-command.md), [State Command](docs/reference/state-command.md), and [Analytics](docs/reference/analytics.md) for details.
189
194
 
190
195
  ---
191
196
 
package/dist/bin/cli.js CHANGED
@@ -122,7 +122,7 @@ program
122
122
  .description("Execute workflow for GitHub issues using Claude Agent SDK")
123
123
  .argument("[issues...]", "Issue numbers to process")
124
124
  .option("--phases <list>", "Phases to run (default: spec,exec,qa)")
125
- .option("--sequential", "Run issues sequentially")
125
+ .option("--sequential", "Stop on first issue failure (default: continue)")
126
126
  .option("-d, --dry-run", "Preview without execution")
127
127
  .option("-v, --verbose", "Verbose output with streaming")
128
128
  .option("--timeout <seconds>", "Timeout per phase in seconds", parseInt)
@@ -145,6 +145,7 @@ program
145
145
  .option("--no-rebase", "Skip pre-PR rebase onto origin/main (use when you want to handle rebasing manually)")
146
146
  .option("--no-pr", "Skip PR creation after successful QA (manual PR workflow)")
147
147
  .option("-f, --force", "Force re-execution of completed issues (bypass pre-flight state guard)")
148
+ .option("--reflect", "Analyze run results and suggest improvements")
148
149
  .action(runCommand);
149
150
  program
150
151
  .command("merge")
@@ -248,6 +248,12 @@ interface RunOptions {
248
248
  * Bypasses the pre-flight state guard that skips ready_for_merge/merged issues.
249
249
  */
250
250
  force?: boolean;
251
+ /**
252
+ * Analyze run results and suggest workflow improvements.
253
+ * Displays observations about timing patterns, phase mismatches, and
254
+ * actionable suggestions after the summary output.
255
+ */
256
+ reflect?: boolean;
251
257
  }
252
258
  /**
253
259
  * Execute a single phase for an issue using Claude Agent SDK
@@ -26,6 +26,7 @@ import { PhaseSpinner } from "../lib/phase-spinner.js";
26
26
  import { getGitDiffStats, getCommitHash, } from "../lib/workflow/git-diff-utils.js";
27
27
  import { getTokenUsageForRun } from "../lib/workflow/token-utils.js";
28
28
  import { reconcileStateAtStartup } from "../lib/workflow/state-utils.js";
29
+ import { analyzeRun, formatReflection } from "../lib/workflow/run-reflect.js";
29
30
  /**
30
31
  * Slugify a title for branch naming
31
32
  */
@@ -1753,7 +1754,7 @@ export async function runCommand(issues, options) {
1753
1754
  else {
1754
1755
  console.log(chalk.gray(` Phases: ${config.phases.join(" → ")}`));
1755
1756
  }
1756
- console.log(chalk.gray(` Mode: ${config.sequential ? "sequential" : "parallel"}`));
1757
+ console.log(chalk.gray(` Mode: ${config.sequential ? "stop-on-failure" : "continue-on-failure"}`));
1757
1758
  if (config.qualityLoop) {
1758
1759
  console.log(chalk.gray(` Quality loop: enabled (max ${config.maxIterations} iterations)`));
1759
1760
  }
@@ -1954,7 +1955,7 @@ export async function runCommand(issues, options) {
1954
1955
  }
1955
1956
  }
1956
1957
  else {
1957
- // Parallel execution (for now, just run sequentially but don't stop on failure)
1958
+ // Default mode: run issues serially but continue on failure (don't stop)
1958
1959
  // TODO: Add proper parallel execution with listr2
1959
1960
  for (const issueNumber of issueNumbers) {
1960
1961
  // Check if shutdown was triggered
@@ -2105,6 +2106,29 @@ export async function runCommand(issues, options) {
2105
2106
  console.log(colors.muted(` 📝 Log: ${logPath}`));
2106
2107
  console.log("");
2107
2108
  }
2109
+ // Reflection analysis (--reflect flag)
2110
+ if (mergedOptions.reflect && results.length > 0) {
2111
+ const reflection = analyzeRun({
2112
+ results,
2113
+ issueInfoMap,
2114
+ runLog: logWriter?.getRunLog() ?? null,
2115
+ config: {
2116
+ phases: config.phases,
2117
+ qualityLoop: config.qualityLoop,
2118
+ },
2119
+ });
2120
+ const reflectionOutput = formatReflection(reflection);
2121
+ if (reflectionOutput) {
2122
+ console.log(reflectionOutput);
2123
+ console.log("");
2124
+ }
2125
+ }
2126
+ // Suggest merge checks for multi-issue batches
2127
+ if (results.length > 1 && passed > 0 && !config.dryRun) {
2128
+ console.log(colors.muted(" 💡 Verify batch integration before merging:"));
2129
+ console.log(colors.muted(" sequant merge --check"));
2130
+ console.log("");
2131
+ }
2108
2132
  if (config.dryRun) {
2109
2133
  console.log(colors.warning(" ℹ️ This was a dry run. Use without --dry-run to execute."));
2110
2134
  console.log("");
@@ -234,9 +234,11 @@ export async function assessVersion(version, options = {}) {
234
234
  // Create assessment issue first (to get issue number for linking)
235
235
  const assessmentBody = generateAssessmentReport(assessment);
236
236
  const assessmentIssueNumber = await createAssessmentIssue(`Upstream: Claude Code ${release.tagName} Assessment`, assessmentBody, dryRun);
237
- // Create issues for actionable findings
238
- for (let i = 0; i < actionableFindings.length; i++) {
239
- const updatedFinding = await createOrLinkFinding(actionableFindings[i], release.tagName, assessmentIssueNumber, dryRun);
237
+ // Create issues for actionable findings (excluding opportunities —
238
+ // opportunities are listed in the assessment but don't get individual issues)
239
+ const issueWorthy = actionableFindings.filter((f) => f.category !== "opportunity");
240
+ for (let i = 0; i < issueWorthy.length; i++) {
241
+ const updatedFinding = await createOrLinkFinding(issueWorthy[i], release.tagName, assessmentIssueNumber, dryRun);
240
242
  // Update in original findings array
241
243
  const originalIndex = findings.findIndex((f) => f.description === updatedFinding.description);
242
244
  if (originalIndex >= 0) {
@@ -370,6 +372,7 @@ function getDefaultBaseline() {
370
372
  Task: [".claude/skills/**/*.md"],
371
373
  MCP: [".claude/settings.json"],
372
374
  },
375
+ outOfScope: [],
373
376
  };
374
377
  }
375
378
  /**
@@ -36,6 +36,11 @@ export declare function getImpactFiles(matchedKeywords: string[], dependencyMap:
36
36
  * Generate a title for a finding
37
37
  */
38
38
  export declare function generateTitle(category: FindingCategory, change: string): string;
39
+ /**
40
+ * Check if a change matches any out-of-scope patterns from the baseline.
41
+ * Out-of-scope changes are skipped entirely during analysis.
42
+ */
43
+ export declare function isOutOfScope(change: string, outOfScope?: string[]): boolean;
39
44
  /**
40
45
  * Analyze a single change against the baseline
41
46
  */
@@ -158,10 +158,34 @@ export function generateTitle(category, change) {
158
158
  return title;
159
159
  }
160
160
  }
161
+ /**
162
+ * Check if a change matches any out-of-scope patterns from the baseline.
163
+ * Out-of-scope changes are skipped entirely during analysis.
164
+ */
165
+ export function isOutOfScope(change, outOfScope = []) {
166
+ const changeLower = change.toLowerCase();
167
+ return outOfScope.some((pattern) => {
168
+ // Use the descriptive part before " - " as the match pattern
169
+ const matchPart = pattern.split(" - ")[0].toLowerCase();
170
+ return changeLower.includes(matchPart);
171
+ });
172
+ }
161
173
  /**
162
174
  * Analyze a single change against the baseline
163
175
  */
164
176
  export function analyzeChange(change, baseline) {
177
+ // Skip out-of-scope changes early
178
+ if (isOutOfScope(change, baseline.outOfScope)) {
179
+ return {
180
+ category: "no-action",
181
+ title: change,
182
+ description: change,
183
+ impact: "none",
184
+ matchedKeywords: [],
185
+ matchedPatterns: [],
186
+ sequantFiles: [],
187
+ };
188
+ }
165
189
  // Match keywords and patterns
166
190
  const matchedKeywords = matchKeywords(change, baseline.keywords);
167
191
  const matchedPatterns = matchPatterns(change);
@@ -52,8 +52,9 @@ function getActionStatus(count, category) {
52
52
  return "Review needed";
53
53
  case "new-tool":
54
54
  case "hook-change":
55
- case "opportunity":
56
55
  return "Issues created";
56
+ case "opportunity":
57
+ return "Noted for review";
57
58
  default:
58
59
  return "None";
59
60
  }
@@ -116,61 +117,32 @@ export function generateAssessmentReport(assessment) {
116
117
  lines.push(`| Opportunities | ${summary.opportunities} | ${getActionStatus(summary.opportunities, "opportunity")} |`);
117
118
  lines.push(`| No Action | ${summary.noAction} | None |`);
118
119
  lines.push("");
119
- // Breaking Changes section
120
- const breakingFindings = findings.filter((f) => f.category === "breaking");
121
- lines.push("### Breaking Changes");
122
- lines.push("");
123
- if (breakingFindings.length === 0) {
124
- lines.push("None detected.");
125
- }
126
- else {
127
- breakingFindings.forEach((f, i) => {
128
- lines.push(formatFinding(f, i));
129
- });
130
- }
131
- lines.push("");
132
- // Deprecations section
133
- const deprecations = findings.filter((f) => f.category === "deprecation");
134
- lines.push("### Deprecations");
135
- lines.push("");
136
- if (deprecations.length === 0) {
137
- lines.push("None detected.");
138
- }
139
- else {
140
- deprecations.forEach((f, i) => {
141
- lines.push(formatFinding(f, i));
142
- });
143
- }
144
- lines.push("");
145
- // New Tools section
146
- const newTools = findings.filter((f) => f.category === "new-tool");
147
- lines.push("### New Tools");
120
+ // Actionable section — breaking changes, deprecations, new tools, hook changes
121
+ const actionableCategories = [
122
+ "breaking",
123
+ "deprecation",
124
+ "new-tool",
125
+ "hook-change",
126
+ ];
127
+ const actionableFindings = findings.filter((f) => actionableCategories.includes(f.category));
128
+ lines.push("### Actionable");
148
129
  lines.push("");
149
- if (newTools.length === 0) {
150
- lines.push("None detected.");
151
- }
152
- else {
153
- newTools.forEach((f, i) => {
154
- lines.push(formatFinding(f, i));
155
- });
156
- }
157
- lines.push("");
158
- // Hook Changes section
159
- const hookChanges = findings.filter((f) => f.category === "hook-change");
160
- lines.push("### Hook Changes");
130
+ lines.push("*Breaking changes, deprecations, and other items that affect sequant.*");
161
131
  lines.push("");
162
- if (hookChanges.length === 0) {
132
+ if (actionableFindings.length === 0) {
163
133
  lines.push("None detected.");
164
134
  }
165
135
  else {
166
- hookChanges.forEach((f, i) => {
136
+ actionableFindings.forEach((f, i) => {
167
137
  lines.push(formatFinding(f, i));
168
138
  });
169
139
  }
170
140
  lines.push("");
171
- // Opportunities section
141
+ // Informational section — opportunities noted for human triage
172
142
  const opportunities = findings.filter((f) => f.category === "opportunity");
173
- lines.push("### Feature Opportunities");
143
+ lines.push("### Informational");
144
+ lines.push("");
145
+ lines.push("*Opportunities noted for human triage. No individual issues auto-created.*");
174
146
  lines.push("");
175
147
  if (opportunities.length === 0) {
176
148
  lines.push("None detected.");
@@ -134,6 +134,8 @@ export interface Baseline {
134
134
  keywords: string[];
135
135
  /** Map of keywords to affected sequant files */
136
136
  dependencyMap: Record<string, string[]>;
137
+ /** Patterns for changes that are out of scope for sequant (skipped during analysis) */
138
+ outOfScope?: string[];
137
139
  }
138
140
  /**
139
141
  * Regex patterns for detecting change types
@@ -0,0 +1,47 @@
1
+ /**
2
+ * PR merge detection and branch status utilities
3
+ *
4
+ * @module pr-status
5
+ * @example
6
+ * ```typescript
7
+ * import { checkPRMergeStatus, isBranchMergedIntoMain, isIssueMergedIntoMain } from './pr-status';
8
+ *
9
+ * // Check PR status via GitHub CLI
10
+ * const status = checkPRMergeStatus(123);
11
+ * if (status === 'MERGED') {
12
+ * console.log('PR is merged');
13
+ * }
14
+ *
15
+ * // Check if branch is merged into main
16
+ * const isMerged = isBranchMergedIntoMain('feature/123-some-feature');
17
+ * ```
18
+ */
19
+ /**
20
+ * PR merge status from GitHub
21
+ */
22
+ export type PRMergeStatus = "MERGED" | "CLOSED" | "OPEN" | null;
23
+ /**
24
+ * Check the merge status of a PR using the gh CLI
25
+ *
26
+ * @param prNumber - The PR number to check
27
+ * @returns "MERGED" | "CLOSED" | "OPEN" | null (null if PR not found or gh unavailable)
28
+ */
29
+ export declare function checkPRMergeStatus(prNumber: number): PRMergeStatus;
30
+ /**
31
+ * Check if a branch has been merged into main using git
32
+ *
33
+ * @param branchName - The branch name to check (e.g., "feature/33-some-title")
34
+ * @returns true if the branch is merged into main, false otherwise
35
+ */
36
+ export declare function isBranchMergedIntoMain(branchName: string): boolean;
37
+ /**
38
+ * Check if a feature branch for an issue is merged into main
39
+ *
40
+ * Tries multiple detection methods:
41
+ * 1. Check if branch exists and is merged via `git branch --merged main`
42
+ * 2. Check for merge commits mentioning the issue
43
+ *
44
+ * @param issueNumber - The issue number to check
45
+ * @returns true if the issue's work is merged into main
46
+ */
47
+ export declare function isIssueMergedIntoMain(issueNumber: number): boolean;
@@ -0,0 +1,129 @@
1
+ /**
2
+ * PR merge detection and branch status utilities
3
+ *
4
+ * @module pr-status
5
+ * @example
6
+ * ```typescript
7
+ * import { checkPRMergeStatus, isBranchMergedIntoMain, isIssueMergedIntoMain } from './pr-status';
8
+ *
9
+ * // Check PR status via GitHub CLI
10
+ * const status = checkPRMergeStatus(123);
11
+ * if (status === 'MERGED') {
12
+ * console.log('PR is merged');
13
+ * }
14
+ *
15
+ * // Check if branch is merged into main
16
+ * const isMerged = isBranchMergedIntoMain('feature/123-some-feature');
17
+ * ```
18
+ */
19
+ import { spawnSync } from "child_process";
20
+ /**
21
+ * Check the merge status of a PR using the gh CLI
22
+ *
23
+ * @param prNumber - The PR number to check
24
+ * @returns "MERGED" | "CLOSED" | "OPEN" | null (null if PR not found or gh unavailable)
25
+ */
26
+ export function checkPRMergeStatus(prNumber) {
27
+ try {
28
+ const result = spawnSync("gh", ["pr", "view", String(prNumber), "--json", "state", "-q", ".state"], { stdio: "pipe", timeout: 10000 });
29
+ if (result.status === 0 && result.stdout) {
30
+ const state = result.stdout.toString().trim().toUpperCase();
31
+ if (state === "MERGED")
32
+ return "MERGED";
33
+ if (state === "CLOSED")
34
+ return "CLOSED";
35
+ if (state === "OPEN")
36
+ return "OPEN";
37
+ }
38
+ }
39
+ catch {
40
+ // gh not available or error - return null
41
+ }
42
+ return null;
43
+ }
44
+ /**
45
+ * Check if a branch has been merged into main using git
46
+ *
47
+ * @param branchName - The branch name to check (e.g., "feature/33-some-title")
48
+ * @returns true if the branch is merged into main, false otherwise
49
+ */
50
+ export function isBranchMergedIntoMain(branchName) {
51
+ try {
52
+ // Get branches merged into main
53
+ const result = spawnSync("git", ["branch", "--merged", "main"], {
54
+ stdio: "pipe",
55
+ timeout: 10000,
56
+ });
57
+ if (result.status === 0 && result.stdout) {
58
+ const mergedBranches = result.stdout.toString();
59
+ // Check if our branch is in the list (handle both local and remote refs)
60
+ return (mergedBranches.includes(branchName) ||
61
+ mergedBranches.includes(`remotes/origin/${branchName}`));
62
+ }
63
+ }
64
+ catch {
65
+ // git command failed - return false
66
+ }
67
+ return false;
68
+ }
69
+ /**
70
+ * Check if a feature branch for an issue is merged into main
71
+ *
72
+ * Tries multiple detection methods:
73
+ * 1. Check if branch exists and is merged via `git branch --merged main`
74
+ * 2. Check for merge commits mentioning the issue
75
+ *
76
+ * @param issueNumber - The issue number to check
77
+ * @returns true if the issue's work is merged into main
78
+ */
79
+ export function isIssueMergedIntoMain(issueNumber) {
80
+ try {
81
+ // Method 1: Check if any feature branch for this issue is merged
82
+ const listResult = spawnSync("git", ["branch", "-a"], {
83
+ stdio: "pipe",
84
+ timeout: 10000,
85
+ });
86
+ if (listResult.status === 0 && listResult.stdout) {
87
+ const branches = listResult.stdout.toString();
88
+ // Find branches matching feature/<issue>-*
89
+ const branchPattern = new RegExp(`feature/${issueNumber}-[^\\s]+`, "g");
90
+ const matchedBranches = branches.match(branchPattern);
91
+ if (matchedBranches) {
92
+ for (const branch of matchedBranches) {
93
+ const cleanBranch = branch.replace(/^\*?\s*/, "").trim();
94
+ if (isBranchMergedIntoMain(cleanBranch)) {
95
+ return true;
96
+ }
97
+ }
98
+ }
99
+ }
100
+ // Method 2: Check for merge commits mentioning the issue
101
+ // Use specific merge patterns to avoid false positives from
102
+ // unrelated commits that merely reference the issue number
103
+ const logResult = spawnSync("git", [
104
+ "log",
105
+ "main",
106
+ "--oneline",
107
+ "-20",
108
+ "--grep",
109
+ `Merge #${issueNumber}`,
110
+ "--grep",
111
+ `Merge.*#${issueNumber}`,
112
+ "--grep",
113
+ `(#${issueNumber})`,
114
+ ], {
115
+ stdio: "pipe",
116
+ timeout: 10000,
117
+ });
118
+ if (logResult.status === 0 && logResult.stdout) {
119
+ const commits = logResult.stdout.toString().trim();
120
+ if (commits.length > 0) {
121
+ return true;
122
+ }
123
+ }
124
+ }
125
+ catch {
126
+ // git command failed - return false
127
+ }
128
+ return false;
129
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Run reflection analysis — analyzes completed run data and suggests improvements.
3
+ *
4
+ * Used by the `--reflect` flag on `sequant run` to provide post-run insights.
5
+ */
6
+ import type { IssueResult } from "./types.js";
7
+ import type { RunLog } from "./run-log-schema.js";
8
+ export interface ReflectionInput {
9
+ results: IssueResult[];
10
+ issueInfoMap: Map<number, {
11
+ title: string;
12
+ labels: string[];
13
+ }>;
14
+ runLog: Omit<RunLog, "endTime"> | null;
15
+ config: {
16
+ phases: string[];
17
+ qualityLoop: boolean;
18
+ };
19
+ }
20
+ export interface ReflectionOutput {
21
+ observations: string[];
22
+ suggestions: string[];
23
+ }
24
+ /**
25
+ * Analyze a completed run and return observations + suggestions.
26
+ */
27
+ export declare function analyzeRun(input: ReflectionInput): ReflectionOutput;
28
+ /**
29
+ * Format reflection output as a box with observations and suggestions.
30
+ * Enforces max 10 content lines.
31
+ */
32
+ export declare function formatReflection(output: ReflectionOutput): string;