sequant 2.1.1 → 2.2.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 +1 -1
- package/dist/bin/cli.js +1 -0
- package/dist/src/commands/init.d.ts +1 -0
- package/dist/src/commands/init.js +122 -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-display.d.ts +17 -0
- package/dist/src/commands/run-display.js +116 -0
- package/dist/src/commands/run.d.ts +4 -26
- package/dist/src/commands/run.js +47 -772
- package/dist/src/commands/status.js +24 -1
- 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/settings.d.ts +236 -0
- package/dist/src/lib/settings.js +482 -37
- package/dist/src/lib/skill-version.d.ts +19 -0
- package/dist/src/lib/skill-version.js +68 -0
- package/dist/src/lib/templates.d.ts +1 -0
- package/dist/src/lib/templates.js +1 -1
- package/dist/src/lib/workflow/batch-executor.js +13 -5
- 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.d.ts +31 -0
- package/dist/src/lib/workflow/phase-executor.js +143 -48
- 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 +161 -0
- package/dist/src/lib/workflow/run-orchestrator.js +510 -0
- package/dist/src/lib/workflow/worktree-manager.d.ts +4 -3
- package/dist/src/lib/workflow/worktree-manager.js +61 -11
- package/package.json +1 -1
- package/templates/skills/assess/SKILL.md +239 -77
- package/templates/skills/exec/SKILL.md +7 -68
- package/templates/skills/fullsolve/SKILL.md +303 -137
- package/templates/skills/qa/SKILL.md +42 -46
- package/templates/skills/qa/scripts/quality-checks.sh +47 -1
- package/templates/skills/spec/SKILL.md +183 -982
- package/templates/skills/spec/references/quality-checklist.md +75 -0
- package/templates/skills/test/SKILL.md +0 -27
- package/templates/skills/testgen/SKILL.md +0 -27
package/dist/src/commands/run.js
CHANGED
|
@@ -1,806 +1,81 @@
|
|
|
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 {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
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
|
-
import { formatDuration } from "../lib/workflow/phase-executor.js";
|
|
38
|
-
import { getIssueInfo, sortByDependencies, parseBatches, getEnvConfig, executeBatch, runIssueWithLogging, } from "../lib/workflow/batch-executor.js";
|
|
8
|
+
import { parseBatches } from "../lib/workflow/batch-executor.js";
|
|
9
|
+
import { RunOrchestrator } from "../lib/workflow/run-orchestrator.js";
|
|
10
|
+
import { displayConfig, displaySummary } from "./run-display.js";
|
|
39
11
|
// 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
|
-
*/
|
|
12
|
+
export * from "./run-compat.js";
|
|
13
|
+
/** Parse CLI args → validate → delegate to RunOrchestrator.run() → display summary. */
|
|
105
14
|
export async function runCommand(issues, options) {
|
|
106
15
|
console.log(ui.headerBox("SEQUANT WORKFLOW"));
|
|
107
|
-
// Version freshness check (cached, non-blocking, respects --quiet)
|
|
108
16
|
if (!options.quiet) {
|
|
109
17
|
try {
|
|
110
|
-
const
|
|
111
|
-
if (
|
|
112
|
-
console.log(chalk.yellow(` ! ${getVersionWarning(
|
|
18
|
+
const v = await checkVersionCached();
|
|
19
|
+
if (v.isOutdated && v.latestVersion) {
|
|
20
|
+
console.log(chalk.yellow(` ! ${getVersionWarning(v.currentVersion, v.latestVersion, v.isLocalInstall)}`));
|
|
113
21
|
console.log("");
|
|
114
22
|
}
|
|
115
23
|
}
|
|
116
24
|
catch {
|
|
117
|
-
|
|
25
|
+
/* non-critical */
|
|
118
26
|
}
|
|
119
27
|
}
|
|
120
|
-
// Check if initialized
|
|
121
28
|
const manifest = await getManifest();
|
|
122
29
|
if (!manifest) {
|
|
123
30
|
console.log(chalk.red("❌ Sequant is not initialized. Run `sequant init` first."));
|
|
124
31
|
return;
|
|
125
32
|
}
|
|
126
|
-
// Load settings and merge with environment config and CLI options
|
|
127
33
|
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"));
|
|
34
|
+
// Validate constraints
|
|
35
|
+
if (options.chain && options.batch?.length) {
|
|
36
|
+
console.log(chalk.red("❌ --chain cannot be used with --batch"));
|
|
172
37
|
return;
|
|
173
38
|
}
|
|
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.`));
|
|
39
|
+
if (options.concurrency !== undefined &&
|
|
40
|
+
(options.concurrency < 1 || !Number.isInteger(options.concurrency))) {
|
|
41
|
+
console.log(chalk.red(`❌ Invalid --concurrency value: ${options.concurrency}. Must be a positive integer.`));
|
|
198
42
|
return;
|
|
199
43
|
}
|
|
200
|
-
|
|
201
|
-
if (mergedOptions.qaGate && !mergedOptions.chain) {
|
|
44
|
+
if (options.qaGate && !options.chain) {
|
|
202
45
|
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
46
|
return;
|
|
206
47
|
}
|
|
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
|
-
}
|
|
48
|
+
let batches = null;
|
|
49
|
+
if (options.batch?.length) {
|
|
50
|
+
batches = parseBatches(options.batch);
|
|
51
|
+
console.log(chalk.gray(` Batch mode: ${batches.map((b) => `[${b.join(", ")}]`).join(" → ")}`));
|
|
431
52
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
stateManager,
|
|
440
|
-
shutdownManager: shutdown,
|
|
441
|
-
packageManager: manifest.packageManager,
|
|
442
|
-
baseBranch: resolvedBaseBranch,
|
|
53
|
+
const init = {
|
|
54
|
+
options,
|
|
55
|
+
settings,
|
|
56
|
+
manifest: {
|
|
57
|
+
stack: manifest.stack,
|
|
58
|
+
packageManager: manifest.packageManager ?? "npm",
|
|
59
|
+
},
|
|
443
60
|
};
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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" });
|
|
61
|
+
const resolved = RunOrchestrator.resolveConfig(init, issues, batches);
|
|
62
|
+
displayConfig(resolved);
|
|
63
|
+
const onProgress = !options.quiet
|
|
64
|
+
? (issue, phase, event, extra) => {
|
|
65
|
+
if (event === "start")
|
|
66
|
+
console.log(` ${colors.running("▸")} #${issue} ${phase}`);
|
|
67
|
+
else if (event === "complete") {
|
|
68
|
+
const dur = extra?.durationSeconds != null
|
|
69
|
+
? ` ${formatElapsedTime(extra.durationSeconds)}`
|
|
70
|
+
: "";
|
|
71
|
+
console.log(` ${colors.success("✔")} #${issue} ${phase}${dur}`);
|
|
514
72
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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
|
-
}
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
// Finalize log
|
|
647
|
-
let logPath = null;
|
|
648
|
-
if (logWriter) {
|
|
649
|
-
logPath = await logWriter.finalize({
|
|
650
|
-
endCommit: getCommitHash(process.cwd()),
|
|
651
|
-
});
|
|
652
|
-
}
|
|
653
|
-
// Calculate success/failure counts
|
|
654
|
-
const passed = results.filter((r) => r.success).length;
|
|
655
|
-
const failed = results.filter((r) => !r.success).length;
|
|
656
|
-
// Record metrics (local analytics)
|
|
657
|
-
if (!config.dryRun && results.length > 0) {
|
|
658
|
-
try {
|
|
659
|
-
const metricsWriter = new MetricsWriter({ verbose: config.verbose });
|
|
660
|
-
// Calculate total duration
|
|
661
|
-
const totalDuration = results.reduce((sum, r) => sum + (r.durationSeconds ?? 0), 0);
|
|
662
|
-
// Get unique phases from all results
|
|
663
|
-
const allPhases = new Set();
|
|
664
|
-
for (const result of results) {
|
|
665
|
-
for (const phaseResult of result.phaseResults) {
|
|
666
|
-
// Only include phases that are valid MetricPhases
|
|
667
|
-
const phase = phaseResult.phase;
|
|
668
|
-
if ([
|
|
669
|
-
"spec",
|
|
670
|
-
"security-review",
|
|
671
|
-
"testgen",
|
|
672
|
-
"exec",
|
|
673
|
-
"test",
|
|
674
|
-
"qa",
|
|
675
|
-
"loop",
|
|
676
|
-
].includes(phase)) {
|
|
677
|
-
allPhases.add(phase);
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
// Calculate aggregate metrics from worktrees
|
|
682
|
-
let totalFilesChanged = 0;
|
|
683
|
-
let totalLinesAdded = 0;
|
|
684
|
-
let totalQaIterations = 0;
|
|
685
|
-
for (const result of results) {
|
|
686
|
-
const worktreeInfo = worktreeMap.get(result.issueNumber);
|
|
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
|
-
}
|
|
761
|
-
console.log("");
|
|
762
|
-
if (logPath) {
|
|
763
|
-
console.log(colors.muted(` Log: ${logPath}`));
|
|
764
|
-
console.log("");
|
|
765
|
-
}
|
|
766
|
-
// Reflection analysis (--reflect flag)
|
|
767
|
-
if (mergedOptions.reflect && results.length > 0) {
|
|
768
|
-
const reflection = analyzeRun({
|
|
769
|
-
results,
|
|
770
|
-
issueInfoMap,
|
|
771
|
-
runLog: logWriter?.getRunLog() ?? null,
|
|
772
|
-
config: {
|
|
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."));
|
|
791
|
-
console.log("");
|
|
792
|
-
}
|
|
793
|
-
// Set exit code if any failed
|
|
794
|
-
if (failed > 0 && !config.dryRun) {
|
|
795
|
-
exitCode = 1;
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
finally {
|
|
799
|
-
// Always dispose shutdown manager to clean up signal handlers
|
|
800
|
-
shutdown.dispose();
|
|
801
|
-
}
|
|
802
|
-
// Exit with error if any failed (outside try/finally so dispose() runs first)
|
|
803
|
-
if (exitCode !== 0) {
|
|
804
|
-
process.exit(exitCode);
|
|
805
|
-
}
|
|
73
|
+
else
|
|
74
|
+
console.log(` ${colors.error("✖")} #${issue} ${phase}`);
|
|
75
|
+
}
|
|
76
|
+
: undefined;
|
|
77
|
+
const result = await RunOrchestrator.run({ ...init, onProgress }, issues, batches);
|
|
78
|
+
displaySummary(result);
|
|
79
|
+
if (result.exitCode !== 0)
|
|
80
|
+
process.exit(result.exitCode);
|
|
806
81
|
}
|