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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +6 -1
- package/dist/bin/cli.js +2 -1
- package/dist/src/commands/run.d.ts +6 -0
- package/dist/src/commands/run.js +26 -2
- package/dist/src/lib/upstream/assessment.js +6 -3
- package/dist/src/lib/upstream/relevance.d.ts +5 -0
- package/dist/src/lib/upstream/relevance.js +24 -0
- package/dist/src/lib/upstream/report.js +18 -46
- package/dist/src/lib/upstream/types.d.ts +2 -0
- package/dist/src/lib/workflow/pr-status.d.ts +47 -0
- package/dist/src/lib/workflow/pr-status.js +129 -0
- package/dist/src/lib/workflow/run-reflect.d.ts +32 -0
- package/dist/src/lib/workflow/run-reflect.js +191 -0
- package/dist/src/lib/workflow/state-cleanup.d.ts +79 -0
- package/dist/src/lib/workflow/state-cleanup.js +250 -0
- package/dist/src/lib/workflow/state-rebuild.d.ts +38 -0
- package/dist/src/lib/workflow/state-rebuild.js +140 -0
- package/dist/src/lib/workflow/state-utils.d.ts +14 -162
- package/dist/src/lib/workflow/state-utils.js +10 -677
- package/dist/src/lib/workflow/worktree-discovery.d.ts +61 -0
- package/dist/src/lib/workflow/worktree-discovery.js +229 -0
- package/package.json +4 -5
- package/templates/skills/exec/SKILL.md +2 -2
- package/templates/skills/fullsolve/SKILL.md +18 -4
- package/templates/skills/loop/SKILL.md +1 -1
- package/templates/skills/qa/SKILL.md +6 -6
- package/templates/skills/solve/SKILL.md +28 -6
- package/templates/skills/spec/SKILL.md +16 -15
- package/templates/skills/testgen/SKILL.md +1 -1
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
|
[](https://www.npmjs.com/package/sequant)
|
|
8
10
|
[](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", "
|
|
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
|
package/dist/src/commands/run.js
CHANGED
|
@@ -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 ? "
|
|
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
|
-
//
|
|
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
|
-
|
|
239
|
-
|
|
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
|
-
//
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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 (
|
|
132
|
+
if (actionableFindings.length === 0) {
|
|
163
133
|
lines.push("None detected.");
|
|
164
134
|
}
|
|
165
135
|
else {
|
|
166
|
-
|
|
136
|
+
actionableFindings.forEach((f, i) => {
|
|
167
137
|
lines.push(formatFinding(f, i));
|
|
168
138
|
});
|
|
169
139
|
}
|
|
170
140
|
lines.push("");
|
|
171
|
-
//
|
|
141
|
+
// Informational section — opportunities noted for human triage
|
|
172
142
|
const opportunities = findings.filter((f) => f.category === "opportunity");
|
|
173
|
-
lines.push("###
|
|
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;
|