sequant 1.17.0 → 1.18.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/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +14 -2
- package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +21 -0
- package/dist/marketplace/external_plugins/sequant/README.md +38 -0
- package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +292 -0
- package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +463 -0
- package/dist/marketplace/external_plugins/sequant/skills/_shared/references/prompt-templates.md +350 -0
- package/dist/marketplace/external_plugins/sequant/skills/_shared/references/subagent-types.md +131 -0
- package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +474 -0
- package/dist/marketplace/external_plugins/sequant/skills/clean/SKILL.md +211 -0
- package/dist/marketplace/external_plugins/sequant/skills/docs/SKILL.md +337 -0
- package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +807 -0
- package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +678 -0
- package/dist/marketplace/external_plugins/sequant/skills/improve/SKILL.md +668 -0
- package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +374 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +570 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/code-quality-exemplars.md +107 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/code-review-checklist.md +65 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/quality-gates.md +179 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/semgrep-rules.md +207 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/testing-requirements.md +109 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/scripts/quality-checks.sh +622 -0
- package/dist/marketplace/external_plugins/sequant/skills/reflect/SKILL.md +175 -0
- package/dist/marketplace/external_plugins/sequant/skills/reflect/references/documentation-tiers.md +70 -0
- package/dist/marketplace/external_plugins/sequant/skills/reflect/references/phase-reflection.md +95 -0
- package/dist/marketplace/external_plugins/sequant/skills/security-review/SKILL.md +358 -0
- package/dist/marketplace/external_plugins/sequant/skills/security-review/references/security-checklists.md +432 -0
- package/dist/marketplace/external_plugins/sequant/skills/solve/SKILL.md +697 -0
- package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +754 -0
- package/dist/marketplace/external_plugins/sequant/skills/spec/references/parallel-groups.md +72 -0
- package/dist/marketplace/external_plugins/sequant/skills/spec/references/recommended-workflow.md +92 -0
- package/dist/marketplace/external_plugins/sequant/skills/spec/references/verification-criteria.md +104 -0
- package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +600 -0
- package/dist/marketplace/external_plugins/sequant/skills/testgen/SKILL.md +576 -0
- package/dist/marketplace/external_plugins/sequant/skills/verify/SKILL.md +281 -0
- package/dist/src/commands/run.d.ts +13 -280
- package/dist/src/commands/run.js +23 -1956
- package/dist/src/commands/sync.js +3 -0
- package/dist/src/commands/update.js +3 -0
- package/dist/src/lib/plugin-version-sync.d.ts +2 -1
- package/dist/src/lib/plugin-version-sync.js +28 -7
- package/dist/src/lib/solve-comment-parser.d.ts +26 -0
- package/dist/src/lib/solve-comment-parser.js +63 -7
- package/dist/src/lib/workflow/batch-executor.d.ts +117 -0
- package/dist/src/lib/workflow/batch-executor.js +574 -0
- package/dist/src/lib/workflow/phase-executor.d.ts +40 -0
- package/dist/src/lib/workflow/phase-executor.js +381 -0
- package/dist/src/lib/workflow/phase-mapper.d.ts +65 -0
- package/dist/src/lib/workflow/phase-mapper.js +147 -0
- package/dist/src/lib/workflow/pr-operations.d.ts +86 -0
- package/dist/src/lib/workflow/pr-operations.js +326 -0
- package/dist/src/lib/workflow/pr-status.d.ts +9 -7
- package/dist/src/lib/workflow/pr-status.js +13 -11
- package/dist/src/lib/workflow/run-summary.d.ts +36 -0
- package/dist/src/lib/workflow/run-summary.js +142 -0
- package/dist/src/lib/workflow/worktree-manager.d.ts +205 -0
- package/dist/src/lib/workflow/worktree-manager.js +918 -0
- package/package.json +3 -1
- package/templates/skills/fullsolve/SKILL.md +11 -1
- package/templates/skills/qa/SKILL.md +41 -1
- package/templates/skills/solve/SKILL.md +86 -0
- package/templates/skills/spec/SKILL.md +53 -0
- package/templates/skills/test/SKILL.md +10 -0
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Batch execution and dependency handling for sequant run.
|
|
3
|
+
*
|
|
4
|
+
* Contains functions for fetching issue metadata, parsing and sorting
|
|
5
|
+
* dependencies, splitting issues into batches, reading environment-based
|
|
6
|
+
* configuration, and orchestrating the execution of individual issues
|
|
7
|
+
* (including quality-loop retries, checkpoint commits, rebasing, and PR
|
|
8
|
+
* creation).
|
|
9
|
+
*/
|
|
10
|
+
import chalk from "chalk";
|
|
11
|
+
import { spawnSync } from "child_process";
|
|
12
|
+
import { createPhaseLogFromTiming } from "./log-writer.js";
|
|
13
|
+
import { PhaseSpinner } from "../phase-spinner.js";
|
|
14
|
+
import { getGitDiffStats, getCommitHash } from "./git-diff-utils.js";
|
|
15
|
+
import { createCheckpointCommit, rebaseBeforePR, createPR, readCacheMetrics, filterResumedPhases, } from "./worktree-manager.js";
|
|
16
|
+
import { executePhaseWithRetry } from "./phase-executor.js";
|
|
17
|
+
import { detectPhasesFromLabels, parseRecommendedWorkflow, determinePhasesForIssue, BUG_LABELS, } from "./phase-mapper.js";
|
|
18
|
+
export async function getIssueInfo(issueNumber) {
|
|
19
|
+
try {
|
|
20
|
+
const result = spawnSync("gh", ["issue", "view", String(issueNumber), "--json", "title,labels"], { stdio: "pipe" });
|
|
21
|
+
if (result.status === 0) {
|
|
22
|
+
const data = JSON.parse(result.stdout.toString());
|
|
23
|
+
return {
|
|
24
|
+
title: data.title || `Issue #${issueNumber}`,
|
|
25
|
+
labels: Array.isArray(data.labels)
|
|
26
|
+
? data.labels.map((l) => l.name)
|
|
27
|
+
: [],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Ignore errors, use defaults
|
|
33
|
+
}
|
|
34
|
+
return { title: `Issue #${issueNumber}`, labels: [] };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Parse dependencies from issue body and labels
|
|
38
|
+
* Returns array of issue numbers this issue depends on
|
|
39
|
+
*/
|
|
40
|
+
export function parseDependencies(issueNumber) {
|
|
41
|
+
try {
|
|
42
|
+
const result = spawnSync("gh", ["issue", "view", String(issueNumber), "--json", "body,labels"], { stdio: "pipe" });
|
|
43
|
+
if (result.status !== 0)
|
|
44
|
+
return [];
|
|
45
|
+
const data = JSON.parse(result.stdout.toString());
|
|
46
|
+
const dependencies = [];
|
|
47
|
+
// Parse from body: "Depends on: #123" or "**Depends on**: #123"
|
|
48
|
+
if (data.body) {
|
|
49
|
+
const bodyMatch = data.body.match(/\*?\*?depends\s+on\*?\*?:?\s*#?(\d+)/gi);
|
|
50
|
+
if (bodyMatch) {
|
|
51
|
+
for (const match of bodyMatch) {
|
|
52
|
+
const numMatch = match.match(/(\d+)/);
|
|
53
|
+
if (numMatch) {
|
|
54
|
+
dependencies.push(parseInt(numMatch[1], 10));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Parse from labels: "depends-on/123" or "depends-on-123"
|
|
60
|
+
if (data.labels && Array.isArray(data.labels)) {
|
|
61
|
+
for (const label of data.labels) {
|
|
62
|
+
const labelName = label.name || label;
|
|
63
|
+
const labelMatch = labelName.match(/depends-on[-/](\d+)/i);
|
|
64
|
+
if (labelMatch) {
|
|
65
|
+
dependencies.push(parseInt(labelMatch[1], 10));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return [...new Set(dependencies)]; // Remove duplicates
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Sort issues by dependencies (topological sort)
|
|
77
|
+
* Issues with no dependencies come first, then issues that depend on them
|
|
78
|
+
*/
|
|
79
|
+
export function sortByDependencies(issueNumbers) {
|
|
80
|
+
// Build dependency graph
|
|
81
|
+
const dependsOn = new Map();
|
|
82
|
+
for (const issue of issueNumbers) {
|
|
83
|
+
const deps = parseDependencies(issue);
|
|
84
|
+
// Only include dependencies that are in our issue list
|
|
85
|
+
dependsOn.set(issue, deps.filter((d) => issueNumbers.includes(d)));
|
|
86
|
+
}
|
|
87
|
+
// Topological sort using Kahn's algorithm
|
|
88
|
+
const inDegree = new Map();
|
|
89
|
+
for (const issue of issueNumbers) {
|
|
90
|
+
inDegree.set(issue, 0);
|
|
91
|
+
}
|
|
92
|
+
for (const deps of dependsOn.values()) {
|
|
93
|
+
for (const dep of deps) {
|
|
94
|
+
inDegree.set(dep, (inDegree.get(dep) || 0) + 1);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Note: inDegree counts how many issues depend on each issue
|
|
98
|
+
// We want to process issues that nothing depends on last
|
|
99
|
+
// So we sort by: issues nothing depends on first, then dependent issues
|
|
100
|
+
const sorted = [];
|
|
101
|
+
const queue = [];
|
|
102
|
+
// Start with issues that have no dependencies
|
|
103
|
+
for (const issue of issueNumbers) {
|
|
104
|
+
const deps = dependsOn.get(issue) || [];
|
|
105
|
+
if (deps.length === 0) {
|
|
106
|
+
queue.push(issue);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const visited = new Set();
|
|
110
|
+
while (queue.length > 0) {
|
|
111
|
+
const issue = queue.shift();
|
|
112
|
+
if (visited.has(issue))
|
|
113
|
+
continue;
|
|
114
|
+
visited.add(issue);
|
|
115
|
+
sorted.push(issue);
|
|
116
|
+
// Find issues that depend on this one
|
|
117
|
+
for (const [other, deps] of dependsOn.entries()) {
|
|
118
|
+
if (deps.includes(issue) && !visited.has(other)) {
|
|
119
|
+
// Check if all dependencies of 'other' are satisfied
|
|
120
|
+
const allDepsSatisfied = deps.every((d) => visited.has(d));
|
|
121
|
+
if (allDepsSatisfied) {
|
|
122
|
+
queue.push(other);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Add any remaining issues (circular dependencies or unvisited)
|
|
128
|
+
for (const issue of issueNumbers) {
|
|
129
|
+
if (!visited.has(issue)) {
|
|
130
|
+
sorted.push(issue);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return sorted;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Parse batch arguments into groups of issues
|
|
137
|
+
*/
|
|
138
|
+
export function parseBatches(batchArgs) {
|
|
139
|
+
return batchArgs.map((batch) => batch
|
|
140
|
+
.split(/\s+/)
|
|
141
|
+
.map((n) => parseInt(n, 10))
|
|
142
|
+
.filter((n) => !isNaN(n)));
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Parse environment variables for CI configuration
|
|
146
|
+
*/
|
|
147
|
+
export function getEnvConfig() {
|
|
148
|
+
const config = {};
|
|
149
|
+
if (process.env.SEQUANT_QUALITY_LOOP === "true") {
|
|
150
|
+
config.qualityLoop = true;
|
|
151
|
+
}
|
|
152
|
+
if (process.env.SEQUANT_MAX_ITERATIONS) {
|
|
153
|
+
const maxIter = parseInt(process.env.SEQUANT_MAX_ITERATIONS, 10);
|
|
154
|
+
if (!isNaN(maxIter)) {
|
|
155
|
+
config.maxIterations = maxIter;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (process.env.SEQUANT_SMART_TESTS === "false") {
|
|
159
|
+
config.noSmartTests = true;
|
|
160
|
+
}
|
|
161
|
+
if (process.env.SEQUANT_TESTGEN === "true") {
|
|
162
|
+
config.testgen = true;
|
|
163
|
+
}
|
|
164
|
+
return config;
|
|
165
|
+
}
|
|
166
|
+
export async function executeBatch(issueNumbers, config, logWriter, stateManager, options, issueInfoMap, worktreeMap, shutdownManager, packageManager, baseBranch) {
|
|
167
|
+
const results = [];
|
|
168
|
+
for (const issueNumber of issueNumbers) {
|
|
169
|
+
// Check if shutdown was triggered
|
|
170
|
+
if (shutdownManager?.shuttingDown) {
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
const issueInfo = issueInfoMap.get(issueNumber) ?? {
|
|
174
|
+
title: `Issue #${issueNumber}`,
|
|
175
|
+
labels: [],
|
|
176
|
+
};
|
|
177
|
+
const worktreeInfo = worktreeMap.get(issueNumber);
|
|
178
|
+
// Start issue logging
|
|
179
|
+
if (logWriter) {
|
|
180
|
+
logWriter.startIssue(issueNumber, issueInfo.title, issueInfo.labels);
|
|
181
|
+
}
|
|
182
|
+
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
|
|
183
|
+
packageManager, undefined, baseBranch);
|
|
184
|
+
results.push(result);
|
|
185
|
+
// Record PR info in log before completing issue
|
|
186
|
+
if (logWriter && result.prNumber && result.prUrl) {
|
|
187
|
+
logWriter.setPRInfo(result.prNumber, result.prUrl);
|
|
188
|
+
}
|
|
189
|
+
// Complete issue logging
|
|
190
|
+
if (logWriter) {
|
|
191
|
+
logWriter.completeIssue();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return results;
|
|
195
|
+
}
|
|
196
|
+
export async function runIssueWithLogging(issueNumber, config, logWriter, stateManager, issueTitle, labels, options, worktreePath, branch, shutdownManager, chainMode, packageManager, isLastInChain, baseBranch) {
|
|
197
|
+
const startTime = Date.now();
|
|
198
|
+
const phaseResults = [];
|
|
199
|
+
let loopTriggered = false;
|
|
200
|
+
let sessionId;
|
|
201
|
+
console.log(chalk.blue(`\n Issue #${issueNumber}`));
|
|
202
|
+
if (worktreePath) {
|
|
203
|
+
console.log(chalk.gray(` Worktree: ${worktreePath}`));
|
|
204
|
+
}
|
|
205
|
+
// Initialize state tracking for this issue
|
|
206
|
+
if (stateManager) {
|
|
207
|
+
try {
|
|
208
|
+
const existingState = await stateManager.getIssueState(issueNumber);
|
|
209
|
+
if (!existingState) {
|
|
210
|
+
await stateManager.initializeIssue(issueNumber, issueTitle, {
|
|
211
|
+
worktree: worktreePath,
|
|
212
|
+
branch,
|
|
213
|
+
qualityLoop: config.qualityLoop,
|
|
214
|
+
maxIterations: config.maxIterations,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
// Update worktree info if it changed
|
|
219
|
+
if (worktreePath && branch) {
|
|
220
|
+
await stateManager.updateWorktreeInfo(issueNumber, worktreePath, branch);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
// State tracking errors shouldn't stop execution
|
|
226
|
+
if (config.verbose) {
|
|
227
|
+
console.log(chalk.yellow(` ⚠️ State tracking error: ${error}`));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// Determine phases for this specific issue
|
|
232
|
+
let phases;
|
|
233
|
+
let detectedQualityLoop = false;
|
|
234
|
+
let specAlreadyRan = false;
|
|
235
|
+
if (options.autoDetectPhases) {
|
|
236
|
+
// Check if labels indicate a simple bug/fix (skip spec entirely)
|
|
237
|
+
const lowerLabels = labels.map((l) => l.toLowerCase());
|
|
238
|
+
const isSimpleBugFix = lowerLabels.some((label) => BUG_LABELS.some((bugLabel) => label.includes(bugLabel)));
|
|
239
|
+
if (isSimpleBugFix) {
|
|
240
|
+
// Simple bug fix: skip spec, go straight to exec → qa
|
|
241
|
+
phases = ["exec", "qa"];
|
|
242
|
+
console.log(chalk.gray(` Bug fix detected: ${phases.join(" → ")}`));
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
// Run spec first to get recommended workflow
|
|
246
|
+
console.log(chalk.gray(` Running spec to determine workflow...`));
|
|
247
|
+
// Create spinner for spec phase (1 of estimated 3: spec, exec, qa)
|
|
248
|
+
const specSpinner = new PhaseSpinner({
|
|
249
|
+
phase: "spec",
|
|
250
|
+
phaseIndex: 1,
|
|
251
|
+
totalPhases: 3, // Estimate; will be refined after spec
|
|
252
|
+
shutdownManager,
|
|
253
|
+
});
|
|
254
|
+
specSpinner.start();
|
|
255
|
+
// Track spec phase start in state
|
|
256
|
+
if (stateManager) {
|
|
257
|
+
try {
|
|
258
|
+
await stateManager.updatePhaseStatus(issueNumber, "spec", "in_progress");
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
// State tracking errors shouldn't stop execution
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const specStartTime = new Date();
|
|
265
|
+
// Note: spec runs in main repo (not worktree) for planning
|
|
266
|
+
const specResult = await executePhaseWithRetry(issueNumber, "spec", config, sessionId, worktreePath, // Will be ignored for spec (non-isolated phase)
|
|
267
|
+
shutdownManager, specSpinner);
|
|
268
|
+
const specEndTime = new Date();
|
|
269
|
+
if (specResult.sessionId) {
|
|
270
|
+
sessionId = specResult.sessionId;
|
|
271
|
+
// Update session ID in state for resume capability
|
|
272
|
+
if (stateManager) {
|
|
273
|
+
try {
|
|
274
|
+
await stateManager.updateSessionId(issueNumber, specResult.sessionId);
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
// State tracking errors shouldn't stop execution
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
phaseResults.push(specResult);
|
|
282
|
+
specAlreadyRan = true;
|
|
283
|
+
// Log spec phase result
|
|
284
|
+
// Note: Spec runs in main repo, not worktree, so no git diff stats
|
|
285
|
+
if (logWriter) {
|
|
286
|
+
const phaseLog = createPhaseLogFromTiming("spec", issueNumber, specStartTime, specEndTime, specResult.success
|
|
287
|
+
? "success"
|
|
288
|
+
: specResult.error?.includes("Timeout")
|
|
289
|
+
? "timeout"
|
|
290
|
+
: "failure", { error: specResult.error });
|
|
291
|
+
logWriter.logPhase(phaseLog);
|
|
292
|
+
}
|
|
293
|
+
// Track spec phase completion in state
|
|
294
|
+
if (stateManager) {
|
|
295
|
+
try {
|
|
296
|
+
const phaseStatus = specResult.success ? "completed" : "failed";
|
|
297
|
+
await stateManager.updatePhaseStatus(issueNumber, "spec", phaseStatus, {
|
|
298
|
+
error: specResult.error,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
// State tracking errors shouldn't stop execution
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (!specResult.success) {
|
|
306
|
+
specSpinner.fail(specResult.error);
|
|
307
|
+
const durationSeconds = (Date.now() - startTime) / 1000;
|
|
308
|
+
return {
|
|
309
|
+
issueNumber,
|
|
310
|
+
success: false,
|
|
311
|
+
phaseResults,
|
|
312
|
+
durationSeconds,
|
|
313
|
+
loopTriggered: false,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
specSpinner.succeed();
|
|
317
|
+
// Parse recommended workflow from spec output
|
|
318
|
+
const parsedWorkflow = specResult.output
|
|
319
|
+
? parseRecommendedWorkflow(specResult.output)
|
|
320
|
+
: null;
|
|
321
|
+
if (parsedWorkflow) {
|
|
322
|
+
// Remove spec from phases since we already ran it
|
|
323
|
+
phases = parsedWorkflow.phases.filter((p) => p !== "spec");
|
|
324
|
+
detectedQualityLoop = parsedWorkflow.qualityLoop;
|
|
325
|
+
console.log(chalk.gray(` Spec recommends: ${phases.join(" → ")}${detectedQualityLoop ? " (quality loop)" : ""}`));
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
// Fall back to label-based detection
|
|
329
|
+
console.log(chalk.yellow(` Could not parse spec recommendation, using label-based detection`));
|
|
330
|
+
const detected = detectPhasesFromLabels(labels);
|
|
331
|
+
phases = detected.phases.filter((p) => p !== "spec");
|
|
332
|
+
detectedQualityLoop = detected.qualityLoop;
|
|
333
|
+
console.log(chalk.gray(` Fallback: ${phases.join(" → ")}`));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
// Use explicit phases with adjustments
|
|
339
|
+
phases = determinePhasesForIssue(config.phases, labels, options);
|
|
340
|
+
if (phases.length !== config.phases.length) {
|
|
341
|
+
console.log(chalk.gray(` Phases adjusted: ${phases.join(" → ")}`));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
// Resume: filter out completed phases if --resume flag is set
|
|
345
|
+
if (options.resume) {
|
|
346
|
+
const resumeResult = filterResumedPhases(issueNumber, phases, true);
|
|
347
|
+
if (resumeResult.skipped.length > 0) {
|
|
348
|
+
console.log(chalk.gray(` Resume: skipping completed phases: ${resumeResult.skipped.join(", ")}`));
|
|
349
|
+
phases = resumeResult.phases;
|
|
350
|
+
}
|
|
351
|
+
// Also skip spec if it was auto-detected as completed
|
|
352
|
+
if (specAlreadyRan &&
|
|
353
|
+
resumeResult.skipped.length === 0 &&
|
|
354
|
+
resumeResult.phases.length === 0) {
|
|
355
|
+
console.log(chalk.gray(` Resume: all phases already completed`));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// Add testgen phase if requested (and spec was in the phases)
|
|
359
|
+
if (options.testgen &&
|
|
360
|
+
(phases.includes("spec") || specAlreadyRan) &&
|
|
361
|
+
!phases.includes("testgen")) {
|
|
362
|
+
// Insert testgen at the beginning if spec already ran, otherwise after spec
|
|
363
|
+
if (specAlreadyRan) {
|
|
364
|
+
phases.unshift("testgen");
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
const specIndex = phases.indexOf("spec");
|
|
368
|
+
if (specIndex !== -1) {
|
|
369
|
+
phases.splice(specIndex + 1, 0, "testgen");
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
let iteration = 0;
|
|
374
|
+
const useQualityLoop = config.qualityLoop || detectedQualityLoop;
|
|
375
|
+
const maxIterations = useQualityLoop ? config.maxIterations : 1;
|
|
376
|
+
let completedSuccessfully = false;
|
|
377
|
+
while (iteration < maxIterations) {
|
|
378
|
+
iteration++;
|
|
379
|
+
if (useQualityLoop && iteration > 1) {
|
|
380
|
+
console.log(chalk.yellow(` Quality loop iteration ${iteration}/${maxIterations}`));
|
|
381
|
+
loopTriggered = true;
|
|
382
|
+
}
|
|
383
|
+
let phasesFailed = false;
|
|
384
|
+
// Calculate total phases for progress indicator
|
|
385
|
+
// If spec already ran in auto-detect mode, it's counted separately
|
|
386
|
+
const totalPhases = specAlreadyRan ? phases.length + 1 : phases.length;
|
|
387
|
+
const phaseIndexOffset = specAlreadyRan ? 1 : 0;
|
|
388
|
+
for (let phaseIdx = 0; phaseIdx < phases.length; phaseIdx++) {
|
|
389
|
+
const phase = phases[phaseIdx];
|
|
390
|
+
const phaseNumber = phaseIdx + 1 + phaseIndexOffset;
|
|
391
|
+
// Create spinner for this phase
|
|
392
|
+
const phaseSpinner = new PhaseSpinner({
|
|
393
|
+
phase,
|
|
394
|
+
phaseIndex: phaseNumber,
|
|
395
|
+
totalPhases,
|
|
396
|
+
shutdownManager,
|
|
397
|
+
iteration: useQualityLoop ? iteration : undefined,
|
|
398
|
+
});
|
|
399
|
+
phaseSpinner.start();
|
|
400
|
+
// Track phase start in state
|
|
401
|
+
if (stateManager) {
|
|
402
|
+
try {
|
|
403
|
+
await stateManager.updatePhaseStatus(issueNumber, phase, "in_progress");
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
// State tracking errors shouldn't stop execution
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
const phaseStartTime = new Date();
|
|
410
|
+
const result = await executePhaseWithRetry(issueNumber, phase, config, sessionId, worktreePath, shutdownManager, phaseSpinner);
|
|
411
|
+
const phaseEndTime = new Date();
|
|
412
|
+
// Capture session ID for subsequent phases
|
|
413
|
+
if (result.sessionId) {
|
|
414
|
+
sessionId = result.sessionId;
|
|
415
|
+
// Update session ID in state for resume capability
|
|
416
|
+
if (stateManager) {
|
|
417
|
+
try {
|
|
418
|
+
await stateManager.updateSessionId(issueNumber, result.sessionId);
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
// State tracking errors shouldn't stop execution
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
phaseResults.push(result);
|
|
426
|
+
// Log phase result with observability data (AC-1, AC-2, AC-3, AC-7)
|
|
427
|
+
if (logWriter) {
|
|
428
|
+
// Capture git diff stats for worktree phases (AC-1, AC-3)
|
|
429
|
+
const diffStats = worktreePath
|
|
430
|
+
? getGitDiffStats(worktreePath, baseBranch)
|
|
431
|
+
: undefined;
|
|
432
|
+
// Capture commit hash after phase (AC-2)
|
|
433
|
+
const commitHash = worktreePath
|
|
434
|
+
? getCommitHash(worktreePath)
|
|
435
|
+
: undefined;
|
|
436
|
+
// Read cache metrics for QA phase (AC-7)
|
|
437
|
+
const cacheMetrics = phase === "qa" ? readCacheMetrics(worktreePath) : undefined;
|
|
438
|
+
const phaseLog = createPhaseLogFromTiming(phase, issueNumber, phaseStartTime, phaseEndTime, result.success
|
|
439
|
+
? "success"
|
|
440
|
+
: result.error?.includes("Timeout")
|
|
441
|
+
? "timeout"
|
|
442
|
+
: "failure", {
|
|
443
|
+
error: result.error,
|
|
444
|
+
verdict: result.verdict,
|
|
445
|
+
// Observability fields (AC-1, AC-2, AC-3, AC-7)
|
|
446
|
+
filesModified: diffStats?.filesModified,
|
|
447
|
+
fileDiffStats: diffStats?.fileDiffStats,
|
|
448
|
+
commitHash,
|
|
449
|
+
cacheMetrics,
|
|
450
|
+
});
|
|
451
|
+
logWriter.logPhase(phaseLog);
|
|
452
|
+
}
|
|
453
|
+
// Track phase completion in state
|
|
454
|
+
if (stateManager) {
|
|
455
|
+
try {
|
|
456
|
+
const phaseStatus = result.success
|
|
457
|
+
? "completed"
|
|
458
|
+
: result.error?.includes("Timeout")
|
|
459
|
+
? "failed"
|
|
460
|
+
: "failed";
|
|
461
|
+
await stateManager.updatePhaseStatus(issueNumber, phase, phaseStatus, { error: result.error });
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
// State tracking errors shouldn't stop execution
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if (result.success) {
|
|
468
|
+
phaseSpinner.succeed();
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
phaseSpinner.fail(result.error);
|
|
472
|
+
phasesFailed = true;
|
|
473
|
+
// If quality loop enabled, run loop phase to fix issues
|
|
474
|
+
if (useQualityLoop && iteration < maxIterations) {
|
|
475
|
+
// Create spinner for loop phase
|
|
476
|
+
const loopSpinner = new PhaseSpinner({
|
|
477
|
+
phase: "loop",
|
|
478
|
+
phaseIndex: phaseNumber,
|
|
479
|
+
totalPhases,
|
|
480
|
+
shutdownManager,
|
|
481
|
+
iteration,
|
|
482
|
+
});
|
|
483
|
+
loopSpinner.start();
|
|
484
|
+
const loopResult = await executePhaseWithRetry(issueNumber, "loop", config, sessionId, worktreePath, shutdownManager, loopSpinner);
|
|
485
|
+
phaseResults.push(loopResult);
|
|
486
|
+
if (loopResult.sessionId) {
|
|
487
|
+
sessionId = loopResult.sessionId;
|
|
488
|
+
}
|
|
489
|
+
if (loopResult.success) {
|
|
490
|
+
loopSpinner.succeed();
|
|
491
|
+
// Continue to next iteration
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
loopSpinner.fail(loopResult.error);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
// Stop on first failure (if not in quality loop or loop failed)
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
// If all phases passed, exit the loop
|
|
503
|
+
if (!phasesFailed) {
|
|
504
|
+
completedSuccessfully = true;
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
// If we're not in quality loop mode, don't retry
|
|
508
|
+
if (!config.qualityLoop) {
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
const durationSeconds = (Date.now() - startTime) / 1000;
|
|
513
|
+
// Success is determined by whether all phases completed in any iteration,
|
|
514
|
+
// not whether all accumulated phase results passed (which would fail after loop recovery)
|
|
515
|
+
const success = completedSuccessfully;
|
|
516
|
+
// Update final issue status in state
|
|
517
|
+
if (stateManager) {
|
|
518
|
+
try {
|
|
519
|
+
const finalStatus = success ? "ready_for_merge" : "in_progress";
|
|
520
|
+
await stateManager.updateIssueStatus(issueNumber, finalStatus);
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
// State tracking errors shouldn't stop execution
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
// Create checkpoint commit in chain mode after QA passes
|
|
527
|
+
if (success && chainMode && worktreePath) {
|
|
528
|
+
createCheckpointCommit(worktreePath, issueNumber, config.verbose);
|
|
529
|
+
}
|
|
530
|
+
// Rebase onto the base branch before PR creation (unless --no-rebase)
|
|
531
|
+
// This ensures the branch is up-to-date and prevents lockfile drift
|
|
532
|
+
// AC-1: Non-chain mode rebases onto the base branch before PR
|
|
533
|
+
// AC-2: Chain mode rebases only the final branch onto the base branch before PR
|
|
534
|
+
// (intermediate branches must stay based on their predecessor)
|
|
535
|
+
const shouldRebase = success &&
|
|
536
|
+
worktreePath &&
|
|
537
|
+
!options.noRebase &&
|
|
538
|
+
(!chainMode || isLastInChain);
|
|
539
|
+
if (shouldRebase) {
|
|
540
|
+
rebaseBeforePR(worktreePath, issueNumber, packageManager, config.verbose, baseBranch);
|
|
541
|
+
}
|
|
542
|
+
// Create PR after successful QA + rebase (unless --no-pr)
|
|
543
|
+
let prNumber;
|
|
544
|
+
let prUrl;
|
|
545
|
+
const shouldCreatePR = success && worktreePath && branch && !options.noPr;
|
|
546
|
+
if (shouldCreatePR) {
|
|
547
|
+
const prResult = createPR(worktreePath, issueNumber, issueTitle, branch, config.verbose, labels);
|
|
548
|
+
if (prResult.success && prResult.prNumber && prResult.prUrl) {
|
|
549
|
+
prNumber = prResult.prNumber;
|
|
550
|
+
prUrl = prResult.prUrl;
|
|
551
|
+
// Update workflow state with PR info
|
|
552
|
+
if (stateManager) {
|
|
553
|
+
try {
|
|
554
|
+
await stateManager.updatePRInfo(issueNumber, {
|
|
555
|
+
number: prResult.prNumber,
|
|
556
|
+
url: prResult.prUrl,
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
catch {
|
|
560
|
+
// State tracking errors shouldn't stop execution
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return {
|
|
566
|
+
issueNumber,
|
|
567
|
+
success,
|
|
568
|
+
phaseResults,
|
|
569
|
+
durationSeconds,
|
|
570
|
+
loopTriggered,
|
|
571
|
+
prNumber,
|
|
572
|
+
prUrl,
|
|
573
|
+
};
|
|
574
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase execution engine for workflow orchestration.
|
|
3
|
+
*
|
|
4
|
+
* Handles executing individual phases via the Claude Agent SDK,
|
|
5
|
+
* including cold-start retry logic and MCP fallback strategies.
|
|
6
|
+
*/
|
|
7
|
+
import { ShutdownManager } from "../shutdown.js";
|
|
8
|
+
import { PhaseSpinner } from "../phase-spinner.js";
|
|
9
|
+
import { Phase, ExecutionConfig, PhaseResult, QaVerdict } from "./types.js";
|
|
10
|
+
export declare function parseQaVerdict(output: string): QaVerdict | null;
|
|
11
|
+
/**
|
|
12
|
+
* Format duration in human-readable format
|
|
13
|
+
*/
|
|
14
|
+
export declare function formatDuration(seconds: number): string;
|
|
15
|
+
/**
|
|
16
|
+
* Execute a single phase for an issue using Claude Agent SDK
|
|
17
|
+
*/
|
|
18
|
+
declare function executePhase(issueNumber: number, phase: Phase, config: ExecutionConfig, sessionId?: string, worktreePath?: string, shutdownManager?: ShutdownManager, spinner?: PhaseSpinner): Promise<PhaseResult & {
|
|
19
|
+
sessionId?: string;
|
|
20
|
+
}>;
|
|
21
|
+
/**
|
|
22
|
+
* Execute a phase with automatic retry for cold-start failures and MCP fallback.
|
|
23
|
+
*
|
|
24
|
+
* Retry strategy:
|
|
25
|
+
* 1. If phase fails within COLD_START_THRESHOLD_SECONDS, retry up to COLD_START_MAX_RETRIES times
|
|
26
|
+
* 2. If still failing and MCP is enabled, retry once with MCP disabled (npx-based MCP servers
|
|
27
|
+
* can fail on first run due to cold-cache issues)
|
|
28
|
+
*
|
|
29
|
+
* The MCP fallback is safe because MCP servers are optional enhancements, not required
|
|
30
|
+
* for core functionality.
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* @internal Exported for testing only
|
|
34
|
+
*/
|
|
35
|
+
export declare function executePhaseWithRetry(issueNumber: number, phase: Phase, config: ExecutionConfig, sessionId?: string, worktreePath?: string, shutdownManager?: ShutdownManager, spinner?: PhaseSpinner,
|
|
36
|
+
/** @internal Injected for testing — defaults to module-level executePhase */
|
|
37
|
+
executePhaseFn?: typeof executePhase): Promise<PhaseResult & {
|
|
38
|
+
sessionId?: string;
|
|
39
|
+
}>;
|
|
40
|
+
export {};
|