sequant 1.15.3 → 1.16.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/dist/bin/cli.js +14 -0
- package/dist/src/commands/merge.d.ts +22 -0
- package/dist/src/commands/merge.js +109 -0
- package/dist/src/commands/run.d.ts +36 -0
- package/dist/src/commands/run.js +195 -8
- package/dist/src/lib/merge-check/combined-branch-test.d.ts +16 -0
- package/dist/src/lib/merge-check/combined-branch-test.js +179 -0
- package/dist/src/lib/merge-check/index.d.ts +41 -0
- package/dist/src/lib/merge-check/index.js +225 -0
- package/dist/src/lib/merge-check/mirroring-check.d.ts +14 -0
- package/dist/src/lib/merge-check/mirroring-check.js +97 -0
- package/dist/src/lib/merge-check/overlap-detection.d.ts +20 -0
- package/dist/src/lib/merge-check/overlap-detection.js +148 -0
- package/dist/src/lib/merge-check/report.d.ts +38 -0
- package/dist/src/lib/merge-check/report.js +238 -0
- package/dist/src/lib/merge-check/residual-pattern-scan.d.ts +26 -0
- package/dist/src/lib/merge-check/residual-pattern-scan.js +222 -0
- package/dist/src/lib/merge-check/types.d.ts +182 -0
- package/dist/src/lib/merge-check/types.js +22 -0
- package/dist/src/lib/workflow/log-writer.d.ts +4 -0
- package/dist/src/lib/workflow/log-writer.js +16 -0
- package/dist/src/lib/workflow/run-log-schema.d.ts +4 -0
- package/dist/src/lib/workflow/run-log-schema.js +4 -0
- package/dist/src/lib/workflow/types.d.ts +4 -0
- package/package.json +1 -1
- package/templates/skills/exec/SKILL.md +1 -0
package/dist/bin/cli.js
CHANGED
|
@@ -44,6 +44,7 @@ import { statsCommand } from "../src/commands/stats.js";
|
|
|
44
44
|
import { dashboardCommand } from "../src/commands/dashboard.js";
|
|
45
45
|
import { stateInitCommand, stateRebuildCommand, stateCleanCommand, } from "../src/commands/state.js";
|
|
46
46
|
import { syncCommand, areSkillsOutdated } from "../src/commands/sync.js";
|
|
47
|
+
import { mergeCommand } from "../src/commands/merge.js";
|
|
47
48
|
import { getManifest } from "../src/lib/manifest.js";
|
|
48
49
|
const program = new Command();
|
|
49
50
|
// Handle --no-color before parsing
|
|
@@ -142,8 +143,21 @@ program
|
|
|
142
143
|
.option("--no-retry", "Disable automatic retry with MCP fallback (useful for debugging)")
|
|
143
144
|
.option("--resume", "Resume from last completed phase (reads phase markers from GitHub)")
|
|
144
145
|
.option("--no-rebase", "Skip pre-PR rebase onto origin/main (use when you want to handle rebasing manually)")
|
|
146
|
+
.option("--no-pr", "Skip PR creation after successful QA (manual PR workflow)")
|
|
145
147
|
.option("-f, --force", "Force re-execution of completed issues (bypass pre-flight state guard)")
|
|
146
148
|
.action(runCommand);
|
|
149
|
+
program
|
|
150
|
+
.command("merge")
|
|
151
|
+
.description("Batch-level integration QA — verify feature branches before merging")
|
|
152
|
+
.argument("[issues...]", "Issue numbers to check (auto-detects from most recent run if omitted)")
|
|
153
|
+
.option("--check", "Run Phase 1 deterministic checks (default)")
|
|
154
|
+
.option("--scan", "Run Phase 1 + Phase 2 residual pattern detection")
|
|
155
|
+
.option("--review", "Run Phase 1 + 2 + 3 AI briefing")
|
|
156
|
+
.option("--all", "Run all phases")
|
|
157
|
+
.option("--post", "Post report to GitHub as PR comments")
|
|
158
|
+
.option("--json", "Output as JSON")
|
|
159
|
+
.option("-v, --verbose", "Enable verbose output")
|
|
160
|
+
.action(mergeCommand);
|
|
147
161
|
program
|
|
148
162
|
.command("logs")
|
|
149
163
|
.description("View and analyze workflow run logs")
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sequant merge - Batch-level integration QA for completed runs
|
|
3
|
+
*
|
|
4
|
+
* Runs deterministic checks on feature branches from a `sequant run` batch
|
|
5
|
+
* to catch integration issues before human review.
|
|
6
|
+
*
|
|
7
|
+
* Phases:
|
|
8
|
+
* - --check (Phase 1): Combined branch test, mirroring, overlap detection
|
|
9
|
+
* - --scan (Phase 1+2): Adds residual pattern detection
|
|
10
|
+
* - --review (Phase 1+2+3): Adds AI briefing (stub)
|
|
11
|
+
* - --all: Runs all phases
|
|
12
|
+
* - --post: Post report to GitHub PRs
|
|
13
|
+
*/
|
|
14
|
+
import type { MergeCommandOptions } from "../lib/merge-check/types.js";
|
|
15
|
+
/**
|
|
16
|
+
* Determine exit code from batch verdict
|
|
17
|
+
*/
|
|
18
|
+
export declare function getExitCode(batchVerdict: string): number;
|
|
19
|
+
/**
|
|
20
|
+
* Main merge command handler
|
|
21
|
+
*/
|
|
22
|
+
export declare function mergeCommand(issues: string[], options: MergeCommandOptions): Promise<void>;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sequant merge - Batch-level integration QA for completed runs
|
|
3
|
+
*
|
|
4
|
+
* Runs deterministic checks on feature branches from a `sequant run` batch
|
|
5
|
+
* to catch integration issues before human review.
|
|
6
|
+
*
|
|
7
|
+
* Phases:
|
|
8
|
+
* - --check (Phase 1): Combined branch test, mirroring, overlap detection
|
|
9
|
+
* - --scan (Phase 1+2): Adds residual pattern detection
|
|
10
|
+
* - --review (Phase 1+2+3): Adds AI briefing (stub)
|
|
11
|
+
* - --all: Runs all phases
|
|
12
|
+
* - --post: Post report to GitHub PRs
|
|
13
|
+
*/
|
|
14
|
+
import { spawnSync } from "child_process";
|
|
15
|
+
import { ui, colors } from "../lib/cli-ui.js";
|
|
16
|
+
import { runMergeChecks, formatReportMarkdown, } from "../lib/merge-check/index.js";
|
|
17
|
+
/**
|
|
18
|
+
* Determine exit code from batch verdict
|
|
19
|
+
*/
|
|
20
|
+
export function getExitCode(batchVerdict) {
|
|
21
|
+
switch (batchVerdict) {
|
|
22
|
+
case "READY":
|
|
23
|
+
return 0;
|
|
24
|
+
case "NEEDS_ATTENTION":
|
|
25
|
+
return 1;
|
|
26
|
+
case "BLOCKED":
|
|
27
|
+
return 2;
|
|
28
|
+
default:
|
|
29
|
+
return 1;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Get the git repository root
|
|
34
|
+
*/
|
|
35
|
+
function getRepoRoot() {
|
|
36
|
+
const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
|
|
37
|
+
stdio: "pipe",
|
|
38
|
+
encoding: "utf-8",
|
|
39
|
+
});
|
|
40
|
+
if (result.status !== 0) {
|
|
41
|
+
throw new Error("Not in a git repository");
|
|
42
|
+
}
|
|
43
|
+
return result.stdout.trim();
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Main merge command handler
|
|
47
|
+
*/
|
|
48
|
+
export async function mergeCommand(issues, options) {
|
|
49
|
+
// Default to --check if no phase flag is specified
|
|
50
|
+
if (!options.check && !options.scan && !options.review && !options.all) {
|
|
51
|
+
options.check = true;
|
|
52
|
+
}
|
|
53
|
+
const repoRoot = getRepoRoot();
|
|
54
|
+
const issueNumbers = issues
|
|
55
|
+
.map((i) => parseInt(i, 10))
|
|
56
|
+
.filter((n) => !isNaN(n));
|
|
57
|
+
// Determine mode label
|
|
58
|
+
let mode = "check";
|
|
59
|
+
if (options.all)
|
|
60
|
+
mode = "all";
|
|
61
|
+
else if (options.review)
|
|
62
|
+
mode = "review";
|
|
63
|
+
else if (options.scan)
|
|
64
|
+
mode = "scan";
|
|
65
|
+
if (!options.json) {
|
|
66
|
+
console.log(ui.headerBox("SEQUANT MERGE"));
|
|
67
|
+
console.log("");
|
|
68
|
+
console.log(colors.muted(issueNumbers.length > 0
|
|
69
|
+
? `Checking issues: ${issueNumbers.map((i) => `#${i}`).join(", ")} (mode: ${mode})`
|
|
70
|
+
: `Auto-detecting issues from most recent run (mode: ${mode})`));
|
|
71
|
+
console.log("");
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const report = await runMergeChecks(issueNumbers, options, repoRoot);
|
|
75
|
+
if (options.json) {
|
|
76
|
+
// JSON output: serialize the report (convert Map to object)
|
|
77
|
+
const jsonReport = {
|
|
78
|
+
...report,
|
|
79
|
+
issueVerdicts: Object.fromEntries(report.issueVerdicts),
|
|
80
|
+
};
|
|
81
|
+
console.log(JSON.stringify(jsonReport, null, 2));
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
// Markdown output
|
|
85
|
+
const markdown = formatReportMarkdown(report);
|
|
86
|
+
console.log(markdown);
|
|
87
|
+
// Phase 3 stub
|
|
88
|
+
if (options.review || options.all) {
|
|
89
|
+
console.log("");
|
|
90
|
+
console.log(colors.muted("Phase 3 (AI briefing) is not yet implemented. Use --check or --scan for deterministic checks."));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Set exit code based on verdict
|
|
94
|
+
const exitCode = getExitCode(report.batchVerdict);
|
|
95
|
+
if (exitCode !== 0) {
|
|
96
|
+
process.exitCode = exitCode;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
101
|
+
if (options.json) {
|
|
102
|
+
console.log(JSON.stringify({ error: message }, null, 2));
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
console.error(ui.errorBox("Merge Check Failed", message));
|
|
106
|
+
}
|
|
107
|
+
process.exitCode = 2;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -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.
|
package/dist/src/commands/run.js
CHANGED
|
@@ -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:
|
|
1452
|
-
timeout:
|
|
1453
|
-
logPath:
|
|
1454
|
-
qualityLoop:
|
|
1455
|
-
maxIterations:
|
|
1456
|
-
noSmartTests:
|
|
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
|
-
...
|
|
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
|
-
|
|
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
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Combined branch testing (AC-1)
|
|
3
|
+
*
|
|
4
|
+
* Creates a temporary branch merging all feature branches from a run batch,
|
|
5
|
+
* runs npm test && npm run build on the combined state, and reports results.
|
|
6
|
+
*/
|
|
7
|
+
import type { BranchInfo, CheckResult, BranchCheckResult, CheckFinding } from "./types.js";
|
|
8
|
+
/**
|
|
9
|
+
* Create temp branch, merge all feature branches, run tests and build.
|
|
10
|
+
*
|
|
11
|
+
* @param branches - Feature branches to merge
|
|
12
|
+
* @param repoRoot - Path to the git repository root
|
|
13
|
+
* @returns CheckResult with combined test findings
|
|
14
|
+
*/
|
|
15
|
+
export declare function runCombinedBranchTest(branches: BranchInfo[], repoRoot: string): CheckResult;
|
|
16
|
+
export declare function buildResult(branchResults: BranchCheckResult[], batchFindings: CheckFinding[], startTime: number): CheckResult;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Combined branch testing (AC-1)
|
|
3
|
+
*
|
|
4
|
+
* Creates a temporary branch merging all feature branches from a run batch,
|
|
5
|
+
* runs npm test && npm run build on the combined state, and reports results.
|
|
6
|
+
*/
|
|
7
|
+
import { spawnSync } from "child_process";
|
|
8
|
+
import { getBranchRef } from "./types.js";
|
|
9
|
+
/**
|
|
10
|
+
* Run a git command and return the result
|
|
11
|
+
*/
|
|
12
|
+
function git(args, cwd) {
|
|
13
|
+
const result = spawnSync("git", args, {
|
|
14
|
+
cwd,
|
|
15
|
+
stdio: "pipe",
|
|
16
|
+
encoding: "utf-8",
|
|
17
|
+
});
|
|
18
|
+
return {
|
|
19
|
+
ok: result.status === 0,
|
|
20
|
+
stdout: result.stdout?.trim() ?? "",
|
|
21
|
+
stderr: result.stderr?.trim() ?? "",
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Run npm command and return result
|
|
26
|
+
*/
|
|
27
|
+
function npm(args, cwd) {
|
|
28
|
+
const result = spawnSync("npm", args, {
|
|
29
|
+
cwd,
|
|
30
|
+
stdio: "pipe",
|
|
31
|
+
encoding: "utf-8",
|
|
32
|
+
timeout: 120_000, // 2 min timeout for test/build
|
|
33
|
+
});
|
|
34
|
+
return {
|
|
35
|
+
ok: result.status === 0,
|
|
36
|
+
stdout: result.stdout?.trim() ?? "",
|
|
37
|
+
stderr: result.stderr?.trim() ?? "",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Create temp branch, merge all feature branches, run tests and build.
|
|
42
|
+
*
|
|
43
|
+
* @param branches - Feature branches to merge
|
|
44
|
+
* @param repoRoot - Path to the git repository root
|
|
45
|
+
* @returns CheckResult with combined test findings
|
|
46
|
+
*/
|
|
47
|
+
export function runCombinedBranchTest(branches, repoRoot) {
|
|
48
|
+
const startTime = Date.now();
|
|
49
|
+
const tempBranch = `merge-check/temp-${Date.now()}`;
|
|
50
|
+
const branchResults = [];
|
|
51
|
+
const batchFindings = [];
|
|
52
|
+
const mergeAttempts = [];
|
|
53
|
+
// Save current branch to restore in finally block
|
|
54
|
+
const originalBranch = git(["rev-parse", "--abbrev-ref", "HEAD"], repoRoot);
|
|
55
|
+
try {
|
|
56
|
+
// Fetch latest from remote
|
|
57
|
+
git(["fetch", "origin"], repoRoot);
|
|
58
|
+
// Create temp branch from main
|
|
59
|
+
const createResult = git(["checkout", "-b", tempBranch, "origin/main"], repoRoot);
|
|
60
|
+
if (!createResult.ok) {
|
|
61
|
+
batchFindings.push({
|
|
62
|
+
check: "combined-branch-test",
|
|
63
|
+
severity: "error",
|
|
64
|
+
message: `Failed to create temp branch: ${createResult.stderr}`,
|
|
65
|
+
});
|
|
66
|
+
return buildResult(branchResults, batchFindings, startTime);
|
|
67
|
+
}
|
|
68
|
+
// Merge each feature branch
|
|
69
|
+
for (const branch of branches) {
|
|
70
|
+
const mergeResult = git(["merge", "--no-ff", "--no-edit", getBranchRef(branch)], repoRoot);
|
|
71
|
+
if (mergeResult.ok) {
|
|
72
|
+
mergeAttempts.push({
|
|
73
|
+
issueNumber: branch.issueNumber,
|
|
74
|
+
branch: branch.branch,
|
|
75
|
+
success: true,
|
|
76
|
+
});
|
|
77
|
+
branchResults.push({
|
|
78
|
+
issueNumber: branch.issueNumber,
|
|
79
|
+
verdict: "PASS",
|
|
80
|
+
findings: [
|
|
81
|
+
{
|
|
82
|
+
check: "combined-branch-test",
|
|
83
|
+
severity: "info",
|
|
84
|
+
message: `Branch merged cleanly into combined state`,
|
|
85
|
+
issueNumber: branch.issueNumber,
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
// Get conflicting files
|
|
92
|
+
const conflictResult = git(["diff", "--name-only", "--diff-filter=U"], repoRoot);
|
|
93
|
+
const conflictFiles = conflictResult.stdout
|
|
94
|
+
? conflictResult.stdout.split("\n")
|
|
95
|
+
: [];
|
|
96
|
+
mergeAttempts.push({
|
|
97
|
+
issueNumber: branch.issueNumber,
|
|
98
|
+
branch: branch.branch,
|
|
99
|
+
success: false,
|
|
100
|
+
conflictFiles,
|
|
101
|
+
error: mergeResult.stderr,
|
|
102
|
+
});
|
|
103
|
+
branchResults.push({
|
|
104
|
+
issueNumber: branch.issueNumber,
|
|
105
|
+
verdict: "FAIL",
|
|
106
|
+
findings: [
|
|
107
|
+
{
|
|
108
|
+
check: "combined-branch-test",
|
|
109
|
+
severity: "error",
|
|
110
|
+
message: `Merge conflict with ${conflictFiles.length} file(s): ${conflictFiles.join(", ")}`,
|
|
111
|
+
issueNumber: branch.issueNumber,
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
});
|
|
115
|
+
// Abort the failed merge and continue
|
|
116
|
+
git(["merge", "--abort"], repoRoot);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// If any merges failed, skip tests but report what we have
|
|
120
|
+
const failedMerges = mergeAttempts.filter((m) => !m.success);
|
|
121
|
+
if (failedMerges.length > 0) {
|
|
122
|
+
batchFindings.push({
|
|
123
|
+
check: "combined-branch-test",
|
|
124
|
+
severity: "error",
|
|
125
|
+
message: `${failedMerges.length}/${branches.length} branches had merge conflicts — skipping test/build`,
|
|
126
|
+
});
|
|
127
|
+
return buildResult(branchResults, batchFindings, startTime);
|
|
128
|
+
}
|
|
129
|
+
// Run npm test
|
|
130
|
+
const testResult = npm(["test"], repoRoot);
|
|
131
|
+
if (testResult.ok) {
|
|
132
|
+
batchFindings.push({
|
|
133
|
+
check: "combined-branch-test",
|
|
134
|
+
severity: "info",
|
|
135
|
+
message: "npm test passed on combined state",
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
batchFindings.push({
|
|
140
|
+
check: "combined-branch-test",
|
|
141
|
+
severity: "error",
|
|
142
|
+
message: `npm test failed on combined state: ${testResult.stderr.slice(0, 500)}`,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
// Run npm run build
|
|
146
|
+
const buildResult2 = npm(["run", "build"], repoRoot);
|
|
147
|
+
if (buildResult2.ok) {
|
|
148
|
+
batchFindings.push({
|
|
149
|
+
check: "combined-branch-test",
|
|
150
|
+
severity: "info",
|
|
151
|
+
message: "npm run build passed on combined state",
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
batchFindings.push({
|
|
156
|
+
check: "combined-branch-test",
|
|
157
|
+
severity: "error",
|
|
158
|
+
message: `npm run build failed on combined state: ${buildResult2.stderr.slice(0, 500)}`,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return buildResult(branchResults, batchFindings, startTime);
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
// Clean up: restore original branch and delete temp branch
|
|
165
|
+
const restoreBranch = originalBranch.ok ? originalBranch.stdout : "main";
|
|
166
|
+
git(["checkout", restoreBranch], repoRoot);
|
|
167
|
+
git(["branch", "-D", tempBranch], repoRoot);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
export function buildResult(branchResults, batchFindings, startTime) {
|
|
171
|
+
const hasErrors = batchFindings.some((f) => f.severity === "error");
|
|
172
|
+
return {
|
|
173
|
+
name: "combined-branch-test",
|
|
174
|
+
passed: !hasErrors,
|
|
175
|
+
branchResults,
|
|
176
|
+
batchFindings,
|
|
177
|
+
durationMs: Date.now() - startTime,
|
|
178
|
+
};
|
|
179
|
+
}
|