sequant 2.1.0 → 2.1.2
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/src/commands/init.js +4 -3
- package/dist/src/commands/run-compat.d.ts +14 -0
- package/dist/src/commands/run-compat.js +12 -0
- package/dist/src/commands/run.d.ts +4 -26
- package/dist/src/commands/run.js +92 -765
- package/dist/src/commands/status.js +9 -0
- package/dist/src/index.d.ts +11 -0
- package/dist/src/index.js +9 -0
- package/dist/src/lib/errors.d.ts +93 -0
- package/dist/src/lib/errors.js +97 -0
- package/dist/src/lib/mcp-config.js +1 -1
- package/dist/src/lib/settings.d.ts +236 -0
- package/dist/src/lib/settings.js +482 -37
- package/dist/src/lib/workflow/batch-executor.js +12 -4
- package/dist/src/lib/workflow/config-resolver.d.ts +50 -0
- package/dist/src/lib/workflow/config-resolver.js +167 -0
- package/dist/src/lib/workflow/error-classifier.d.ts +17 -7
- package/dist/src/lib/workflow/error-classifier.js +113 -15
- package/dist/src/lib/workflow/phase-executor.js +14 -2
- package/dist/src/lib/workflow/run-log-schema.d.ts +12 -0
- package/dist/src/lib/workflow/run-log-schema.js +7 -1
- package/dist/src/lib/workflow/run-orchestrator.d.ts +124 -0
- package/dist/src/lib/workflow/run-orchestrator.js +482 -0
- package/package.json +1 -1
- package/templates/skills/assess/SKILL.md +84 -35
- package/templates/skills/exec/SKILL.md +7 -27
- package/templates/skills/fullsolve/SKILL.md +329 -137
- package/templates/skills/qa/SKILL.md +23 -46
package/dist/src/commands/run.js
CHANGED
|
@@ -1,806 +1,133 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* sequant run - Execute workflow for GitHub issues
|
|
3
|
-
*
|
|
4
|
-
* Orchestrator module that composes focused workflow modules:
|
|
5
|
-
* - worktree-manager: Worktree lifecycle (ensure, list, cleanup, changed files)
|
|
6
|
-
* - phase-executor: Phase execution with retry and failure handling
|
|
7
|
-
* - phase-mapper: Label-to-phase detection and workflow parsing
|
|
8
|
-
* - batch-executor: Batch execution, dependency sorting, issue logging
|
|
9
|
-
*/
|
|
1
|
+
/** sequant run — Thin CLI adapter that delegates to RunOrchestrator. */
|
|
10
2
|
import chalk from "chalk";
|
|
11
|
-
import { spawnSync } from "child_process";
|
|
12
|
-
import pLimit from "p-limit";
|
|
13
3
|
import { getManifest } from "../lib/manifest.js";
|
|
14
4
|
import { formatElapsedTime } from "../lib/phase-spinner.js";
|
|
15
5
|
import { getSettings } from "../lib/settings.js";
|
|
16
|
-
import { LogWriter } from "../lib/workflow/log-writer.js";
|
|
17
|
-
import { StateManager } from "../lib/workflow/state-manager.js";
|
|
18
|
-
import { DEFAULT_PHASES, DEFAULT_CONFIG, } from "../lib/workflow/types.js";
|
|
19
|
-
import { ShutdownManager } from "../lib/shutdown.js";
|
|
20
6
|
import { checkVersionCached, getVersionWarning } from "../lib/version-check.js";
|
|
21
|
-
import { MetricsWriter } from "../lib/workflow/metrics-writer.js";
|
|
22
|
-
import { determineOutcome, } from "../lib/workflow/metrics-schema.js";
|
|
23
7
|
import { ui, colors } from "../lib/cli-ui.js";
|
|
24
|
-
import { getCommitHash } from "../lib/workflow/git-diff-utils.js";
|
|
25
|
-
import { getTokenUsageForRun } from "../lib/workflow/token-utils.js";
|
|
26
|
-
import { reconcileStateAtStartup } from "../lib/workflow/state-utils.js";
|
|
27
|
-
import { analyzeRun, formatReflection } from "../lib/workflow/run-reflect.js";
|
|
28
|
-
/** @internal Log a non-fatal warning: one-line summary always, detail in verbose. */
|
|
29
|
-
export function logNonFatalWarning(message, error, verbose) {
|
|
30
|
-
console.log(chalk.yellow(message));
|
|
31
|
-
if (verbose) {
|
|
32
|
-
console.log(chalk.gray(` ${error}`));
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
// Extracted modules
|
|
36
|
-
import { detectDefaultBranch, ensureWorktrees, ensureWorktreesChain, getWorktreeDiffStats, } from "../lib/workflow/worktree-manager.js";
|
|
37
8
|
import { formatDuration } from "../lib/workflow/phase-executor.js";
|
|
38
|
-
import {
|
|
9
|
+
import { parseBatches } from "../lib/workflow/batch-executor.js";
|
|
10
|
+
import { RunOrchestrator } from "../lib/workflow/run-orchestrator.js";
|
|
11
|
+
import { analyzeRun, formatReflection } from "../lib/workflow/run-reflect.js";
|
|
39
12
|
// Re-export public API for backwards compatibility
|
|
40
|
-
export
|
|
41
|
-
|
|
42
|
-
export { detectPhasesFromLabels, parseRecommendedWorkflow, determinePhasesForIssue, } from "../lib/workflow/phase-mapper.js";
|
|
43
|
-
export { getIssueInfo, sortByDependencies, parseBatches, getEnvConfig, executeBatch, runIssueWithLogging, } from "../lib/workflow/batch-executor.js";
|
|
44
|
-
/**
|
|
45
|
-
* Normalize Commander.js --no-X flags into typed RunOptions fields.
|
|
46
|
-
* Replaces the `as any` cast (#402 AC-4).
|
|
47
|
-
*/
|
|
48
|
-
export function normalizeCommanderOptions(options) {
|
|
49
|
-
const raw = options;
|
|
50
|
-
return {
|
|
51
|
-
...options,
|
|
52
|
-
...(raw.log === false && { noLog: true }),
|
|
53
|
-
...(raw.smartTests === false && { noSmartTests: true }),
|
|
54
|
-
...(raw.mcp === false && { noMcp: true }),
|
|
55
|
-
...(raw.retry === false && { noRetry: true }),
|
|
56
|
-
...(raw.rebase === false && { noRebase: true }),
|
|
57
|
-
...(raw.pr === false && { noPr: true }),
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Execute a single issue with log bookkeeping (start/complete/PR info).
|
|
62
|
-
* Replaces the duplicated per-issue wrapper in sequential and parallel loops (#402 AC-1).
|
|
63
|
-
*/
|
|
64
|
-
async function executeOneIssue(args) {
|
|
65
|
-
const { issueNumber, batchCtx, chain, parallelIssueNumber } = args;
|
|
66
|
-
const { config, options, issueInfoMap, worktreeMap, logWriter, stateManager, shutdownManager, packageManager, baseBranch, onProgress, } = batchCtx;
|
|
67
|
-
const issueInfo = issueInfoMap.get(issueNumber) ?? {
|
|
68
|
-
title: `Issue #${issueNumber}`,
|
|
69
|
-
labels: [],
|
|
70
|
-
};
|
|
71
|
-
const worktreeInfo = worktreeMap.get(issueNumber);
|
|
72
|
-
// Start issue logging
|
|
73
|
-
if (logWriter) {
|
|
74
|
-
logWriter.startIssue(issueNumber, issueInfo.title, issueInfo.labels);
|
|
75
|
-
}
|
|
76
|
-
const ctx = {
|
|
77
|
-
issueNumber,
|
|
78
|
-
title: issueInfo.title,
|
|
79
|
-
labels: issueInfo.labels,
|
|
80
|
-
config,
|
|
81
|
-
options,
|
|
82
|
-
services: { logWriter, stateManager, shutdownManager },
|
|
83
|
-
worktree: worktreeInfo
|
|
84
|
-
? { path: worktreeInfo.path, branch: worktreeInfo.branch }
|
|
85
|
-
: undefined,
|
|
86
|
-
chain,
|
|
87
|
-
packageManager,
|
|
88
|
-
baseBranch,
|
|
89
|
-
onProgress,
|
|
90
|
-
};
|
|
91
|
-
const result = await runIssueWithLogging(ctx);
|
|
92
|
-
// Record PR info in log before completing issue
|
|
93
|
-
if (logWriter && result.prNumber && result.prUrl) {
|
|
94
|
-
logWriter.setPRInfo(result.prNumber, result.prUrl, parallelIssueNumber);
|
|
95
|
-
}
|
|
96
|
-
// Complete issue logging
|
|
97
|
-
if (logWriter) {
|
|
98
|
-
logWriter.completeIssue(parallelIssueNumber);
|
|
99
|
-
}
|
|
100
|
-
return result;
|
|
101
|
-
}
|
|
102
|
-
/**
|
|
103
|
-
* Main run command
|
|
104
|
-
*/
|
|
13
|
+
export * from "./run-compat.js";
|
|
14
|
+
/** Parse CLI args → validate → delegate to RunOrchestrator.run() → display summary. */
|
|
105
15
|
export async function runCommand(issues, options) {
|
|
106
16
|
console.log(ui.headerBox("SEQUANT WORKFLOW"));
|
|
107
|
-
// Version freshness check (cached, non-blocking, respects --quiet)
|
|
108
17
|
if (!options.quiet) {
|
|
109
18
|
try {
|
|
110
|
-
const
|
|
111
|
-
if (
|
|
112
|
-
console.log(chalk.yellow(` ! ${getVersionWarning(
|
|
19
|
+
const v = await checkVersionCached();
|
|
20
|
+
if (v.isOutdated && v.latestVersion) {
|
|
21
|
+
console.log(chalk.yellow(` ! ${getVersionWarning(v.currentVersion, v.latestVersion, v.isLocalInstall)}`));
|
|
113
22
|
console.log("");
|
|
114
23
|
}
|
|
115
24
|
}
|
|
116
25
|
catch {
|
|
117
|
-
|
|
26
|
+
/* non-critical */
|
|
118
27
|
}
|
|
119
28
|
}
|
|
120
|
-
// Check if initialized
|
|
121
29
|
const manifest = await getManifest();
|
|
122
30
|
if (!manifest) {
|
|
123
31
|
console.log(chalk.red("❌ Sequant is not initialized. Run `sequant init` first."));
|
|
124
32
|
return;
|
|
125
33
|
}
|
|
126
|
-
// Load settings and merge with environment config and CLI options
|
|
127
34
|
const settings = await getSettings();
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const normalizedOptions = normalizeCommanderOptions(options);
|
|
132
|
-
const mergedOptions = {
|
|
133
|
-
// Settings defaults (phases removed - now auto-detected)
|
|
134
|
-
sequential: normalizedOptions.sequential ?? settings.run.sequential,
|
|
135
|
-
concurrency: normalizedOptions.concurrency ?? settings.run.concurrency,
|
|
136
|
-
timeout: normalizedOptions.timeout ?? settings.run.timeout,
|
|
137
|
-
logPath: normalizedOptions.logPath ?? settings.run.logPath,
|
|
138
|
-
qualityLoop: normalizedOptions.qualityLoop ?? settings.run.qualityLoop,
|
|
139
|
-
maxIterations: normalizedOptions.maxIterations ?? settings.run.maxIterations,
|
|
140
|
-
noSmartTests: normalizedOptions.noSmartTests ?? !settings.run.smartTests,
|
|
141
|
-
// Agent settings (from agents section, not run section)
|
|
142
|
-
isolateParallel: normalizedOptions.isolateParallel ?? settings.agents.isolateParallel,
|
|
143
|
-
// Env overrides
|
|
144
|
-
...envConfig,
|
|
145
|
-
// CLI explicit options override all
|
|
146
|
-
...normalizedOptions,
|
|
147
|
-
};
|
|
148
|
-
// Determine if we should auto-detect phases from labels
|
|
149
|
-
const autoDetectPhases = !options.phases && settings.run.autoDetectPhases;
|
|
150
|
-
mergedOptions.autoDetectPhases = autoDetectPhases;
|
|
151
|
-
// Resolve base branch: CLI flag → settings.run.defaultBase → auto-detect → 'main'
|
|
152
|
-
const resolvedBaseBranch = options.base ??
|
|
153
|
-
settings.run.defaultBase ??
|
|
154
|
-
detectDefaultBranch(mergedOptions.verbose ?? false);
|
|
155
|
-
// Parse issue numbers (or use batch mode)
|
|
156
|
-
let issueNumbers;
|
|
157
|
-
let batches = null;
|
|
158
|
-
if (mergedOptions.batch && mergedOptions.batch.length > 0) {
|
|
159
|
-
batches = parseBatches(mergedOptions.batch);
|
|
160
|
-
issueNumbers = batches.flat();
|
|
161
|
-
console.log(chalk.gray(` Batch mode: ${batches.map((b) => `[${b.join(", ")}]`).join(" → ")}`));
|
|
162
|
-
}
|
|
163
|
-
else {
|
|
164
|
-
issueNumbers = issues.map((i) => parseInt(i, 10)).filter((n) => !isNaN(n));
|
|
165
|
-
}
|
|
166
|
-
if (issueNumbers.length === 0) {
|
|
167
|
-
console.log(chalk.red("❌ No valid issue numbers provided."));
|
|
168
|
-
console.log(chalk.gray("\nUsage: npx sequant run <issues...> [options]"));
|
|
169
|
-
console.log(chalk.gray("Example: npx sequant run 1 2 3 --sequential"));
|
|
170
|
-
console.log(chalk.gray('Batch example: npx sequant run --batch "1 2" --batch "3"'));
|
|
171
|
-
console.log(chalk.gray("Chain example: npx sequant run 1 2 3 --chain"));
|
|
35
|
+
// Validate constraints
|
|
36
|
+
if (options.chain && options.batch?.length) {
|
|
37
|
+
console.log(chalk.red("❌ --chain cannot be used with --batch"));
|
|
172
38
|
return;
|
|
173
39
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (!mergedOptions.sequential) {
|
|
178
|
-
mergedOptions.sequential = true;
|
|
179
|
-
}
|
|
180
|
-
if (batches) {
|
|
181
|
-
console.log(chalk.red("❌ --chain cannot be used with --batch"));
|
|
182
|
-
console.log(chalk.gray(" Chain mode creates a linear dependency chain between issues."));
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
// Warn about long chains
|
|
186
|
-
if (issueNumbers.length > 5) {
|
|
187
|
-
console.log(chalk.yellow(` ! Warning: Chain has ${issueNumbers.length} issues (recommended max: 5)`));
|
|
188
|
-
console.log(chalk.yellow(" Long chains increase merge complexity and review difficulty."));
|
|
189
|
-
console.log(chalk.yellow(" Consider breaking into smaller chains or using batch mode."));
|
|
190
|
-
console.log("");
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
// Validate concurrency value
|
|
194
|
-
if (mergedOptions.concurrency !== undefined &&
|
|
195
|
-
(mergedOptions.concurrency < 1 ||
|
|
196
|
-
!Number.isInteger(mergedOptions.concurrency))) {
|
|
197
|
-
console.log(chalk.red(`❌ Invalid --concurrency value: ${mergedOptions.concurrency}. Must be a positive integer.`));
|
|
40
|
+
if (options.concurrency !== undefined &&
|
|
41
|
+
(options.concurrency < 1 || !Number.isInteger(options.concurrency))) {
|
|
42
|
+
console.log(chalk.red(`❌ Invalid --concurrency value: ${options.concurrency}. Must be a positive integer.`));
|
|
198
43
|
return;
|
|
199
44
|
}
|
|
200
|
-
|
|
201
|
-
if (mergedOptions.qaGate && !mergedOptions.chain) {
|
|
45
|
+
if (options.qaGate && !options.chain) {
|
|
202
46
|
console.log(chalk.red("❌ --qa-gate requires --chain flag"));
|
|
203
|
-
console.log(chalk.gray(" QA gate ensures each issue passes QA before the next issue starts."));
|
|
204
|
-
console.log(chalk.gray(" Usage: npx sequant run 1 2 3 --sequential --chain --qa-gate"));
|
|
205
47
|
return;
|
|
206
48
|
}
|
|
207
|
-
|
|
208
|
-
if (
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const orderChanged = !originalOrder.every((n, i) => n === issueNumbers[i]);
|
|
212
|
-
if (orderChanged) {
|
|
213
|
-
console.log(chalk.gray(` Dependency order: ${issueNumbers.map((n) => `#${n}`).join(" → ")}`));
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
// Build config
|
|
217
|
-
// Note: config.phases is only used when --phases is explicitly set or autoDetect fails
|
|
218
|
-
const explicitPhases = mergedOptions.phases
|
|
219
|
-
? mergedOptions.phases.split(",").map((p) => p.trim())
|
|
220
|
-
: null;
|
|
221
|
-
// Determine MCP enablement: CLI flag (--no-mcp) → settings.run.mcp → default (true)
|
|
222
|
-
const mcpEnabled = mergedOptions.noMcp
|
|
223
|
-
? false
|
|
224
|
-
: (settings.run.mcp ?? DEFAULT_CONFIG.mcp);
|
|
225
|
-
// Resolve retry setting: CLI flag → settings.run.retry → default (true)
|
|
226
|
-
const retryEnabled = mergedOptions.noRetry
|
|
227
|
-
? false
|
|
228
|
-
: (settings.run.retry ?? true);
|
|
229
|
-
const isSequential = mergedOptions.sequential ?? false;
|
|
230
|
-
const isParallel = !isSequential && issueNumbers.length > 1;
|
|
231
|
-
const config = {
|
|
232
|
-
...DEFAULT_CONFIG,
|
|
233
|
-
phases: explicitPhases ?? DEFAULT_PHASES,
|
|
234
|
-
sequential: isSequential,
|
|
235
|
-
concurrency: mergedOptions.concurrency ?? DEFAULT_CONFIG.concurrency,
|
|
236
|
-
parallel: isParallel,
|
|
237
|
-
dryRun: mergedOptions.dryRun ?? false,
|
|
238
|
-
verbose: mergedOptions.verbose ?? false,
|
|
239
|
-
phaseTimeout: mergedOptions.timeout ?? DEFAULT_CONFIG.phaseTimeout,
|
|
240
|
-
qualityLoop: mergedOptions.qualityLoop ?? false,
|
|
241
|
-
maxIterations: mergedOptions.maxIterations ?? DEFAULT_CONFIG.maxIterations,
|
|
242
|
-
noSmartTests: mergedOptions.noSmartTests ?? false,
|
|
243
|
-
mcp: mcpEnabled,
|
|
244
|
-
retry: retryEnabled,
|
|
245
|
-
agent: mergedOptions.agent ?? settings.run.agent,
|
|
246
|
-
aiderSettings: settings.run.aider,
|
|
247
|
-
isolateParallel: mergedOptions.isolateParallel,
|
|
248
|
-
};
|
|
249
|
-
// Propagate verbose mode to UI config so spinners use text-only mode.
|
|
250
|
-
// This prevents animated spinner control characters from colliding with
|
|
251
|
-
// verbose console.log() calls from StateManager/MetricsWriter (#282).
|
|
252
|
-
if (config.verbose) {
|
|
253
|
-
ui.configure({ verbose: true });
|
|
254
|
-
}
|
|
255
|
-
// Initialize log writer if JSON logging enabled
|
|
256
|
-
// Default: enabled via settings (logJson: true), can be disabled with --no-log
|
|
257
|
-
let logWriter = null;
|
|
258
|
-
const shouldLog = !mergedOptions.noLog &&
|
|
259
|
-
!config.dryRun &&
|
|
260
|
-
(mergedOptions.logJson ?? settings.run.logJson);
|
|
261
|
-
if (shouldLog) {
|
|
262
|
-
const runConfig = {
|
|
263
|
-
phases: config.phases,
|
|
264
|
-
sequential: config.sequential,
|
|
265
|
-
qualityLoop: config.qualityLoop,
|
|
266
|
-
maxIterations: config.maxIterations,
|
|
267
|
-
chain: mergedOptions.chain,
|
|
268
|
-
qaGate: mergedOptions.qaGate,
|
|
269
|
-
};
|
|
270
|
-
try {
|
|
271
|
-
logWriter = new LogWriter({
|
|
272
|
-
logPath: mergedOptions.logPath ?? settings.run.logPath,
|
|
273
|
-
verbose: config.verbose,
|
|
274
|
-
startCommit: getCommitHash(process.cwd()),
|
|
275
|
-
});
|
|
276
|
-
await logWriter.initialize(runConfig);
|
|
277
|
-
}
|
|
278
|
-
catch (err) {
|
|
279
|
-
// Log initialization failure is non-fatal - warn and continue without logging
|
|
280
|
-
// Common causes: permissions issues, disk full, invalid path
|
|
281
|
-
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
282
|
-
console.log(chalk.yellow(` ! Log initialization failed, continuing without logging: ${errorMessage}`));
|
|
283
|
-
logWriter = null;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
// Initialize state manager for persistent workflow state tracking
|
|
287
|
-
// State tracking is always enabled (unless dry run)
|
|
288
|
-
let stateManager = null;
|
|
289
|
-
if (!config.dryRun) {
|
|
290
|
-
stateManager = new StateManager({ verbose: config.verbose });
|
|
291
|
-
}
|
|
292
|
-
// Initialize shutdown manager for graceful interruption handling
|
|
293
|
-
const shutdown = new ShutdownManager();
|
|
294
|
-
// Register log writer finalization as cleanup task
|
|
295
|
-
if (logWriter) {
|
|
296
|
-
const writer = logWriter; // Capture for closure
|
|
297
|
-
shutdown.registerCleanup("Finalize run logs", async () => {
|
|
298
|
-
await writer.finalize();
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
// Display configuration (columnar alignment)
|
|
302
|
-
const pad = (label) => label.padEnd(15);
|
|
303
|
-
console.log(chalk.gray(` ${pad("Stack")}${manifest.stack}`));
|
|
304
|
-
if (autoDetectPhases) {
|
|
305
|
-
console.log(chalk.gray(` ${pad("Phases")}auto-detect from labels`));
|
|
306
|
-
}
|
|
307
|
-
else {
|
|
308
|
-
console.log(chalk.gray(` ${pad("Phases")}${config.phases.join(" \u2192 ")}`));
|
|
309
|
-
}
|
|
310
|
-
console.log(chalk.gray(` ${pad("Mode")}${config.sequential ? "sequential (stop-on-failure)" : `parallel (concurrency: ${config.concurrency})`}`));
|
|
311
|
-
if (config.qualityLoop) {
|
|
312
|
-
console.log(chalk.gray(` ${pad("Quality loop")}enabled (max ${config.maxIterations} iterations)`));
|
|
313
|
-
}
|
|
314
|
-
if (mergedOptions.testgen) {
|
|
315
|
-
console.log(chalk.gray(` ${pad("Testgen")}enabled`));
|
|
316
|
-
}
|
|
317
|
-
if (config.noSmartTests) {
|
|
318
|
-
console.log(chalk.gray(` ${pad("Smart tests")}disabled`));
|
|
319
|
-
}
|
|
320
|
-
if (config.dryRun) {
|
|
321
|
-
console.log(chalk.yellow(` ${pad("!")}DRY RUN - no actual execution`));
|
|
322
|
-
}
|
|
323
|
-
if (logWriter) {
|
|
324
|
-
console.log(chalk.gray(` ${pad("Logging")}JSON (run ${logWriter.getRunId()?.slice(0, 8)}...)`));
|
|
325
|
-
}
|
|
326
|
-
if (stateManager) {
|
|
327
|
-
console.log(chalk.gray(` ${pad("State")}enabled`));
|
|
328
|
-
}
|
|
329
|
-
if (mergedOptions.force) {
|
|
330
|
-
console.log(chalk.yellow(` ${pad("Force")}enabled (bypass state guard)`));
|
|
331
|
-
}
|
|
332
|
-
console.log(chalk.gray(` ${pad("Issues")}${issueNumbers.map((n) => `#${n}`).join(", ")}`));
|
|
333
|
-
// ============================================================================
|
|
334
|
-
// Pre-flight State Guard (#305)
|
|
335
|
-
// ============================================================================
|
|
336
|
-
// AC-5: Auto-cleanup at run start - reconcile stale ready_for_merge states
|
|
337
|
-
if (stateManager && !config.dryRun) {
|
|
338
|
-
try {
|
|
339
|
-
const reconcileResult = await reconcileStateAtStartup({
|
|
340
|
-
verbose: config.verbose,
|
|
341
|
-
});
|
|
342
|
-
if (reconcileResult.success && reconcileResult.advanced.length > 0) {
|
|
343
|
-
console.log(chalk.gray(` State reconciled: ${reconcileResult.advanced.map((n) => `#${n}`).join(", ")} → merged`));
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
catch (error) {
|
|
347
|
-
// AC-8: Graceful degradation - don't block execution on reconciliation failure
|
|
348
|
-
logNonFatalWarning(` ! State reconciliation failed, continuing...`, error, config.verbose);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
// AC-1 & AC-2: Pre-flight state guard - skip completed issues unless --force
|
|
352
|
-
if (stateManager && !config.dryRun && !mergedOptions.force) {
|
|
353
|
-
const skippedIssues = [];
|
|
354
|
-
const activeIssues = [];
|
|
355
|
-
for (const issueNumber of issueNumbers) {
|
|
356
|
-
try {
|
|
357
|
-
const issueState = await stateManager.getIssueState(issueNumber);
|
|
358
|
-
if (issueState &&
|
|
359
|
-
(issueState.status === "ready_for_merge" ||
|
|
360
|
-
issueState.status === "merged")) {
|
|
361
|
-
skippedIssues.push(issueNumber);
|
|
362
|
-
console.log(chalk.yellow(` ! #${issueNumber}: already ${issueState.status} — skipping (use --force to re-run)`));
|
|
363
|
-
}
|
|
364
|
-
else {
|
|
365
|
-
activeIssues.push(issueNumber);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
catch (error) {
|
|
369
|
-
// AC-8: Graceful degradation - if state check fails, include the issue
|
|
370
|
-
logNonFatalWarning(` ! State lookup failed for #${issueNumber}, including anyway...`, error, config.verbose);
|
|
371
|
-
activeIssues.push(issueNumber);
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
// Update issueNumbers to only include active issues
|
|
375
|
-
if (skippedIssues.length > 0) {
|
|
376
|
-
issueNumbers = activeIssues;
|
|
377
|
-
if (issueNumbers.length === 0) {
|
|
378
|
-
console.log(chalk.yellow(`\n All issues already completed. Use --force to re-run.`));
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
console.log(chalk.gray(` Active issues: ${issueNumbers.map((n) => `#${n}`).join(", ")}`));
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
// Worktree isolation is enabled by default for multi-issue runs
|
|
385
|
-
const useWorktreeIsolation = mergedOptions.worktreeIsolation !== false && issueNumbers.length > 0;
|
|
386
|
-
if (useWorktreeIsolation) {
|
|
387
|
-
console.log(chalk.gray(` Worktree isolation: enabled`));
|
|
388
|
-
}
|
|
389
|
-
if (resolvedBaseBranch) {
|
|
390
|
-
console.log(chalk.gray(` Base branch: ${resolvedBaseBranch}`));
|
|
391
|
-
}
|
|
392
|
-
if (mergedOptions.chain) {
|
|
393
|
-
console.log(chalk.gray(` Chain mode: enabled (each issue branches from previous)`));
|
|
394
|
-
}
|
|
395
|
-
if (mergedOptions.qaGate) {
|
|
396
|
-
console.log(chalk.gray(` QA gate: enabled (chain waits for QA pass)`));
|
|
397
|
-
}
|
|
398
|
-
// Fetch issue info for all issues first
|
|
399
|
-
const issueInfoMap = new Map();
|
|
400
|
-
for (const issueNumber of issueNumbers) {
|
|
401
|
-
issueInfoMap.set(issueNumber, await getIssueInfo(issueNumber));
|
|
402
|
-
}
|
|
403
|
-
// Create worktrees for all issues before execution (if isolation enabled)
|
|
404
|
-
let worktreeMap = new Map();
|
|
405
|
-
if (useWorktreeIsolation && !config.dryRun) {
|
|
406
|
-
const issueData = issueNumbers.map((num) => ({
|
|
407
|
-
number: num,
|
|
408
|
-
title: issueInfoMap.get(num)?.title || `Issue #${num}`,
|
|
409
|
-
}));
|
|
410
|
-
// Use chain mode or standard worktree creation
|
|
411
|
-
if (mergedOptions.chain) {
|
|
412
|
-
worktreeMap = await ensureWorktreesChain(issueData, config.verbose, manifest.packageManager, resolvedBaseBranch);
|
|
413
|
-
}
|
|
414
|
-
else {
|
|
415
|
-
worktreeMap = await ensureWorktrees(issueData, config.verbose, manifest.packageManager, resolvedBaseBranch);
|
|
416
|
-
}
|
|
417
|
-
// Register cleanup tasks for newly created worktrees (not pre-existing ones)
|
|
418
|
-
for (const [issueNum, worktree] of worktreeMap.entries()) {
|
|
419
|
-
if (!worktree.existed) {
|
|
420
|
-
shutdown.registerCleanup(`Cleanup worktree for #${issueNum}`, async () => {
|
|
421
|
-
// Remove worktree (leaves branch intact for recovery)
|
|
422
|
-
const result = spawnSync("git", ["worktree", "remove", "--force", worktree.path], {
|
|
423
|
-
stdio: "pipe",
|
|
424
|
-
});
|
|
425
|
-
if (result.status !== 0 && config.verbose) {
|
|
426
|
-
console.log(chalk.yellow(` Warning: Could not remove worktree ${worktree.path}`));
|
|
427
|
-
}
|
|
428
|
-
});
|
|
429
|
-
}
|
|
430
|
-
}
|
|
49
|
+
let batches = null;
|
|
50
|
+
if (options.batch?.length) {
|
|
51
|
+
batches = parseBatches(options.batch);
|
|
52
|
+
console.log(chalk.gray(` Batch mode: ${batches.map((b) => `[${b.join(", ")}]`).join(" → ")}`));
|
|
431
53
|
}
|
|
432
|
-
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
baseBranch: resolvedBaseBranch,
|
|
443
|
-
};
|
|
444
|
-
// Execute with graceful shutdown handling
|
|
445
|
-
const results = [];
|
|
446
|
-
let exitCode = 0;
|
|
447
|
-
try {
|
|
448
|
-
if (batches) {
|
|
449
|
-
// Batch execution: run batches sequentially, issues within batch based on mode
|
|
450
|
-
for (let batchIdx = 0; batchIdx < batches.length; batchIdx++) {
|
|
451
|
-
const batch = batches[batchIdx];
|
|
452
|
-
console.log(chalk.blue(`\n Batch ${batchIdx + 1}/${batches.length}: Issues ${batch.map((n) => `#${n}`).join(", ")}`));
|
|
453
|
-
const batchResults = await executeBatch(batch, batchCtx);
|
|
454
|
-
results.push(...batchResults);
|
|
455
|
-
// Check if batch failed and we should stop
|
|
456
|
-
const batchFailed = batchResults.some((r) => !r.success);
|
|
457
|
-
if (batchFailed && config.sequential) {
|
|
458
|
-
console.log(chalk.yellow(`\n ! Batch ${batchIdx + 1} failed, stopping batch execution`));
|
|
459
|
-
break;
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
else if (config.sequential) {
|
|
464
|
-
// Sequential execution
|
|
465
|
-
for (let i = 0; i < issueNumbers.length; i++) {
|
|
466
|
-
const issueNumber = issueNumbers[i];
|
|
467
|
-
const result = await executeOneIssue({
|
|
468
|
-
issueNumber,
|
|
469
|
-
batchCtx,
|
|
470
|
-
chain: mergedOptions.chain
|
|
471
|
-
? { enabled: true, isLast: i === issueNumbers.length - 1 }
|
|
472
|
-
: undefined,
|
|
473
|
-
});
|
|
474
|
-
results.push(result);
|
|
475
|
-
// Check if shutdown was triggered
|
|
476
|
-
if (shutdown.shuttingDown) {
|
|
477
|
-
break;
|
|
478
|
-
}
|
|
479
|
-
if (!result.success) {
|
|
480
|
-
// Check if QA gate is enabled and QA specifically failed
|
|
481
|
-
if (mergedOptions.qaGate) {
|
|
482
|
-
const qaResult = result.phaseResults.find((p) => p.phase === "qa");
|
|
483
|
-
const qaFailed = qaResult && !qaResult.success;
|
|
484
|
-
if (qaFailed) {
|
|
485
|
-
// QA gate: pause chain with clear messaging
|
|
486
|
-
console.log(chalk.yellow("\n ⏸️ QA Gate"));
|
|
487
|
-
console.log(chalk.yellow(` Issue #${issueNumber} QA did not pass. Chain paused.`));
|
|
488
|
-
console.log(chalk.gray(" Fix QA issues and re-run, or run /loop to auto-fix."));
|
|
489
|
-
// Update state to waiting_for_qa_gate
|
|
490
|
-
if (stateManager) {
|
|
491
|
-
try {
|
|
492
|
-
await stateManager.updateIssueStatus(issueNumber, "waiting_for_qa_gate");
|
|
493
|
-
}
|
|
494
|
-
catch {
|
|
495
|
-
// State tracking errors shouldn't stop execution
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
break;
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
const chainInfo = mergedOptions.chain ? " (chain stopped)" : "";
|
|
502
|
-
console.log(chalk.yellow(`\n ! Issue #${issueNumber} failed, stopping sequential execution${chainInfo}`));
|
|
503
|
-
break;
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
else {
|
|
508
|
-
// Default mode: run issues concurrently with configurable concurrency limit
|
|
509
|
-
const limit = pLimit(config.concurrency);
|
|
510
|
-
// Track progress for concurrent issues
|
|
511
|
-
const issueStatus = new Map();
|
|
512
|
-
for (const num of issueNumbers) {
|
|
513
|
-
issueStatus.set(num, { state: "running" });
|
|
514
|
-
}
|
|
515
|
-
const renderProgressLine = () => {
|
|
516
|
-
const parts = issueNumbers.map((num) => {
|
|
517
|
-
const info = issueStatus.get(num);
|
|
518
|
-
if (info.state === "done")
|
|
519
|
-
return colors.success(`#${num} \u2714`);
|
|
520
|
-
if (info.state === "failed")
|
|
521
|
-
return colors.error(`#${num} \u2716`);
|
|
522
|
-
return colors.muted(`#${num} \u00B7`);
|
|
523
|
-
});
|
|
524
|
-
return ` ${parts.join(" ")}`;
|
|
525
|
-
};
|
|
526
|
-
const updateProgress = (completedIssue) => {
|
|
527
|
-
if (mergedOptions.quiet)
|
|
528
|
-
return;
|
|
529
|
-
if (process.stdout.isTTY) {
|
|
530
|
-
// TTY: overwrite the progress line in place
|
|
531
|
-
process.stdout.write(`\r${renderProgressLine()}`);
|
|
532
|
-
}
|
|
533
|
-
// Print a per-issue completion summary (works on both TTY and non-TTY)
|
|
534
|
-
if (completedIssue != null) {
|
|
535
|
-
const info = issueStatus.get(completedIssue);
|
|
536
|
-
const duration = info.durationSeconds != null
|
|
537
|
-
? ` (${formatElapsedTime(info.durationSeconds)})`
|
|
538
|
-
: "";
|
|
539
|
-
if (info.state === "done") {
|
|
540
|
-
const line = ` ${colors.success("\u2714")} #${completedIssue} completed${duration}`;
|
|
541
|
-
if (process.stdout.isTTY) {
|
|
542
|
-
// Move to a new line before printing the summary, then re-render progress
|
|
543
|
-
process.stdout.write(`\n${line}\n${renderProgressLine()}`);
|
|
544
|
-
}
|
|
545
|
-
else {
|
|
546
|
-
console.log(line);
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
else {
|
|
550
|
-
const errorSuffix = info.error ? `: ${info.error}` : "";
|
|
551
|
-
const line = ` ${colors.error("\u2716")} #${completedIssue} failed${duration}${errorSuffix}`;
|
|
552
|
-
if (process.stdout.isTTY) {
|
|
553
|
-
process.stdout.write(`\n${line}\n${renderProgressLine()}`);
|
|
554
|
-
}
|
|
555
|
-
else {
|
|
556
|
-
console.log(line);
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
};
|
|
561
|
-
// Per-phase progress callback for parallel mode (AC-1, AC-3)
|
|
562
|
-
const parallelStartTime = Date.now();
|
|
563
|
-
let lastPhaseEventTime = Date.now();
|
|
564
|
-
const onPhaseProgress = (issue, phase, event, extra) => {
|
|
565
|
-
if (mergedOptions.quiet)
|
|
566
|
-
return;
|
|
567
|
-
lastPhaseEventTime = Date.now();
|
|
568
|
-
let line;
|
|
569
|
-
if (event === "start") {
|
|
570
|
-
line = ` ${colors.running("\u25B8")} #${issue} ${phase}`;
|
|
571
|
-
}
|
|
572
|
-
else if (event === "complete") {
|
|
573
|
-
const dur = extra?.durationSeconds != null
|
|
574
|
-
? ` ${formatElapsedTime(extra.durationSeconds)}`
|
|
575
|
-
: "";
|
|
576
|
-
line = ` ${colors.success("\u2714")} #${issue} ${phase}${dur}`;
|
|
577
|
-
}
|
|
578
|
-
else {
|
|
579
|
-
line = ` ${colors.error("\u2716")} #${issue} ${phase}`;
|
|
580
|
-
}
|
|
581
|
-
console.log(line);
|
|
582
|
-
};
|
|
583
|
-
// 5-minute heartbeat timer, suppressed when phase events occur within window
|
|
584
|
-
const HEARTBEAT_INTERVAL_MS = 300_000;
|
|
585
|
-
const HEARTBEAT_SUPPRESS_MS = 60_000;
|
|
586
|
-
const heartbeatTimer = setInterval(() => {
|
|
587
|
-
if (mergedOptions.quiet)
|
|
588
|
-
return;
|
|
589
|
-
// Suppress if a phase event occurred recently
|
|
590
|
-
if (Date.now() - lastPhaseEventTime < HEARTBEAT_SUPPRESS_MS)
|
|
591
|
-
return;
|
|
592
|
-
const elapsedSec = Math.round((Date.now() - parallelStartTime) / 1000);
|
|
593
|
-
console.log(` ${colors.muted(`Still running... (${formatElapsedTime(elapsedSec)} elapsed)`)}`);
|
|
594
|
-
}, HEARTBEAT_INTERVAL_MS);
|
|
595
|
-
updateProgress();
|
|
596
|
-
const settledResults = await Promise.allSettled(issueNumbers.map((issueNumber) => limit(async () => {
|
|
597
|
-
// Check if shutdown was triggered before starting
|
|
598
|
-
if (shutdown.shuttingDown) {
|
|
599
|
-
return {
|
|
600
|
-
issueNumber,
|
|
601
|
-
success: false,
|
|
602
|
-
phaseResults: [],
|
|
603
|
-
durationSeconds: 0,
|
|
604
|
-
loopTriggered: false,
|
|
605
|
-
};
|
|
606
|
-
}
|
|
607
|
-
const result = await executeOneIssue({
|
|
608
|
-
issueNumber,
|
|
609
|
-
batchCtx: { ...batchCtx, onProgress: onPhaseProgress },
|
|
610
|
-
parallelIssueNumber: issueNumber,
|
|
611
|
-
});
|
|
612
|
-
// Update progress with completion details
|
|
613
|
-
issueStatus.set(issueNumber, {
|
|
614
|
-
state: result.success ? "done" : "failed",
|
|
615
|
-
durationSeconds: result.durationSeconds,
|
|
616
|
-
error: result.phaseResults.find((p) => !p.success)?.error,
|
|
617
|
-
});
|
|
618
|
-
updateProgress(issueNumber);
|
|
619
|
-
return result;
|
|
620
|
-
})));
|
|
621
|
-
// Clean up heartbeat timer
|
|
622
|
-
clearInterval(heartbeatTimer);
|
|
623
|
-
// Clear the progress line
|
|
624
|
-
if (process.stdout.isTTY && !mergedOptions.quiet) {
|
|
625
|
-
process.stdout.write("\n");
|
|
626
|
-
}
|
|
627
|
-
// Collect results from settled promises
|
|
628
|
-
for (let i = 0; i < settledResults.length; i++) {
|
|
629
|
-
const settled = settledResults[i];
|
|
630
|
-
if (settled.status === "fulfilled") {
|
|
631
|
-
results.push(settled.value);
|
|
632
|
-
}
|
|
633
|
-
else {
|
|
634
|
-
// Defensive fallback — runIssueWithLogging catches errors internally,
|
|
635
|
-
// so this path is unreachable in normal operation.
|
|
636
|
-
results.push({
|
|
637
|
-
issueNumber: issueNumbers[i],
|
|
638
|
-
success: false,
|
|
639
|
-
phaseResults: [],
|
|
640
|
-
durationSeconds: 0,
|
|
641
|
-
loopTriggered: false,
|
|
642
|
-
});
|
|
643
|
-
}
|
|
54
|
+
console.log(chalk.gray(` ${"Stack".padEnd(15)}${manifest.stack}`));
|
|
55
|
+
const onProgress = !options.quiet
|
|
56
|
+
? (issue, phase, event, extra) => {
|
|
57
|
+
if (event === "start")
|
|
58
|
+
console.log(` ${colors.running("▸")} #${issue} ${phase}`);
|
|
59
|
+
else if (event === "complete") {
|
|
60
|
+
const dur = extra?.durationSeconds != null
|
|
61
|
+
? ` ${formatElapsedTime(extra.durationSeconds)}`
|
|
62
|
+
: "";
|
|
63
|
+
console.log(` ${colors.success("✔")} #${issue} ${phase}${dur}`);
|
|
644
64
|
}
|
|
65
|
+
else
|
|
66
|
+
console.log(` ${colors.error("✖")} #${issue} ${phase}`);
|
|
645
67
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
if (worktreeInfo?.path) {
|
|
688
|
-
const stats = getWorktreeDiffStats(worktreeInfo.path);
|
|
689
|
-
totalFilesChanged += stats.filesChanged;
|
|
690
|
-
totalLinesAdded += stats.linesAdded;
|
|
691
|
-
}
|
|
692
|
-
// Count QA iterations (loop phases indicate retries)
|
|
693
|
-
if (result.loopTriggered) {
|
|
694
|
-
totalQaIterations += result.phaseResults.filter((p) => p.phase === "loop").length;
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
// Build CLI flags for metrics
|
|
698
|
-
const cliFlags = [];
|
|
699
|
-
if (mergedOptions.sequential)
|
|
700
|
-
cliFlags.push("--sequential");
|
|
701
|
-
if (mergedOptions.chain)
|
|
702
|
-
cliFlags.push("--chain");
|
|
703
|
-
if (mergedOptions.qaGate)
|
|
704
|
-
cliFlags.push("--qa-gate");
|
|
705
|
-
if (mergedOptions.qualityLoop)
|
|
706
|
-
cliFlags.push("--quality-loop");
|
|
707
|
-
if (mergedOptions.testgen)
|
|
708
|
-
cliFlags.push("--testgen");
|
|
709
|
-
// Read token usage from SessionEnd hook files (AC-5, AC-6)
|
|
710
|
-
const tokenUsage = getTokenUsageForRun(undefined, true); // cleanup after reading
|
|
711
|
-
// Record the run
|
|
712
|
-
await metricsWriter.recordRun({
|
|
713
|
-
issues: issueNumbers,
|
|
714
|
-
phases: Array.from(allPhases),
|
|
715
|
-
outcome: determineOutcome(passed, results.length),
|
|
716
|
-
duration: totalDuration,
|
|
717
|
-
model: process.env.ANTHROPIC_MODEL ?? "opus",
|
|
718
|
-
flags: cliFlags,
|
|
719
|
-
metrics: {
|
|
720
|
-
tokensUsed: tokenUsage.tokensUsed,
|
|
721
|
-
filesChanged: totalFilesChanged,
|
|
722
|
-
linesAdded: totalLinesAdded,
|
|
723
|
-
acceptanceCriteria: 0, // Would need to parse from issue
|
|
724
|
-
qaIterations: totalQaIterations,
|
|
725
|
-
// Token breakdown (AC-6)
|
|
726
|
-
inputTokens: tokenUsage.inputTokens || undefined,
|
|
727
|
-
outputTokens: tokenUsage.outputTokens || undefined,
|
|
728
|
-
cacheTokens: tokenUsage.cacheTokens || undefined,
|
|
729
|
-
},
|
|
730
|
-
});
|
|
731
|
-
if (config.verbose) {
|
|
732
|
-
console.log(chalk.gray(` Metrics recorded to .sequant/metrics.json`));
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
catch (metricsError) {
|
|
736
|
-
// Metrics recording errors shouldn't stop execution
|
|
737
|
-
logNonFatalWarning(` ! Metrics recording failed, continuing...`, metricsError, config.verbose);
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
// Summary
|
|
741
|
-
console.log("\n" + ui.divider());
|
|
742
|
-
console.log(colors.info(" Summary"));
|
|
743
|
-
console.log(ui.divider());
|
|
744
|
-
console.log(`\n ${colors.success(`${passed} passed`)} ${colors.muted("\u00B7")} ${colors.error(`${failed} failed`)}`);
|
|
745
|
-
for (const result of results) {
|
|
746
|
-
const status = result.success
|
|
747
|
-
? ui.statusIcon("success")
|
|
748
|
-
: ui.statusIcon("error");
|
|
749
|
-
const duration = result.durationSeconds
|
|
750
|
-
? colors.muted(` (${formatDuration(result.durationSeconds)})`)
|
|
751
|
-
: "";
|
|
752
|
-
const phases = result.phaseResults
|
|
753
|
-
.map((p) => p.success ? colors.success(p.phase) : colors.error(p.phase))
|
|
754
|
-
.join(" → ");
|
|
755
|
-
const loopInfo = result.loopTriggered ? colors.warning(" [loop]") : "";
|
|
756
|
-
const prInfo = result.prUrl
|
|
757
|
-
? colors.muted(` → PR #${result.prNumber}`)
|
|
758
|
-
: "";
|
|
759
|
-
console.log(` ${status} #${result.issueNumber}: ${phases}${loopInfo}${prInfo}${duration}`);
|
|
760
|
-
}
|
|
68
|
+
: undefined;
|
|
69
|
+
const result = await RunOrchestrator.run({
|
|
70
|
+
options,
|
|
71
|
+
settings,
|
|
72
|
+
manifest: {
|
|
73
|
+
stack: manifest.stack,
|
|
74
|
+
packageManager: manifest.packageManager ?? "npm",
|
|
75
|
+
},
|
|
76
|
+
onProgress,
|
|
77
|
+
}, issues, batches);
|
|
78
|
+
displaySummary(result);
|
|
79
|
+
if (result.exitCode !== 0)
|
|
80
|
+
process.exit(result.exitCode);
|
|
81
|
+
}
|
|
82
|
+
function displaySummary(result) {
|
|
83
|
+
const { results, logPath, config, mergedOptions } = result;
|
|
84
|
+
if (results.length === 0)
|
|
85
|
+
return;
|
|
86
|
+
const passed = results.filter((r) => r.success).length;
|
|
87
|
+
const failed = results.filter((r) => !r.success).length;
|
|
88
|
+
console.log("\n" + ui.divider());
|
|
89
|
+
console.log(colors.info(" Summary"));
|
|
90
|
+
console.log(ui.divider());
|
|
91
|
+
console.log(`\n ${colors.success(`${passed} passed`)} ${colors.muted("·")} ${colors.error(`${failed} failed`)}`);
|
|
92
|
+
for (const r of results) {
|
|
93
|
+
const status = r.success
|
|
94
|
+
? ui.statusIcon("success")
|
|
95
|
+
: ui.statusIcon("error");
|
|
96
|
+
const duration = r.durationSeconds
|
|
97
|
+
? colors.muted(` (${formatDuration(r.durationSeconds)})`)
|
|
98
|
+
: "";
|
|
99
|
+
const phases = r.phaseResults
|
|
100
|
+
.map((p) => (p.success ? colors.success(p.phase) : colors.error(p.phase)))
|
|
101
|
+
.join(" → ");
|
|
102
|
+
const loopInfo = r.loopTriggered ? colors.warning(" [loop]") : "";
|
|
103
|
+
const prInfo = r.prUrl ? colors.muted(` → PR #${r.prNumber}`) : "";
|
|
104
|
+
console.log(` ${status} #${r.issueNumber}: ${phases}${loopInfo}${prInfo}${duration}`);
|
|
105
|
+
}
|
|
106
|
+
console.log("");
|
|
107
|
+
if (logPath) {
|
|
108
|
+
console.log(colors.muted(` Log: ${logPath}`));
|
|
761
109
|
console.log("");
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
phases: config.phases,
|
|
774
|
-
qualityLoop: config.qualityLoop,
|
|
775
|
-
},
|
|
776
|
-
});
|
|
777
|
-
const reflectionOutput = formatReflection(reflection);
|
|
778
|
-
if (reflectionOutput) {
|
|
779
|
-
console.log(reflectionOutput);
|
|
780
|
-
console.log("");
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
// Suggest merge checks for multi-issue batches
|
|
784
|
-
if (results.length > 1 && passed > 0 && !config.dryRun) {
|
|
785
|
-
console.log(colors.muted(" Tip: Verify batch integration before merging:"));
|
|
786
|
-
console.log(colors.muted(" sequant merge --check"));
|
|
787
|
-
console.log("");
|
|
788
|
-
}
|
|
789
|
-
if (config.dryRun) {
|
|
790
|
-
console.log(colors.warning(" ℹ️ This was a dry run. Use without --dry-run to execute."));
|
|
110
|
+
}
|
|
111
|
+
if (mergedOptions.reflect && results.length > 0) {
|
|
112
|
+
const reflection = analyzeRun({
|
|
113
|
+
results,
|
|
114
|
+
issueInfoMap: result.issueInfoMap,
|
|
115
|
+
runLog: result.logWriter?.getRunLog() ?? null,
|
|
116
|
+
config: { phases: config.phases, qualityLoop: config.qualityLoop },
|
|
117
|
+
});
|
|
118
|
+
const reflectionOutput = formatReflection(reflection);
|
|
119
|
+
if (reflectionOutput) {
|
|
120
|
+
console.log(reflectionOutput);
|
|
791
121
|
console.log("");
|
|
792
122
|
}
|
|
793
|
-
// Set exit code if any failed
|
|
794
|
-
if (failed > 0 && !config.dryRun) {
|
|
795
|
-
exitCode = 1;
|
|
796
|
-
}
|
|
797
123
|
}
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
124
|
+
if (results.length > 1 && passed > 0 && !config.dryRun) {
|
|
125
|
+
console.log(colors.muted(" Tip: Verify batch integration before merging:"));
|
|
126
|
+
console.log(colors.muted(" sequant merge --check"));
|
|
127
|
+
console.log("");
|
|
801
128
|
}
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
129
|
+
if (config.dryRun) {
|
|
130
|
+
console.log(colors.warning(" ℹ️ This was a dry run. Use without --dry-run to execute."));
|
|
131
|
+
console.log("");
|
|
805
132
|
}
|
|
806
133
|
}
|