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.
Files changed (63) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +14 -2
  3. package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +21 -0
  4. package/dist/marketplace/external_plugins/sequant/README.md +38 -0
  5. package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +292 -0
  6. package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +463 -0
  7. package/dist/marketplace/external_plugins/sequant/skills/_shared/references/prompt-templates.md +350 -0
  8. package/dist/marketplace/external_plugins/sequant/skills/_shared/references/subagent-types.md +131 -0
  9. package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +474 -0
  10. package/dist/marketplace/external_plugins/sequant/skills/clean/SKILL.md +211 -0
  11. package/dist/marketplace/external_plugins/sequant/skills/docs/SKILL.md +337 -0
  12. package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +807 -0
  13. package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +678 -0
  14. package/dist/marketplace/external_plugins/sequant/skills/improve/SKILL.md +668 -0
  15. package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +374 -0
  16. package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +570 -0
  17. package/dist/marketplace/external_plugins/sequant/skills/qa/references/code-quality-exemplars.md +107 -0
  18. package/dist/marketplace/external_plugins/sequant/skills/qa/references/code-review-checklist.md +65 -0
  19. package/dist/marketplace/external_plugins/sequant/skills/qa/references/quality-gates.md +179 -0
  20. package/dist/marketplace/external_plugins/sequant/skills/qa/references/semgrep-rules.md +207 -0
  21. package/dist/marketplace/external_plugins/sequant/skills/qa/references/testing-requirements.md +109 -0
  22. package/dist/marketplace/external_plugins/sequant/skills/qa/scripts/quality-checks.sh +622 -0
  23. package/dist/marketplace/external_plugins/sequant/skills/reflect/SKILL.md +175 -0
  24. package/dist/marketplace/external_plugins/sequant/skills/reflect/references/documentation-tiers.md +70 -0
  25. package/dist/marketplace/external_plugins/sequant/skills/reflect/references/phase-reflection.md +95 -0
  26. package/dist/marketplace/external_plugins/sequant/skills/security-review/SKILL.md +358 -0
  27. package/dist/marketplace/external_plugins/sequant/skills/security-review/references/security-checklists.md +432 -0
  28. package/dist/marketplace/external_plugins/sequant/skills/solve/SKILL.md +697 -0
  29. package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +754 -0
  30. package/dist/marketplace/external_plugins/sequant/skills/spec/references/parallel-groups.md +72 -0
  31. package/dist/marketplace/external_plugins/sequant/skills/spec/references/recommended-workflow.md +92 -0
  32. package/dist/marketplace/external_plugins/sequant/skills/spec/references/verification-criteria.md +104 -0
  33. package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +600 -0
  34. package/dist/marketplace/external_plugins/sequant/skills/testgen/SKILL.md +576 -0
  35. package/dist/marketplace/external_plugins/sequant/skills/verify/SKILL.md +281 -0
  36. package/dist/src/commands/run.d.ts +13 -280
  37. package/dist/src/commands/run.js +23 -1956
  38. package/dist/src/commands/sync.js +3 -0
  39. package/dist/src/commands/update.js +3 -0
  40. package/dist/src/lib/plugin-version-sync.d.ts +2 -1
  41. package/dist/src/lib/plugin-version-sync.js +28 -7
  42. package/dist/src/lib/solve-comment-parser.d.ts +26 -0
  43. package/dist/src/lib/solve-comment-parser.js +63 -7
  44. package/dist/src/lib/workflow/batch-executor.d.ts +117 -0
  45. package/dist/src/lib/workflow/batch-executor.js +574 -0
  46. package/dist/src/lib/workflow/phase-executor.d.ts +40 -0
  47. package/dist/src/lib/workflow/phase-executor.js +381 -0
  48. package/dist/src/lib/workflow/phase-mapper.d.ts +65 -0
  49. package/dist/src/lib/workflow/phase-mapper.js +147 -0
  50. package/dist/src/lib/workflow/pr-operations.d.ts +86 -0
  51. package/dist/src/lib/workflow/pr-operations.js +326 -0
  52. package/dist/src/lib/workflow/pr-status.d.ts +9 -7
  53. package/dist/src/lib/workflow/pr-status.js +13 -11
  54. package/dist/src/lib/workflow/run-summary.d.ts +36 -0
  55. package/dist/src/lib/workflow/run-summary.js +142 -0
  56. package/dist/src/lib/workflow/worktree-manager.d.ts +205 -0
  57. package/dist/src/lib/workflow/worktree-manager.js +918 -0
  58. package/package.json +3 -1
  59. package/templates/skills/fullsolve/SKILL.md +11 -1
  60. package/templates/skills/qa/SKILL.md +41 -1
  61. package/templates/skills/solve/SKILL.md +86 -0
  62. package/templates/skills/spec/SKILL.md +53 -0
  63. 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 {};