sequant 1.15.4 → 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.
@@ -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.4",
4
+ "version": "1.16.0",
5
5
  "author": {
6
6
  "name": "sequant-io",
7
7
  "email": "hello@sequant.io"
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
@@ -145,6 +146,18 @@ program
145
146
  .option("--no-pr", "Skip PR creation after successful QA (manual PR workflow)")
146
147
  .option("-f, --force", "Force re-execution of completed issues (bypass pre-flight state guard)")
147
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);
148
161
  program
149
162
  .command("logs")
150
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
+ }
@@ -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
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Merge check orchestrator
3
+ *
4
+ * Coordinates all merge-check modules and resolves branches
5
+ * from run logs and git state.
6
+ */
7
+ import { type RunLog } from "../workflow/run-log-schema.js";
8
+ import type { BranchInfo, CheckResult, MergeCommandOptions, MergeReport } from "./types.js";
9
+ import { formatReportMarkdown, formatBranchReportMarkdown, postReportToGitHub } from "./report.js";
10
+ /**
11
+ * Find the most recent run log file
12
+ */
13
+ export declare function findMostRecentLog(logDir: string): RunLog | null;
14
+ /**
15
+ * Resolve branches from issue numbers.
16
+ *
17
+ * Uses git worktree list and remote branch patterns to find
18
+ * the feature branches for each issue.
19
+ */
20
+ export declare function resolveBranches(issueNumbers: number[], repoRoot: string, runLog?: RunLog | null): BranchInfo[];
21
+ /**
22
+ * Determine which checks to run based on command options.
23
+ *
24
+ * --scan, --review, and --all currently return the same checks because
25
+ * Phase 3 (AI briefing) is not yet implemented. When Phase 3 is added,
26
+ * --review and --all will include additional AI-powered checks.
27
+ * The distinction is preserved so callers can detect review mode
28
+ * and show the Phase 3 stub message (see merge.ts).
29
+ */
30
+ export declare function getChecksToRun(options: MergeCommandOptions): string[];
31
+ /**
32
+ * Run all merge checks and produce a report.
33
+ *
34
+ * @param issueNumbers - Issue numbers to check (empty = auto-detect from most recent run)
35
+ * @param options - Command options controlling which checks to run
36
+ * @param repoRoot - Path to the git repository root
37
+ * @returns MergeReport with all findings
38
+ */
39
+ export declare function runMergeChecks(issueNumbers: number[], options: MergeCommandOptions, repoRoot: string): Promise<MergeReport>;
40
+ export type { MergeReport, MergeCommandOptions, BranchInfo, CheckResult };
41
+ export { formatReportMarkdown, formatBranchReportMarkdown, postReportToGitHub };
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Merge check orchestrator
3
+ *
4
+ * Coordinates all merge-check modules and resolves branches
5
+ * from run logs and git state.
6
+ */
7
+ import * as fs from "fs";
8
+ import * as path from "path";
9
+ import * as os from "os";
10
+ import { spawnSync } from "child_process";
11
+ import { RunLogSchema, LOG_PATHS, } from "../workflow/run-log-schema.js";
12
+ import { getGitDiffStats } from "../workflow/git-diff-utils.js";
13
+ import { DEFAULT_MIRROR_PAIRS } from "./types.js";
14
+ import { runCombinedBranchTest } from "./combined-branch-test.js";
15
+ import { runMirroringCheck } from "./mirroring-check.js";
16
+ import { runOverlapDetection } from "./overlap-detection.js";
17
+ import { runResidualPatternScan } from "./residual-pattern-scan.js";
18
+ import { buildReport, formatReportMarkdown, formatBranchReportMarkdown, postReportToGitHub, } from "./report.js";
19
+ /**
20
+ * Resolve log directory path
21
+ */
22
+ function resolveLogDir(customPath) {
23
+ if (customPath) {
24
+ return customPath.replace("~", os.homedir());
25
+ }
26
+ const projectPath = LOG_PATHS.project;
27
+ if (fs.existsSync(projectPath)) {
28
+ return projectPath;
29
+ }
30
+ const userPath = LOG_PATHS.user.replace("~", os.homedir());
31
+ if (fs.existsSync(userPath)) {
32
+ return userPath;
33
+ }
34
+ return projectPath;
35
+ }
36
+ /**
37
+ * Find the most recent run log file
38
+ */
39
+ export function findMostRecentLog(logDir) {
40
+ if (!fs.existsSync(logDir)) {
41
+ return null;
42
+ }
43
+ const files = fs
44
+ .readdirSync(logDir)
45
+ .filter((f) => f.startsWith("run-") && f.endsWith(".json"))
46
+ .sort()
47
+ .reverse();
48
+ if (files.length === 0) {
49
+ return null;
50
+ }
51
+ const content = fs.readFileSync(path.join(logDir, files[0]), "utf-8");
52
+ const parsed = RunLogSchema.safeParse(JSON.parse(content));
53
+ if (!parsed.success) {
54
+ return null;
55
+ }
56
+ return parsed.data;
57
+ }
58
+ /**
59
+ * Resolve branches from issue numbers.
60
+ *
61
+ * Uses git worktree list and remote branch patterns to find
62
+ * the feature branches for each issue.
63
+ */
64
+ export function resolveBranches(issueNumbers, repoRoot, runLog) {
65
+ const branches = [];
66
+ // Get remote branches matching feature pattern
67
+ const branchResult = spawnSync("git", ["-C", repoRoot, "branch", "-r", "--list", "origin/feature/*"], { stdio: "pipe", encoding: "utf-8" });
68
+ const remoteBranches = branchResult.stdout
69
+ ? branchResult.stdout
70
+ .split("\n")
71
+ .map((b) => b.trim().replace("origin/", ""))
72
+ .filter(Boolean)
73
+ : [];
74
+ // Also check worktrees for local-only branches
75
+ const worktreeResult = spawnSync("git", ["-C", repoRoot, "worktree", "list", "--porcelain"], { stdio: "pipe", encoding: "utf-8" });
76
+ const worktreePaths = new Map();
77
+ let currentPath = "";
78
+ for (const line of (worktreeResult.stdout ?? "").split("\n")) {
79
+ if (line.startsWith("worktree ")) {
80
+ currentPath = line.slice(9);
81
+ }
82
+ else if (line.startsWith("branch refs/heads/")) {
83
+ const branch = line.slice(18);
84
+ worktreePaths.set(branch, currentPath);
85
+ }
86
+ }
87
+ // Get run log issue info for titles
88
+ const issueInfo = new Map();
89
+ if (runLog) {
90
+ for (const issue of runLog.issues) {
91
+ issueInfo.set(issue.issueNumber, {
92
+ title: issue.title,
93
+ prNumber: issue.prNumber,
94
+ });
95
+ }
96
+ }
97
+ for (const issueNumber of issueNumbers) {
98
+ // Find the branch for this issue
99
+ const branchPattern = new RegExp(`^feature/${issueNumber}-`);
100
+ const branch = remoteBranches.find((b) => branchPattern.test(b)) ??
101
+ Array.from(worktreePaths.keys()).find((b) => branchPattern.test(b));
102
+ if (!branch) {
103
+ console.error(`No branch found for issue #${issueNumber}`);
104
+ continue;
105
+ }
106
+ // Get modified files from the branch
107
+ const worktreePath = worktreePaths.get(branch);
108
+ let filesModified = [];
109
+ if (worktreePath) {
110
+ // Use worktree for diff
111
+ const diffStats = getGitDiffStats(worktreePath);
112
+ filesModified = diffStats.filesModified;
113
+ }
114
+ else {
115
+ // Use remote branch diff
116
+ const diffResult = spawnSync("git", [
117
+ "-C",
118
+ repoRoot,
119
+ "diff",
120
+ "--name-only",
121
+ `origin/main...origin/${branch}`,
122
+ ], { stdio: "pipe", encoding: "utf-8" });
123
+ filesModified = diffResult.stdout
124
+ ? diffResult.stdout.split("\n").filter(Boolean)
125
+ : [];
126
+ }
127
+ const info = issueInfo.get(issueNumber);
128
+ const title = info?.title ?? fetchIssueTitle(issueNumber) ?? `Issue #${issueNumber}`;
129
+ branches.push({
130
+ issueNumber,
131
+ title,
132
+ branch,
133
+ worktreePath,
134
+ prNumber: info?.prNumber,
135
+ filesModified,
136
+ });
137
+ }
138
+ return branches;
139
+ }
140
+ /**
141
+ * Fetch issue title from GitHub via gh CLI.
142
+ * Returns null if gh is not available or the issue doesn't exist.
143
+ */
144
+ function fetchIssueTitle(issueNumber) {
145
+ const result = spawnSync("gh", ["issue", "view", String(issueNumber), "--json", "title", "--jq", ".title"], { stdio: "pipe", encoding: "utf-8", timeout: 10_000 });
146
+ if (result.status !== 0 || !result.stdout?.trim()) {
147
+ return null;
148
+ }
149
+ return result.stdout.trim();
150
+ }
151
+ /**
152
+ * Determine which checks to run based on command options.
153
+ *
154
+ * --scan, --review, and --all currently return the same checks because
155
+ * Phase 3 (AI briefing) is not yet implemented. When Phase 3 is added,
156
+ * --review and --all will include additional AI-powered checks.
157
+ * The distinction is preserved so callers can detect review mode
158
+ * and show the Phase 3 stub message (see merge.ts).
159
+ */
160
+ export function getChecksToRun(options) {
161
+ const phase1 = ["combined-branch-test", "mirroring", "overlap-detection"];
162
+ const phase2 = ["residual-pattern-scan"];
163
+ // Phase 3 checks will be added here when AI briefing is implemented
164
+ // const phase3 = ["ai-briefing"];
165
+ if (options.all || options.review || options.scan) {
166
+ return [...phase1, ...phase2];
167
+ }
168
+ // Default --check: Phase 1 only
169
+ return phase1;
170
+ }
171
+ /**
172
+ * Run all merge checks and produce a report.
173
+ *
174
+ * @param issueNumbers - Issue numbers to check (empty = auto-detect from most recent run)
175
+ * @param options - Command options controlling which checks to run
176
+ * @param repoRoot - Path to the git repository root
177
+ * @returns MergeReport with all findings
178
+ */
179
+ export async function runMergeChecks(issueNumbers, options, repoRoot) {
180
+ const logDir = resolveLogDir();
181
+ let runLog = null;
182
+ // Auto-detect issues from most recent run log if none specified
183
+ if (issueNumbers.length === 0) {
184
+ runLog = findMostRecentLog(logDir);
185
+ if (!runLog) {
186
+ throw new Error("No run logs found. Specify issue numbers or run `sequant run` first.");
187
+ }
188
+ issueNumbers = runLog.issues.map((i) => i.issueNumber);
189
+ }
190
+ else {
191
+ // Still try to load run log for metadata
192
+ runLog = findMostRecentLog(logDir);
193
+ }
194
+ // Resolve branches for each issue
195
+ const branches = resolveBranches(issueNumbers, repoRoot, runLog);
196
+ if (branches.length === 0) {
197
+ throw new Error("No feature branches found for the specified issues. " +
198
+ "Ensure branches exist (pushed to remote or in local worktrees).");
199
+ }
200
+ // Determine which checks to run
201
+ const checksToRun = getChecksToRun(options);
202
+ const checkResults = [];
203
+ // Phase 1: Deterministic checks
204
+ if (checksToRun.includes("combined-branch-test")) {
205
+ checkResults.push(runCombinedBranchTest(branches, repoRoot));
206
+ }
207
+ if (checksToRun.includes("mirroring")) {
208
+ checkResults.push(runMirroringCheck(branches, DEFAULT_MIRROR_PAIRS));
209
+ }
210
+ if (checksToRun.includes("overlap-detection")) {
211
+ checkResults.push(runOverlapDetection(branches, repoRoot));
212
+ }
213
+ // Phase 2: Residual pattern detection
214
+ if (checksToRun.includes("residual-pattern-scan")) {
215
+ checkResults.push(runResidualPatternScan(branches, repoRoot));
216
+ }
217
+ // Build report
218
+ const report = buildReport(branches, checkResults, runLog?.runId);
219
+ // Post to GitHub if requested
220
+ if (options.post) {
221
+ postReportToGitHub(report);
222
+ }
223
+ return report;
224
+ }
225
+ export { formatReportMarkdown, formatBranchReportMarkdown, postReportToGitHub };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Template/source mirroring check (AC-2)
3
+ *
4
+ * Detects paired directories and verifies that when a file is modified
5
+ * in one location, the corresponding file in the mirror is also modified.
6
+ */
7
+ import type { BranchInfo, CheckResult, MirrorPair } from "./types.js";
8
+ /**
9
+ * Run mirroring check across all branches
10
+ *
11
+ * For each file modified by a branch, if it falls within a mirrored directory,
12
+ * verify that the corresponding mirror file was also modified.
13
+ */
14
+ export declare function runMirroringCheck(branches: BranchInfo[], mirrorPairs: MirrorPair[]): CheckResult;